From fa6a4f1a2711e93d97747c24bc96b8ee16f8881b Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Mon, 13 Jan 2020 08:42:11 -0500 Subject: [PATCH] Create a way to inject promotions into listing results #59 Updated application to add promotion service. Added promotion to DTO layer. Added call to listings fetch to inject promotions in certain conditions. Refactored code to make URL parameters more robust. Reduced duplication by setting ID matching in DtoFilters to be default functionality. Signed-off-by: Martin Lowe <martin.lowe@eclipse-foundation.org> --- .../marketplace/dao/impl/DefaultMongoDao.java | 9 +- .../marketplace/dto/Listing.java | 17 ++ .../marketplace/dto/Promotion.java | 51 ++++++ .../dto/codecs/PromotionCodec.java | 86 +++++++++ .../marketplace/dto/filter/CatalogFilter.java | 34 +--- .../dto/filter/CategoryFilter.java | 35 +--- .../marketplace/dto/filter/DtoFilter.java | 37 +++- .../dto/filter/ErrorReportFilter.java | 18 +- .../marketplace/dto/filter/InstallFilter.java | 18 +- .../dto/filter/InstallMetricsFilter.java | 30 --- .../marketplace/dto/filter/ListingFilter.java | 20 +- .../dto/filter/ListingVersionFilter.java | 14 +- .../marketplace/dto/filter/MarketFilter.java | 8 +- .../dto/filter/MetricPeriodFilter.java | 10 +- .../dto/filter/PromotionFilter.java | 27 +++ .../dto/providers/PromotionCodecProvider.java | 35 ++++ .../marketplace/model/MongoQuery.java | 50 +++-- .../marketplace/model/QueryParameters.java | 73 ++++++++ .../marketplace/model/RequestWrapper.java | 41 +++-- .../namespace/DatabaseFieldNames.java | 2 + .../marketplace/namespace/DtoTableNames.java | 3 +- .../namespace/MicroprofilePropertyNames.java | 21 +++ .../namespace/UrlParameterNames.java | 64 +++++-- .../request/CacheBypassFilter.java | 2 +- .../marketplace/resource/CatalogResource.java | 10 +- .../resource/CategoryResource.java | 10 +- .../resource/ErrorReportResource.java | 8 +- .../marketplace/resource/InstallResource.java | 18 +- .../marketplace/resource/ListingResource.java | 48 +++-- .../resource/ListingVersionResource.java | 11 +- .../marketplace/resource/MarketResource.java | 11 +- .../resource/PromotionResource.java | 152 +++++++++++++++ .../marketplace/service/CachingService.java | 15 +- .../marketplace/service/PromotionService.java | 37 ++++ .../service/impl/DefaultPromotionService.java | 173 ++++++++++++++++++ .../service/impl/GuavaCachingService.java | 19 +- .../helper/SortableHelperTest.java | 3 - .../service/impl/GuavaCachingServiceTest.java | 15 +- src/test/resources/application.properties | 10 +- 39 files changed, 961 insertions(+), 284 deletions(-) create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/Promotion.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/codecs/PromotionCodec.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/filter/PromotionFilter.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/providers/PromotionCodecProvider.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/model/QueryParameters.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/namespace/MicroprofilePropertyNames.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/resource/PromotionResource.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/service/PromotionService.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/service/impl/DefaultPromotionService.java 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 460817d..8a2bbc2 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 a6c93d3..ec53965 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 0000000..3339a0d --- /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 0000000..5af459f --- /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 e2aaae1..4c15814 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 3092ecf..7f7bfd3 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 bb5d8e0..d4cedac 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 3f1ff92..6f5f461 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 e7c1e99..2cd260d 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 38e75c0..3d84749 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 9aadaa4..2687c79 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 ddc414b..4334ca9 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 664333f..f0dc7b5 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 1f6b9bb..59bf384 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 0000000..d785240 --- /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 0000000..80f2604 --- /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 547f3ba..033f390 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 0000000..d91173a --- /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 43fc346..0fd4ce7 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 f3d5f57..b6cb1af 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 a3d92ef..c81d4b2 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 0000000..1ae381e --- /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 9404591..cfd91e6 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 636c40b..7a251fb 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 03454b2..87aa67b 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 416acc1..f671ea1 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 7d4c2f0..a596ddd 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 c49d802..19aa49c 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 fa5cd87..1674ff4 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 66f318a..1d312b8 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 aa0fd95..24110f5 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 0000000..9cd8b03 --- /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 4821cab..5c7fc58 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 0000000..7d82f09 --- /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 0000000..c8f8ab6 --- /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 d3940a7..6addf93 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 f6b07e6..41d65df 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 798c1d7..1979514 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 9c5f91e..b4ffee9 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 -- GitLab