diff --git a/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java b/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java index 460817d00fa210460fdb21ceb6ce33fb210dd674..8a2bbc21dde5f8baf441a4e7c89dfd2908b4aa26 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java @@ -26,6 +26,7 @@ import org.eclipsefoundation.marketplace.dao.MongoDao; import org.eclipsefoundation.marketplace.exception.MaintenanceException; import org.eclipsefoundation.marketplace.model.MongoQuery; import org.eclipsefoundation.marketplace.namespace.DtoTableNames; +import org.eclipsefoundation.marketplace.namespace.MicroprofilePropertyNames; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,16 +45,16 @@ import io.quarkus.mongodb.ReactiveMongoCollection; public class DefaultMongoDao implements MongoDao { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMongoDao.class); - @ConfigProperty(name = "mongodb.database") + @ConfigProperty(name = MicroprofilePropertyNames.MONGODB_DB_NAME) String databaseName; - @ConfigProperty(name = "mongodb.default.limit") + @ConfigProperty(name = MicroprofilePropertyNames.MONGODB_RETURN_LIMIT) int defaultLimit; - @ConfigProperty(name = "mongodb.default.limit.max") + @ConfigProperty(name = MicroprofilePropertyNames.MONGODB_RETURN_LIMIT_MAX) int defaultMax; - @ConfigProperty(name = "mongodb.maintenance", defaultValue = "false") + @ConfigProperty(name = MicroprofilePropertyNames.MONGODB_MAINTENANCE_FLAG, defaultValue = "false") boolean maintenanceFlag; @Inject diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java index a6c93d3badc9da38f09e263ccfc937a236bd16f9..ec5396583fd0d0bb6c5e372be3fe29474db9800f 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java @@ -63,6 +63,7 @@ public class Listing extends NodeBase { private List<Author> authors; private List<Tag> tags; private List<ListingVersion> versions; + private boolean isPromotion; /** * Default constructor, sets lists to empty lists to stop null pointers @@ -381,6 +382,21 @@ public class Listing extends NodeBase { this.versions = new ArrayList<>(versions); } + /** + * @return the isPromotion + */ + public boolean isPromotion() { + return isPromotion; + } + + /** + * @param isPromotion the isPromotion to set + */ + @JsonbTransient + public void setPromotion(boolean isPromotion) { + this.isPromotion = isPromotion; + } + @Override public boolean validate() { return super.validate() && license != null && !authors.isEmpty() && !categoryIds.isEmpty() @@ -446,6 +462,7 @@ public class Listing extends NodeBase { sb.append(", tags=").append(tags); sb.append(", versions=").append(versions); sb.append(", screenshots=").append(screenshots); + sb.append(", isPromotion=").append(isPromotion); sb.append(']'); return sb.toString(); } diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Promotion.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Promotion.java new file mode 100644 index 0000000000000000000000000000000000000000..3339a0d56d3ad841a42b2a8d764d5f639d2e688b --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Promotion.java @@ -0,0 +1,51 @@ +package org.eclipsefoundation.marketplace.dto; + +public class Promotion { + + private String id; + private String listingId; + private int weight = 1; + + /** + * @return the id + */ + public String getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return the listingId + */ + public String getListingId() { + return listingId; + } + + /** + * @param listingId the listingId to set + */ + public void setListingId(String listingId) { + this.listingId = listingId; + } + + /** + * @return the weight + */ + public int getWeight() { + return weight; + } + + /** + * @param weight the weight to set + */ + public void setWeight(int weight) { + this.weight = weight; + } + +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/PromotionCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/PromotionCodec.java new file mode 100644 index 0000000000000000000000000000000000000000..5af459f4f625423ba1f87aacf2c6f6d9c6508eb7 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/PromotionCodec.java @@ -0,0 +1,86 @@ +/* Copyright (c) 2019 Eclipse Foundation and others. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License 2.0 + * which is available at http://www.eclipse.org/legal/epl-v20.html, + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.marketplace.dto.codecs; + +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.bson.BsonReader; +import org.bson.BsonString; +import org.bson.BsonValue; +import org.bson.BsonWriter; +import org.bson.Document; +import org.bson.codecs.Codec; +import org.bson.codecs.CollectibleCodec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.eclipsefoundation.marketplace.dto.Promotion; +import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; + +import com.mongodb.MongoClient; + +/** + * Codec for reading and writing {@linkplain Promotion} objectss to database objects. + * + * @author Martin Lowe + * + */ +public class PromotionCodec implements CollectibleCodec<Promotion> { + + private final Codec<Document> documentCodec; + + /** + * Creates the codec and initializes the codecs and converters needed to create + * a listing from end to end. + */ + public PromotionCodec() { + this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class); + } + + @Override + public void encode(BsonWriter writer, Promotion value, EncoderContext encoderContext) { + Document doc = new Document(); + doc.put(DatabaseFieldNames.DOCID, value.getId()); + doc.put(DatabaseFieldNames.LISTING_ID, value.getListingId()); + documentCodec.encode(writer, doc, encoderContext); + } + + @Override + public Class<Promotion> getEncoderClass() { + return Promotion.class; + } + + @Override + public Promotion decode(BsonReader reader, DecoderContext decoderContext) { + Document value = documentCodec.decode(reader, decoderContext); + + Promotion out = new Promotion(); + out.setId(value.getString(DatabaseFieldNames.DOCID)); + out.setListingId(value.getString(DatabaseFieldNames.LISTING_ID)); + out.setWeight(value.getInteger(DatabaseFieldNames.PROMOTION_WEIGHTING, 1)); + return out; + } + + @Override + public Promotion generateIdIfAbsentFromDocument(Promotion document) { + if (!documentHasId(document)) { + document.setId(UUID.randomUUID().toString()); + } + return document; + } + + @Override + public boolean documentHasId(Promotion document) { + return !StringUtils.isBlank(document.getId()); + } + + @Override + public BsonValue getDocumentId(Promotion document) { + return new BsonString(document.getId()); + } + +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java index e2aaae11e759b477c0182a58d85366be0b16323a..4c15814fee2cae9e232e0ed9fc20c61e20b6bdcd 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java @@ -6,51 +6,21 @@ */ package org.eclipsefoundation.marketplace.dto.filter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - import javax.enterprise.context.ApplicationScoped; -import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.Catalog; -import org.eclipsefoundation.marketplace.model.RequestWrapper; -import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; -import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; - -import com.mongodb.client.model.Filters; /** - * Filter implementation for the Catalog class. + * Filter implementation for the {@link Catalog} class. * * @author Martin Lowe + * */ @ApplicationScoped public class CatalogFilter implements DtoFilter<Catalog> { - @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { - List<Bson> filters = new ArrayList<>(); - // perform following checks only if there is no doc root - if (root == null) { - // ID check - Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID); - if (id.isPresent()) { - filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get())); - } - } - return filters; - } - - @Override - public List<Bson> getAggregates(RequestWrapper wrap) { - return Collections.emptyList(); - } - @Override public Class<Catalog> getType() { return Catalog.class; } - } diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java index 3092ecf53f48b7dbeaf00bbf31eb386a8e859034..7f7bfd3614b92aef175f0bf14c4eaf85cb72d563 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java @@ -6,47 +6,20 @@ */ package org.eclipsefoundation.marketplace.dto.filter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - import javax.enterprise.context.ApplicationScoped; -import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.Category; -import org.eclipsefoundation.marketplace.model.RequestWrapper; -import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; -import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; -import com.mongodb.client.model.Filters; /** - * @author martin - * + * Filter implementation for the {@link Category} class. + * + * @author Martin Lowe + * */ @ApplicationScoped public class CategoryFilter implements DtoFilter<Category> { - @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { - List<Bson> filters = new ArrayList<>(); - // perform following checks only if there is no doc root - if (root == null) { - // ID check - Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID); - if (id.isPresent()) { - filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get())); - } - } - return filters; - } - - @Override - public List<Bson> getAggregates(RequestWrapper wrap) { - return Collections.emptyList(); - } - @Override public Class<Category> getType() { return Category.class; diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java index bb5d8e05a9cfa228241336aca7604afb710847f9..d4cedace6e3e6d1fe0282604708d03ed3ce203e7 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java @@ -6,10 +6,15 @@ */ package org.eclipsefoundation.marketplace.dto.filter; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Optional; import org.bson.conversions.Bson; -import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.QueryParameters; +import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; +import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.Filters; @@ -24,21 +29,35 @@ public interface DtoFilter<T> { /** * Retrieve filter objects for the current arguments. * - * @param wrap wrapper for the current request + * @param params parameters to use in filter construction * @param nestedPath current path for nesting of filters * @return list of filters for the current request, or empty if there are no * applicable filters. */ - List<Bson> getFilters(RequestWrapper wrap, String nestedPath); + default List<Bson> getFilters(QueryParameters params, String root) { + List<Bson> filters = new ArrayList<>(); + // perform following checks only if there is no doc root + if (root == null) { + // ID check + Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName()); + if (id.isPresent()) { + filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get())); + } + } + return filters; + + } /** * Retrieve aggregate filter operations for the current arguments. * - * @param wrap wrapper for the current request + * @param params parameters to use in aggregate construction * @return list of aggregates for the current request, or empty if there are no * applicable aggregates. */ - List<Bson> getAggregates(RequestWrapper wrap); + default List<Bson> getAggregates(QueryParameters params) { + return Collections.emptyList(); + } /** * Returns the type of data this object will filter for. @@ -52,19 +71,19 @@ public interface DtoFilter<T> { * match operation to port filter operations into an aggregate pipeline. This is * handy when importing nested types and enabling filters. * - * @param wrap wrapper for the current request + * @param params parameters for the current call * @param nestedPath current path for nesting of filters * @return a list of aggregate pipeline operations representing the filters for * the current request. */ - default Bson wrapFiltersToAggregate(RequestWrapper wrap, String nestedPath) { - List<Bson> filters = getFilters(wrap, nestedPath); + default Bson wrapFiltersToAggregate(QueryParameters params, String nestedPath) { + List<Bson> filters = getFilters(params, nestedPath); if (!filters.isEmpty()) { return Aggregates.match(Filters.elemMatch(nestedPath, Filters.and(filters))); } return null; } - + /** * Whether this type of data should be restrained to a limited set, or return * all data that is found. diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ErrorReportFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ErrorReportFilter.java index 3f1ff92874463f8a42cb744b58941091db114cad..6f5f4618ef2012d09b87d82740dd21cdacbafb83 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ErrorReportFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ErrorReportFilter.java @@ -15,7 +15,7 @@ import javax.enterprise.context.ApplicationScoped; import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.ErrorReport; -import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.QueryParameters; import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; @@ -30,40 +30,40 @@ import com.mongodb.client.model.Filters; public class ErrorReportFilter implements DtoFilter<ErrorReport> { @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { + public List<Bson> getFilters(QueryParameters params, String root) { List<Bson> filters = new ArrayList<>(); // ErrorReport ID check - Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID); + Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName()); if (id.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get())); } // select by multiple IDs - List<String> ids = wrap.getParams(UrlParameterNames.IDS); + List<String> ids = params.getValues(UrlParameterNames.IDS.getParameterName()); if (!ids.isEmpty()) { filters.add(Filters.in(DatabaseFieldNames.DOCID, ids)); } // listing ID check - Optional<String> listingId = wrap.getFirstParam(UrlParameterNames.LISTING_ID); + Optional<String> listingId = params.getFirstIfPresent(UrlParameterNames.LISTING_ID.getParameterName()); if (listingId.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.LISTING_ID, listingId.get())); } // listing ID check - Optional<String> isRead = wrap.getFirstParam(UrlParameterNames.READ); + Optional<String> isRead = params.getFirstIfPresent(UrlParameterNames.READ.getParameterName()); if (isRead.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.ERROR_READ, Boolean.valueOf(isRead.get()))); } // select by feature ID - List<String> featureId = wrap.getParams(UrlParameterNames.FEATURE_ID); + List<String> featureId = params.getValues(UrlParameterNames.FEATURE_ID.getParameterName()); if (!featureId.isEmpty()) { filters.add(Filters.in(DatabaseFieldNames.ERROR_FEATURE_IDS, featureId)); } // text search - Optional<String> text = wrap.getFirstParam(UrlParameterNames.QUERY_STRING); + Optional<String> text = params.getFirstIfPresent(UrlParameterNames.QUERY_STRING.getParameterName()); if (text.isPresent()) { filters.add(Filters.text(text.get())); } @@ -71,7 +71,7 @@ public class ErrorReportFilter implements DtoFilter<ErrorReport> { } @Override - public List<Bson> getAggregates(RequestWrapper wrap) { + public List<Bson> getAggregates(QueryParameters params) { return Collections.emptyList(); } diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java index e7c1e9954e439120f3f9405f3a688d45e9118160..2cd260db2b78ee2b7f85bccb56130b32221f09da 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java @@ -17,7 +17,7 @@ import javax.enterprise.context.ApplicationScoped; import org.apache.commons.lang3.StringUtils; import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.Install; -import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.QueryParameters; import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; @@ -32,39 +32,39 @@ import com.mongodb.client.model.Filters; public class InstallFilter implements DtoFilter<Install> { @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { + public List<Bson> getFilters(QueryParameters params, String root) { List<Bson> filters = new ArrayList<>(); // perform following checks only if there is no doc root if (root == null) { // ID check - Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID); + Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName()); if (id.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.LISTING_ID, id.get())); } } // version check - Optional<String> version = wrap.getFirstParam(UrlParameterNames.VERSION); + Optional<String> version = params.getFirstIfPresent(UrlParameterNames.VERSION.getParameterName()); if (version.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.INSTALL_VERSION, version.get())); } // OS filter - Optional<String> os = wrap.getFirstParam(UrlParameterNames.OS); + Optional<String> os = params.getFirstIfPresent(UrlParameterNames.OS.getParameterName()); if (os.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.OS, os.get())); } // eclipse version - Optional<String> eclipseVersion = wrap.getFirstParam(UrlParameterNames.ECLIPSE_VERSION); + Optional<String> eclipseVersion = params.getFirstIfPresent(UrlParameterNames.ECLIPSE_VERSION.getParameterName()); if (eclipseVersion.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.ECLIPSE_VERSION, eclipseVersion.get())); } // TODO this sorts by naturally by character rather than by actual number (e.g. // 1.9 is technically greater than 1.10) // solution version - Java version - Optional<String> javaVersion = wrap.getFirstParam(UrlParameterNames.JAVA_VERSION); + Optional<String> javaVersion = params.getFirstIfPresent(UrlParameterNames.JAVA_VERSION.getParameterName()); if (javaVersion.isPresent()) { filters.add(Filters.gte(DatabaseFieldNames.INSTALL_JAVA_VERSION, javaVersion.get())); } - Optional<String> date = wrap.getFirstParam(UrlParameterNames.DATE_FROM); + Optional<String> date = params.getFirstIfPresent(UrlParameterNames.DATE_FROM.getParameterName()); if (date.isPresent() && StringUtils.isNumeric(date.get())) { filters.add(Filters.gte(DatabaseFieldNames.INSTALL_DATE, new Date(Integer.valueOf(date.get())))); } @@ -72,7 +72,7 @@ public class InstallFilter implements DtoFilter<Install> { } @Override - public List<Bson> getAggregates(RequestWrapper wrap) { + public List<Bson> getAggregates(QueryParameters params) { return Collections.emptyList(); } diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricsFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricsFilter.java index 38e75c081cbd92a0b6feed2be1c50b6bff033e56..3d8474901b063d9625417fc1982b9017739ad1ca 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricsFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricsFilter.java @@ -6,20 +6,9 @@ */ package org.eclipsefoundation.marketplace.dto.filter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - import javax.enterprise.context.ApplicationScoped; -import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.InstallMetrics; -import org.eclipsefoundation.marketplace.model.RequestWrapper; -import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; -import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; - -import com.mongodb.client.model.Filters; /** * Filter implementation for the {@linkplain InstallMetrics} class. @@ -30,25 +19,6 @@ import com.mongodb.client.model.Filters; @ApplicationScoped public class InstallMetricsFilter implements DtoFilter<InstallMetrics> { - @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { - List<Bson> filters = new ArrayList<>(); - // perform following checks only if there is no doc root - if (root == null) { - // ID check - Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID); - if (id.isPresent()) { - filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get())); - } - } - return filters; - } - - @Override - public List<Bson> getAggregates(RequestWrapper wrap) { - return Collections.emptyList(); - } - @Override public Class<InstallMetrics> getType() { return InstallMetrics.class; diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java index 9aadaa4e0d7ff710be10a06ef3468bf0c22b7ac1..2687c7933e1b5fc7266820c80a323c658b6be1f6 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java @@ -16,7 +16,7 @@ import javax.inject.Inject; import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.Listing; import org.eclipsefoundation.marketplace.dto.ListingVersion; -import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.QueryParameters; import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; import org.eclipsefoundation.marketplace.namespace.DtoTableNames; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; @@ -39,37 +39,37 @@ public class ListingFilter implements DtoFilter<Listing> { DtoFilter<ListingVersion> listingVersionFilter; @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { + public List<Bson> getFilters(QueryParameters params, String root) { List<Bson> filters = new ArrayList<>(); // perform following checks only if there is no doc root if (root == null) { // ID check - Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID); + Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName()); if (id.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get())); } } // select by multiple IDs - List<String> ids = wrap.getParams(UrlParameterNames.IDS); + List<String> ids = params.getValues(UrlParameterNames.IDS.getParameterName()); if (!ids.isEmpty()) { filters.add(Filters.in(DatabaseFieldNames.DOCID, ids)); } // Listing license type check - Optional<String> licType = wrap.getFirstParam(DatabaseFieldNames.LICENSE_TYPE); + Optional<String> licType = params.getFirstIfPresent(DatabaseFieldNames.LICENSE_TYPE); if (licType.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.LICENSE_TYPE, licType.get())); } // select by multiple tags - List<String> tags = wrap.getParams(UrlParameterNames.TAGS); + List<String> tags = params.getValues(UrlParameterNames.TAGS.getParameterName()); if (!tags.isEmpty()) { filters.add(Filters.in(DatabaseFieldNames.LISTING_TAGS + ".title", tags)); } // text search - Optional<String> text = wrap.getFirstParam(UrlParameterNames.QUERY_STRING); + Optional<String> text = params.getFirstIfPresent(UrlParameterNames.QUERY_STRING.getParameterName()); if (text.isPresent()) { filters.add(Filters.text(text.get())); } @@ -77,18 +77,18 @@ public class ListingFilter implements DtoFilter<Listing> { } @Override - public List<Bson> getAggregates(RequestWrapper wrap) { + public List<Bson> getAggregates(QueryParameters params) { List<Bson> aggs = new ArrayList<>(); // adds a $lookup aggregate, joining categories on categoryIDS as "categories" aggs.add(Aggregates.lookup(DtoTableNames.LISTING_VERSION.getTableName(), DatabaseFieldNames.DOCID, DatabaseFieldNames.LISTING_ID, DatabaseFieldNames.LISTING_VERSIONS)); - Bson filters = listingVersionFilter.wrapFiltersToAggregate(wrap, DatabaseFieldNames.LISTING_VERSIONS); + Bson filters = listingVersionFilter.wrapFiltersToAggregate(params, DatabaseFieldNames.LISTING_VERSIONS); if (filters != null) { aggs.add(filters); } aggs.add(Aggregates.lookup(DtoTableNames.CATEGORY.getTableName(), DatabaseFieldNames.CATEGORY_IDS, DatabaseFieldNames.DOCID, DatabaseFieldNames.LISTING_CATEGORIES)); - List<String> marketIds = wrap.getParams(UrlParameterNames.MARKET_IDS); + List<String> marketIds = params.getValues(UrlParameterNames.MARKET_IDS.getParameterName()); if (!marketIds.isEmpty()) { aggs.add(Aggregates.match(Filters.in("categories.market_ids", marketIds))); } diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java index ddc414b1ea60595807964bb56ee204594740389e..4334ca984077adafcdbcd33e372e5a3b6d417a01 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java @@ -16,7 +16,7 @@ import javax.enterprise.context.ApplicationScoped; import org.apache.commons.lang3.StringUtils; import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.ListingVersion; -import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.QueryParameters; import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; @@ -32,29 +32,29 @@ import com.mongodb.client.model.Filters; public class ListingVersionFilter implements DtoFilter<ListingVersion> { @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { + public List<Bson> getFilters(QueryParameters params, String root) { List<Bson> filters = new ArrayList<>(); // perform following checks only if there is no doc root if (root == null) { // ID check - Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID); + Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName()); if (id.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get())); } } // solution version - OS filter - Optional<String> os = wrap.getFirstParam(UrlParameterNames.OS); + Optional<String> os = params.getFirstIfPresent(UrlParameterNames.OS.getParameterName()); if (os.isPresent()) { filters.add(Filters.eq("platforms", os.get())); } // solution version - eclipse version - Optional<String> eclipseVersion = wrap.getFirstParam(UrlParameterNames.ECLIPSE_VERSION); + Optional<String> eclipseVersion = params.getFirstIfPresent(UrlParameterNames.ECLIPSE_VERSION.getParameterName()); if (eclipseVersion.isPresent()) { filters.add(Filters.eq("compatible_versions", eclipseVersion.get())); } // solution version - Java version - Optional<String> javaVersion = wrap.getFirstParam(UrlParameterNames.JAVA_VERSION); + Optional<String> javaVersion = params.getFirstIfPresent(UrlParameterNames.JAVA_VERSION.getParameterName()); if (javaVersion.isPresent() && StringUtils.isNumeric(javaVersion.get())) { filters.add(Filters.gte("min_java_version", Integer.valueOf(javaVersion.get()))); } @@ -63,7 +63,7 @@ public class ListingVersionFilter implements DtoFilter<ListingVersion> { } @Override - public List<Bson> getAggregates(RequestWrapper wrap) { + public List<Bson> getAggregates(QueryParameters params) { return Collections.emptyList(); } diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java index 664333f0dd70f7c7fac09f7fad41507b1ed15461..f0dc7b5bff5595fe0b93fbea98750f16f43f496c 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java @@ -20,7 +20,7 @@ import javax.enterprise.context.ApplicationScoped; import org.bson.BsonArray; import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.Market; -import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.QueryParameters; import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; import org.eclipsefoundation.marketplace.namespace.DtoTableNames; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; @@ -40,12 +40,12 @@ import com.mongodb.client.model.Variable; public class MarketFilter implements DtoFilter<Market> { @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { + public List<Bson> getFilters(QueryParameters params, String root) { List<Bson> filters = new ArrayList<>(); // perform following checks only if there is no doc root if (root == null) { // ID check - Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID); + Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName()); if (id.isPresent()) { filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get())); } @@ -54,7 +54,7 @@ public class MarketFilter implements DtoFilter<Market> { } @Override - public List<Bson> getAggregates(RequestWrapper wrap) { + public List<Bson> getAggregates(QueryParameters params) { List<Bson> aggs = new ArrayList<>(); String tempFieldName = "tmp"; diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java index 1f6b9bbc8e7f1f3911eb7d58318a5eecf3b1547a..59bf384535084921c92503597b5229da6c97dbe3 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java @@ -19,7 +19,7 @@ import org.bson.BsonDocument; import org.bson.BsonString; import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.MetricPeriod; -import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.QueryParameters; import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; @@ -38,15 +38,15 @@ import com.mongodb.client.model.Projections; public class MetricPeriodFilter implements DtoFilter<MetricPeriod> { @Override - public List<Bson> getFilters(RequestWrapper wrap, String root) { + public List<Bson> getFilters(QueryParameters params, String root) { return Collections.emptyList(); } @Override - public List<Bson> getAggregates(RequestWrapper wrap) { + public List<Bson> getAggregates(QueryParameters params) { // check that we have required fields first - Optional<String> startDate = wrap.getFirstParam(UrlParameterNames.START); - Optional<String> endDate = wrap.getFirstParam(UrlParameterNames.END); + Optional<String> startDate = params.getFirstIfPresent(UrlParameterNames.START.getParameterName()); + Optional<String> endDate = params.getFirstIfPresent(UrlParameterNames.END.getParameterName()); List<Bson> aggregates = new ArrayList<>(); if (startDate.isPresent() && endDate.isPresent()) { // check for all listings that are after the start date diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/PromotionFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/PromotionFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..d7852402e29dca1aca9cb4663d834897e3783066 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/PromotionFilter.java @@ -0,0 +1,27 @@ +/* Copyright (c) 2019 Eclipse Foundation and others. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License 2.0 + * which is available at http://www.eclipse.org/legal/epl-v20.html, + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.marketplace.dto.filter; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipsefoundation.marketplace.dto.Promotion; + + +/** + * Filter implementation for the {@link Promotion} class. + * + * @author Martin Lowe + * + */ +@ApplicationScoped +public class PromotionFilter implements DtoFilter<Promotion> { + + @Override + public Class<Promotion> getType() { + return Promotion.class; + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/PromotionCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/PromotionCodecProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..80f26042991aab079f009dbe17db60b14bd27c95 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/PromotionCodecProvider.java @@ -0,0 +1,35 @@ +/* Copyright (c) 2019 Eclipse Foundation and others. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License 2.0 + * which is available at http://www.eclipse.org/legal/epl-v20.html, + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.marketplace.dto.providers; + +import org.bson.codecs.Codec; +import org.bson.codecs.configuration.CodecProvider; +import org.bson.codecs.configuration.CodecRegistry; +import org.eclipsefoundation.marketplace.dto.Promotion; +import org.eclipsefoundation.marketplace.dto.codecs.PromotionCodec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides the {@link PromotionCodec} to MongoDB for conversions of + * {@link Promotion} objects. + * + * @author Martin Lowe + */ +public class PromotionCodecProvider implements CodecProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(PromotionCodecProvider.class); + + @SuppressWarnings("unchecked") + @Override + public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) { + if (clazz == Promotion.class) { + LOGGER.debug("Registering custom Promotion class MongoDB codec"); + return (Codec<T>) new PromotionCodec(); + } + return null; + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java b/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java index 547f3ba149d5bc60da6cddcd4925d913bb509449..033f3907626238a07e3b504da8b109ce7bc2e0c7 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java +++ b/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java @@ -7,7 +7,9 @@ package org.eclipsefoundation.marketplace.model; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.StringUtils; @@ -26,15 +28,14 @@ import com.mongodb.client.model.Filters; /** * Wrapper for initializing MongoDB BSON filters, sort clauses, and document - * type when interacting with MongoDB. This should only be called from within - * the scope of a request with a defined {@link ResourceDataType} + * type when interacting with MongoDB. * * @author Martin Lowe */ public class MongoQuery<T> { private static final Logger LOGGER = LoggerFactory.getLogger(MongoQuery.class); - private RequestWrapper wrapper; + private QueryParameters params; private DtoFilter<T> dtoFilter; private Bson filter; @@ -43,9 +44,14 @@ public class MongoQuery<T> { private List<Bson> aggregates; public MongoQuery(RequestWrapper wrapper, DtoFilter<T> dtoFilter) { - this.wrapper = wrapper; + this(wrapper, Collections.emptyMap(), dtoFilter); + } + + public MongoQuery(RequestWrapper wrapper, Map<String, List<String>> params, DtoFilter<T> dtoFilter) { this.dtoFilter = dtoFilter; this.aggregates = new ArrayList<>(); + // allow for parameters to be either explicitly set or use wrapper params + this.params = new QueryParameters(wrapper == null ? params : wrapper.asMap()); init(); } @@ -54,7 +60,7 @@ public class MongoQuery<T> { * type object. This can be called again to reset the parameters if needed due * to updated fields. */ - public void init() { + private void init() { // clear old values if set to default this.filter = null; this.sort = null; @@ -63,10 +69,10 @@ public class MongoQuery<T> { // get the filters for the current DTO List<Bson> filters = new ArrayList<>(); - filters.addAll(dtoFilter.getFilters(wrapper, null)); + filters.addAll(dtoFilter.getFilters(params, null)); // get fields that make up the required fields to enable pagination and check - Optional<String> sortOpt = wrapper.getFirstParam(UrlParameterNames.SORT); + Optional<String> sortOpt = params.getFirstIfPresent(UrlParameterNames.SORT.getParameterName()); if (sortOpt.isPresent()) { String sortVal = sortOpt.get(); SortOrder ord = SortOrder.getOrderFromValue(sortOpt.get()); @@ -76,13 +82,14 @@ public class MongoQuery<T> { if (SortOrder.RANDOM.equals(ord)) { this.order = SortOrder.RANDOM; } else if (ord != SortOrder.NONE) { - setSort(sortVal.substring(0, idx), sortVal.substring(idx + 1), filters); + setSort(sortVal.substring(0, idx), sortVal.substring(idx + 1)); } } + if (!filters.isEmpty()) { this.filter = Filters.and(filters); } - this.aggregates = dtoFilter.getAggregates(wrapper); + this.aggregates = dtoFilter.getAggregates(params); if (LOGGER.isDebugEnabled()) { LOGGER.debug("MongoDB query initialized with filter: {}", this.filter); @@ -106,20 +113,21 @@ public class MongoQuery<T> { out.add(Aggregates.match(filter)); } // add base aggregates (joins) - out.addAll(aggregates); + out.addAll(aggregates); if (sort != null) { out.add(Aggregates.sort(sort)); } // check if the page param has been set, defaulting to the first page if not set - Optional<String> pageOpt = wrapper.getFirstParam(UrlParameterNames.PAGE); int page = 1; + Optional<String> pageOpt = params.getFirstIfPresent(UrlParameterNames.PAGE.getParameterName()); if (pageOpt.isPresent() && StringUtils.isNumeric(pageOpt.get())) { int tmpPage = Integer.parseInt(pageOpt.get()); if (tmpPage > 0) { page = tmpPage; - LOGGER.debug("Found a set page of {} for current query",page); + LOGGER.debug("Found a set page of {} for current query", page); } } + out.add(Aggregates.skip((page - 1) * limit)); // add sample if we aren't sorting if ((sort == null || SortOrder.RANDOM.equals(order)) && dtoFilter.useLimit()) { @@ -136,14 +144,14 @@ public class MongoQuery<T> { * present and numeric, otherwise returns -1. */ public int getLimit() { - Optional<String> limitVal = wrapper.getFirstParam(UrlParameterNames.LIMIT); + Optional<String> limitVal = params.getFirstIfPresent(UrlParameterNames.LIMIT.getParameterName()); if (limitVal.isPresent() && StringUtils.isNumeric(limitVal.get())) { return Integer.parseInt(limitVal.get()); } return -1; } - private void setSort(String sortField, String sortOrder, List<Bson> filters) { + private void setSort(String sortField, String sortOrder) { List<Sortable<?>> fields = SortableHelper.getSortableFields(getDocType()); Optional<Sortable<?>> fieldContainer = SortableHelper.getSortableFieldByName(fields, sortField); if (fieldContainer.isPresent()) { @@ -186,20 +194,6 @@ public class MongoQuery<T> { return dtoFilter.getType(); } - /** - * @return the wrapper - */ - public RequestWrapper getWrapper() { - return wrapper; - } - - /** - * @param wrapper the wrapper to set - */ - public void setWrapper(RequestWrapper wrapper) { - this.wrapper = wrapper; - } - @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/QueryParameters.java b/src/main/java/org/eclipsefoundation/marketplace/model/QueryParameters.java new file mode 100644 index 0000000000000000000000000000000000000000..d91173a6ad63cac4e777455eae587bf5c76c9699 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/model/QueryParameters.java @@ -0,0 +1,73 @@ +package org.eclipsefoundation.marketplace.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Container for query parameters, using a map allowing for multiple values set + * to a single key. + * + * @author Martin Lowe + * + */ +public class QueryParameters { + private final Map<String, List<String>> parameters; + + /** + * Generates an empty internal parameter map + */ + public QueryParameters() { + this(Collections.emptyMap()); + } + + /** + * Creates a copy of the passed parameter map. + * + * @param parameters map of parameter keys and values to use. + */ + public QueryParameters(Map<String, List<String>> parameters) { + this.parameters = new HashMap<>(parameters); + } + + /** + * Returns a copy of the values available for the given key. + * + * @param key string key to retrieve values for + * @return list of values if set, or an empty list. + */ + public List<String> getValues(String key) { + return parameters.getOrDefault(key, Collections.emptyList()); + } + + /** + * Helper for map to retrieve first value available if one has been set for + * params. + * + * @param key key to retrieve first value for + * @param params parameter map for query to retrieve value from + * @return value wrapped in optional if present, otherwise empty optional. + */ + public Optional<String> getFirstIfPresent(String key) { + List<String> vals = parameters.get(key); + if (vals != null && !vals.isEmpty()) { + return Optional.ofNullable(vals.get(0)); + } + return Optional.empty(); + } + + public void add(String key, String value) { + this.parameters.computeIfAbsent(key, k -> new ArrayList<>()).add(value); + } + + public void remove(String key) { + this.parameters.remove(key); + } + + public Map<String, List<String>> asMap() { + return new HashMap<>(parameters); + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java b/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java index 43fc3469c2b916fc78933073cc350bddcf709a25..0fd4ce782a86eeda26aa84398d242cd9f4749f92 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java +++ b/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java @@ -6,7 +6,6 @@ */ package org.eclipsefoundation.marketplace.model; -import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -14,6 +13,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import javax.enterprise.context.RequestScoped; import javax.servlet.http.HttpServletRequest; @@ -23,6 +23,7 @@ import javax.ws.rs.core.UriInfo; import org.apache.commons.lang3.StringUtils; import org.eclipsefoundation.marketplace.namespace.DeprecatedHeader; import org.eclipsefoundation.marketplace.namespace.RequestHeaderNames; +import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; import org.eclipsefoundation.marketplace.request.CacheBypassFilter; import org.jboss.resteasy.core.ResteasyContext; @@ -38,7 +39,7 @@ import org.jboss.resteasy.core.ResteasyContext; public class RequestWrapper { private static final String EMPTY_KEY_MESSAGE = "Key must not be null or blank"; - private Map<String, List<String>> params; + private QueryParameters params; private UriInfo uriInfo; private HttpServletRequest request; @@ -65,12 +66,12 @@ public class RequestWrapper { * @return the first value set in the parameter map for the given key, or null * if absent. */ - public Optional<String> getFirstParam(String key) { - if (StringUtils.isBlank(key)) { + public Optional<String> getFirstParam(UrlParameterNames parameter) { + if (parameter == null) { throw new IllegalArgumentException(EMPTY_KEY_MESSAGE); } - List<String> vals = getParams().get(key); + List<String> vals = getParams().getValues(parameter.getParameterName()); if (vals == null || vals.isEmpty()) { return Optional.empty(); } @@ -85,12 +86,12 @@ public class RequestWrapper { * @return the value list for the given key if it exists, or an empty collection * if none exists. */ - public List<String> getParams(String key) { - if (StringUtils.isBlank(key)) { + public List<String> getParams(UrlParameterNames parameter) { + if (parameter == null) { throw new IllegalArgumentException(EMPTY_KEY_MESSAGE); } - List<String> vals = getParams().get(key); + List<String> vals = getParams().getValues(parameter.getParameterName()); if (vals == null || vals.isEmpty()) { return Collections.emptyList(); } @@ -101,23 +102,23 @@ public class RequestWrapper { * Adds the given value for the given key, preserving previous values if they * exist. * - * @param key string key to add the value to, must not be null - * @param value the value to add to the key + * @param key string key to add the value to, must not be null + * @param value the value to add to the key */ public void addParam(String key, String value) { if (StringUtils.isBlank(key)) { throw new IllegalArgumentException(EMPTY_KEY_MESSAGE); } Objects.requireNonNull(value); - getParams().computeIfAbsent(key, k -> new ArrayList<>()).add(value); + getParams().add(key, value); } /** * Sets the value as the value for the given key, removing previous values if * they exist. * - * @param key string key to add the value to, must not be null - * @param value the value to add to the key + * @param key string key to add the value to, must not be null + * @param value the value to add to the key */ public void setParam(String key, String value) { if (StringUtils.isBlank(key)) { @@ -129,6 +130,11 @@ public class RequestWrapper { addParam(key, value); } + public List<UrlParameterNames> getActiveParameters() { + return params.asMap().keySet().stream().map(UrlParameterNames::getByParameterName).filter(Objects::nonNull) + .collect(Collectors.toList()); + } + /** * Returns this QueryParams object as a Map of param values indexed by the param * name. @@ -136,15 +142,16 @@ public class RequestWrapper { * @return a copy of the internal param map */ public Map<String, List<String>> asMap() { - return new HashMap<>(getParams()); + return new HashMap<>(getParams().asMap()); } - private Map<String, List<String>> getParams() { + private QueryParameters getParams() { if (params == null) { - params = new HashMap<>(); + Map<String, List<String>> requestParams = new HashMap<>(); if (uriInfo != null) { - params.putAll(uriInfo.getQueryParameters()); + requestParams.putAll(uriInfo.getQueryParameters()); } + params = new QueryParameters(requestParams); } return this.params; } diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java index f3d5f575d09a909a509a13979c8389951f126c53..b6cb1af181a5c5cd5b453a81ae4fe47d7984304f 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java +++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java @@ -88,6 +88,8 @@ public final class DatabaseFieldNames { public static final String MONTH_OFFSET_PREFIX = "offset_"; public static final String LISTING_IDS = "listing_ids"; + public static final String PROMOTION_WEIGHTING = "weight"; + private DatabaseFieldNames() { } } diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java index a3d92ef242999a641a750c1f35fd8526f72119d7..c81d4b2eea8206ec03e44993220f93f33a4305c4 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java +++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java @@ -15,6 +15,7 @@ import org.eclipsefoundation.marketplace.dto.Listing; import org.eclipsefoundation.marketplace.dto.ListingVersion; import org.eclipsefoundation.marketplace.dto.Market; import org.eclipsefoundation.marketplace.dto.MetricPeriod; +import org.eclipsefoundation.marketplace.dto.Promotion; /** * Mapping of DTO classes to their respective tables in the DB. @@ -26,7 +27,7 @@ public enum DtoTableNames { LISTING(Listing.class, "listings"), CATEGORY(Category.class, "categories"), CATALOG(Catalog.class, "catalogs"), MARKET(Market.class, "markets"), ERRORREPORT(ErrorReport.class, "errorreports"), INSTALL(Install.class, "installs"), INSTALL_METRIC(InstallMetrics.class, "install_metrics"), METRIC_PERIOD(MetricPeriod.class, "installs"), - LISTING_VERSION(ListingVersion.class, "listing_versions"); + LISTING_VERSION(ListingVersion.class, "listing_versions"),PROMOTION(Promotion.class, "promotion"); private Class<?> baseClass; private String tableName; diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/MicroprofilePropertyNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/MicroprofilePropertyNames.java new file mode 100644 index 0000000000000000000000000000000000000000..1ae381e871779cb7439e35e6d012065b50123480 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/MicroprofilePropertyNames.java @@ -0,0 +1,21 @@ +package org.eclipsefoundation.marketplace.namespace; + +/** + * Contains Microprofile property names used by this application. + * + * @author Martin Lowe + * + */ +public class MicroprofilePropertyNames { + public static final String PROMO_WEIGHT_DEFAULT = "eclipse.promotion.weighting.default"; + public static final String PROMO_SERVE_COUNT = "eclipse.promotion.serve-count"; + public static final String CACHE_TTL_MAX_SECONDS = "cache.ttl.write.seconds"; + public static final String CACHE_SIZE_MAX = "cache.max.size"; + public static final String MONGODB_DB_NAME = "mongodb.database"; + public static final String MONGODB_RETURN_LIMIT = "mongodb.default.limit"; + public static final String MONGODB_RETURN_LIMIT_MAX = "mongodb.default.limit.max"; + public static final String MONGODB_MAINTENANCE_FLAG = "mongodb.maintenance"; + + private MicroprofilePropertyNames() { + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java index 94045917d1f52f458542dee941d48ba7b40f8bc6..cfd91e6a29da4ff30560af7bcdb727a2576dc697 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java +++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java @@ -14,27 +14,51 @@ package org.eclipsefoundation.marketplace.namespace; * * @author Martin Lowe */ -public final class UrlParameterNames { +public enum UrlParameterNames { - public static final String QUERY_STRING = "q"; - public static final String PAGE = "page"; - public static final String LIMIT = "limit"; - public static final String SORT = "sort"; - public static final String OS = "os"; - public static final String ECLIPSE_VERSION = "eclipse_version"; - public static final String JAVA_VERSION = "min_java_version"; - public static final String IDS = "ids"; - public static final String TAGS = "tags"; - public static final String MARKET_IDS = "market_ids"; - public static final String ID = "id"; - public static final String LISTING_ID = "listing_id"; - public static final String READ = "read"; - public static final String FEATURE_ID = "feature_id"; - public static final String VERSION = "version"; - public static final String DATE_FROM = "from"; - public static final String END = "end"; - public static final String START = "start"; + QUERY_STRING("q"), + PAGE("page"), + LIMIT("limit"), + SORT("sort"), + OS("os"), + ECLIPSE_VERSION("eclipse_version"), + JAVA_VERSION("min_java_version"), + IDS("ids"), + TAGS("tags"), + MARKET_IDS("market_ids"), + ID("id"), + LISTING_ID("listing_id"), + READ("read"), + FEATURE_ID("feature_id"), + VERSION("version"), + DATE_FROM("from"), + END("end"), + START("start"); + + private String parameterName; + private UrlParameterNames(String parameterName) { + this.parameterName = parameterName; + } + + /** + * @return the URL parameters name + */ + public String getParameterName() { + return parameterName; + } - private UrlParameterNames() { + /** + * Retrieves the UrlParameterName for the given name. + * + * @param name the name to retrieve a URL parameter for + * @return the URL parameter name if it exists, or null if no match is found + */ + public static UrlParameterNames getByParameterName(String name) { + for (UrlParameterNames param: values()) { + if (param.getParameterName().equalsIgnoreCase(name)) { + return param; + } + } + return null; } } diff --git a/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java b/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java index 636c40b5e8cdd6fb6cd35a5cd8142c799f1651e3..7a251fbbaca65da10e787ee29c590fd19c20aef2 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java @@ -39,7 +39,7 @@ public class CacheBypassFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) throws IOException { // check for random sort order, which always bypasses cache - String[] sortVals = request.getParameterValues(UrlParameterNames.SORT); + String[] sortVals = request.getParameterValues(UrlParameterNames.SORT.getParameterName()); if (sortVals != null) { for (String sortVal : sortVals) { // check if the sort order for request matches RANDOM diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java index 03454b235b75625765b18510c3eeedfdc396cd36..87aa67bfd980ce0d5333638a7b7ec6f6db5a29c2 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java @@ -68,7 +68,7 @@ public class CatalogResource { MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter); // retrieve the possible cached object Optional<List<Catalog>> cachedResults = cachingService.get("all", params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached Catalogs"); return Response.serverError().build(); @@ -88,7 +88,7 @@ public class CatalogResource { @RolesAllowed({ "marketplace_catalog_put", "marketplace_admin_access" }) public Response putCatalog(Catalog catalog) { if (catalog.getId() != null) { - params.addParam(UrlParameterNames.ID, catalog.getId()); + params.addParam(UrlParameterNames.ID.getParameterName(), catalog.getId()); } MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter); // add the object, and await the result @@ -108,12 +108,12 @@ public class CatalogResource { @GET @Path("/{catalogId}") public Response select(@PathParam("catalogId") String catalogId) { - params.addParam(UrlParameterNames.ID, catalogId); + params.addParam(UrlParameterNames.ID.getParameterName(), catalogId); MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter); // retrieve a cached version of the value for the current listing Optional<List<Catalog>> cachedResults = cachingService.get(catalogId, params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", catalogId); return Response.serverError().build(); @@ -134,7 +134,7 @@ public class CatalogResource { @RolesAllowed({ "marketplace_catalog_delete", "marketplace_admin_access" }) @Path("/{catalogId}") public Response delete(@PathParam("catalogId") String catalogId) { - params.addParam(UrlParameterNames.ID, catalogId); + params.addParam(UrlParameterNames.ID.getParameterName(), catalogId); MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter); // delete the currently selected asset diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java index 416acc1e38b4f066295e0a70e3b6607f0aa550e8..f671ea1ba141e4ae536d637de9842ccb5c686e8f 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java @@ -68,7 +68,7 @@ public class CategoryResource { MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter); // retrieve the possible cached object Optional<List<Category>> cachedResults = cachingService.get("all", params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached Categorys"); return Response.serverError().build(); @@ -88,7 +88,7 @@ public class CategoryResource { @RolesAllowed({"marketplace_category_put", "marketplace_admin_access"}) public Response putCategory(Category category) { if (category.getId() != null) { - params.addParam(UrlParameterNames.ID, category.getId()); + params.addParam(UrlParameterNames.ID.getParameterName(), category.getId()); } MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter); // add the object, and await the result @@ -108,12 +108,12 @@ public class CategoryResource { @GET @Path("/{categoryId}") public Response select(@PathParam("categoryId") String categoryId) { - params.addParam(UrlParameterNames.ID, categoryId); + params.addParam(UrlParameterNames.ID.getParameterName(), categoryId); MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter); // retrieve a cached version of the value for the current listing Optional<List<Category>> cachedResults = cachingService.get(categoryId, params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", categoryId); return Response.serverError().build(); @@ -134,7 +134,7 @@ public class CategoryResource { @RolesAllowed({ "marketplace_category_delete", "marketplace_admin_access" }) @Path("/{categoryId}") public Response delete(@PathParam("categoryId") String categoryId) { - params.addParam(UrlParameterNames.ID, categoryId); + params.addParam(UrlParameterNames.ID.getParameterName(), categoryId); MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter); // delete the currently selected asset diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java index 7d4c2f0ba330b387a0fd27118497357ef4cb949a..a596dddae908b35968f0b105ff7c2a5c12437e85 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java @@ -70,7 +70,7 @@ public class ErrorReportResource { MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter); // retrieve the possible cached object Optional<List<ErrorReport>> cachedResults = cachingService.get("all", params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached ErrorReports"); return Response.serverError().build(); @@ -91,7 +91,7 @@ public class ErrorReportResource { public Response putErrorReport(ErrorReport errorReport) { // attach ID if present for update if (errorReport.getId() != null) { - params.addParam(UrlParameterNames.ID, errorReport.getId()); + params.addParam(UrlParameterNames.ID.getParameterName(), errorReport.getId()); } MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter); @@ -113,12 +113,12 @@ public class ErrorReportResource { @PermitAll @Path("/{errorReportId}") public Response select(@PathParam("errorReportId") String errorReportId) { - params.addParam(UrlParameterNames.ID, errorReportId); + params.addParam(UrlParameterNames.ID.getParameterName(), errorReportId); MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter); // retrieve a cached version of the value for the current ErrorReport Optional<List<ErrorReport>> cachedResults = cachingService.get(errorReportId, params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached ErrorReport for ID {}", errorReportId); return Response.serverError().build(); diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java index c49d8023e4d807e7c499497fff24c77fba16d75a..19aa49c1af412909720c76ce8a198686e29c5135 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java @@ -92,10 +92,10 @@ public class InstallResource { @PermitAll @Path("/{listingId}") public Response selectInstallCount(@PathParam("listingId") String listingId) { - wrapper.addParam(UrlParameterNames.ID, listingId); + wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId); MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter); Optional<Long> cachedResults = countCache.get(listingId, wrapper, - () -> StreamHelper.awaitCompletionStage(dao.count(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.count(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached install metrics for ID {}", listingId); return Response.serverError().build(); @@ -118,11 +118,11 @@ public class InstallResource { @PermitAll @Path("/{listingId}/{version}") public Response selectInstallCount(@PathParam("listingId") String listingId, @PathParam("version") String version) { - wrapper.addParam(UrlParameterNames.ID, listingId); - wrapper.addParam(UrlParameterNames.VERSION, version); + wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId); + wrapper.addParam(UrlParameterNames.VERSION.getParameterName(), version); MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter); Optional<Long> cachedResults = countCache.get(getCompositeKey(listingId, version), wrapper, - () -> StreamHelper.awaitCompletionStage(dao.count(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.count(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", listingId); return Response.serverError().build(); @@ -143,10 +143,10 @@ public class InstallResource { @PermitAll @Path("/{listingId}/metrics") public Response selectInstallMetrics(@PathParam("listingId") String listingId) { - wrapper.addParam(UrlParameterNames.ID, listingId); + wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId); MongoQuery<InstallMetrics> q = new MongoQuery<>(wrapper, metricFilter); Optional<List<InstallMetrics>> cachedResults = installCache.get(listingId, wrapper, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached install metrics for ID {}", listingId); return Response.serverError().build(); @@ -240,8 +240,8 @@ public class InstallResource { String end = DateTimeHelper.toRFC3339(c.getTime()); c.add(Calendar.MONTH, -1); String start = DateTimeHelper.toRFC3339(c.getTime()); - wrapper.setParam(UrlParameterNames.END, end); - wrapper.setParam(UrlParameterNames.START, start); + wrapper.setParam(UrlParameterNames.END.getParameterName(), end); + wrapper.setParam(UrlParameterNames.START.getParameterName(), start); // create the query wrapper to pass to DB dao. No cache needed as this info // won't be cached diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java index fa5cd87ec89574c8113487214cd867c19348c3a8..1674ff47e03a47ec16691ade77e502539b7f3fb1 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java @@ -9,6 +9,7 @@ */ package org.eclipsefoundation.marketplace.resource; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -35,8 +36,10 @@ import org.eclipsefoundation.marketplace.helper.StreamHelper; import org.eclipsefoundation.marketplace.model.Error; import org.eclipsefoundation.marketplace.model.MongoQuery; import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.SortOrder; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; import org.eclipsefoundation.marketplace.service.CachingService; +import org.eclipsefoundation.marketplace.service.PromotionService; import org.jboss.resteasy.annotations.jaxrs.PathParam; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,16 +61,20 @@ public class ListingResource { @Inject MongoDao dao; @Inject + DtoFilter<Listing> dtoFilter; + @Inject CachingService<List<Listing>> cachingService; + @Inject - RequestWrapper params; + PromotionService promoService; + @Inject - DtoFilter<Listing> dtoFilter; + RequestWrapper params; @Inject ResponseHelper responseBuider; /** - * Endpoint for /listing/ to retrieve all listings from the database along with + * Endpoint for /listings/ to retrieve all listings from the database along with * the given query string parameters. * * @param listingId int version of the listing ID @@ -78,19 +85,36 @@ public class ListingResource { public Response select() { MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter); // retrieve the possible cached object - Optional<List<Listing>> cachedResults = cachingService.get("all", params, + Optional<List<Listing>> cachedResults = cachingService.get("all", params, null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listings"); return Response.serverError().build(); } + // make a copy to inject promotions and not affect cached copies + List<Listing> listings = new ArrayList<>(cachedResults.get()); + + // check if promotions should be injected + List<UrlParameterNames> active = params.getActiveParameters(); + Optional<String> pageOpt = params.getFirstParam(UrlParameterNames.PAGE); + Optional<String> sortOpt = params.getFirstParam(UrlParameterNames.SORT); + if (active.stream().anyMatch(p -> !UrlParameterNames.PAGE.equals(p) && !UrlParameterNames.SORT.equals(p))) { + LOGGER.debug("Not injecting promotions, only '{}' and '{}' are allowed. Passed: {}", + UrlParameterNames.PAGE.getParameterName(), UrlParameterNames.SORT.getParameterName(), active); + } else if (pageOpt.isPresent() && !pageOpt.get().equals("1")) { + LOGGER.debug("Not injecting promotions, promotions are only injected on the first page"); + } else if (sortOpt.isPresent() && !SortOrder.getOrderFromValue(sortOpt.get()).equals(SortOrder.RANDOM)) { + LOGGER.debug("Not injecting promotions, promotions are only injected in unsorted results"); + } else { + listings = promoService.retrievePromotions(params, listings); + } // return the results as a response - return responseBuider.build("all", params, cachedResults.get()); + return responseBuider.build("all", params, listings); } /** - * Endpoint for /listing/ to post a new listing to the persistence layer. + * Endpoint for /listings/ to post a new listing to the persistence layer. * * @param listing the listing object to insert into the database. * @return response for the browser @@ -99,7 +123,7 @@ public class ListingResource { @RolesAllowed({ "marketplace_listing_put", "marketplace_admin_access" }) public Response putListing(Listing listing) { if (listing.getId() != null) { - params.addParam(UrlParameterNames.ID, listing.getId()); + params.addParam(UrlParameterNames.ID.getParameterName(), listing.getId()); } MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter); @@ -111,7 +135,7 @@ public class ListingResource { } /** - * Endpoint for /listing/\<listingId\> to retrieve a specific listing from the + * Endpoint for /listings/\<listingId\> to retrieve a specific listing from the * database. * * @param listingId the listing ID @@ -121,11 +145,11 @@ public class ListingResource { @PermitAll @Path("/{listingId}") public Response select(@PathParam("listingId") String listingId) { - params.addParam(UrlParameterNames.ID, listingId); + params.addParam(UrlParameterNames.ID.getParameterName(), listingId); MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter); // retrieve a cached version of the value for the current listing - Optional<List<Listing>> cachedResults = cachingService.get(listingId, params, + Optional<List<Listing>> cachedResults = cachingService.get(listingId, params, null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", listingId); @@ -137,7 +161,7 @@ public class ListingResource { } /** - * Endpoint for /listing/\<listingId\> to delete a specific listing from the + * Endpoint for /listings/\<listingId\> to delete a specific listing from the * database. * * @param listingId the listing ID @@ -147,7 +171,7 @@ public class ListingResource { @RolesAllowed({ "marketplace_listing_delete", "marketplace_admin_access" }) @Path("/{listingId}") public Response delete(@PathParam("listingId") String listingId) { - params.addParam(UrlParameterNames.ID, listingId); + params.addParam(UrlParameterNames.ID.getParameterName(), listingId); MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter); // delete the currently selected asset DeleteResult result = StreamHelper.awaitCompletionStage(dao.delete(q)); diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java index 66f318ad967d8e13517d70f30c0d315a987997c2..1d312b8b373f794001f3d2a85342fce3e8a0f143 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java @@ -68,7 +68,7 @@ public class ListingVersionResource { MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter); // retrieve the possible cached object Optional<List<ListingVersion>> cachedResults = cachingService.get("all", params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached ListingVersions"); return Response.serverError().build(); @@ -85,9 +85,10 @@ public class ListingVersionResource { * @return response for the browser */ @PUT + @RolesAllowed({ "marketplace_version_put", "marketplace_admin_access" }) public Response putListingVersion(ListingVersion listingVersion) { if (listingVersion.getId() != null) { - params.addParam(UrlParameterNames.ID, listingVersion.getId()); + params.addParam(UrlParameterNames.ID.getParameterName(), listingVersion.getId()); } MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter); // add the object, and await the result @@ -107,12 +108,12 @@ public class ListingVersionResource { @GET @Path("/{listingVersionId}") public Response select(@PathParam("listingVersionId") String listingVersionId) { - params.addParam(UrlParameterNames.ID, listingVersionId); + params.addParam(UrlParameterNames.ID.getParameterName(), listingVersionId); MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter); // retrieve a cached version of the value for the current listing Optional<List<ListingVersion>> cachedResults = cachingService.get(listingVersionId, params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", listingVersionId); return Response.serverError().build(); @@ -133,7 +134,7 @@ public class ListingVersionResource { @RolesAllowed({ "marketplace_version_delete", "marketplace_admin_access" }) @Path("/{listingVersionId}") public Response delete(@PathParam("listingVersionId") String listingVersionId) { - params.addParam(UrlParameterNames.ID, listingVersionId); + params.addParam(UrlParameterNames.ID.getParameterName(), listingVersionId); MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter); // delete the currently selected asset diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java index aa0fd9593b5f838c366dc087bcaceb4e5f676b34..24110f5b82a53376bb047bf7ca93054c601acfca 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java @@ -68,7 +68,7 @@ public class MarketResource { MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter); // retrieve the possible cached object Optional<List<Market>> cachedResults = cachingService.get("all", params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached Categorys"); return Response.serverError().build(); @@ -88,7 +88,7 @@ public class MarketResource { @RolesAllowed({ "marketplace_market_put", "marketplace_admin_access" }) public Response putMarket(Market market) { if (market.getId() != null) { - params.addParam(UrlParameterNames.ID, market.getId()); + params.addParam(UrlParameterNames.ID.getParameterName(), market.getId()); } MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter); @@ -110,12 +110,12 @@ public class MarketResource { @PermitAll @Path("/{marketId}") public Response select(@PathParam("marketId") String marketId) { - params.addParam(UrlParameterNames.ID, marketId); + params.addParam(UrlParameterNames.ID.getParameterName(), marketId); MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter); // retrieve a cached version of the value for the current listing Optional<List<Market>> cachedResults = cachingService.get(marketId, params, - () -> StreamHelper.awaitCompletionStage(dao.get(q))); + null, () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", marketId); return Response.serverError().build(); @@ -133,9 +133,10 @@ public class MarketResource { * @return response for the browser */ @DELETE + @RolesAllowed({ "marketplace_market_delete", "marketplace_admin_access" }) @Path("/{marketId}") public Response delete(@PathParam("marketId") String marketId) { - params.addParam(UrlParameterNames.ID, marketId); + params.addParam(UrlParameterNames.ID.getParameterName(), marketId); MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter); // delete the currently selected asset diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/PromotionResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/PromotionResource.java new file mode 100644 index 0000000000000000000000000000000000000000..9cd8b03c25ce47f4402eaf5e777674917bda0f49 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/PromotionResource.java @@ -0,0 +1,152 @@ +package org.eclipsefoundation.marketplace.resource; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import javax.annotation.security.RolesAllowed; +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.eclipsefoundation.marketplace.dao.MongoDao; +import org.eclipsefoundation.marketplace.dto.Promotion; +import org.eclipsefoundation.marketplace.dto.filter.DtoFilter; +import org.eclipsefoundation.marketplace.helper.ResponseHelper; +import org.eclipsefoundation.marketplace.helper.StreamHelper; +import org.eclipsefoundation.marketplace.model.Error; +import org.eclipsefoundation.marketplace.model.MongoQuery; +import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; +import org.eclipsefoundation.marketplace.service.CachingService; +import org.jboss.resteasy.annotations.jaxrs.PathParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mongodb.client.result.DeleteResult; + +/** + * Resource for interacting with promotions within the API. + * + * @author Martin Lowe + * + */ +@Path("/promotions") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@RequestScoped +public class PromotionResource { + private static final Logger LOGGER = LoggerFactory.getLogger(PromotionResource.class); + + @Inject + MongoDao dao; + @Inject + DtoFilter<Promotion> dtoFilter; + @Inject + CachingService<List<Promotion>> cachingService; + + @Inject + RequestWrapper params; + @Inject + ResponseHelper responseBuider; + + /** + * Endpoint for /promotions/ to retrieve all promotions from the database along + * with the given query string parameters. + * + * @return response for the browser with requested data, or an error response + */ + @GET + @RolesAllowed({ "marketplace_promotion_get", "marketplace_admin_access" }) + public Response select() { + MongoQuery<Promotion> q = new MongoQuery<>(params, dtoFilter); + // retrieve the possible cached object + Optional<List<Promotion>> cachedResults = cachingService.get("all", params, Collections.emptyMap(), + () -> StreamHelper.awaitCompletionStage(dao.get(q))); + if (!cachedResults.isPresent()) { + LOGGER.error("Error while retrieving cached promotions"); + return Response.serverError().build(); + } + + // return the results as a response + return responseBuider.build("all", params, cachedResults.get()); + } + + /** + * Endpoint for /promotions/ to post a new promotion to the persistence layer. + * + * @param promotion the promotion object to insert into the database. + * @return response for the browser with requested data, or an error response + */ + @PUT + @RolesAllowed({ "marketplace_promotion_put", "marketplace_admin_access" }) + public Response putPromotion(Promotion promotion) { + if (promotion.getId() != null) { + params.addParam(UrlParameterNames.ID.getParameterName(), promotion.getId()); + } + MongoQuery<Promotion> q = new MongoQuery<>(params, null, dtoFilter); + // add the object, and await the result + StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(promotion))); + + // return the results as a response + return Response.ok().build(); + } + + /** + * Endpoint for /promotions/\<promotionId\> to retrieve a specific promotion + * from the database. + * + * @param promotionId the promotion ID + * @return response for the browser with requested data, or an error response + */ + @GET + @RolesAllowed({ "marketplace_promotion_get", "marketplace_admin_access" }) + @Path("/{promotionId}") + public Response select(@PathParam("promotionId") String promotionId) { + params.addParam(UrlParameterNames.ID.getParameterName(), promotionId); + + MongoQuery<Promotion> q = new MongoQuery<>(params, null, dtoFilter); + // retrieve a cached version of the value for the current listing + Optional<List<Promotion>> cachedResults = cachingService.get(promotionId, params, Collections.emptyMap(), + () -> StreamHelper.awaitCompletionStage(dao.get(q))); + if (!cachedResults.isPresent()) { + LOGGER.error("Error while retrieving cached listing for ID {}", promotionId); + return Response.serverError().build(); + } + + // return the results as a response + return responseBuider.build(promotionId, params, cachedResults.get()); + } + + /** + * Endpoint for /promotions/\<promotionId\> to retrieve a specific promotion + * from the database. + * + * @param promotionId the promotion ID + * @return response for the browser with requested data, or an error response + */ + @DELETE + @RolesAllowed({ "marketplace_promotion_delete", "marketplace_admin_access" }) + @Path("/{promotionId}") + public Response delete(@PathParam("promotionId") String promotionId) { + params.addParam(UrlParameterNames.ID.getParameterName(), promotionId); + + MongoQuery<Promotion> q = new MongoQuery<>(params, null, dtoFilter); + // delete the currently selected asset + DeleteResult result = StreamHelper.awaitCompletionStage(dao.delete(q)); + if (result.getDeletedCount() == 0 || !result.wasAcknowledged()) { + return new Error(Status.NOT_FOUND, "Did not find an asset to delete for current call").asResponse(); + } + // return the results as a response + return Response.ok().build(); + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java b/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java index 4821cab5aa58db293363723caaef12504f1901ae..5c7fc58bcb7199b858d04777743c6140840b1da0 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java +++ b/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java @@ -6,6 +6,8 @@ */ package org.eclipsefoundation.marketplace.service; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; @@ -27,11 +29,13 @@ public interface CachingService<T> { * and returned. * * @param id the ID of the object to be stored in cache - * @param params the query parameters for the current request + * @param wrapper the query parameters for the current request + * @param params parameters to use in place of wrapper parameters when set * @param callable a runnable that returns an object of type T * @return the cached result */ - Optional<T> get(String id, RequestWrapper params, Callable<? extends T> callable); + Optional<T> get(String id, RequestWrapper wrapper, Map<String, List<String>> params, + Callable<? extends T> callable); /** * Returns the expiration date in millis since epoch. @@ -73,16 +77,19 @@ public interface CachingService<T> { * * @param id identity string of the item to cache * @param wrapper parameters associated with the request for information + * @param params parameters to use in place of wrapper parameters when set * @return the unique cache key for the request. */ - default String getCacheKey(String id, RequestWrapper wrapper) { + default String getCacheKey(String id, RequestWrapper wrapper, Map<String, List<String>> params) { StringBuilder sb = new StringBuilder(); sb.append('[').append(wrapper.getEndpoint()).append(']'); sb.append("id:").append(id); + // get the used set of parameters for filtering data + Map<String, List<String>> actual = params == null ? wrapper.asMap() : params; // join all the non-empty params to the key to create distinct entries for // filtered values - wrapper.asMap().entrySet().stream().filter(e -> !e.getValue().isEmpty()) + actual.entrySet().stream().filter(e -> !e.getValue().isEmpty()) .map(e -> e.getKey() + '=' + StringUtils.join(e.getValue(), ',')) .forEach(s -> sb.append('|').append(s)); diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/PromotionService.java b/src/main/java/org/eclipsefoundation/marketplace/service/PromotionService.java new file mode 100644 index 0000000000000000000000000000000000000000..7d82f0947c8a0e361818183b0613a63714179779 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/service/PromotionService.java @@ -0,0 +1,37 @@ +package org.eclipsefoundation.marketplace.service; + +import java.util.List; + +import org.eclipsefoundation.marketplace.dto.Listing; +import org.eclipsefoundation.marketplace.dto.Promotion; +import org.eclipsefoundation.marketplace.model.RequestWrapper; + +/** + * Interface for retrieving promotions within the application. + * + * @author Martin Lowe + * + */ +public interface PromotionService { + + /** + * Retrieves listings associated with the given list of promotions + * + * @param wrapper wrapper for the current request + * @param promos list of promotions to retrieve listings for + * @return a list of listings for the list of promos, where data could be found, + * or an empty list if no corresponding listings could be found for the + * passed promotions. + */ + List<Listing> getListingsForPromotions(RequestWrapper wrapper, List<Promotion> promos); + + /** + * Adds a number of promotions into the given listing set. + * + * @param wrapper wrapper for the current request + * @param listings listings to inject promotions into + * @return a list containing the new promotions, if any are found, along with + * the original listings. + */ + List<Listing> retrievePromotions(RequestWrapper wrapper, List<Listing> listings); +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/impl/DefaultPromotionService.java b/src/main/java/org/eclipsefoundation/marketplace/service/impl/DefaultPromotionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c8f8ab60a940bcff2e0a4e037f31dc6b91f2eafb --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/service/impl/DefaultPromotionService.java @@ -0,0 +1,173 @@ +package org.eclipsefoundation.marketplace.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipsefoundation.marketplace.dao.MongoDao; +import org.eclipsefoundation.marketplace.dto.Listing; +import org.eclipsefoundation.marketplace.dto.Promotion; +import org.eclipsefoundation.marketplace.dto.filter.DtoFilter; +import org.eclipsefoundation.marketplace.helper.StreamHelper; +import org.eclipsefoundation.marketplace.model.MongoQuery; +import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.namespace.MicroprofilePropertyNames; +import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; +import org.eclipsefoundation.marketplace.service.CachingService; +import org.eclipsefoundation.marketplace.service.PromotionService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default implementation of promotion service. Uses weighting to allow for + * promotions to appear more often than others. By using the property + * {@link MicroprofilePropertyNames.PROMO_WEIGHT_DEFAULT}, weighting defaults + * can shift outside of code builds once data is modified. + * + * @author Martin Lowe + * + */ +@ApplicationScoped +public class DefaultPromotionService implements PromotionService { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPromotionService.class); + + @ConfigProperty(name = MicroprofilePropertyNames.PROMO_SERVE_COUNT, defaultValue = "2") + int promoCount; + @ConfigProperty(name = MicroprofilePropertyNames.PROMO_WEIGHT_DEFAULT, defaultValue = "1") + int defaultWeight; + + @Inject + MongoDao dao; + + @Inject + DtoFilter<Listing> listingFilter; + @Inject + CachingService<List<Listing>> listingCache; + + @Inject + DtoFilter<Promotion> promotionFilter; + @Inject + CachingService<List<Promotion>> promoCache; + + // random used for shuffling collections + private Random r = new Random(); + + @Override + public List<Listing> getListingsForPromotions(RequestWrapper wrapper, List<Promotion> promos) { + if (promos == null || promos.isEmpty()) { + LOGGER.debug("No promotions were passed, returning empty list"); + return Collections.emptyList(); + } + // create mapping to get a list of specific IDs, and to add context to the call + // for caching + Map<String, List<String>> adds = new HashMap<>(); + adds.put("type", Arrays.asList("Listing")); + adds.put(UrlParameterNames.IDS.getParameterName(), + promos.stream().map(Promotion::getListingId).collect(Collectors.toList())); + + MongoQuery<Listing> q = new MongoQuery<>(null, adds, listingFilter); + // retrieve the possible cached object + Optional<List<Listing>> cachedResults = listingCache.get("promo|listings", wrapper, adds, + () -> StreamHelper.awaitCompletionStage(dao.get(q))); + if (!cachedResults.isPresent()) { + LOGGER.error("Error while retrieving cached promotion listings"); + return Collections.emptyList(); + } + + // return the results as a response + return cachedResults.get(); + } + + @Override + public List<Listing> retrievePromotions(RequestWrapper wrapper, List<Listing> listings) { + // create an empty promo query to get all promos + MongoQuery<Promotion> q = new MongoQuery<>(null, Collections.emptyMap(), promotionFilter); + // retrieve the possible cached object + Optional<List<Promotion>> cachedResults = promoCache.get("all|promo", wrapper, Collections.emptyMap(), + () -> StreamHelper.awaitCompletionStage(dao.get(q))); + if (!cachedResults.isPresent() || cachedResults.get().isEmpty()) { + LOGGER.debug("Could not find any promotions to inject, returning"); + return listings; + } + // make a copy of the array to not impact cached values + List<Promotion> promos = new ArrayList<>(cachedResults.get()); + List<Promotion> promoHolding = new ArrayList<>(promoCount); + LOGGER.debug("Found {} promotions, maximum number to inject {}", promos.size(), promoCount); + // check each promotion to see if it should be injected + Promotion curr = getWeightedPromotion(promos); + if (curr != null) { + promos.remove(curr); + } + while (curr != null && promoHolding.size() <= promoCount) { + // create a local final field referencing the current promotion for stream ref + final Promotion p = curr; + LOGGER.debug("Checking promo {}", p.getListingId()); + // check if current promo matches any of the listing IDs + if (promos.stream().noneMatch(l -> l.getId().equals(p.getListingId()))) { + LOGGER.debug("Preparing promo with listing ID '{}' to be injected into result set", + curr.getListingId()); + + promoHolding.add(curr); + } + curr = getWeightedPromotion(promos); + if (curr != null) { + promos.remove(curr); + } + } + + // if we couldn't find enough promos, reinsert records + List<Listing> out = new ArrayList<>(listings); + if (promoHolding.isEmpty()) { + LOGGER.debug("Could not find any promos to inject"); + } else { + for (Listing listing : getListingsForPromotions(wrapper, promoHolding)) { + LOGGER.debug("Injecting promo with listing ID '{}' ", listing.getId()); + listing.setPromotion(true); + out.add(0, listing); + } + } + return out; + } + + /** + * Using the weighting set in the promotions (or default if not set), retrieve a + * random result, taking weighting into account. + * + * @param promos list of promotions to retrieve a result from. This list will be + * modified as part of this call to shuffle and pop the chosen + * entry. + * @return the chosen weighted and randomized promotion, or null if none are + * appropriate. + */ + private Promotion getWeightedPromotion(List<Promotion> promos) { + // return if there are no promotions to choose from + if (promos.isEmpty()) { + return null; + } + int totalWeighting = promos.stream().mapToInt(Promotion::getWeight).sum(); + // get a random number in the range of the total weighting + int rnd = r.nextInt(totalWeighting); + Promotion result = null; + for (Promotion p: promos) { + // reduce the random number by the weight + rnd -= p.getWeight(); + // check if we are in range of the current entry + if (rnd <= 0) { + result = p; + break; + } + } + return result; + } + +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java b/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java index d3940a77d970a8c9925dfa25fe2662b68c483dc3..6addf934f7f8b821c57178228dd88970d8ce2a3a 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java +++ b/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java @@ -7,6 +7,7 @@ package org.eclipsefoundation.marketplace.service.impl; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -19,6 +20,7 @@ import javax.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.namespace.MicroprofilePropertyNames; import org.eclipsefoundation.marketplace.service.CachingService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,9 +50,9 @@ import com.google.common.util.concurrent.UncheckedExecutionException; public class GuavaCachingService<T> implements CachingService<T> { private static final Logger LOGGER = LoggerFactory.getLogger(GuavaCachingService.class); - @ConfigProperty(name = "cache.max.size", defaultValue = "10000") + @ConfigProperty(name = MicroprofilePropertyNames.CACHE_SIZE_MAX, defaultValue = "10000") long maxSize; - @ConfigProperty(name = "cache.ttl.write.seconds", defaultValue = "900") + @ConfigProperty(name = MicroprofilePropertyNames.CACHE_TTL_MAX_SECONDS, defaultValue = "900") long ttlWrite; // actual cache object @@ -71,15 +73,17 @@ public class GuavaCachingService<T> implements CachingService<T> { } @Override - public Optional<T> get(String id, RequestWrapper params, Callable<? extends T> callable) { + public Optional<T> get(String id, RequestWrapper wrapper, Map<String, List<String>> params, + Callable<? extends T> callable) { Objects.requireNonNull(id); - Objects.requireNonNull(params); + Objects.requireNonNull(wrapper); Objects.requireNonNull(callable); - String cacheKey = getCacheKey(id, params); + String cacheKey = getCacheKey(id, wrapper, params); + LOGGER.debug("Retrieving cache value for '{}'", cacheKey); try { // check if the cache is bypassed for the request - if (params.isCacheBypass()) { + if (wrapper.isCacheBypass()) { T result = callable.call(); // if the cache has a value for key, update it if (cache.asMap().containsKey(cacheKey)) { @@ -104,7 +108,7 @@ public class GuavaCachingService<T> implements CachingService<T> { @Override public Optional<Long> getExpiration(String id, RequestWrapper params) { - return Optional.ofNullable(ttl.get(getCacheKey(Objects.requireNonNull(id), Objects.requireNonNull(params)))); + return Optional.ofNullable(ttl.get(getCacheKey(Objects.requireNonNull(id), Objects.requireNonNull(params), null))); } @Override @@ -126,4 +130,5 @@ public class GuavaCachingService<T> implements CachingService<T> { public long getMaxAge() { return ttlWrite; } + } diff --git a/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java b/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java index f6b07e61c51c3070a94cd7941b60c3ba69ce3329..41d65df20003061de18bcdd6534a2750422078dd 100644 --- a/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java +++ b/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java @@ -9,20 +9,17 @@ package org.eclipsefoundation.marketplace.helper; import java.util.List; import java.util.Optional; -import org.eclipsefoundation.marketplace.helper.SortableHelper; import org.eclipsefoundation.marketplace.helper.SortableHelper.Sortable; import org.eclipsefoundation.marketplace.model.SortableField; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.quarkus.test.junit.DisabledOnSubstrate; import io.quarkus.test.junit.QuarkusTest; /** * @author Martin Lowe * */ -@DisabledOnSubstrate @QuarkusTest public class SortableHelperTest { diff --git a/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java b/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java index 798c1d7ef3e640eb1d876ab96eecc9124efa6dfb..197951446084e4abea7e9a0aa3e4c316ff63ef0e 100644 --- a/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java +++ b/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java @@ -6,6 +6,7 @@ */ package org.eclipsefoundation.marketplace.service.impl; +import java.util.Collections; import java.util.Optional; import javax.inject.Inject; @@ -60,18 +61,18 @@ public class GuavaCachingServiceTest { // without post construct init via javax management, cache will not be properly // set - Assertions.assertTrue(!gcsManual.get("sampleKey", sample, Object::new).isPresent(), + Assertions.assertTrue(!gcsManual.get("sampleKey", sample, Collections.emptyMap(), Object::new).isPresent(), "Object should not be generated when there is no cache initialized"); // initialize the cache w/ configs gcsManual.init(); // run a command to interact with cache - Assertions.assertTrue(gcsManual.get("sampleKey", sample, Object::new).isPresent(), + Assertions.assertTrue(gcsManual.get("sampleKey", sample, Collections.emptyMap(), Object::new).isPresent(), "Object should be generated once cache is instantiated"); // test the injected cache service (which is the normal use case) - Assertions.assertTrue(gcs.get("sampleKey", sample, Object::new).isPresent(), + Assertions.assertTrue(gcs.get("sampleKey", sample, Collections.emptyMap(), Object::new).isPresent(), "Object should be generated once cache is instantiated"); } @@ -81,7 +82,7 @@ public class GuavaCachingServiceTest { String key = "k"; // get the cached obj from a fresh cache - Optional<Object> cachedObj = gcs.get(key, sample, () -> cachableObject); + Optional<Object> cachedObj = gcs.get(key, sample, Collections.emptyMap(), () -> cachableObject); Assertions.assertTrue(cachedObj.isPresent()); Assertions.assertEquals(cachableObject, cachedObj.get()); @@ -90,19 +91,19 @@ public class GuavaCachingServiceTest { @Test public void testGetNullCallable() { Assertions.assertThrows(NullPointerException.class, () -> { - gcs.get("key", sample, null); + gcs.get("key", sample, Collections.emptyMap(), null); }); } @Test public void testGetNullCallableResult() { - Optional<Object> emptyObj = gcs.get("failure key", sample, () -> null); + Optional<Object> emptyObj = gcs.get("failure key", sample, Collections.emptyMap(), () -> null); Assertions.assertFalse(emptyObj.isPresent()); } @Test public void testGetExceptionalCallable() { - Optional<Object> emptyObj = gcs.get("k", sample, () -> { + Optional<Object> emptyObj = gcs.get("k", sample, Collections.emptyMap(), () -> { throw new IllegalStateException(); }); Assertions.assertFalse(emptyObj.isPresent()); diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 9c5f91e89329cd818f9bf6a1fb1d238b28cd365b..b4ffee9e0fa2c862efe1e81792ab78ded2dbaefa 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -9,10 +9,18 @@ quarkus.log.level=TRACE quarkus.log.file.path=/tmp/logs/quarkus.log ## DATASOURCE CONFIG -quarkus.mongodb.connection-string=mongodb://192.168.1.178:27017 +quarkus.mongodb.connection-string = mongodb://localhost:27017 +quarkus.mongodb.credentials.username=root +quarkus.mongodb.write-concern.safe=true +quarkus.mongodb.min-pool-size=100 +quarkus.mongodb.max-pool-size=1000 +quarkus.mongodb.write-concern.retry-writes=true mongodb.database=mpc mongodb.default.limit=25 mongodb.default.limit.max=100 +# MISC +quarkus.resteasy.gzip.enabled=true + # TEST PROPERTIES sample.secret.property=application-value \ No newline at end of file