From bb922a46ef2e7c1a9d6412441d3ca2e8a8b370a8 Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Wed, 25 Sep 2019 15:05:00 -0400 Subject: [PATCH] Added resources, filters, request scope, clarity in naming Added the following resource layers: Catalog, Category. Added normalized filter support for Listing, Catalog, Category (DtoFilters). Changed model objects pertaining to request objects to be request scoped rather than unmanaged. Updated documentation in accords to how to build native images and setup. Fixed POM file references blocking native-image build. Change-Id: Id0c0fbced337f0b1b687d2ff2655a38c06b18807 Signed-off-by: Martin Lowe <martin.lowe@eclipse-foundation.org> --- .gitignore | 4 +- README.md | 49 +++++- config/sample.secret.properties | 4 + config/test.secret.properties | 2 + pom.xml | 26 ++-- src/main/docker/Dockerfile.native | 11 +- .../config/SecretConfigSource.java | 23 ++- .../marketplace/dao/impl/DefaultMongoDao.java | 13 +- .../marketplace/dto/Catalog.java | 55 ++----- .../marketplace/dto/Category.java | 89 ++++++++--- .../marketplace/dto/Listing.java | 33 +++++ .../marketplace/dto/Tab.java | 60 ++++++++ .../marketplace/dto/codecs/CatalogCodec.java | 101 +++++++++++++ .../marketplace/dto/codecs/CategoryCodec.java | 72 +++++++++ .../marketplace/dto/codecs/ListingCodec.java | 9 +- .../dto/converters/CategoryConverter.java | 39 +++++ .../dto/converters/TabConverter.java | 39 +++++ .../marketplace/dto/filter/CatalogFilter.java | 49 ++++++ .../dto/filter/CategoryFilter.java | 40 +++++ .../{model => dto}/filter/DtoFilter.java | 8 +- .../{model => dto}/filter/ListingFilter.java | 63 +++++--- .../dto/providers/CatalogCodecProvider.java | 35 +++++ .../dto/providers/CategoryCodecProvider.java | 35 +++++ .../marketplace/model/MongoQuery.java | 139 +++++++++--------- .../{QueryParams.java => RequestWrapper.java} | 60 ++++---- .../marketplace/model/ResourceDataType.java | 24 +++ .../marketplace/namespace/DtoTableNames.java | 10 +- .../namespace/MongoFieldNames.java | 17 +++ .../namespace/UrlParameterNames.java | 1 + .../AnnotationClassInjectionFilter.java | 48 ++++++ .../marketplace/resource/CatalogResource.java | 85 +++++++++++ .../resource/CategoryResource.java | 86 +++++++++++ .../marketplace/resource/ListingResource.java | 39 ++--- .../marketplace/service/CachingService.java | 6 +- .../service/impl/GuavaCachingService.java | 4 +- src/main/node/index.js | 30 +++- src/main/resources/secret.sample.properties | 4 - .../helper/SortableHelperTest.java | 2 + .../service/impl/GuavaCachingServiceTest.java | 8 +- src/test/resources/secret.properties | 2 - 40 files changed, 1157 insertions(+), 267 deletions(-) create mode 100644 config/sample.secret.properties create mode 100644 config/test.secret.properties create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/Tab.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CatalogCodec.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CategoryCodec.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/converters/CategoryConverter.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/converters/TabConverter.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java rename src/main/java/org/eclipsefoundation/marketplace/{model => dto}/filter/DtoFilter.java (66%) rename src/main/java/org/eclipsefoundation/marketplace/{model => dto}/filter/ListingFilter.java (67%) create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/providers/CatalogCodecProvider.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/dto/providers/CategoryCodecProvider.java rename src/main/java/org/eclipsefoundation/marketplace/model/{QueryParams.java => RequestWrapper.java} (73%) create mode 100644 src/main/java/org/eclipsefoundation/marketplace/model/ResourceDataType.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/resource/AnnotationClassInjectionFilter.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java create mode 100644 src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java delete mode 100644 src/main/resources/secret.sample.properties delete mode 100644 src/test/resources/secret.properties diff --git a/.gitignore b/.gitignore index 12a4326..381cf9f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,8 +35,8 @@ pom.xml.versionsBackup release.properties # Secrets config -src/main/resources/secret.properties -test/main/resources/secret.properties +secret.properties +secret.properties #NodeJS node_modules/ \ No newline at end of file diff --git a/README.md b/README.md index cbfc11a..8623a7a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Proof of concept project within the Microservice initiative, the Foundation look 1. Installed and configured JDK 1.8+ 1. Apache Maven 3.5.3+ 1. Running instance of MongoDB +1. GraalVM (for compilation of native-image) ### Optional requirements @@ -20,30 +21,52 @@ This section will outline configuration values that need to be checked and updat 1. In order to properly detect MongoDB, a connection string needs to be updated. `quarkus.mongodb.connection-string` designates the location of MongoDB to quarkus in the form of `mongodb://<host>:<port>/`. By default, this value points at `mongodb://localhost:27017`, the default location for local installs of MongoDB. 1. Update `quarkus.mongodb.credentials.username` to be a known user with write permissions to MongoDB instance. -1. Create a copy of `./src/main/resources/secret.sample.properties` named `secret.properties` in the same folder +1. Create a copy of `./config/sample.secret.properties` named `secret.properties` in a location of your choosing on the system, with the config folder in the project root being default configured. If changed, keep this path as it is needed to start the environment later. 1. Update `quarkus.mongodb.credentials.password` to be the password for the MongoDB user in the newly created `secret.properties` file. 1. By default, this application binds to port 8090. If port 8090 is occupied by another service, the value of `quarkus.http.port` can be modified to designate a different port. +If you are compiling from source, in order to properly pass tests in packaging, some additional set up sill need to be done. There are two options for setting up test variables for the project. + +1. Option 1 - Combined file + - This method is useful when working in development environments, as the compilation and running of the application using the development build command uses the same configuration file. Thanks to scoping native to Quarkus using profiles, there is no risk of overlap between the 2 profiles. + - To ensure that the tests pass, copy the contents of the `config/test.secret.properties` file into the development copy of the secret properties. + +1. Option 2 - Separate files + - This method is suitable for production environments where test data should be kept separate from real world settings. This can be used for the following build instructions: + - Build and run test + - Build native + - Build native & docker image + - Create a copy of `config/test.secret.properties` somewhere on the file system, with the config folder in the project root being default configured. If changed, keep this path as it is needed for compilations of the product. + +For users looking to build native images and docker files, an install of GraalVM is required to compile the image. Please retrieve the version **19.1.1** from the [GraalVM release page](https://github.com/oracle/graal/releases) for your given environment. Once installed, please ensure your `GRAAL_HOME`, `GRAALVM_HOME` are set to the installed directory, and the GraalVM `/bin` folder has been added to your `PATH`. Run `sudo gu install native-image` to retrieve imaging functionality from GitHub for GraalVM on Linux and MacOS based environments. + + ## Build * Development - $ mvn compile quarkus:dev + $ mvn compile quarkus:dev -Dconfig.secret.path=<full path to secret file> * Build and run test - $ mvn clean package + $ mvn clean package -Dconfig.secret.path=<full path to test secret file> * Build native - $ mvn package -Pnative + $ mvn package -Pnative -Dconfig.secret.path=<full path to test secret file> * Build native & docker image - $ mvn package -Pnative -Dnative-image.docker-build=true + $ mvn package -Pnative -Dnative-image.docker-build=true -Dconfig.secret.path=<full path to test secret file> + docker build -f src/main/docker/Dockerfile.native -t eclipse/mpc . --build-arg SECRET_LOCATION=/var/secret --build-arg LOCAL_SECRETS=config/secret.properties + docker run -i --rm -p 8080:8090 eclipse/mpc See https://quarkus.io for more information. +The property ` -Dconfig.secret.path` is added to each line as the location needs to be fed in at runtime where to find the secret properties data. By default, Quarkus includes surefire as part of its native imagine build plug-in, which needs the given path in order for the given packages to pass. + +The Docker build-arg `LOCAL_SECRETS` can be configured on the `docker build` command if the secrets file exists outside of the standard location of `config/secret.properties`. It has been set to the default value in the sample command for example purposes on usage. + ## Sample data For ease of use, a script has been created to load sample data into a MongoDB instance using Node JS and a running instance of the API. This script will load a large amount of listings into the running MongoDB using the API for use in testing different queries without having to retrieve real world data. @@ -53,7 +76,21 @@ For ease of use, a script has been created to load sample data into a MongoDB in ### Additional MongoDB commands needed: -- db.listings.createIndex({body:"text", teaser:"text",title:"text"}) +- use mpc; +- db.listings.createIndex( + { + body:"text", + teaser:"text", + title:"text" + }, + { + weights: { + title: 10, + teaser: 3 + }, + name: "TextIndex" + }); +- db.categories.createIndex({id:1}); ## Copyright diff --git a/config/sample.secret.properties b/config/sample.secret.properties new file mode 100644 index 0000000..1505989 --- /dev/null +++ b/config/sample.secret.properties @@ -0,0 +1,4 @@ +quarkus.mongodb.credentials.password=sample +quarkus.oauth2.client-secret=sample + +eclipse.secret.token=123456789abcdefghijklmnopqrstuvwxyz \ No newline at end of file diff --git a/config/test.secret.properties b/config/test.secret.properties new file mode 100644 index 0000000..977b873 --- /dev/null +++ b/config/test.secret.properties @@ -0,0 +1,2 @@ +%test.sample.secret.property=secret-value +%test.eclipse.secret.token=example diff --git a/pom.xml b/pom.xml index 4414c26..5bb4f62 100644 --- a/pom.xml +++ b/pom.xml @@ -8,11 +8,12 @@ <artifactId>marketplace-rest-api</artifactId> <version>0.1-ALPHA</version> <properties> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <surefire-plugin.version>2.22.0</surefire-plugin.version> - <quarkus.version>0.21.1</quarkus.version> + <quarkus.version>0.22.0</quarkus.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> - <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <sonar.sources>src/main</sonar.sources> <sonar.tests>src/test</sonar.tests> <sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin> @@ -39,22 +40,20 @@ <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> + <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> + <scope>test</scope> </dependency> <dependency> - <groupId>org.jboss.logmanager</groupId> - <artifactId>jboss-logmanager</artifactId> - <version>2.1.14.Final</version> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-mongodb-client</artifactId> </dependency> - - <!-- Custom dependencies --> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-jsonb-runtime</artifactId> - <version>0.12.0</version> + <artifactId>quarkus-resteasy-jsonb</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> @@ -62,8 +61,15 @@ </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-mongodb-client</artifactId> + <artifactId>quarkus-arc</artifactId> + </dependency> + <dependency> + <groupId>org.jboss.logmanager</groupId> + <artifactId>jboss-logmanager</artifactId> + <version>2.1.14.Final</version> </dependency> + + <!-- Custom dependencies --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native index 8630c7e..7fbe72e 100644 --- a/src/main/docker/Dockerfile.native +++ b/src/main/docker/Dockerfile.native @@ -15,8 +15,17 @@ # ### FROM registry.fedoraproject.org/fedora-minimal +ARG SECRET_LOCATION=/tmp +ENV SECRET_LOCATION ${SECRET_LOCATION} + +ARG LOCAL_SECRETS=config/secret.properties +ENV LOCAL_SECRETS ${LOCAL_SECRETS} + +WORKDIR $SECRET_LOCATION +COPY $LOCAL_SECRETS secret.properties + WORKDIR /work/ COPY target/*-runner /work/application RUN chmod 775 /work EXPOSE 8080 -CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file +CMD ./application -Dquarkus.http.host=0.0.0.0 -Dconfig.secret.path=${SECRET_LOCATION}/secret.properties \ No newline at end of file diff --git a/src/main/java/org/eclipsefoundation/marketplace/config/SecretConfigSource.java b/src/main/java/org/eclipsefoundation/marketplace/config/SecretConfigSource.java index 0b47d48..bcac9a4 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/config/SecretConfigSource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/config/SecretConfigSource.java @@ -10,12 +10,12 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.ServiceLoader; +import org.apache.commons.lang3.StringUtils; import org.eclipse.microprofile.config.spi.ConfigSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,30 +33,32 @@ import org.slf4j.LoggerFactory; public class SecretConfigSource implements ConfigSource { private static final Logger LOGGER = LoggerFactory.getLogger(SecretConfigSource.class); + private String secretPath; + private Map<String, String> secrets; @Override public Map<String, String> getProperties() { if (secrets == null) { this.secrets = new HashMap<>(); - // use the class loader to get the secret.properties file path - URL fURL = getClass().getClassLoader().getResource("secret.properties"); - if (fURL == null) { + this.secretPath = System.getProperty("config.secret.path"); + if (StringUtils.isEmpty(secretPath)) { + LOGGER.error("Configuration 'config.secret.path' not set, cannot generate secret properties"); return this.secrets; } - // load the secrets file in - File f = new File(fURL.getFile()); + File f = new File(secretPath); if (!f.exists() || !f.canRead()) { + LOGGER.error("File at path {} either does not exist or cannot be read", secretPath); return this.secrets; } // read each of the lines of secret config that should be added - try (FileReader reader = new FileReader(f); BufferedReader br = new BufferedReader(reader)) { + try (BufferedReader br = new BufferedReader(new FileReader(f))) { Properties p = new Properties(); p.load(br); secrets.putAll((Map) p); - + } catch (IOException e) { LOGGER.error("Error while reading in secrets configuration file.", e); } @@ -71,10 +73,7 @@ public class SecretConfigSource implements ConfigSource { @Override public String getValue(String propertyName) { - // if secrets not found, retrieve them - if (secrets == null) - this.getProperties(); - return secrets.get(propertyName); + return getProperties().get(propertyName); } @Override 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 65fdaf0..586d631 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java @@ -61,14 +61,11 @@ public class DefaultMongoDao implements MongoDao { LOGGER.debug("Querying MongoDB using the following query: {}", q); } - // when looking for random data, use aggregate pipeline data rather than filter - if (q.isAggregate()) { - LOGGER.debug("Getting aggregate results"); - return getCollection(q.getDocType()).aggregate(q.getPipeline(getLimit(q)), q.getDocType()).distinct() - .toList().run(); - } - LOGGER.debug("Getting find results"); - return getCollection(q.getDocType()).find(q.getFindOptions().limit(getLimit(q))).toList().run(); + LOGGER.error("{}", q); + + LOGGER.debug("Getting aggregate results"); + return getCollection(q.getDocType()).aggregate(q.getPipeline(getLimit(q)), q.getDocType()).limit(getLimit(q)) + .distinct().toList().run(); } @Override diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Catalog.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Catalog.java index 0d85405..3c5b496 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/Catalog.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Catalog.java @@ -9,35 +9,36 @@ */ package org.eclipsefoundation.marketplace.dto; +import java.util.ArrayList; +import java.util.List; + /** * Represents a listing catalog. * * @author Martin Lowe */ public class Catalog { - private int id; + private String id; private String title; private String url; private boolean selfContained; + private boolean searchEnabled; private String icon; private String description; private String dependenciesRepository; - private boolean searchEnabled; - private boolean popularEnabled; - private boolean recentEnabled; - private boolean newsEnabled; + private List<Tab> tabs; /** * @return the id */ - public int getId() { + public String getId() { return id; } /** * @param id the id to set */ - public void setId(int id) { + public void setId(String id) { this.id = id; } @@ -140,45 +141,17 @@ public class Catalog { } /** - * @return the popularEnabled - */ - public boolean isPopularEnabled() { - return popularEnabled; - } - - /** - * @param popularEnabled the popularEnabled to set - */ - public void setPopularEnabled(boolean popularEnabled) { - this.popularEnabled = popularEnabled; - } - - /** - * @return the recentEnabled - */ - public boolean isRecentEnabled() { - return recentEnabled; - } - - /** - * @param recentEnabled the recentEnabled to set - */ - public void setRecentEnabled(boolean recentEnabled) { - this.recentEnabled = recentEnabled; - } - - /** - * @return the newsEnabled + * @return the tabs */ - public boolean isNewsEnabled() { - return newsEnabled; + public List<Tab> getTabs() { + return new ArrayList<>(tabs); } /** - * @param newsEnabled the newsEnabled to set + * @param tabs the tabs to set */ - public void setNewsEnabled(boolean newsEnabled) { - this.newsEnabled = newsEnabled; + public void setTabs(List<Tab> tabs) { + this.tabs = new ArrayList<>(tabs); } } \ No newline at end of file diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Category.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Category.java index 3bf6d87..29d3060 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/Category.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Category.java @@ -9,6 +9,9 @@ */ package org.eclipsefoundation.marketplace.dto; +import java.util.ArrayList; +import java.util.List; + import io.quarkus.runtime.annotations.RegisterForReflection; /** @@ -19,11 +22,10 @@ import io.quarkus.runtime.annotations.RegisterForReflection; */ @RegisterForReflection public class Category { - private int id; - private int marketId; + private Integer id; private String name; private String url; - private Listing node; + private List<Integer> marketIds; /** * @return the id @@ -39,20 +41,6 @@ public class Category { this.id = id; } - /** - * @return the marketId - */ - public int getMarketId() { - return marketId; - } - - /** - * @param marketId the marketId to set - */ - public void setMarketId(int marketId) { - this.marketId = marketId; - } - /** * @return the name */ @@ -82,16 +70,71 @@ public class Category { } /** - * @return the node + * @return the marketIds */ - public Listing getNode() { - return node; + public List<Integer> getMarketIds() { + return new ArrayList<>(marketIds); } /** - * @param node the node to set + * @param marketIds the marketIds to set */ - public void setNode(Listing node) { - this.node = node; + public void setMarketIds(List<Integer> marketIds) { + this.marketIds = new ArrayList<>(marketIds); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + result = prime * result + ((marketIds == null) ? 0 : marketIds.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Category other = (Category) obj; + if (id != other.id) + return false; + if (marketIds == null) { + if (other.marketIds != null) + return false; + } else if (!marketIds.equals(other.marketIds)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (url == null) { + if (other.url != null) + return false; + } else if (!url.equals(other.url)) + return false; + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Category [id="); + builder.append(id); + builder.append(", name="); + builder.append(name); + builder.append(", url="); + builder.append(url); + builder.append(", marketIds="); + builder.append(marketIds); + builder.append("]"); + return builder.toString(); } } diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java index 9029651..f12e7ec 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java @@ -64,6 +64,8 @@ public class Listing { private long updateDate; @JsonbProperty(MongoFieldNames.LICENSE_TYPE) private String license; + private List<Integer> categoryIds; + private List<Category> categories; private List<Organization> organizations; private List<Author> authors; private List<Tag> tags; @@ -77,6 +79,8 @@ public class Listing { this.organizations = new ArrayList<>(); this.tags = new ArrayList<>(); this.versions = new ArrayList<>(); + this.categoryIds = new ArrayList<>(); + this.categories = new ArrayList<>(); } /** @@ -318,6 +322,35 @@ public class Listing { this.license = license; } + /** + * @return the categoryIds + */ + public List<Integer> getCategoryIds() { + return categoryIds; + } + + /** + * @param categoryIds the categoryIds to set + */ + public void setCategoryIds(List<Integer> categoryIds) { + this.categoryIds = new ArrayList<>(categoryIds); + } + + /** + * @return the categories + */ + public List<Category> getCategories() { + return new ArrayList<>(categories); + } + + /** + * @param categories the categories to set + */ + @JsonbTransient + public void setCategories(List<Category> categories) { + this.categories = new ArrayList<>(categories); + } + /** * @return the organizations */ diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Tab.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Tab.java new file mode 100644 index 0000000..f2a80a7 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Tab.java @@ -0,0 +1,60 @@ +/* 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; + +/** + * @author Martin Lowe + * + */ +public class Tab { + private String title; + private String url; + private String type; + + /** + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * @param title the title to set + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(String type) { + this.type = type; + } + +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CatalogCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CatalogCodec.java new file mode 100644 index 0000000..a08d4ad --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CatalogCodec.java @@ -0,0 +1,101 @@ +/* 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.stream.Collectors; + +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.Catalog; +import org.eclipsefoundation.marketplace.dto.converters.TabConverter; +import org.eclipsefoundation.marketplace.namespace.MongoFieldNames; + +import com.mongodb.MongoClient; + +/** + * MongoDB codec for transcoding of {@link Catalog} and {@link Document} + * objects. Used when writing or retrieving objects of given type from the + * database. + * + * @author Martin Lowe + */ +public class CatalogCodec implements CollectibleCodec<Catalog> { + private final Codec<Document> documentCodec; + + // converter objects for handling internal objects + private final TabConverter tabConverter; + + /** + * Creates the codec and initializes the codecs and converters needed to create + * a listing from end to end. + */ + public CatalogCodec() { + this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class); + this.tabConverter = new TabConverter(); + } + + @Override + public void encode(BsonWriter writer, Catalog value, EncoderContext encoderContext) { + Document doc = new Document(); + + doc.put(MongoFieldNames.DOCID, value.getId()); + doc.put(MongoFieldNames.CATALOG_TITLE, value.getTitle()); + doc.put(MongoFieldNames.CATALOG_URL, value.getUrl()); + doc.put(MongoFieldNames.CATALOG_ICON, value.getIcon()); + doc.put(MongoFieldNames.CATALOG_SELF_CONTAINED, value.isSelfContained()); + doc.put(MongoFieldNames.CATALOG_SEARCH_ENABLED, value.isSearchEnabled()); + doc.put(MongoFieldNames.CATALOG_DEPENDENCIES_REPOSITORY, value.getDependenciesRepository()); + doc.put(MongoFieldNames.CATALOG_TABS, + value.getTabs().stream().map(tabConverter::convert).collect(Collectors.toList())); + documentCodec.encode(writer, doc, encoderContext); + } + + @Override + public Class<Catalog> getEncoderClass() { + return Catalog.class; + } + + @Override + public Catalog decode(BsonReader reader, DecoderContext decoderContext) { + Document document = documentCodec.decode(reader, decoderContext); + Catalog out = new Catalog(); + out.setId(document.getString(MongoFieldNames.DOCID)); + out.setUrl(document.getString(MongoFieldNames.CATALOG_URL)); + out.setTitle(document.getString(MongoFieldNames.CATALOG_TITLE)); + out.setIcon(document.getString(MongoFieldNames.CATALOG_ICON)); + out.setSelfContained(document.getBoolean(MongoFieldNames.CATALOG_SELF_CONTAINED)); + out.setSearchEnabled(document.getBoolean(MongoFieldNames.CATALOG_SEARCH_ENABLED)); + out.setDependenciesRepository(document.getString(MongoFieldNames.CATALOG_DEPENDENCIES_REPOSITORY)); + out.setTabs(document.getList(MongoFieldNames.CATALOG_TABS, Document.class).stream().map(tabConverter::convert) + .collect(Collectors.toList())); + return out; + } + + @Override + public Catalog generateIdIfAbsentFromDocument(Catalog document) { + return document; + } + + @Override + public boolean documentHasId(Catalog document) { + return document.getId() != null; + } + + @Override + public BsonValue getDocumentId(Catalog document) { + // TODO Auto-generated method stub + return new BsonString(document.getId()); + } + +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CategoryCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CategoryCodec.java new file mode 100644 index 0000000..187cd7d --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CategoryCodec.java @@ -0,0 +1,72 @@ +/* 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 org.bson.BsonInt32; +import org.bson.BsonReader; +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.Category; +import org.eclipsefoundation.marketplace.dto.converters.CategoryConverter; + +import com.mongodb.MongoClient; + +/** + * @author martin + * + */ +public class CategoryCodec implements CollectibleCodec<Category> { + private final Codec<Document> documentCodec; + + private CategoryConverter cc; + + /** + * Creates the codec and initializes the codecs and converters needed to create + * a listing from end to end. + */ + public CategoryCodec() { + this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class); + this.cc = new CategoryConverter(); + } + + @Override + public void encode(BsonWriter writer, Category value, EncoderContext encoderContext) { + documentCodec.encode(writer, cc.convert(value), encoderContext); + } + + @Override + public Class<Category> getEncoderClass() { + return Category.class; + } + + @Override + public Category decode(BsonReader reader, DecoderContext decoderContext) { + return cc.convert(documentCodec.decode(reader, decoderContext)); + } + + @Override + public Category generateIdIfAbsentFromDocument(Category document) { + // TODO Auto-generated method stub + return document; + } + + @Override + public boolean documentHasId(Category document) { + return document.getId() > 0; + } + + @Override + public BsonValue getDocumentId(Category document) { + return new BsonInt32(document.getId()); + } + +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java index b1b3e65..8327390 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java @@ -21,6 +21,7 @@ import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.eclipsefoundation.marketplace.dto.Listing; import org.eclipsefoundation.marketplace.dto.converters.AuthorConverter; +import org.eclipsefoundation.marketplace.dto.converters.CategoryConverter; import org.eclipsefoundation.marketplace.dto.converters.OrganizationConverter; import org.eclipsefoundation.marketplace.dto.converters.SolutionVersionConverter; import org.eclipsefoundation.marketplace.dto.converters.TagConverter; @@ -43,6 +44,7 @@ public class ListingCodec implements CollectibleCodec<Listing> { private final OrganizationConverter organizationConverter; private final TagConverter tagConverter; private final SolutionVersionConverter versionConverter; + private final CategoryConverter categoryConverter; /** * Creates the codec and initializes the codecs and converters needed to create @@ -54,6 +56,7 @@ public class ListingCodec implements CollectibleCodec<Listing> { this.organizationConverter = new OrganizationConverter(); this.tagConverter = new TagConverter(); this.versionConverter = new SolutionVersionConverter(); + this.categoryConverter = new CategoryConverter(); } @Override @@ -77,6 +80,7 @@ public class ListingCodec implements CollectibleCodec<Listing> { doc.put(MongoFieldNames.UPDATE_DATE, new Date(value.getUpdateDate())); doc.put(MongoFieldNames.CREATION_DATE, new Date(value.getCreationDate())); doc.put(MongoFieldNames.FOUNDATION_MEMBER_FLAG, value.isFoundationMember()); + doc.put(MongoFieldNames.CATEGORY_IDS, value.getCategoryIds()); // for nested document types, use the converters to safely transform into BSON // documents @@ -116,6 +120,7 @@ public class ListingCodec implements CollectibleCodec<Listing> { out.setLicense(document.getString(MongoFieldNames.LICENSE_TYPE)); out.setFavoriteCount(document.getLong(MongoFieldNames.MARKETPLACE_FAVORITES)); out.setFoundationMember(document.getBoolean(MongoFieldNames.FOUNDATION_MEMBER_FLAG)); + out.setCategoryIds(document.getList(MongoFieldNames.CATEGORY_IDS, Integer.class)); // for nested document types, use the converters to safely transform into POJO out.setAuthors(document.getList(MongoFieldNames.LISTING_AUTHORS, Document.class).stream() @@ -126,7 +131,9 @@ public class ListingCodec implements CollectibleCodec<Listing> { .collect(Collectors.toList())); out.setVersions(document.getList(MongoFieldNames.LISTING_VERSIONS, Document.class).stream() .map(versionConverter::convert).collect(Collectors.toList())); - + out.setCategories(document.getList(MongoFieldNames.LISTING_CATEGORIES, Document.class).stream() + .map(categoryConverter::convert).collect(Collectors.toList())); + // convert date to epoch milli out.setCreationDate(document.getDate(MongoFieldNames.CREATION_DATE).toInstant().toEpochMilli()); out.setUpdateDate(document.getDate(MongoFieldNames.UPDATE_DATE).toInstant().toEpochMilli()); diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/CategoryConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/CategoryConverter.java new file mode 100644 index 0000000..74b55bf --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/CategoryConverter.java @@ -0,0 +1,39 @@ +/* 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.converters; + +import org.bson.Document; +import org.eclipsefoundation.marketplace.dto.Category; +import org.eclipsefoundation.marketplace.namespace.MongoFieldNames; + +/** + * @author martin + * + */ +public class CategoryConverter implements Converter<Category> { + + @Override + public Category convert(Document src) { + Category out = new Category(); + out.setId(src.getInteger(MongoFieldNames.DOCID)); + out.setName(src.getString(MongoFieldNames.CATEGORY_NAME)); + out.setUrl(src.getString(MongoFieldNames.CATEGORY_URL)); + out.setMarketIds(src.getList(MongoFieldNames.MARKET_IDS, Integer.class)); + return out; + } + + @Override + public Document convert(Category src) { + Document doc = new Document(); + doc.put(MongoFieldNames.DOCID, src.getId()); + doc.put(MongoFieldNames.CATEGORY_NAME, src.getName()); + doc.put(MongoFieldNames.CATEGORY_URL, src.getUrl()); + doc.put(MongoFieldNames.MARKET_IDS, src.getMarketIds()); + return doc; + } + +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/TabConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/TabConverter.java new file mode 100644 index 0000000..932a8bd --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/TabConverter.java @@ -0,0 +1,39 @@ +/* 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.converters; + +import org.bson.Document; +import org.eclipsefoundation.marketplace.dto.Tab; + +/** + * Converter implementation for the {@link Tab} object. + * + * @author Martin Lowe + */ +public class TabConverter implements Converter<Tab> { + + @Override + public Tab convert(Document src) { + Tab org = new Tab(); + + org.setTitle(src.getString("title")); + org.setType(src.getString("type")); + org.setUrl(src.getString("url")); + + return org; + } + + @Override + public Document convert(Tab src) { + Document doc = new Document(); + doc.put("title", src.getTitle()); + doc.put("type", src.getType()); + doc.put("url", src.getUrl()); + return doc; + } + +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java new file mode 100644 index 0000000..86e4663 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java @@ -0,0 +1,49 @@ +/* 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; + +import org.bson.conversions.Bson; +import org.eclipsefoundation.marketplace.dto.Catalog; +import org.eclipsefoundation.marketplace.model.RequestWrapper; + +/** + * Filter implementation for the Listing class. Checks the following fields: + * + * <ul> + + * </ul> + * + * @author Martin Lowe + */ +@ApplicationScoped +public class CatalogFilter implements DtoFilter<Catalog> { + + @Override + public List<Bson> getFilters(RequestWrapper qps) { + List<Bson> filters = new ArrayList<>(); + + + 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 new file mode 100644 index 0000000..71c92fe --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java @@ -0,0 +1,40 @@ +/* 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 java.util.Collections; +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; + +import org.bson.conversions.Bson; +import org.eclipsefoundation.marketplace.dto.Category; +import org.eclipsefoundation.marketplace.model.RequestWrapper; + +/** + * @author martin + * + */ +@ApplicationScoped +public class CategoryFilter implements DtoFilter<Category> { + + @Override + public List<Bson> getFilters(RequestWrapper qps) { + return Collections.emptyList(); + } + + @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/model/filter/DtoFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java similarity index 66% rename from src/main/java/org/eclipsefoundation/marketplace/model/filter/DtoFilter.java rename to src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java index c09da61..3050326 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/filter/DtoFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/DtoFilter.java @@ -4,12 +4,12 @@ * which is available at http://www.eclipse.org/legal/epl-v20.html, * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipsefoundation.marketplace.model.filter; +package org.eclipsefoundation.marketplace.dto.filter; import java.util.List; import org.bson.conversions.Bson; -import org.eclipsefoundation.marketplace.model.QueryParams; +import org.eclipsefoundation.marketplace.model.RequestWrapper; /** * @author martin @@ -17,7 +17,9 @@ import org.eclipsefoundation.marketplace.model.QueryParams; */ public interface DtoFilter<T> { - List<Bson> getFilters(QueryParams qps); + List<Bson> getFilters(RequestWrapper wrap); + + List<Bson> getAggregates(RequestWrapper wrap); Class<T> getType(); } diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/filter/ListingFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java similarity index 67% rename from src/main/java/org/eclipsefoundation/marketplace/model/filter/ListingFilter.java rename to src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java index 7a09325..b3bc730 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/filter/ListingFilter.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java @@ -4,39 +4,44 @@ * which is available at http://www.eclipse.org/legal/epl-v20.html, * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipsefoundation.marketplace.model.filter; +package org.eclipsefoundation.marketplace.dto.filter; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import javax.enterprise.context.ApplicationScoped; + import org.bson.conversions.Bson; import org.eclipsefoundation.marketplace.dto.Listing; -import org.eclipsefoundation.marketplace.model.QueryParams; +import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.namespace.DtoTableNames; import org.eclipsefoundation.marketplace.namespace.MongoFieldNames; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; +import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.Filters; /** * Filter implementation for the Listing class. Checks the following fields: * - * <ul> - * <li>platform_version - * <li>java_version - * <li>os - * <li>license_type - * <li>q - * <li>ids - * <li>tags - * </ul> + * <ul> + * <li>platform_version + * <li>java_version + * <li>os + * <li>license_type + * <li>q + * <li>ids + * <li>tags + * </ul> * * @author Martin Lowe */ +@ApplicationScoped public class ListingFilter implements DtoFilter<Listing> { @Override - public List<Bson> getFilters(QueryParams qps) { + public List<Bson> getFilters(RequestWrapper qps) { List<Bson> filters = new ArrayList<>(); // Listing ID check @@ -44,7 +49,7 @@ public class ListingFilter implements DtoFilter<Listing> { if (id.isPresent()) { filters.add(Filters.eq(MongoFieldNames.LISTING_ID, Long.valueOf(id.get()))); } - + // select by multiple IDs List<String> ids = qps.getParams(UrlParameterNames.IDS); if (!ids.isEmpty()) { @@ -56,7 +61,7 @@ public class ListingFilter implements DtoFilter<Listing> { if (licType.isPresent()) { filters.add(Filters.eq(MongoFieldNames.LICENSE_TYPE, licType.get())); } - + // handle version sub document selection List<Bson> versionFilters = new ArrayList<>(); // solution version - OS filter @@ -64,13 +69,14 @@ public class ListingFilter implements DtoFilter<Listing> { if (os.isPresent()) { versionFilters.add(Filters.eq("platforms", os.get())); } - // solution version - eclipse version + // solution version - eclipse version Optional<String> eclipseVersion = qps.getFirstParam(UrlParameterNames.ECLIPSE_VERSION); if (eclipseVersion.isPresent()) { versionFilters.add(Filters.eq("compatible_versions", 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 + // 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 = qps.getFirstParam(UrlParameterNames.JAVA_VERSION); if (javaVersion.isPresent()) { versionFilters.add(Filters.gte("min_java_version", javaVersion.get())); @@ -78,7 +84,7 @@ public class ListingFilter implements DtoFilter<Listing> { if (!versionFilters.isEmpty()) { filters.add(Filters.elemMatch("versions", Filters.and(versionFilters))); } - + // select by multiple tags List<String> tags = qps.getParams(UrlParameterNames.TAGS); if (!tags.isEmpty()) { @@ -90,10 +96,29 @@ public class ListingFilter implements DtoFilter<Listing> { if (text.isPresent()) { filters.add(Filters.text(text.get())); } - return filters; } + @Override + public List<Bson> getAggregates(RequestWrapper wrap) { + List<Bson> aggs = new ArrayList<>(); + // adds a $lookup aggregate, joining categories on categoryIDS as "categories" + aggs.add(Aggregates.lookup(DtoTableNames.CATEGORY.getTableName(), MongoFieldNames.CATEGORY_IDS, "id", + "categories")); + List<String> marketIdsRaw = wrap.getParams(UrlParameterNames.MARKET_IDS); + List<Integer> marketIds = new ArrayList<>(marketIdsRaw.size()); + try { + marketIdsRaw.forEach(s -> marketIds.add(Integer.valueOf(s))); + } catch (NumberFormatException e) { + // suppress + } + + if (!marketIds.isEmpty()) { + aggs.add(Aggregates.match(Filters.in("categories.market_ids", marketIds))); + } + return aggs; + } + @Override public Class<Listing> getType() { return Listing.class; diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CatalogCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CatalogCodecProvider.java new file mode 100644 index 0000000..02bf75e --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CatalogCodecProvider.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.Catalog; +import org.eclipsefoundation.marketplace.dto.codecs.CatalogCodec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides the {@link CatalogCodec} to MongoDB for conversions of + * {@link Catalog} objects. + * + * @author Martin Lowe + */ +public class CatalogCodecProvider implements CodecProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(CatalogCodecProvider.class); + + @SuppressWarnings("unchecked") + @Override + public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) { + if (clazz == Catalog.class) { + LOGGER.debug("Registering custom Listing class MongoDB codec"); + return (Codec<T>) new CatalogCodec(); + } + return null; + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CategoryCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CategoryCodecProvider.java new file mode 100644 index 0000000..1c99846 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CategoryCodecProvider.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.Category; +import org.eclipsefoundation.marketplace.dto.codecs.CategoryCodec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides the {@link CategoryCodec} to MongoDB for conversions of + * {@link Category} objects. + * + * @author Martin Lowe + */ +public class CategoryCodecProvider implements CodecProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(CategoryCodecProvider.class); + + @SuppressWarnings("unchecked") + @Override + public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) { + if (clazz == Category.class) { + LOGGER.debug("Registering custom Category class MongoDB codec"); + return (Codec<T>) new CategoryCodec(); + } + 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 a19b052..742d111 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java +++ b/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java @@ -8,16 +8,17 @@ package org.eclipsefoundation.marketplace.model; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.bson.BsonDocument; import org.bson.conversions.Bson; +import org.eclipsefoundation.marketplace.dto.filter.DtoFilter; import org.eclipsefoundation.marketplace.helper.SortableHelper; import org.eclipsefoundation.marketplace.helper.SortableHelper.Sortable; -import org.eclipsefoundation.marketplace.model.filter.ListingFilter; import org.eclipsefoundation.marketplace.namespace.UrlParameterNames; +import org.eclipsefoundation.marketplace.resource.AnnotationClassInjectionFilter; +import org.eclipsefoundation.marketplace.service.CachingService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,39 +26,31 @@ import com.mongodb.MongoClient; import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.Filters; -import io.quarkus.mongodb.FindOptions; - /** - * Wrapper for intializing MongoDB BSON filters, sort clauses, and document type - * when interacting with MongoDB. + * 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} * * @author Martin Lowe */ public class MongoQuery<T> { private static final Logger LOGGER = LoggerFactory.getLogger(MongoQuery.class); - private Class<T> docType; - private QueryParams qps; + private CachingService<List<T>> cache; + private RequestWrapper qps; + private DtoFilter<T> dtoFilter; + private Bson filter; private Bson sort; + private SortOrder order; + private List<Bson> aggregates; - // flag that indicates that aggregate should be used as filter - private boolean useAggregate = false; - - /** - * Intializes the query object using the document type and parameters passed. - * - * @param docType type of object being queried - * @param qps the parameters for the current request - */ - public MongoQuery(Class<T> docType, QueryParams qps) { - Objects.requireNonNull(docType); - Objects.requireNonNull(qps); - - this.docType = docType; + public MongoQuery(RequestWrapper qps, DtoFilter<T> dtoFilter, CachingService<List<T>> cache) { this.qps = qps; + this.dtoFilter = dtoFilter; + this.cache = cache; + this.aggregates = new ArrayList<>(); - // initializes the filters for the current query init(); } @@ -67,11 +60,17 @@ public class MongoQuery<T> { * to updated fields. */ public void init() { - List<Bson> filters = new ArrayList<>(); - filters.addAll(new ListingFilter().getFilters(qps)); + // clear old values if set to default + this.filter = null; + this.sort = null; + this.order= SortOrder.NONE; + this.aggregates = new ArrayList<>(); + // get the filters for the current DTO + List<Bson> filters = new ArrayList<>(); + filters.addAll(dtoFilter.getFilters(qps)); + // get fields that make up the required fields to enable pagination and check - Optional<String> lastOpt = qps.getFirstParam(UrlParameterNames.LAST_SEEN); Optional<String> sortOpt = qps.getFirstParam(UrlParameterNames.SORT); if (sortOpt.isPresent()) { String sortVal = sortOpt.get(); @@ -79,35 +78,22 @@ public class MongoQuery<T> { int idx = sortVal.indexOf(' '); // check if the sort string matches the RANDOM sort order if (SortOrder.RANDOM.equals(SortOrder.getOrderByName(sortVal))) { - this.useAggregate = true; + this.order = SortOrder.RANDOM; } else if (idx > 0) { - setSort(sortVal.substring(0, idx), sortVal.substring(idx + 1), lastOpt, filters); + setSort(sortVal.substring(0, idx), sortVal.substring(idx + 1), filters); } } + LOGGER.error("{}", filters); if (!filters.isEmpty()) { this.filter = Filters.and(filters); } + this.aggregates = dtoFilter.getAggregates(qps); + if (LOGGER.isDebugEnabled()) { LOGGER.debug("MongoDB query initialized with filter: {}", this.filter); } } - /** - * Generates a FindOptions object, which is required in the Quarkus - * implementation of MongoDB to set a sort filter. This set of options includes - * the filter generated for current parameters and the sort filter for the - * current query. - * - * @return a set of options for use in filtering documents in a MongoDB Find - * operation - */ - public FindOptions getFindOptions() { - FindOptions fOpts = new FindOptions(); - fOpts.filter(this.filter); - fOpts.sort(this.sort); - return fOpts; - } - /** * Generates a list of BSON documents representing an aggregation pipeline using * random sampling to get data. @@ -120,11 +106,19 @@ public class MongoQuery<T> { throw new IllegalStateException("Aggregate pipeline document limit must be greater than 0"); } List<Bson> out = new ArrayList<>(); + // add filters first if (filter != null) { out.add(Aggregates.match(filter)); } - out.add(Aggregates.sample(limit)); - + // add base aggregates (joins) + out.addAll(aggregates); + // add sample if we aren't sorting + if (sort == null || SortOrder.RANDOM.equals(order)) { + out.add(Aggregates.sample(limit)); + } + if (sort != null) { + out.add(Aggregates.sort(sort)); + } return out; } @@ -143,36 +137,38 @@ public class MongoQuery<T> { return -1; } - private void setSort(String sortField, String sortOrder, Optional<String> lastOpt, List<Bson> filters) { + private void setSort(String sortField, String sortOrder, List<Bson> filters) { + Optional<String> lastOpt = qps.getFirstParam(UrlParameterNames.LAST_SEEN); + List<Sortable<?>> fields = SortableHelper.getSortableFields(getDocType()); Optional<Sortable<?>> fieldContainer = SortableHelper.getSortableFieldByName(fields, sortField); + + LOGGER.error("{}:{}", sortField, sortOrder); if (fieldContainer.isPresent()) { - SortOrder so = SortOrder.getOrderByName(sortOrder); + this.order = SortOrder.getOrderByName(sortOrder); + LOGGER.error("{}", order); // add sorting query if the sortOrder matches a defined order - switch (so) { + switch (order) { case RANDOM: // TODO support for random, implement the following (in this order) // 1. Add not in clause that checks Cache for previously read objects // 2. Set useAggregate flag to true to signal to DAO to use aggregate selection // rather than traditional find - this.useAggregate = true; break; case ASCENDING: // if last seen is set, add a filter to shift the results if (lastOpt.isPresent()) { - filters.add( - Filters.gte(sortField, fieldContainer.get().castValue(lastOpt.get()))); + filters.add(Filters.gte(sortField, fieldContainer.get().castValue(lastOpt.get()))); } - this.sort = Filters.eq(sortField, so.getOrder()); + this.sort = Filters.eq(sortField, order.getOrder()); break; case DESCENDING: // if last seen is set, add a filter to shift the results if (lastOpt.isPresent()) { - filters.add( - Filters.lte(sortField, fieldContainer.get().castValue(lastOpt.get()))); + filters.add(Filters.lte(sortField, fieldContainer.get().castValue(lastOpt.get()))); } - this.sort = Filters.eq(sortField, so.getOrder()); + this.sort = Filters.eq(sortField, order.getOrder()); break; default: // intentionally empty, no sort @@ -194,41 +190,38 @@ public class MongoQuery<T> { * @return the docType */ public Class<T> getDocType() { - return docType; - } - - /** - * @param docType the docType to set - */ - public void setDocType(Class<T> docType) { - this.docType = docType; + return (Class<T>) qps.getAttribute(AnnotationClassInjectionFilter.ATTRIBUTE_NAME); } /** * @return the qps */ - public QueryParams getQps() { + public RequestWrapper getQps() { return qps; } /** * @param qps the qps to set */ - public void setQps(QueryParams qps) { + public void setQps(RequestWrapper qps) { this.qps = qps; } - public boolean isAggregate() { - return this.useAggregate; - } - @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("MongoQuery<").append(docType.getSimpleName()); - sb.append(">[query=").append(filter.toBsonDocument(BsonDocument.class, MongoClient.getDefaultCodecRegistry()).toJson()); + sb.append("MongoQuery<").append(getDocType().getSimpleName()); + sb.append(">[query="); + if (filter != null) { + sb.append(filter.toBsonDocument(BsonDocument.class, MongoClient.getDefaultCodecRegistry()).toJson()); + } + sb.append(",aggregates="); + getPipeline(1).forEach(bson -> { + sb.append(bson.toBsonDocument(BsonDocument.class, MongoClient.getDefaultCodecRegistry()).toJson()); + sb.append(','); + }); + sb.append(']'); - return sb.toString(); } diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/QueryParams.java b/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java similarity index 73% rename from src/main/java/org/eclipsefoundation/marketplace/model/QueryParams.java rename to src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java index 6d5c933..37ded5c 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/model/QueryParams.java +++ b/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java @@ -14,9 +14,14 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import javax.enterprise.context.RequestScoped; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.UriInfo; import org.apache.commons.lang3.StringUtils; +import org.jboss.resteasy.core.ResteasyContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Wrapper class for query parameter functionality, wrapping a Map of String to @@ -25,23 +30,24 @@ import org.apache.commons.lang3.StringUtils; * entered. * * @author Martin Lowe - * */ -public class QueryParams { +@RequestScoped +public class RequestWrapper { + private static final Logger LOGGER = LoggerFactory.getLogger(RequestWrapper.class); private static final String EMPTY_KEY_MESSAGE = "Key must not be null or blank"; - private String endpoint; private Map<String, List<String>> params; + + private UriInfo uriInfo; + private HttpServletRequest request; /** * Generates a wrapper around the * @param uriInfo */ - public QueryParams(UriInfo uriInfo) { - Objects.requireNonNull(uriInfo); - - this.endpoint = uriInfo.getPath(); - this.params = new HashMap<>(uriInfo.getQueryParameters(false)); + RequestWrapper() { + this.uriInfo = ResteasyContext.getContextData(UriInfo.class); + this.request = ResteasyContext.getContextData(HttpServletRequest.class); } /** @@ -57,7 +63,7 @@ public class QueryParams { throw new IllegalArgumentException(EMPTY_KEY_MESSAGE); } - List<String> vals = this.params.get(key); + List<String> vals = getParams().get(key); if (vals == null || vals.isEmpty()) { return Optional.empty(); } @@ -77,13 +83,13 @@ public class QueryParams { throw new IllegalArgumentException(EMPTY_KEY_MESSAGE); } - List<String> vals = this.params.get(key); + List<String> vals = getParams().get(key); if (vals == null || vals.isEmpty()) { return Collections.emptyList(); } return vals; } - + /** * Adds the given value for the given key, preserving previous values if they * exist. @@ -97,19 +103,7 @@ public class QueryParams { throw new IllegalArgumentException(EMPTY_KEY_MESSAGE); } Objects.requireNonNull(value); - this.params.computeIfAbsent(key, k -> new ArrayList<>()).add(value); - } - - /** - * Removes the value for the given key. - * - * @param key string key to add the value to, must not be null - */ - public void unsetParam(String key) { - if (StringUtils.isBlank(key)) { - throw new IllegalArgumentException(EMPTY_KEY_MESSAGE); - } - this.params.remove(key); + getParams().computeIfAbsent(key, k -> new ArrayList<>()).add(value); } /** @@ -118,7 +112,17 @@ public class QueryParams { * @return a copy of the internal param map */ public Map<String, List<String>> asMap() { - return new HashMap<>(params); + return new HashMap<>(getParams()); + } + + private Map<String, List<String>> getParams() { + if (params == null) { + params = new HashMap<>(); + if (uriInfo != null) { + params.putAll(uriInfo.getQueryParameters()); + } + } + return this.params; } /** @@ -126,6 +130,10 @@ public class QueryParams { * @return */ public String getEndpoint() { - return this.endpoint; + return uriInfo.getPath(); + } + + public Object getAttribute(String key) { + return request.getAttribute(key); } } diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/ResourceDataType.java b/src/main/java/org/eclipsefoundation/marketplace/model/ResourceDataType.java new file mode 100644 index 0000000..99f7258 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/model/ResourceDataType.java @@ -0,0 +1,24 @@ +/* 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.model; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * + * @author Martin Lowe + */ +@Retention(RUNTIME) +@Target(TYPE) +public @interface ResourceDataType { + + Class<?> value(); +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java index 4b7b48e..4c40791 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java +++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java @@ -6,6 +6,8 @@ */ package org.eclipsefoundation.marketplace.namespace; +import org.eclipsefoundation.marketplace.dto.Catalog; +import org.eclipsefoundation.marketplace.dto.Category; import org.eclipsefoundation.marketplace.dto.Listing; /** @@ -15,7 +17,9 @@ import org.eclipsefoundation.marketplace.dto.Listing; * */ public enum DtoTableNames { - LISTING(Listing.class, "listings"); + LISTING(Listing.class, "listings"), + CATEGORY(Category.class, "categories"), + CATALOG(Catalog.class, "catalogs"); private Class<?> baseClass; private String tableName; @@ -33,4 +37,8 @@ public enum DtoTableNames { } return null; } + + public String getTableName() { + return this.tableName; + } } diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/MongoFieldNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/MongoFieldNames.java index 51075f6..408e075 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/namespace/MongoFieldNames.java +++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/MongoFieldNames.java @@ -39,7 +39,24 @@ public final class MongoFieldNames { public static final String SUPPORT_PAGE_URL = "support_url"; public static final String LISTING_VERSIONS = "versions"; public static final String LISTING_TAGS = "tags"; + public static final String CATEGORY_IDS = "category_ids"; + public static final String LISTING_CATEGORIES = "categories"; + // catalog fields + public static final String CATALOG_TABS = "tabs"; + public static final String CATALOG_SELF_CONTAINED = "self_contained"; + public static final String CATALOG_SEARCH_ENABLED = "search_enabled"; + public static final String CATALOG_ICON = "icon"; + public static final String CATALOG_URL = "url"; + public static final String CATALOG_DESCRIPTION = "description"; + public static final String CATALOG_TITLE = "title"; + public static final String CATALOG_DEPENDENCIES_REPOSITORY = "dependencies_repository"; + + // category fields + public static final String MARKET_IDS = "market_ids"; + public static final String CATEGORY_NAME = "name"; + public static final String CATEGORY_URL = "url"; + private MongoFieldNames() { } } diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java index b17ce3f..6fc96cd 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java +++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java @@ -20,6 +20,7 @@ public final class UrlParameterNames { 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"; private UrlParameterNames() { } diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/AnnotationClassInjectionFilter.java b/src/main/java/org/eclipsefoundation/marketplace/resource/AnnotationClassInjectionFilter.java new file mode 100644 index 0000000..3eaba56 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/AnnotationClassInjectionFilter.java @@ -0,0 +1,48 @@ +/* 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.resource; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.Context; +import javax.ws.rs.ext.Provider; + +import org.eclipsefoundation.marketplace.model.ResourceDataType; + +/** + * Pre-processes the request to inject the datatype class name into the request. + * This is later used in reflection to circumvent type erasure within Query + * objects. + * + * @author Martin Lowe + */ +@Provider +public class AnnotationClassInjectionFilter implements ContainerRequestFilter { + public static final String ATTRIBUTE_NAME = "enclosed-data-type"; + + @Context + HttpServletRequest request; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + List<Object> resources = requestContext.getUriInfo().getMatchedResources(); + if (!resources.isEmpty()) { + // Quarkus compiles wrapper classes around beans, needs superclass call to get original class + Class<?> clazz = resources.get(0).getClass().getSuperclass(); + // get the resource data type + ResourceDataType[] dataTypes = clazz.getAnnotationsByType(ResourceDataType.class); + if (dataTypes.length > 0) { + ResourceDataType firstType = dataTypes[0]; + request.setAttribute(ATTRIBUTE_NAME, firstType.value()); + } + } + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java new file mode 100644 index 0000000..f2ff862 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java @@ -0,0 +1,85 @@ +/* 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.resource; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.eclipsefoundation.marketplace.dao.MongoDao; +import org.eclipsefoundation.marketplace.dto.Catalog; +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.model.ResourceDataType; +import org.eclipsefoundation.marketplace.service.CachingService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author martin + * + */ +@ResourceDataType(Catalog.class) +@Path("/catalogs") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@RequestScoped +public class CatalogResource { + private static final Logger LOGGER = LoggerFactory.getLogger(CatalogResource.class); + + @Inject + MongoDao dao; + @Inject + CachingService<List<Catalog>> cachingService; + @Inject + RequestWrapper params; + @Inject + DtoFilter<Catalog> dtoFilter; + + @GET + public Response select() { + MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter, cachingService); + // retrieve the possible cached object + Optional<List<Catalog>> cachedResults = cachingService.get("all", params, + () -> StreamHelper.awaitCompletionStage(dao.get(q))); + if (!cachedResults.isPresent()) { + LOGGER.error("Error while retrieving cached Catalogs"); + return Response.serverError().build(); + } + + // return the results as a response + return Response.ok(cachedResults.get()).build(); + } + + /** + * Endpoint for /Catalog/ to post a new Catalog to the persistence layer. + * + * @param catalog the Catalog object to insert into the database. + * @return response for the browser + */ + @POST + public Response postCatalog(Catalog catalog) { + MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter, cachingService); + // add the object, and await the result + StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(catalog))); + + // return the results as a response + return Response.ok().build(); + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java new file mode 100644 index 0000000..a9a7cf5 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.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.resource; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.eclipsefoundation.marketplace.dao.MongoDao; +import org.eclipsefoundation.marketplace.dto.Category; +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.model.ResourceDataType; +import org.eclipsefoundation.marketplace.service.CachingService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author martin + * + */ +@ResourceDataType(Category.class) +@Path("/categories") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@RequestScoped +public class CategoryResource { + private static final Logger LOGGER = LoggerFactory.getLogger(CategoryResource.class); + + @Inject + MongoDao dao; + @Inject + CachingService<List<Category>> cachingService; + @Inject + RequestWrapper params; + @Inject + DtoFilter<Category> dtoFilter; + + + @GET + public Response select() { + MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter, cachingService); + // retrieve the possible cached object + Optional<List<Category>> cachedResults = cachingService.get("all", params, + () -> StreamHelper.awaitCompletionStage(dao.get(q))); + if (!cachedResults.isPresent()) { + LOGGER.error("Error while retrieving cached Categorys"); + return Response.serverError().build(); + } + + // return the results as a response + return Response.ok(cachedResults.get()).build(); + } + + /** + * Endpoint for /Category/ to post a new Category to the persistence layer. + * + * @param category the Category object to insert into the database. + * @return response for the browser + */ + @POST + public Response postCategory(Category category) { + MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter, cachingService); + // add the object, and await the result + StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(category))); + + // return the results as a response + return Response.ok().build(); + } +} diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java index 6f8bc83..c2368b1 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java +++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java @@ -13,24 +13,24 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; import org.eclipsefoundation.marketplace.dao.MongoDao; import org.eclipsefoundation.marketplace.dto.Install; import org.eclipsefoundation.marketplace.dto.Listing; +import org.eclipsefoundation.marketplace.dto.filter.DtoFilter; import org.eclipsefoundation.marketplace.helper.StreamHelper; import org.eclipsefoundation.marketplace.model.MongoQuery; -import org.eclipsefoundation.marketplace.model.QueryParams; +import org.eclipsefoundation.marketplace.model.RequestWrapper; +import org.eclipsefoundation.marketplace.model.ResourceDataType; import org.eclipsefoundation.marketplace.namespace.MongoFieldNames; import org.eclipsefoundation.marketplace.service.CachingService; import org.jboss.resteasy.annotations.jaxrs.PathParam; @@ -42,22 +42,22 @@ import org.slf4j.LoggerFactory; * * @author Martin Lowe */ +@ResourceDataType(Listing.class) @Path("/listings") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) +@RequestScoped public class ListingResource { private static final Logger LOGGER = LoggerFactory.getLogger(ListingResource.class); @Inject MongoDao dao; - @Inject CachingService<List<Listing>> cachingService; - - @Context - private UriInfo uriInfo; - @Context - private HttpHeaders headers; + @Inject + RequestWrapper params; + @Inject + DtoFilter<Listing> dtoFilter; /** * Endpoint for /listing/ to retrieve all listings from the database along with @@ -68,10 +68,7 @@ public class ListingResource { */ @GET public Response select() { - // retrieve the query parameters, and add to a modifiable map - QueryParams params = new QueryParams(uriInfo); - - MongoQuery<Listing> q = new MongoQuery<>(Listing.class, params); + MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter, cachingService); // retrieve the possible cached object Optional<List<Listing>> cachedResults = cachingService.get("all", params, () -> StreamHelper.awaitCompletionStage(dao.get(q))); @@ -92,12 +89,10 @@ public class ListingResource { */ @POST public Response postListing(Listing listing) { - // retrieve the query parameters, and add to a modifiable map - QueryParams params = new QueryParams(uriInfo); + MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter,cachingService); // add the object, and await the result - StreamHelper - .awaitCompletionStage(dao.add(new MongoQuery<Listing>(Listing.class, params), Arrays.asList(listing))); + StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(listing))); // return the results as a response return Response.ok().build(); @@ -113,14 +108,12 @@ public class ListingResource { @GET @Path("/{listingId}") public Response select(@PathParam("listingId") int listingId) { - - // retrieve the query parameters, and add to a modifiable map - QueryParams params = new QueryParams(uriInfo); params.addParam(MongoFieldNames.LISTING_ID, Integer.toString(listingId)); + MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter, cachingService); // retrieve a cached version of the value for the current listing Optional<List<Listing>> cachedResults = cachingService.get(Integer.toString(listingId), params, - () -> StreamHelper.awaitCompletionStage(dao.get(new MongoQuery<Listing>(Listing.class, params)))); + () -> StreamHelper.awaitCompletionStage(dao.get(q))); if (!cachedResults.isPresent()) { LOGGER.error("Error while retrieving cached listing for ID {}", listingId); return Response.serverError().build(); @@ -144,7 +137,7 @@ public class ListingResource { } /** - + * * Endpoint for /listing/\<listingId\>/installs/\<version\> to retrieve install * metrics for a specific listing version from the database. * diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java b/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java index 7f32275..74a99f9 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java +++ b/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java @@ -11,7 +11,7 @@ import java.util.Set; import java.util.concurrent.Callable; import org.apache.commons.lang3.StringUtils; -import org.eclipsefoundation.marketplace.model.QueryParams; +import org.eclipsefoundation.marketplace.model.RequestWrapper; /** * Interface defining the caching service to be used within the application. @@ -31,7 +31,7 @@ public interface CachingService<T> { * @param callable a runnable that returns an object of type T * @return the cached result */ - Optional<T> get(String id, QueryParams params, Callable<? extends T> callable); + Optional<T> get(String id, RequestWrapper params, Callable<? extends T> callable); /** * Retrieves a set of cache keys available to the current cache. @@ -60,7 +60,7 @@ public interface CachingService<T> { * @param qps parameters associated with the request for information * @return the unique cache key for the request. */ - default String getCacheKey(String id, QueryParams qps) { + default String getCacheKey(String id, RequestWrapper qps) { StringBuilder sb = new StringBuilder(); sb.append('[').append(qps.getEndpoint()).append(']'); sb.append("id:").append(id); 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 e11eb3f..2d71b3a 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java +++ b/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java @@ -17,7 +17,7 @@ import javax.annotation.PostConstruct; import javax.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipsefoundation.marketplace.model.QueryParams; +import org.eclipsefoundation.marketplace.model.RequestWrapper; import org.eclipsefoundation.marketplace.service.CachingService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,7 +68,7 @@ public class GuavaCachingService<T> implements CachingService<T> { } @Override - public Optional<T> get(String id, QueryParams params, Callable<? extends T> callable) { + public Optional<T> get(String id, RequestWrapper params, Callable<? extends T> callable) { Objects.requireNonNull(id); Objects.requireNonNull(params); Objects.requireNonNull(callable); diff --git a/src/main/node/index.js b/src/main/node/index.js index fadd630..4f7af38 100644 --- a/src/main/node/index.js +++ b/src/main/node/index.js @@ -19,8 +19,11 @@ const lic_types = ["EPL-2.0","EPL-1.0","GPL"]; const platforms = ["windows","macos","linux"]; const eclipseVs = ["4.6","4.7","4.8","4.9","4.10","4.11","4.12"]; const javaVs = ["1.5", "1.6", "1.7", "1.8", "1.9", "1.10"]; +const categoryIds = [...Array(20).keys()]; +const marketIds = [...Array(5).keys()]; -createEntry(0); +createListing(0); +createCategory(0); function shuff(arr) { var out = Array.from(arr); @@ -44,16 +47,25 @@ function splice(arr) { return out; } -function createEntry(count) { +function createListing(count) { if (count >= max) { return; } axios.post(argv.s+"/listings/", generateJSON(count++)) - .then(createEntry(count)) + .then(createListing(count)) .catch(err => console.log(err)); } +function createCategory(count) { + if (count >= 20) { + return; + } + + axios.post(argv.s+"/categories/", generateCategoryJSON(count++)) + .then(createCategory(count)) + .catch(err => console.log(err)); +} function generateJSON(id) { var solutions = []; @@ -100,6 +112,16 @@ function generateJSON(id) { "url": "" } ], - "versions": solutions + "versions": solutions, + "category_ids": splice(categoryIds) + }; +} + +function generateCategoryJSON(id) { + return { + "id": id, + "name": randomWords({exactly:1, wordsPerString:Math.ceil(Math.random()*4)})[0], + "url": "https://www.eclipse.org", + "market_ids": [splice(marketIds)[0]] }; } diff --git a/src/main/resources/secret.sample.properties b/src/main/resources/secret.sample.properties deleted file mode 100644 index 0ac971f..0000000 --- a/src/main/resources/secret.sample.properties +++ /dev/null @@ -1,4 +0,0 @@ -quarkus.mongodb.credentials.password=example -quarkus.oauth2.client-secret=example - -eclipse.secret.token=example diff --git a/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java b/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java index 41d65df..fc6a73a 100644 --- a/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java +++ b/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java @@ -14,12 +14,14 @@ 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 0eda27a..0da665b 100644 --- a/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java +++ b/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java @@ -10,24 +10,26 @@ import java.util.Optional; import javax.inject.Inject; -import org.eclipsefoundation.marketplace.model.QueryParams; -import org.jboss.resteasy.specimpl.ResteasyUriInfo; +import org.eclipsefoundation.marketplace.model.RequestWrapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.quarkus.test.junit.DisabledOnSubstrate; import io.quarkus.test.junit.QuarkusTest; /** * @author martin * */ +@DisabledOnSubstrate @QuarkusTest public class GuavaCachingServiceTest { @Inject GuavaCachingService<Object> gcs; - private QueryParams sample = new QueryParams(new ResteasyUriInfo("","")); + @Inject + RequestWrapper sample; /** * Clear the cache before every test diff --git a/src/test/resources/secret.properties b/src/test/resources/secret.properties deleted file mode 100644 index 4945bcb..0000000 --- a/src/test/resources/secret.properties +++ /dev/null @@ -1,2 +0,0 @@ -sample.secret.property=secret-value -eclipse.secret.token=example -- GitLab