diff --git a/README.md b/README.md
index 067bfb1245370dd14ee2864e5a071ae7145f00ad..7198173b25d9e6bbe1a77c6a88156826c59914d7 100644
--- a/README.md
+++ b/README.md
@@ -8,24 +8,27 @@ 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 (Docker instructions below)
+1. Running instance of MariaDB (Docker instructions below)
 1. GraalVM (for compilation of native-image)
 
 ### Optional requirements
 
 1. Node JS + NPM (if using sample data script)
+1. OAuth 2.0 server
 
 ## Configuration
 
 This section will outline configuration values that need to be checked and updated to run the API in a local environment. Unless otherwise stated, all values to be updated will be in `./src/main/resources/application.properties`.
 
-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. In order to properly detect MariaDB, a connection string needs to be updated. `quarkus.datasource.url` designates the location of MariaDB to quarkus in the form of `jdbc:mariadb://<host>:<port>/<db>`. By default, this value points at `jdbc:mariadb://localhost:3306/mpc_db`, the default location for local installs of MariaDB with a database of `mpc_db`.
+1. Update `quarkus.datasource.username` to be a known user with write permissions to MariaDB instance.
 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. Update `quarkus.datasource.password` to be the password for the MariaDB user in the newly created `secret.properties` file.
+1. Log in to the MariaDB instance and ensure that the database defined in the JDBC string exists. By default, the name of the database is `mpc_db`. This database can be created using the command `CREATE DATABASE mpc_db;`. 
 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. 
-1. In order to protect endpoints for write operations, an introspection endpoint has been configured to validate OAuth tokens. This introspection endpoint should match the requirements set out by the OAuth group for such endpoints. The URL should be set in `quarkus.oauth2.introspection-url`.
-1. As part of the set up of this client, an OAuth client ID and secret need to be defined in the `secret.properties` file. These values should be set in `quarkus.oauth2.client-id` and `quarkus.oauth2.client-secret`. These are required for introspection to avoid token fishing attempts.
+1. In order to protect endpoints for write operations, an introspection endpoint has been configured to validate OAuth tokens. This introspection endpoint should match the requirements set out by the OAuth group for such endpoints. The URL should be set in `quarkus.oauth2.introspection-url`.  
+    * A property meant for development purposes has been added to this stack to bypass OAuth calls. If set, all calls will return as if authenticated as an admin. The property and value `eclipse.oauth.override=true` can be set in the `application.properties` file to enable this feature.
+1. As part of the set up of this client, an OAuth client ID and secret should be defined in the `secret.properties` file. These values should be set in `quarkus.oauth2.client-id` and `quarkus.oauth2.client-secret`. These are required for introspection to avoid token fishing attempts.
 
 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.
 
@@ -75,92 +78,37 @@ The Docker build-arg `GRAALVM_HOME` must be configured on the `docker build` com
 
 ## 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.
+For ease of use, a script has been created to load sample data into the database instance using Node JS and a running instance of the API. This script will load a large amount of listings into the running database using the API for use in testing different queries without having to retrieve real world data.
 
 1. In root of project, run `npm install` to retrieve dependencies for sample data loader script.
-1. Run `npm run-script load-listings -- -s <api-url>`, replacing `<api-url>` with the URL of a running instance of this API (e.g. http://localhost:8090). This should take a couple of moments, as by default the loader will load 5000 dummy entries into MongoDB. This can be changed using a `-c` flag followed by the number of entries to be created. 
+1. Run `npm run-script load-listings -- -s <api-url>`, replacing `<api-url>` with the URL of a running instance of this API (e.g. http://localhost:8090). This should take a couple of moments, as by default the loader will load dummy entries into the dataset. Options for this script can be seen using the help command.
 
-## Dockerizing MongoDB
+## Dockerizing MariaDB
 
-The following command can be used in any environment with Docker fully configured to run MongoDB with some basic settings. The password and username can be configured to be more secure, so long as the application.properties and secret.properties are updated to reflect the changes (`quarkus.mongodb.credentials.username` and `quarkus.mongodb.credentials.password` respectively).  
+For a simple image, using the command `docker run --name mpc-api_mariadb_1 -e MYSQL_ROOT_PASSWORD=my-secret-pw -p 3306:3306 -d mariadb:latest` will create a Docker container for mariadb. For the current use case, no other applications need to run in sequence for development and a docker file can be skipped. The password and port can be changed to suit needs, so long as it is reflected within the configuration for the JDBC connection string. For more configurable settings, please refer to the [Docker Hub page for the image](https://hub.docker.com/_/mariadb).
 
-```
-docker run -p 127.0.0.1:27017:27017/tcp \
-    --env MONGO_INITDB_ROOT_USERNAME=root \
-    --env MONGO_INITDB_ROOT_PASSWORD=example \
-    mongo
-```
-
-### Additional MongoDB commands needed:
+### Additional MariaDB commands needed:
 
-```
-use mpc;
-db.listings.createIndex(
-     {
-       body:"text", 
-       teaser:"text",
-       title:"text"
-     },
-     {
-      weights: {
-       title: 10,
-       teaser: 3
-     },
-     name: "TextIndex"
-  });
-db.listings.createIndex({ listing_id: 1 }, {name: "lid"});
-db.listings.createIndex({ changed: 1 }, {name: "updated"});
-db.listings.createIndex({ created: 1 }, {name: "created"});
-db.listings.createIndex({ license_type: 1 }, {name: "lic_type"});
-db.listings.createIndex({ category_ids: 1 }, {name: "cats"});
-db.listings.createIndex({ market_ids: 1 }, {name: "mkts"});
-db.listing_versions.createIndex({ listing_id: 1 }, {name: "lid"});
-db.listing_versions.createIndex({ compatible_versions: 1 }, {name: "compat"});
-db.listing_versions.createIndex({ min_java_version: 1 }, {name: "min_java"});
-db.listing_versions.createIndex({ platforms: 1 }, {name: "platforms"});
-db.installs.createIndex({listing_id: 1 }, {name: "lid"});
-```
+TODO
 
 ### Creating a backup
 
-In order to create a backup of an existing data set, read access to the MongoDB instance is required, and is best done with shell access.  
-
-To use the following commands, replace `<user>` with the name of the user with write access to the MPC data set. Each of these commands will initiate a password challenge request, and cannot be together in a script easily. Adding the `--password=<>` is not recommended as it is a vulnerability as the logs will remain on the server with plain text passwords. Additionally, the `<date>` placeholder should be replaced with whatever date stamp has been set in the snapshot .gz files.  
-
-In container:  
+Data for installs has been separated from the raw table data as keeping it separate is better for backing up. As a separate entity with no direct relationship to any other table, it doesn't need to be locked to prevent data inconsistencies/mismatches. Since Metric Period and Install Metrics are transient tables that are populated by procedures, they don't need to be backed up and have been ignored from the dump. Below is a set of sample commands to backup the data:
 
 ```
-mkdir snapshot
-mongodump --username=<user> --archive=snapshot/listings_<date>.gz --db=mpc --collection=listings --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --gzip
-mongodump --username=<user> --archive=snapshot/markets_<date>.gz --db=mpc --collection=markets --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --gzip
-mongodump --username=<user> --archive=snapshot/categories_<date>.gz --db=mpc --collection=categories --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --gzip
-mongodump --username=<user> --archive=snapshot/catalogs_<date>.gz --db=mpc --collection=catalogs --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --gzip
-mongodump --username=<user> --archive=snapshot/listing_versions_<date>.gz --db=mpc --collection=listing_versions --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --gzip
+mysqldump --user=root --password --lock-tables mpc_db --ignore-table=mpc_db.Install --ignore-table=mpc_db.MetricPeriod --ignore-table=mpc_db.InstallMetrics --ignore-table=mpc_db.InstallMetrics_MetricPeriod > /data/backup/db.sql
+mysqldump --user=root --password mpc_db Install > /data/backup/db_installs.sql
 ```
 
-If using Docker to host:
-`docker cp dev_mongo_1:/snapshot ./snapshot`
-
 ### Restoring from backup
 
-In order to restore data from backup snapshot, write access to the MongoDB instance is required, and is best done with shell access. As `mongorestore` doesn't update or overwrite records, the existing data set should be wiped before proceeding with restoring from backup snapshots. 
-
-To use the following commands, replace `<user>` with the name of the user with write access to the MPC data set. Each of these commands will initiate a password challenge request, and cannot be together in a script easily. Adding the `--password=<>` is not recommended as it is a vulnerability as the logs will remain on the server with plain text passwords. Additionally, the `<date>` placeholder should be replaced with whatever date stamp has been set in the snapshot .gz files.
-
-From host machine to Docker:
-`docker cp ./snapshot dev_mongo_1:/`
-
-In container:  
+TODO
 
 ```
-mongorestore --username=<user> --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --nsInclude=mpc.* --gzip --archive=snapshot/listings_<date>.gz
-mongorestore --username=<user> --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --nsInclude=mpc.* --gzip --archive=snapshot/markets_<date>.gz
-mongorestore --username=<user> --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --nsInclude=mpc.* --gzip --archive=snapshot/catalogs_<date>.gz
-mongorestore --username=<user> --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --nsInclude=mpc.* --gzip --archive=snapshot/categories_<date>.gz
-mongorestore --username=<user> --authenticationDatabase admin --authenticationMechanism SCRAM-SHA-256 --nsInclude=mpc.* --gzip --archive=snapshot/listing_versions_<date>.gz
+mysql --user=root --password mpc_db < /data/backup/db.sql
+mysql --user=root --password mpc_db Install < /data/backup/db_installs.sql
 ```
 
-
 ## Copyright 
 
 Copyright (c) 2019 Eclipse Foundation and others.
diff --git a/config/sample.secret.properties b/config/sample.secret.properties
index 8c0310acf3793a7cecb604a60b395158e18324ab..46d75f218fa5ac82381cca2f6e2a1844038a81f8 100644
--- a/config/sample.secret.properties
+++ b/config/sample.secret.properties
@@ -1,4 +1,4 @@
-quarkus.mongodb.credentials.password=sample
+quarkus.datasource.password=sample-pw
 quarkus.oauth2.client-id=sample
 quarkus.oauth2.client-secret=sample
 
diff --git a/pom.xml b/pom.xml
index 718611a928c247c55499aa82e469edff0864550b..1be700cd66c8acc91e067bf55c7f01d42dc7bedd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
 	<properties>
 		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 		<surefire-plugin.version>2.22.0</surefire-plugin.version>
-		<quarkus.version>0.28.0</quarkus.version>
+		<quarkus.version>1.3.0.Final</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>
@@ -47,10 +47,19 @@
 			<artifactId>rest-assured</artifactId>
 			<scope>test</scope>
 		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-mongodb-client</artifactId>
-		</dependency>
+	    <dependency>
+	        <groupId>io.quarkus</groupId>
+	        <artifactId>quarkus-hibernate-orm</artifactId>
+	    </dependency>
+	    <dependency>
+	        <groupId>org.hibernate</groupId>
+	        <artifactId>hibernate-search-elasticsearch</artifactId>
+	        <version>5.11.5.Final</version>
+	    </dependency>
+	    <dependency>
+	        <groupId>io.quarkus</groupId>
+	        <artifactId>quarkus-jdbc-mariadb</artifactId>
+	    </dependency>
 		<dependency>
 			<groupId>io.quarkus</groupId>
 			<artifactId>quarkus-undertow</artifactId>
@@ -92,6 +101,12 @@
 			<artifactId>commons-text</artifactId>
 			<version>1.8</version>
 		</dependency>
+		<!-- https://mvnrepository.com/artifact/org.apache.solr/solr-solrj -->
+		<dependency>
+		    <groupId>org.apache.solr</groupId>
+		    <artifactId>solr-solrj</artifactId>
+		    <version>8.4.1</version>
+		</dependency>
 
 		<!-- Caching -->
 		<dependency>
diff --git a/snapshot/catalogs_2019-12-18.gz b/snapshot/catalogs_2019-12-18.gz
deleted file mode 100644
index c244835332054776ac5315e4630c6811639ef19d..0000000000000000000000000000000000000000
Binary files a/snapshot/catalogs_2019-12-18.gz and /dev/null differ
diff --git a/snapshot/categories_2019-12-18.gz b/snapshot/categories_2019-12-18.gz
deleted file mode 100644
index 1ff859730e0ba0997a132d2521c1105e973dffbc..0000000000000000000000000000000000000000
Binary files a/snapshot/categories_2019-12-18.gz and /dev/null differ
diff --git a/snapshot/listing_versions_2019-12-18.gz b/snapshot/listing_versions_2019-12-18.gz
deleted file mode 100644
index 6fc71cd28a9cb276145f8db3fc6ae7bbe91c572f..0000000000000000000000000000000000000000
Binary files a/snapshot/listing_versions_2019-12-18.gz and /dev/null differ
diff --git a/snapshot/listings_2019-12-18.gz b/snapshot/listings_2019-12-18.gz
deleted file mode 100644
index c3e426ceb7233ad0f33709d46e5525d09e31d229..0000000000000000000000000000000000000000
Binary files a/snapshot/listings_2019-12-18.gz and /dev/null differ
diff --git a/snapshot/markets_2019-12-18.gz b/snapshot/markets_2019-12-18.gz
deleted file mode 100644
index 369c1cb3a9d0b575576764a1c2053d8bd77a1186..0000000000000000000000000000000000000000
Binary files a/snapshot/markets_2019-12-18.gz and /dev/null differ
diff --git a/snapshot/snapshot_prod_2020-06-15.tar.gz b/snapshot/snapshot_prod_2020-06-15.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..59198c47cdd00adc163937bc7d130edc6357b91d
Binary files /dev/null and b/snapshot/snapshot_prod_2020-06-15.tar.gz differ
diff --git a/snapshot/snapshot_rnd_2020-06-15.tar.gz b/snapshot/snapshot_rnd_2020-06-15.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..a6f7766d7f962e224fa6ec040aaa83b0a61b9f03
Binary files /dev/null and b/snapshot/snapshot_rnd_2020-06-15.tar.gz differ
diff --git a/src/main/java/org/eclipsefoundation/marketplace/config/JsonBConfig.java b/src/main/java/org/eclipsefoundation/core/config/JsonBConfig.java
similarity index 95%
rename from src/main/java/org/eclipsefoundation/marketplace/config/JsonBConfig.java
rename to src/main/java/org/eclipsefoundation/core/config/JsonBConfig.java
index cb868867ddcffb024a17a5d0c30f87457baf041b..ddfc5f040f370d9a3cf400338a6e69f8961d5b3a 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/config/JsonBConfig.java
+++ b/src/main/java/org/eclipsefoundation/core/config/JsonBConfig.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.config;
+package org.eclipsefoundation.core.config;
 
 import javax.json.bind.Jsonb;
 import javax.json.bind.JsonbBuilder;
diff --git a/src/main/java/org/eclipsefoundation/core/config/RoleAugmentor.java b/src/main/java/org/eclipsefoundation/core/config/RoleAugmentor.java
new file mode 100644
index 0000000000000000000000000000000000000000..5a8bc4f5c39cefdcf4edf759a8db18f82cf92c3d
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/core/config/RoleAugmentor.java
@@ -0,0 +1,55 @@
+package org.eclipsefoundation.core.config;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import javax.enterprise.context.ApplicationScoped;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.SecurityIdentityAugmentor;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+
+/**
+ * Custom override for production that can be enabled to set user roles to
+ * include the role set in the property, defaulting to admin access.
+ * 
+ * @author Martin Lowe
+ */
+@ApplicationScoped
+public class RoleAugmentor implements SecurityIdentityAugmentor {
+
+	// properties that allow this functionality to be configured
+	@ConfigProperty(name = "eclipse.oauth.override", defaultValue = "false")
+	boolean overrideRole;
+	@ConfigProperty(name = "eclipse.oauth.override.role", defaultValue = "marketplace_admin_access")
+	String overrideRoleName;
+
+	@Override
+	public int priority() {
+		return 0;
+	}
+
+	@Override
+	public CompletionStage<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
+		// create a future to contain the original/updated role
+		CompletableFuture<SecurityIdentity> cs = new CompletableFuture<>();
+		if (overrideRole) {
+			// create a new builder and copy principal, attributes, credentials and roles
+			// from the original
+			QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder()
+					.setPrincipal(identity.getPrincipal()).addAttributes(identity.getAttributes())
+					.addCredentials(identity.getCredentials()).addRoles(identity.getRoles());
+
+			// add custom role source here
+			builder.addRole(overrideRoleName);
+			// put the updated role in the future
+			cs.complete(builder.build());
+		} else {
+			// put the unmodified identity in the future
+			cs.complete(identity);
+		}
+		return cs;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/marketplace/config/SecretConfigSource.java b/src/main/java/org/eclipsefoundation/core/config/SecretConfigSource.java
similarity index 92%
rename from src/main/java/org/eclipsefoundation/marketplace/config/SecretConfigSource.java
rename to src/main/java/org/eclipsefoundation/core/config/SecretConfigSource.java
index 5c620114e266a87dc95452f3298f34ee21c59755..4ece893fc451a6d53301f21bbce702806be445f6 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/config/SecretConfigSource.java
+++ b/src/main/java/org/eclipsefoundation/core/config/SecretConfigSource.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.config;
+package org.eclipsefoundation.core.config;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -42,10 +42,10 @@ public class SecretConfigSource implements ConfigSource {
 			String secretPath = System.getProperty("config.secret.path");
 			// Fallback to checking env if not set in JVM
 			if (StringUtils.isEmpty(secretPath)) {
-				secretPath = System.getenv("config.secret.path");
+				secretPath = System.getenv("CONFIG_SECRET_PATH");
 			}
 			if (StringUtils.isEmpty(secretPath)) {
-				LOGGER.error("Configuration 'config.secret.path' not set, cannot generate secret properties.");
+				LOGGER.warn("Configuration 'config.secret.path' not set, cannot generate secret properties.");
 				return this.secrets;
 			}
 			// load the secrets file in
diff --git a/src/main/java/org/eclipsefoundation/marketplace/exception/MaintenanceException.java b/src/main/java/org/eclipsefoundation/core/exception/MaintenanceException.java
similarity index 88%
rename from src/main/java/org/eclipsefoundation/marketplace/exception/MaintenanceException.java
rename to src/main/java/org/eclipsefoundation/core/exception/MaintenanceException.java
index f745228a88bac30fb48ff99e5aca3958608208f9..aef21a945ffdb032c20bc4b1386e66ac420efbb6 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/exception/MaintenanceException.java
+++ b/src/main/java/org/eclipsefoundation/core/exception/MaintenanceException.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.exception;
+package org.eclipsefoundation.core.exception;
 
 /**
  * @author martin
diff --git a/src/main/java/org/eclipsefoundation/marketplace/health/BaseApplicationHealthCheck.java b/src/main/java/org/eclipsefoundation/core/health/BaseApplicationHealthCheck.java
similarity index 94%
rename from src/main/java/org/eclipsefoundation/marketplace/health/BaseApplicationHealthCheck.java
rename to src/main/java/org/eclipsefoundation/core/health/BaseApplicationHealthCheck.java
index 4f9476b37f3ea0659ace9a4de35b4245d3e2ed64..ad27e8c26da376159fa8fe2142a58abdc4e91fe2 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/health/BaseApplicationHealthCheck.java
+++ b/src/main/java/org/eclipsefoundation/core/health/BaseApplicationHealthCheck.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.health;
+package org.eclipsefoundation.core.health;
 
 import javax.enterprise.context.ApplicationScoped;
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/health/BeanHealth.java b/src/main/java/org/eclipsefoundation/core/health/BeanHealth.java
similarity index 95%
rename from src/main/java/org/eclipsefoundation/marketplace/health/BeanHealth.java
rename to src/main/java/org/eclipsefoundation/core/health/BeanHealth.java
index 46a754b13e10a7395c0c6dd9eb3d4b231957cab7..4864cc655435fde55be3a31bf00b9b485e368c11 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/health/BeanHealth.java
+++ b/src/main/java/org/eclipsefoundation/core/health/BeanHealth.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.health;
+package org.eclipsefoundation.core.health;
 
 import org.eclipse.microprofile.health.HealthCheckResponse;
 import org.eclipse.microprofile.health.HealthCheckResponse.State;
diff --git a/src/main/java/org/eclipsefoundation/marketplace/helper/DateTimeHelper.java b/src/main/java/org/eclipsefoundation/core/helper/DateTimeHelper.java
similarity index 97%
rename from src/main/java/org/eclipsefoundation/marketplace/helper/DateTimeHelper.java
rename to src/main/java/org/eclipsefoundation/core/helper/DateTimeHelper.java
index 77c743faee67d0f87268fef8cdd7629805ddc473..b1b93d97a0363facafd8d298a9ba535b695d02fe 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/helper/DateTimeHelper.java
+++ b/src/main/java/org/eclipsefoundation/core/helper/DateTimeHelper.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.helper;
+package org.eclipsefoundation.core.helper;
 
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
diff --git a/src/main/java/org/eclipsefoundation/marketplace/helper/ResponseHelper.java b/src/main/java/org/eclipsefoundation/core/helper/ResponseHelper.java
similarity index 94%
rename from src/main/java/org/eclipsefoundation/marketplace/helper/ResponseHelper.java
rename to src/main/java/org/eclipsefoundation/core/helper/ResponseHelper.java
index d991e7c015e7a902cf6af093fb468f5267997ace..1ca5701d4ee34234a3a1a150acc396aa79e32459 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/helper/ResponseHelper.java
+++ b/src/main/java/org/eclipsefoundation/core/helper/ResponseHelper.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.helper;
+package org.eclipsefoundation.core.helper;
 
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
@@ -20,8 +20,8 @@ import javax.ws.rs.core.CacheControl;
 import javax.ws.rs.core.Response;
 import javax.xml.bind.DatatypeConverter;
 
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
-import org.eclipsefoundation.marketplace.service.CachingService;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 
 /**
  * Helper class that transforms data into a response usable for the RESTeasy
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/Error.java b/src/main/java/org/eclipsefoundation/core/model/Error.java
similarity index 96%
rename from src/main/java/org/eclipsefoundation/marketplace/model/Error.java
rename to src/main/java/org/eclipsefoundation/core/model/Error.java
index e6fca8e555df037a4b229b0b7b2cf11b5c6b34ec..70e01c96a2d1be1408d7df7769381730e4df5257 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/model/Error.java
+++ b/src/main/java/org/eclipsefoundation/core/model/Error.java
@@ -7,7 +7,7 @@
  * 
  * SPDX-License-Identifier: EPL-2.0
 */
-package org.eclipsefoundation.marketplace.model;
+package org.eclipsefoundation.core.model;
 
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java b/src/main/java/org/eclipsefoundation/core/model/RequestWrapper.java
similarity index 94%
rename from src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java
rename to src/main/java/org/eclipsefoundation/core/model/RequestWrapper.java
index 0fd4ce782a86eeda26aa84398d242cd9f4749f92..b76f300dbaa12e7a0bc0bd5fe31a59c337ffdf57 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/model/RequestWrapper.java
+++ b/src/main/java/org/eclipsefoundation/core/model/RequestWrapper.java
@@ -4,8 +4,9 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.model;
+package org.eclipsefoundation.core.model;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -21,10 +22,10 @@ import javax.servlet.http.HttpServletResponse;
 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.eclipsefoundation.core.namespace.DeprecatedHeader;
+import org.eclipsefoundation.core.namespace.RequestHeaderNames;
+import org.eclipsefoundation.core.request.CacheBypassFilter;
+import org.eclipsefoundation.marketplace.model.UserAgent;
 import org.jboss.resteasy.core.ResteasyContext;
 
 /**
@@ -39,7 +40,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 QueryParameters params;
+	private Map<String, List<String>> params;
 
 	private UriInfo uriInfo;
 	private HttpServletRequest request;
diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/DeprecatedHeader.java b/src/main/java/org/eclipsefoundation/core/namespace/DeprecatedHeader.java
similarity index 97%
rename from src/main/java/org/eclipsefoundation/marketplace/namespace/DeprecatedHeader.java
rename to src/main/java/org/eclipsefoundation/core/namespace/DeprecatedHeader.java
index 65d1f3bf6d2049b49002c484c90cb5ff870f3967..9524b40a689e29e7022e0e48ee5bba7e4f374c05 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/namespace/DeprecatedHeader.java
+++ b/src/main/java/org/eclipsefoundation/core/namespace/DeprecatedHeader.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.namespace;
+package org.eclipsefoundation.core.namespace;
 
 import java.util.Date;
 import java.util.Objects;
diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/RequestHeaderNames.java b/src/main/java/org/eclipsefoundation/core/namespace/RequestHeaderNames.java
similarity index 91%
rename from src/main/java/org/eclipsefoundation/marketplace/namespace/RequestHeaderNames.java
rename to src/main/java/org/eclipsefoundation/core/namespace/RequestHeaderNames.java
index 78f9e8ba1a1212e8a8fccd343e15c54b7129a752..ed1ab76d9fd53439b855607a16720d9bf0513c08 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/namespace/RequestHeaderNames.java
+++ b/src/main/java/org/eclipsefoundation/core/namespace/RequestHeaderNames.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.namespace;
+package org.eclipsefoundation.core.namespace;
 
 /**
  * Namespace for Eclipse-specific header names used in application.
diff --git a/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java b/src/main/java/org/eclipsefoundation/core/request/CacheBypassFilter.java
similarity index 94%
rename from src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java
rename to src/main/java/org/eclipsefoundation/core/request/CacheBypassFilter.java
index 7a251fbbaca65da10e787ee29c590fd19c20aef2..b8f71f7d87161f8a30f504dbee062e7404538bfa 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/request/CacheBypassFilter.java
+++ b/src/main/java/org/eclipsefoundation/core/request/CacheBypassFilter.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.request;
+package org.eclipsefoundation.core.request;
 
 import java.io.IOException;
 
@@ -15,8 +15,8 @@ import javax.ws.rs.container.ContainerRequestFilter;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.ext.Provider;
 
-import org.eclipsefoundation.marketplace.model.SortOrder;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
+import org.eclipsefoundation.persistence.model.SortOrder;
 
 /**
  * Checks passed parameters and if any match one of the criteria for bypassing
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/CacheResource.java b/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java
similarity index 91%
rename from src/main/java/org/eclipsefoundation/marketplace/resource/CacheResource.java
rename to src/main/java/org/eclipsefoundation/core/resource/CacheResource.java
index 8f8f985e6149a01b72b614c23b8d8a5d46a437ae..e3b9b00c532947c73e00bb5f1c2d8a7a6cb1c338 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/CacheResource.java
+++ b/src/main/java/org/eclipsefoundation/core/resource/CacheResource.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.resource;
+package org.eclipsefoundation.core.resource;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -23,8 +23,8 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipsefoundation.marketplace.namespace.RequestHeaderNames;
-import org.eclipsefoundation.marketplace.service.CachingService;
+import org.eclipsefoundation.core.namespace.RequestHeaderNames;
+import org.eclipsefoundation.core.service.CachingService;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 
 /**
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/JsonBMapper.java b/src/main/java/org/eclipsefoundation/core/resource/mapper/JsonBMapper.java
similarity index 90%
rename from src/main/java/org/eclipsefoundation/marketplace/resource/mapper/JsonBMapper.java
rename to src/main/java/org/eclipsefoundation/core/resource/mapper/JsonBMapper.java
index 8d40c3e3c249d9b5e1758707bfe2bc32a13c4f63..ae2ca9fe40931370e566738637cdd40d1e5c17b6 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/JsonBMapper.java
+++ b/src/main/java/org/eclipsefoundation/core/resource/mapper/JsonBMapper.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.resource.mapper;
+package org.eclipsefoundation.core.resource.mapper;
 
 import javax.ws.rs.ProcessingException;
 import javax.ws.rs.core.Response;
@@ -12,7 +12,7 @@ import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
 
-import org.eclipsefoundation.marketplace.model.Error;
+import org.eclipsefoundation.core.model.Error;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/MaintenanceMapper.java b/src/main/java/org/eclipsefoundation/core/resource/mapper/MaintenanceMapper.java
similarity index 85%
rename from src/main/java/org/eclipsefoundation/marketplace/resource/mapper/MaintenanceMapper.java
rename to src/main/java/org/eclipsefoundation/core/resource/mapper/MaintenanceMapper.java
index d7621373087d852e21dca58068ef6bf228f97768..2d7df2fffca15d97b3bf40d0324b11768c57865c 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/MaintenanceMapper.java
+++ b/src/main/java/org/eclipsefoundation/core/resource/mapper/MaintenanceMapper.java
@@ -4,15 +4,15 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.resource.mapper;
+package org.eclipsefoundation.core.resource.mapper;
 
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
 
-import org.eclipsefoundation.marketplace.exception.MaintenanceException;
-import org.eclipsefoundation.marketplace.model.Error;
+import org.eclipsefoundation.core.exception.MaintenanceException;
+import org.eclipsefoundation.core.model.Error;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NotFoundMapper.java b/src/main/java/org/eclipsefoundation/core/resource/mapper/NotFoundMapper.java
similarity index 90%
rename from src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NotFoundMapper.java
rename to src/main/java/org/eclipsefoundation/core/resource/mapper/NotFoundMapper.java
index f30e48759c1be39d0107b5371fbf024a8c10a7e1..efb456b52a856a76e3ecf697a368db0a7edcc39a 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NotFoundMapper.java
+++ b/src/main/java/org/eclipsefoundation/core/resource/mapper/NotFoundMapper.java
@@ -7,7 +7,7 @@
  * 
  * SPDX-License-Identifier: EPL-2.0
 */
-package org.eclipsefoundation.marketplace.resource.mapper;
+package org.eclipsefoundation.core.resource.mapper;
 
 import javax.ws.rs.NotFoundException;
 import javax.ws.rs.core.Response;
@@ -15,7 +15,7 @@ import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
 
-import org.eclipsefoundation.marketplace.model.Error;
+import org.eclipsefoundation.core.model.Error;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NullPointerMapper.java b/src/main/java/org/eclipsefoundation/core/resource/mapper/NullPointerMapper.java
similarity index 77%
rename from src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NullPointerMapper.java
rename to src/main/java/org/eclipsefoundation/core/resource/mapper/NullPointerMapper.java
index d874c3445efdf66cf7c52134d4d68cfbb2ef1058..51d231389c89ad2c3ac89d9551e5d09aea73458c 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NullPointerMapper.java
+++ b/src/main/java/org/eclipsefoundation/core/resource/mapper/NullPointerMapper.java
@@ -7,14 +7,14 @@
  * 
  * SPDX-License-Identifier: EPL-2.0
 */
-package org.eclipsefoundation.marketplace.resource.mapper;
+package org.eclipsefoundation.core.resource.mapper;
 
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
 
-import org.eclipsefoundation.marketplace.model.Error;
+import org.eclipsefoundation.core.model.Error;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -29,6 +29,6 @@ public class NullPointerMapper implements ExceptionMapper<RuntimeException> {
 	@Override
 	public Response toResponse(RuntimeException exception) {
 		LOGGER.error(exception.getMessage(), exception);
-		return new Error(Status.INTERNAL_SERVER_ERROR, "Internal  server error while processing request").asResponse();
+		return new Error(Status.INTERNAL_SERVER_ERROR, "Internal server error while processing request: " + exception.getMessage()).asResponse();
 	}
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NumberFormatMapper.java b/src/main/java/org/eclipsefoundation/core/resource/mapper/NumberFormatMapper.java
similarity index 90%
rename from src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NumberFormatMapper.java
rename to src/main/java/org/eclipsefoundation/core/resource/mapper/NumberFormatMapper.java
index acdd4117de5df94a35bce0bdca650d8d37caad82..90b3c9c08b92f3eef8cb031b8e5e34a9c73360f5 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/NumberFormatMapper.java
+++ b/src/main/java/org/eclipsefoundation/core/resource/mapper/NumberFormatMapper.java
@@ -7,14 +7,14 @@
  * 
  * SPDX-License-Identifier: EPL-2.0
 */
-package org.eclipsefoundation.marketplace.resource.mapper;
+package org.eclipsefoundation.core.resource.mapper;
 
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
 
-import org.eclipsefoundation.marketplace.model.Error;
+import org.eclipsefoundation.core.model.Error;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/RuntimeMapper.java b/src/main/java/org/eclipsefoundation/core/resource/mapper/RuntimeMapper.java
similarity index 89%
rename from src/main/java/org/eclipsefoundation/marketplace/resource/mapper/RuntimeMapper.java
rename to src/main/java/org/eclipsefoundation/core/resource/mapper/RuntimeMapper.java
index a6b591efda218962346c1351f36a98e86e7b619f..7d6e8083cf47b58c395e51a3e32fb7bbcc8935e9 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/mapper/RuntimeMapper.java
+++ b/src/main/java/org/eclipsefoundation/core/resource/mapper/RuntimeMapper.java
@@ -4,14 +4,14 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.resource.mapper;
+package org.eclipsefoundation.core.resource.mapper;
 
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
 
-import org.eclipsefoundation.marketplace.model.Error;
+import org.eclipsefoundation.core.model.Error;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java b/src/main/java/org/eclipsefoundation/core/service/CachingService.java
similarity index 96%
rename from src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java
rename to src/main/java/org/eclipsefoundation/core/service/CachingService.java
index 5c7fc58bcb7199b858d04777743c6140840b1da0..815d677bb10190119f55d1d6dcbf63f88b664c23 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/service/CachingService.java
+++ b/src/main/java/org/eclipsefoundation/core/service/CachingService.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.service;
+package org.eclipsefoundation.core.service;
 
 import java.util.List;
 import java.util.Map;
@@ -13,7 +13,7 @@ import java.util.Set;
 import java.util.concurrent.Callable;
 
 import org.apache.commons.lang3.StringUtils;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.core.model.RequestWrapper;
 
 /**
  * Interface defining the caching service to be used within the application.
diff --git a/src/main/java/org/eclipsefoundation/marketplace/config/CustomMariaDBDialect.java b/src/main/java/org/eclipsefoundation/marketplace/config/CustomMariaDBDialect.java
new file mode 100644
index 0000000000000000000000000000000000000000..095599c3f5f32216b7efbebb3d9697a0ee0652cf
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/config/CustomMariaDBDialect.java
@@ -0,0 +1,23 @@
+package org.eclipsefoundation.marketplace.config;
+
+import org.hibernate.dialect.MariaDB103Dialect;
+import org.hibernate.dialect.function.StandardSQLFunction;
+
+/**
+ * Custom dialect extension to register missing RAND functions.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public class CustomMariaDBDialect extends MariaDB103Dialect {
+
+	/**
+	 * Register random as a function within the dialect. OOTB, rand exists without
+	 * an argument, and will fail if an arg is passed. This is a valid state within
+	 * MariaDB as the arg acts as a seed.
+	 */
+	public CustomMariaDBDialect() {
+		super();
+		registerFunction("rand", new StandardSQLFunction("rand"));
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultHibernateDao.java b/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultHibernateDao.java
new file mode 100644
index 0000000000000000000000000000000000000000..4254d8f9ca3cabef2332d3aedc989e9d25d8583b
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultHibernateDao.java
@@ -0,0 +1,164 @@
+/* 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.dao.impl;
+
+import java.util.List;
+import java.util.concurrent.CompletionStage;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+import javax.transaction.Transactional;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.health.HealthCheckResponse;
+import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
+import org.eclipsefoundation.core.exception.MaintenanceException;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement.Clause;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.search.dao.SearchIndexDAO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of the DB DAO, persisting via the Hibernate framework.
+ * 
+ * @author Martin Lowe
+ */
+@ApplicationScoped
+public class DefaultHibernateDao implements PersistenceDao {
+	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHibernateDao.class);
+
+	@Inject
+	EntityManager em;
+	@Inject
+	SearchIndexDAO indexDAO;
+
+	@ConfigProperty(name = "eclipse.db.default.limit")
+	int defaultLimit;
+
+	@ConfigProperty(name = "eclipse.db.default.limit.max")
+	int defaultMax;
+
+	@ConfigProperty(name = "eclipse.db.maintenance", defaultValue = "false")
+	boolean maintenanceFlag;
+
+	@Override
+	public <T extends BareNode> List<T> get(RDBMSQuery<T> q) {
+		if (maintenanceFlag) {
+			throw new MaintenanceException();
+		}
+		if (LOGGER.isDebugEnabled()) {
+			LOGGER.debug("Querying DB using the following query: {}", q);
+		}
+		LOGGER.error("SQL: {}\nParams: {}", q.getFilter().getSelectSql(), q.getFilter().getParams());
+
+		// build base query
+		TypedQuery<T> query = em.createQuery(q.getFilter().getSelectSql(), q.getDocType());
+
+		// add ordinal parameters
+		int ord = 1;
+		for (Clause c : q.getFilter().getClauses()) {
+			for (Object param : c.getParams()) {
+				query.setParameter(ord++, param);
+			}
+		}
+
+		// check if result set should be limited
+		if (q.getDTOFilter().useLimit()) {
+			query = query.setFirstResult(getOffset(q)).setMaxResults(getLimit(q));
+		}
+		// run the query
+		return query.getResultList();
+	}
+
+	@Transactional
+	@Override
+	public <T extends BareNode> void add(RDBMSQuery<T> q, List<T> documents) {
+		if (maintenanceFlag) {
+			throw new MaintenanceException();
+		}
+		if (LOGGER.isDebugEnabled()) {
+			LOGGER.debug("Adding {} documents to DB of type {}", documents.size(), q.getDocType().getSimpleName());
+		}
+		// for each doc, check if update or create
+		for (T doc : documents) {
+			if (doc.getId() != null) {
+				// ensure this object exists before merging on it
+				if (em.find(q.getDocType(), doc.getId()) != null) {
+					LOGGER.debug("Merging document with existing document with id '{}'", doc.getId());
+					em.merge(doc);
+				} else {
+					LOGGER.debug("Persisting new document with id '{}'", doc.getId());
+					em.persist(doc);
+				}
+			} else {
+				LOGGER.debug("Persisting new document with generated UUID ID");
+				em.persist(doc);
+			}
+		}
+		// indexDAO.createOrUpdate(q, documents);
+	}
+
+	@Transactional
+	@Override
+	public <T extends BareNode> void delete(RDBMSQuery<T> q) {
+		if (maintenanceFlag) {
+			throw new MaintenanceException();
+		}
+
+		if (LOGGER.isDebugEnabled()) {
+			LOGGER.debug("Removing documents from DB using the following query: {}", q);
+		}
+		// retrieve results for the given deletion query to delete using entity manager
+		List<T> results = get(q);
+		if (results.isEmpty()) {
+			throw new NoResultException("Could not find any documents with given filters");
+		}
+		// remove all matched documents
+		results.forEach(em::remove);
+		indexDAO.remove(results);
+	}
+
+	@Transactional
+	@Override
+	public <T extends BareNode> CompletionStage<Long> count(RDBMSQuery<T> q) {
+		if (maintenanceFlag) {
+			throw new MaintenanceException();
+		}
+		if (LOGGER.isDebugEnabled()) {
+			LOGGER.debug("Counting documents in DB that match the following query: {}", q);
+		}
+		throw new RuntimeException();
+	}
+
+	@Override
+	public HealthCheckResponse health() {
+		HealthCheckResponseBuilder b = HealthCheckResponse.named("DB readiness");
+		if (maintenanceFlag) {
+			return b.down().withData("error", "Maintenance flag is set").build();
+		}
+		return b.up().build();
+	}
+
+	private int getLimit(RDBMSQuery<?> q) {
+		return q.getLimit() > 0 ? Math.min(q.getLimit(), defaultMax) : defaultLimit;
+	}
+
+	private int getOffset(RDBMSQuery<?> q) {
+		// if first page, no offset
+		if (q.getPage() <= 1) {
+			return 0;
+		}
+		int limit = getLimit(q);
+		return (limit * q.getPage()) - limit;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java b/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java
deleted file mode 100644
index 8a2bbc21dde5f8baf441a4e7c89dfd2908b4aa26..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dao/impl/DefaultMongoDao.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/* 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.dao.impl;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.stream.Collectors;
-
-import javax.enterprise.context.ApplicationScoped;
-import javax.enterprise.inject.Instance;
-import javax.inject.Inject;
-
-import org.bson.codecs.configuration.CodecProvider;
-import org.bson.conversions.Bson;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipse.microprofile.health.HealthCheckResponse;
-import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
-import org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder;
-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;
-
-import com.mongodb.client.model.ReplaceOptions;
-import com.mongodb.client.result.DeleteResult;
-
-import io.quarkus.mongodb.ReactiveMongoClient;
-import io.quarkus.mongodb.ReactiveMongoCollection;
-
-/**
- * Default implementation of the MongoDB DAO.
- * 
- * @author Martin Lowe
- */
-@ApplicationScoped
-public class DefaultMongoDao implements MongoDao {
-	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMongoDao.class);
-
-	@ConfigProperty(name = MicroprofilePropertyNames.MONGODB_DB_NAME)
-	String databaseName;
-
-	@ConfigProperty(name = MicroprofilePropertyNames.MONGODB_RETURN_LIMIT)
-	int defaultLimit;
-
-	@ConfigProperty(name = MicroprofilePropertyNames.MONGODB_RETURN_LIMIT_MAX)
-	int defaultMax;
-
-	@ConfigProperty(name = MicroprofilePropertyNames.MONGODB_MAINTENANCE_FLAG, defaultValue = "false")
-	boolean maintenanceFlag;
-
-	@Inject
-	Instance<CodecProvider> providers;
-
-	@Inject
-	ReactiveMongoClient mongoClient;
-
-	@Override
-	public <T> CompletionStage<List<T>> get(MongoQuery<T> q) {
-		if (maintenanceFlag) {
-			throw new MaintenanceException();
-		}
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Querying MongoDB using the following query: {}", q);
-		}
-
-		LOGGER.debug("Getting aggregate results");
-		// build base query
-		PublisherBuilder<T> builder = getCollection(q.getDocType()).aggregate(q.getPipeline(getLimit(q)),
-				q.getDocType());
-		// check if result set should be limited
-		if (q.getDTOFilter().useLimit()) {
-			builder = builder.limit(getLimit(q));
-		}
-		// run the query
-		return builder.distinct().toList().run();
-	}
-
-	@Override
-	public <T> CompletionStage<Void> add(MongoQuery<T> q, List<T> documents) {
-		if (maintenanceFlag) {
-			throw new MaintenanceException();
-		}
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Adding {} documents to MongoDB of type {}", documents.size(), q.getDocType().getSimpleName());
-		}
-
-		// set up upserting to not fail on updates
-		ReplaceOptions ro = new ReplaceOptions().upsert(true).bypassDocumentValidation(true);
-
-		// maintain a list of updates
-		List<CompletionStage<?>> stages = new ArrayList<>(documents.size());
-		Bson filter = q.getFilter();
-		for (T doc : documents) {
-			if (filter == null) {
-				stages.add(getCollection(q.getDocType()).insertOne(doc));
-			} else {
-				stages.add(getCollection(q.getDocType()).replaceOne(filter, doc, ro));
-			}
-		}
-
-		// convert the stages to futures, and wrap them in a completable future
-		List<CompletableFuture<?>> all = stages.stream().map(CompletionStage::toCompletableFuture)
-				.collect(Collectors.toList());
-		return CompletableFuture.allOf(all.toArray(new CompletableFuture[all.size()]));
-	}
-
-	@Override
-	public <T> CompletionStage<DeleteResult> delete(MongoQuery<T> q) {
-		if (maintenanceFlag) {
-			throw new MaintenanceException();
-		}
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Removing documents from MongoDB using the following query: {}", q);
-		}
-		return getCollection(q.getDocType()).deleteMany(q.getFilter());
-	}
-
-	@Override
-	public <T> CompletionStage<Long> count(MongoQuery<T> q) {
-		if (maintenanceFlag) {
-			throw new MaintenanceException();
-		}
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Counting documents in MongoDB that match the following query: {}", q);
-		}
-		return getCollection(q.getDocType()).countDocuments(q.getFilter());
-	}
-
-	@Override
-	public HealthCheckResponse health() {
-		HealthCheckResponseBuilder b = HealthCheckResponse.named("MongoDB readiness");
-		if (maintenanceFlag) {
-			return b.down().withData("error", "Maintenance flag is set").build();
-		}
-		return b.up().build();
-	}
-
-	private <T> int getLimit(MongoQuery<T> q) {
-		return q.getLimit() > 0 ? Math.min(q.getLimit(), defaultMax) : defaultLimit;
-	}
-
-	private <T> ReactiveMongoCollection<T> getCollection(Class<T> type) {
-		return mongoClient.getDatabase(databaseName).getCollection(DtoTableNames.getTableName(type), type);
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Author.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Author.java
index 08f0c8068353e068eff0e17335fc8ae0890879f0..a63375d24df2cb68396ccab3f1d019c9512d60e3 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Author.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Author.java
@@ -6,27 +6,24 @@
  */
 package org.eclipsefoundation.marketplace.dto;
 
-import io.quarkus.runtime.annotations.RegisterForReflection;
+import java.util.Objects;
+
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.BareNode;
 
 /**
  * Represents an author for listings.
  * 
  * @author Martin Lowe
  */
-@RegisterForReflection
-public class Author {
+@Entity
+@Table
+public class Author extends BareNode {
 	private String username;
 	private String fullName;
 
-	public Author(String username, String fullName) {
-		this.username = username;
-		this.fullName = fullName;
-	}
-
-	public Author() {
-		// purposefully empty
-	}
-
 	/**
 	 * @return the username
 	 */
@@ -54,4 +51,24 @@ public class Author {
 	public void setFullName(String fullName) {
 		this.fullName = fullName;
 	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Objects.hash(fullName, username);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Author other = (Author) obj;
+		return Objects.equals(fullName, other.fullName) && Objects.equals(username, other.username);
+	}
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Catalog.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Catalog.java
index 879be3fb2d872b22a2db5b2005b8d9b94487dabd..a22576ba3215265b283843bb39ea027fbc118058 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Catalog.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Catalog.java
@@ -9,25 +9,35 @@
 */
 package org.eclipsefoundation.marketplace.dto;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashSet;
 import java.util.Objects;
+import java.util.Set;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.NodeBase;
 
 /**
  * Represents a listing catalog.
  * 
  * @author Martin Lowe
  */
+@Entity
+@Table
 public class Catalog extends NodeBase {
 	private boolean selfContained;
 	private boolean searchEnabled;
 	private String icon;
 	private String description;
 	private String dependenciesRepository;
-	private List<Tab> tabs;
+	@OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
+	private Set<Tab> tabs;
 	
 	public Catalog() {
-		this.tabs = new ArrayList<>();
+		this.tabs = new HashSet<>();
 	}
 
 	/**
@@ -103,15 +113,15 @@ public class Catalog extends NodeBase {
 	/**
 	 * @return the tabs
 	 */
-	public List<Tab> getTabs() {
-		return new ArrayList<>(tabs);
+	public Set<Tab> getTabs() {
+		return new HashSet<>(tabs);
 	}
 
 	/**
 	 * @param tabs the tabs to set
 	 */
-	public void setTabs(List<Tab> tabs) {
-		this.tabs = new ArrayList<>(tabs);
+	public void setTabs(Set<Tab> tabs) {
+		this.tabs = new HashSet<>(tabs);
 	}
 
 	@Override
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Category.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Category.java
index 789e25574f9ea02696a2eef25d1275a8c00cdc42..c3778976806c91f4504fb69541173e2b6571f9e7 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Category.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Category.java
@@ -9,7 +9,10 @@
 */
 package org.eclipsefoundation.marketplace.dto;
 
-import io.quarkus.runtime.annotations.RegisterForReflection;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.NodeBase;
 
 /**
  * Represents a listing category in the marketplace
@@ -17,7 +20,7 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
  * @author Martin Lowe
  *
  */
-@RegisterForReflection
+@Entity
+@Table
 public class Category extends NodeBase {
-	// only needs bare node
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/ErrorReport.java b/src/main/java/org/eclipsefoundation/marketplace/dto/ErrorReport.java
index 5e1f2a33b555f156b1dac7918540e78109f1ca40..b8912f63f771806bc3484131fe415cd7e0614d91 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/ErrorReport.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/ErrorReport.java
@@ -6,45 +6,32 @@
  */
 package org.eclipsefoundation.marketplace.dto;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Objects;
+
+import javax.persistence.Entity;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.BareNode;
 
 /**
  * Represents an error report in the database.
  * 
  * @author Martin Lowe
  */
-public class ErrorReport {
-
-	private String id;
+@Entity
+@Table
+public class ErrorReport extends BareNode {
 	private String title;
 	private String body;
 	private String status;
 	private String statusMessage;
-	private List<String> featureIDs;
+	@OneToOne(targetEntity = FeatureId.class)
+	private FeatureId featureID;
 	private String detailedMessage;
-	private boolean read;
+	private boolean isRead;
 	private String listingId;
 	
-	/**
-	 * 
-	 */
-	public ErrorReport() {
-		this.featureIDs = new ArrayList<>();
-	}
-	
-	/**
-	 * @return the id
-	 */
-	public String getId() {
-		return id;
-	}
-	/**
-	 * @param id the id to set
-	 */
-	public void setId(String id) {
-		this.id = id;
-	}
 	/**
 	 * @return the title
 	 */
@@ -96,14 +83,14 @@ public class ErrorReport {
 	/**
 	 * @return the featureIDs
 	 */
-	public List<String> getFeatureIDs() {
-		return new ArrayList<>(featureIDs);
+	public FeatureId getFeatureID() {
+		return featureID;
 	}
 	/**
 	 * @param featureIDs the featureIDs to set
 	 */
-	public void setFeatureIds(List<String> featureIDs) {
-		this.featureIDs = new ArrayList<>(featureIDs);
+	public void setFeatureIds(FeatureId featureId) {
+		this.featureID = featureId;
 	}
 	/**
 	 * @return the detailedMessage
@@ -121,13 +108,13 @@ public class ErrorReport {
 	 * @return the read
 	 */
 	public boolean isRead() {
-		return read;
+		return isRead;
 	}
 	/**
-	 * @param read the read to set
+	 * @param isRead the read to set
 	 */
-	public void setRead(boolean read) {
-		this.read = read;
+	public void setRead(boolean isRead) {
+		this.isRead = isRead;
 	}
 	/**
 	 * @return the listingId
@@ -141,5 +128,27 @@ public class ErrorReport {
 	public void setListingId(String listingId) {
 		this.listingId = listingId;
 	}
-	
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result
+				+ Objects.hash(body, detailedMessage, featureID, isRead, listingId, status, statusMessage, title);
+		return result;
+	}
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		ErrorReport other = (ErrorReport) obj;
+		return Objects.equals(body, other.body) && Objects.equals(detailedMessage, other.detailedMessage)
+				&& Objects.equals(featureID, other.featureID) && isRead == other.isRead
+				&& Objects.equals(listingId, other.listingId) && Objects.equals(status, other.status)
+				&& Objects.equals(statusMessage, other.statusMessage) && Objects.equals(title, other.title);
+	}
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/FeatureId.java b/src/main/java/org/eclipsefoundation/marketplace/dto/FeatureId.java
index 2c749b6b4855b8468fac14c2a7df076662d1142c..f64e2436f1025116f6734f4194750178280e054a 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/FeatureId.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/FeatureId.java
@@ -6,17 +6,26 @@
  */
 package org.eclipsefoundation.marketplace.dto;
 
+import java.util.Objects;
+
 import javax.json.bind.annotation.JsonbProperty;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.BareNode;
 
 /**
  * Represents a feature in a listing solution version.
  * 
  * @author Martin Lowe
  */
-public class FeatureId {
+@Entity
+@Table
+public class FeatureId extends BareNode {
 	@JsonbProperty("feature_id")
 	private String name;
 	private String installState;
+	private String versionId;
 	
 	/**
 	 * @return the featureId
@@ -24,23 +33,61 @@ public class FeatureId {
 	public String getName() {
 		return name;
 	}
+
 	/**
 	 * @param name the featureId to set
 	 */
 	public void setName(String name) {
 		this.name = name;
 	}
+
 	/**
 	 * @return the installState
 	 */
 	public String getInstallState() {
 		return installState;
 	}
+
 	/**
 	 * @param installState the installState to set
 	 */
 	public void setInstallState(String installState) {
 		this.installState = installState;
 	}
-	
+
+	/**
+	 * @return the listingId
+	 */
+	public String getVersionId() {
+		return versionId;
+	}
+
+	/**
+	 * @param versionId the versionId to set
+	 */
+	public void setVersionId(String versionId) {
+		this.versionId = versionId;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Objects.hash(installState, name, versionId);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		FeatureId other = (FeatureId) obj;
+		return Objects.equals(installState, other.installState) && Objects.equals(name, other.name)
+				&& Objects.equals(versionId, other.versionId);
+	}
+
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Install.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Install.java
index 8085ca6b7fb491e1ef2ef8aad34a94d2f8fb869e..20fea2fb7f889d045d91840a1a2032bab2c3450a 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Install.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Install.java
@@ -7,37 +7,33 @@
 package org.eclipsefoundation.marketplace.dto;
 
 import java.util.Date;
+import java.util.Objects;
+import java.util.UUID;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.BareNode;
 
 /**
  * Domain object representing the data stored for installs.
  * 
  * @author Martin Lowe
  */
-public class Install {
+@Entity
+@Table
+public class Install extends BareNode {
 
-	private String id;
 	private Date installDate;
 	private String os;
 	private String version;
-	private String listingId;
+	@Column(columnDefinition = "BINARY(16)")
+	private UUID listingId;
 	private String javaVersion;
 	private String eclipseVersion;
 	private String locale;
 
-	/**
-	 * @return the id
-	 */
-	public String getId() {
-		return id;
-	}
-
-	/**
-	 * @param id the id to set
-	 */
-	public void setId(String id) {
-		this.id = id;
-	}
-
 	/**
 	 * @return the installDate
 	 */
@@ -83,14 +79,14 @@ public class Install {
 	/**
 	 * @return the listingId
 	 */
-	public String getListingId() {
+	public UUID getListingId() {
 		return listingId;
 	}
 
 	/**
 	 * @param listingId the listingId to set
 	 */
-	public void setListingId(String listingId) {
+	public void setListingId(UUID listingId) {
 		this.listingId = listingId;
 	}
 
@@ -141,11 +137,35 @@ public class Install {
 				&& installDate != null;
 	}
 
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result
+				+ Objects.hash(eclipseVersion, installDate, javaVersion, listingId, locale, os, version);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Install other = (Install) obj;
+		return Objects.equals(eclipseVersion, other.eclipseVersion) && Objects.equals(installDate, other.installDate)
+				&& Objects.equals(javaVersion, other.javaVersion) && Objects.equals(listingId, other.listingId)
+				&& Objects.equals(locale, other.locale) && Objects.equals(os, other.os)
+				&& Objects.equals(version, other.version);
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder builder = new StringBuilder();
 		builder.append("Install [id=");
-		builder.append(id);
+		builder.append(getId());
 		builder.append(", installDate=");
 		builder.append(installDate);
 		builder.append(", os=");
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/InstallMetrics.java b/src/main/java/org/eclipsefoundation/marketplace/dto/InstallMetrics.java
index 5f2201d69c0cc7131c7af10ec0e6062459e6ec4a..3655e961941db5dc6decc5f307d4b0b4077283b0 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/InstallMetrics.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/InstallMetrics.java
@@ -6,8 +6,17 @@
  */
 package org.eclipsefoundation.marketplace.dto;
 
-import java.util.List;
+import java.util.HashSet;
 import java.util.Objects;
+import java.util.Set;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.OneToMany;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.BareNode;
 
 /**
  * Holds a set of install metrics for the last year for a given listing.
@@ -15,51 +24,28 @@ import java.util.Objects;
  * @author Martin Lowe
  *
  */
-public class InstallMetrics {
+@Entity
+@Table
+public class InstallMetrics extends BareNode {
 
-	private String listingId;
-	private List<MetricPeriod> periods;
+	@OneToOne
+	private Listing listing;
+	@OneToMany(cascade = CascadeType.REMOVE)
+	private Set<MetricPeriod> periods;
 	private int total;
 
-	public InstallMetrics() {
-	}
-
-	/**
-	 * @param listingId
-	 * @param periods
-	 */
-	public InstallMetrics(String listingId, List<MetricPeriod> periods, int total) {
-		this.listingId = listingId;
-		this.periods = periods;
-		this.total = total;
-	}
-
-	/**
-	 * @return the listingId
-	 */
-	public String getListingId() {
-		return listingId;
-	}
-
-	/**
-	 * @param listingId the listingId to set
-	 */
-	public void setListingId(String listingId) {
-		this.listingId = listingId;
-	}
-
 	/**
 	 * @return the periods
 	 */
-	public List<MetricPeriod> getPeriods() {
-		return periods;
+	public Set<MetricPeriod> getPeriods() {
+		return new HashSet<>(periods);
 	}
 
 	/**
 	 * @param periods the periods to set
 	 */
-	public void setPeriods(List<MetricPeriod> periods) {
-		this.periods = periods;
+	public void setPeriods(Set<MetricPeriod> periods) {
+		this.periods = new HashSet<>(periods);
 	}
 
 	/**
@@ -78,7 +64,7 @@ public class InstallMetrics {
 
 	@Override
 	public int hashCode() {
-		return Objects.hash(listingId, periods, total);
+		return Objects.hash(periods, total);
 	}
 
 	@Override
@@ -93,16 +79,14 @@ public class InstallMetrics {
 			return false;
 		}
 		InstallMetrics other = (InstallMetrics) obj;
-		return Objects.equals(listingId, other.listingId) && Objects.equals(periods, other.periods)
+		return  Objects.equals(periods, other.periods)
 				&& Objects.equals(total, other.total);
 	}
 
 	@Override
 	public String toString() {
 		StringBuilder builder = new StringBuilder();
-		builder.append("InstallMetrics [listingId=");
-		builder.append(listingId);
-		builder.append(", periods=");
+		builder.append("InstallMetrics [periods=");
 		builder.append(periods);
 		builder.append(", total=");
 		builder.append(total);
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java
index ec5396583fd0d0bb6c5e372be3fe29474db9800f..c79d1c22dd1ebd5ce17f53d686a2ba531593c070 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java
@@ -9,73 +9,128 @@
 */
 package org.eclipsefoundation.marketplace.dto;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.time.temporal.ChronoField;
+import java.util.Calendar;
+import java.util.HashSet;
 import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
 
 import javax.json.bind.annotation.JsonbProperty;
 import javax.json.bind.annotation.JsonbTransient;
+import javax.persistence.CascadeType;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.Index;
+import javax.persistence.JoinColumn;
+import javax.persistence.Lob;
+import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import javax.persistence.OneToOne;
+import javax.persistence.PostLoad;
+import javax.persistence.Table;
+import javax.persistence.Transient;
 
 import org.eclipsefoundation.marketplace.model.SortableField;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-
-import io.quarkus.runtime.annotations.RegisterForReflection;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.dto.NodeBase;
+import org.eclipsefoundation.persistence.model.SortableField;
+import org.eclipsefoundation.search.model.Indexed;
+import org.hibernate.annotations.NotFound;
+import org.hibernate.annotations.NotFoundAction;
 
 /**
  * Domain object representing a marketplace listing.
  * 
  * @author Martin Lowe
  */
-@RegisterForReflection
+@Entity
+@Table(
+	indexes = {
+		@Index(columnList="licenseType"),
+		@Index(columnList="changed"),
+		@Index(columnList="seed")
+	}
+)
 public class Listing extends NodeBase {
 
 	private String supportUrl;
 	private String homepageUrl;
+	@Lob
+	@Indexed
 	private String teaser;
+	@Indexed
+	@Lob
 	private String body;
 	private String status;
 	private String logo;
 	private boolean foundationMember;
-
+	@Transient
 	@SortableField(name = "installs_count")
 	private Integer installsTotal;
-
+	@Transient
 	@SortableField(name = "installs_count_recent")
 	private Integer installsRecent;
 
 	@SortableField
 	private long favoriteCount;
-
-	@SortableField(name = DatabaseFieldNames.CREATION_DATE)
-	@JsonbProperty(DatabaseFieldNames.CREATION_DATE)
-	private String creationDate;
-
-	@SortableField(name = DatabaseFieldNames.UPDATE_DATE)
-	@JsonbProperty(DatabaseFieldNames.UPDATE_DATE)
-	private String updateDate;
+	@SortableField
+	private String created;
+	@SortableField
+	private String changed;
+	
 	@JsonbProperty(DatabaseFieldNames.LICENSE_TYPE)
-	private String license;
-	private List<String> marketIds;
-	private List<String> categoryIds;
-	private List<String> screenshots;
-	private List<Category> categories;
+	private String licenseType;
+	@ElementCollection
+	private Set<String> screenshots;
+	@ManyToMany
+	@JoinColumn
+	private Set<Category> categories;
+	@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
+	@JoinColumn
 	private Organization organization;
-	private List<Author> authors;
-	private List<Tag> tags;
-	private List<ListingVersion> versions;
-	private boolean isPromotion;
+	@OneToMany(cascade = CascadeType.PERSIST)
+	private Set<Author> authors;
+	@ManyToMany(cascade = CascadeType.PERSIST)
+	private Set<Tag> tags;
+	@OneToMany
+	@JoinColumn(name = "listingId")
+	private Set<ListingVersion> versions;
+	
+	@JsonbTransient
+	@OneToOne(mappedBy = "listing")
+	@NotFound(action = NotFoundAction.IGNORE)
+	private InstallMetrics metrics;
 
 	/**
 	 * Default constructor, sets lists to empty lists to stop null pointers
 	 */
 	public Listing() {
-		this.authors = new ArrayList<>();
-		this.tags = new ArrayList<>();
-		this.versions = new ArrayList<>();
-		this.marketIds = new ArrayList<>();
-		this.categoryIds = new ArrayList<>();
-		this.categories = new ArrayList<>();
-		this.screenshots = new ArrayList<>();
+		this.authors = new HashSet<>();
+		this.tags = new HashSet<>();
+		this.versions = new HashSet<>();
+		this.categories = new HashSet<>();
+		this.screenshots = new HashSet<>();
+	}
+
+	@PostLoad
+	private void post() {
+		if (metrics != null) {
+			// get total installs
+			this.installsTotal = metrics.getTotal();
+			
+			// get recent installs
+			Calendar c = Calendar.getInstance();
+			int thisMonth = c.get(Calendar.MONTH);
+			Optional<MetricPeriod> current = metrics.getPeriods().stream()
+					.filter(p -> p.getStart().toInstant().get(ChronoField.MONTH_OF_YEAR) == thisMonth).findFirst();
+			// check if we have an entry for the current month
+			if (current.isPresent()) {
+				this.installsRecent = current.get().getCount();
+			}
+		}
 	}
 
 	/**
@@ -223,103 +278,73 @@ public class Listing extends NodeBase {
 	}
 
 	/**
-	 * @return the creationDate
+	 * @return the created date
 	 */
-	public String getCreationDate() {
-		return creationDate;
+	public String getCreated() {
+		return created;
 	}
 
 	/**
-	 * @param creationDate the creationDate to set
+	 * @param created the created to set
 	 */
-	public void setCreationDate(String creationDate) {
-		this.creationDate = creationDate;
+	public void setCreated(String created) {
+		this.created = created;
 	}
 
 	/**
 	 * @return the updateDate
 	 */
-	public String getUpdateDate() {
-		return updateDate;
-	}
-
-	/**
-	 * @param updateDate the updateDate to set
-	 */
-	public void setUpdateDate(String updateDate) {
-		this.updateDate = updateDate;
-	}
-
-	/**
-	 * @return the license
-	 */
-	public String getLicense() {
-		return license;
-	}
-
-	/**
-	 * @param license the license to set
-	 */
-	public void setLicense(String license) {
-		this.license = license;
-	}
-
-	/**
-	 * @return the categoryIds
-	 */
-	@JsonbTransient
-	public List<String> getCategoryIds() {
-		return categoryIds;
+	public String getChanged() {
+		return changed;
 	}
 
 	/**
-	 * @param categoryIds the categoryIds to set
+	 * @param changed the changed to set
 	 */
-	public void setCategoryIds(List<String> categoryIds) {
-		this.categoryIds = new ArrayList<>(categoryIds);
+	public void setChanged(String changed) {
+		this.changed = changed;
 	}
 
 	/**
-	 * @return the categoryIds
+	 * @return the licenseType
 	 */
-	public List<String> getMarketIds() {
-		return new ArrayList<>(marketIds);
+	public String getLicenseType() {
+		return licenseType;
 	}
 
 	/**
-	 * @param marketIds the categoryIds to set
+	 * @param licenseType the licenseType to set
 	 */
-	public void setMarketIds(List<String> marketIds) {
-		this.marketIds = new ArrayList<>(marketIds);
+	public void setLicenseType(String licenseType) {
+		this.licenseType = licenseType;
 	}
 
 	/**
 	 * @return the screenshots
 	 */
-	public List<String> getScreenshots() {
-		return new ArrayList<>(screenshots);
+	public Set<String> getScreenshots() {
+		return new HashSet<>(screenshots);
 	}
 
 	/**
 	 * @param screenshots the screenshots to set
 	 */
-	public void setScreenshots(List<String> screenshots) {
-		this.screenshots = new ArrayList<>(screenshots);
+	public void setScreenshots(Set<String> screenshots) {
+		this.screenshots = new HashSet<>(screenshots);
 	}
 
 	/**
 	 * @return the categories
 	 */
-	public List<Category> getCategories() {
-		return new ArrayList<>(categories);
+	public Set<Category> getCategories() {
+		return new HashSet<>(categories);
 	}
 
 	/**
 	 * @param categories the categories to set
 	 */
-	@JsonbTransient
-	public void setCategories(List<Category> categories) {
-		this.categories = new ArrayList<>(categories);
+	public void setCategories(Set<Category> categories) {
+		this.categories = new HashSet<>(categories);
 	}
 
 	/**
@@ -339,47 +364,61 @@ public class Listing extends NodeBase {
 	/**
 	 * @return the authors
 	 */
-	public List<Author> getAuthors() {
-		return new ArrayList<>(authors);
+	public Set<Author> getAuthors() {
+		return new HashSet<>(authors);
 	}
 
 	/**
 	 * @param authors the authors to set
 	 */
-	public void setAuthors(List<Author> authors) {
+	public void setAuthors(Set<Author> authors) {
 		Objects.requireNonNull(authors);
-		this.authors = new ArrayList<>(authors);
+		this.authors = new HashSet<>(authors);
 	}
 
 	/**
 	 * @return the tags
 	 */
-	public List<Tag> getTags() {
-		return new ArrayList<>(tags);
+	public Set<Tag> getTags() {
+		return new HashSet<>(tags);
 	}
 
 	/**
 	 * @param tags the tags to set
 	 */
-	public void setTags(List<Tag> tags) {
+	public void setTags(Set<Tag> tags) {
 		Objects.requireNonNull(tags);
-		this.tags = new ArrayList<>(tags);
+		this.tags = new HashSet<>(tags);
 	}
 
 	/**
 	 * @return the versions
 	 */
-	public List<ListingVersion> getVersions() {
-		return new ArrayList<>(versions);
+	public Set<ListingVersion> getVersions() {
+		return new HashSet<>(versions);
 	}
 
 	/**
 	 * @param versions the versions to set
 	 */
 	@JsonbTransient
-	public void setVersions(List<ListingVersion> versions) {
+	public void setVersions(Set<ListingVersion> versions) {
 		Objects.requireNonNull(versions);
-		this.versions = new ArrayList<>(versions);
+		this.versions = new HashSet<>(versions);
+	}
+
+	/**
+	 * @return the metrics
+	 */
+	public InstallMetrics getMetrics() {
+		return metrics;
+	}
+
+	/**
+	 * @param metrics the metrics to set
+	 */
+	public void setMetrics(InstallMetrics metrics) {
+		this.metrics = metrics;
 	}
 
 	/**
@@ -399,17 +438,25 @@ public class Listing extends NodeBase {
 
 	@Override
 	public boolean validate() {
-		return super.validate() && license != null && !authors.isEmpty() && !categoryIds.isEmpty()
-				&& !versions.isEmpty();
+		return super.validate() && licenseType != null && !authors.isEmpty() && !versions.isEmpty();
+	}
+
+	@Override
+	public void initializeLazyFields() {
+		super.initializeLazyFields();
+		this.authors.forEach(BareNode::initializeLazyFields);
+		this.categories.forEach(BareNode::initializeLazyFields);
+		this.tags.forEach(BareNode::initializeLazyFields);
+		this.versions.forEach(BareNode::initializeLazyFields);
 	}
 
 	@Override
 	public int hashCode() {
 		final int prime = 31;
 		int result = super.hashCode();
-		result = prime * result + Objects.hash(authors, body, categories, categoryIds, creationDate, favoriteCount,
-				foundationMember, homepageUrl, installsRecent, installsTotal, license, logo, organization, status,
-				supportUrl, tags, teaser, updateDate, versions);
+		result = prime * result + Objects.hash(this.getAuthors(), body, this.getCategories(), created, favoriteCount, foundationMember,
+				homepageUrl, installsRecent, installsTotal, licenseType, logo, organization, status, supportUrl, this.getTags(),
+				teaser, changed, this.getVersions());
 		return result;
 	}
 
@@ -425,16 +472,15 @@ public class Listing extends NodeBase {
 			return false;
 		}
 		Listing other = (Listing) obj;
-		return Objects.equals(authors, other.authors) && Objects.equals(body, other.body)
-				&& Objects.equals(categories, other.categories) && Objects.equals(categoryIds, other.categoryIds)
-				&& creationDate == other.creationDate && favoriteCount == other.favoriteCount
-				&& foundationMember == other.foundationMember && Objects.equals(homepageUrl, other.homepageUrl)
-				&& installsRecent == other.installsRecent && installsTotal == other.installsTotal
-				&& Objects.equals(logo, other.logo) && Objects.equals(organization, other.organization)
-				&& Objects.equals(status, other.status) && Objects.equals(supportUrl, other.supportUrl)
-				&& Objects.equals(tags, other.tags) && Objects.equals(teaser, other.teaser)
-				&& updateDate == other.updateDate && Objects.equals(versions, other.versions)
-				&& Objects.equals(screenshots, other.screenshots);
+		return Objects.equals(this.getAuthors(), other.getAuthors()) && Objects.equals(body, other.body)
+				&& Objects.equals(this.getCategories(), other.getCategories()) && created == other.created
+				&& favoriteCount == other.favoriteCount && foundationMember == other.foundationMember
+				&& Objects.equals(homepageUrl, other.homepageUrl) && installsRecent == other.installsRecent
+				&& installsTotal == other.installsTotal && Objects.equals(logo, other.logo)
+				&& Objects.equals(this.getOrganization(), other.getOrganization()) && Objects.equals(status, other.status)
+				&& Objects.equals(supportUrl, other.supportUrl) && Objects.equals(this.getTags(), other.getTags())
+				&& Objects.equals(teaser, other.teaser) && changed == other.changed
+				&& Objects.equals(this.getVersions(), other.getVersions()) && Objects.equals(this.getScreenshots(), other.getScreenshots());
 	}
 
 	@Override
@@ -454,9 +500,9 @@ public class Listing extends NodeBase {
 		sb.append(", installsTotal=").append(installsTotal);
 		sb.append(", installsRecent=").append(installsRecent);
 		sb.append(", favoriteCount=").append(favoriteCount);
-		sb.append(", creationDate=").append(creationDate);
-		sb.append(", updateDate=").append(updateDate);
-		sb.append(", license=").append(license);
+		sb.append(", created=").append(created);
+		sb.append(", updateDate=").append(changed);
+		sb.append(", license=").append(licenseType);
 		sb.append(", organization=").append(organization);
 		sb.append(", authors=").append(authors);
 		sb.append(", tags=").append(tags);
@@ -466,4 +512,4 @@ public class Listing extends NodeBase {
 		sb.append(']');
 		return sb.toString();
 	}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/ListingVersion.java b/src/main/java/org/eclipsefoundation/marketplace/dto/ListingVersion.java
index 6f900526d96f21d44cbb4e29c6e5fc929ca837e9..6cce757488503806a732e956e1ec3fd583699027 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/ListingVersion.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/ListingVersion.java
@@ -6,60 +6,59 @@
  */
 package org.eclipsefoundation.marketplace.dto;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashSet;
 import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
 
-import org.apache.commons.lang3.StringUtils;
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Index;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.BareNode;
 
 /**
- * Domain object representing a marketplace listing version
+ * Domain object representing a marketplace listing version. EAGER loading is
+ * required as the entity is nested, and gets called by hash sets.
  * 
  * @author Martin Lowe
  */
-public class ListingVersion {
-
-	private String id;
-	private String listingId;
+@Entity
+@Table(
+	indexes = {
+		@Index(columnList="minJavaVersion"),
+		@Index(columnList="listingId")
+	}
+)
+public class ListingVersion extends BareNode {
 	private String version;
-	private List<String> eclipseVersions;
-	private List<String> platforms;
-	private String minJavaVersion;
+	@ElementCollection(fetch = FetchType.EAGER)
+	private Set<String> eclipseVersions;
+	@ElementCollection(fetch = FetchType.EAGER)
+	private Set<String> platforms;
+	private int minJavaVersion;
 	private String updateSiteUrl;
-	private List<FeatureId> featureIds;
+	@OneToMany(fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST, CascadeType.MERGE })
+	private Set<FeatureId> featureIds;
+	@Column(columnDefinition = "BINARY(16)")
+	private UUID listingId;
 
 	public ListingVersion() {
-		this.eclipseVersions = new ArrayList<>();
-		this.platforms = new ArrayList<>();
-		this.featureIds = new ArrayList<>();
-	}
-
-	/**
-	 * @return the id
-	 */
-	public String getId() {
-		return id;
-	}
-
-	/**
-	 * @param id the id to set
-	 */
-	public void setId(String id) {
-		this.id = id;
+		this.eclipseVersions = new HashSet<>();
+		this.platforms = new HashSet<>();
+		this.featureIds = new HashSet<>();
 	}
-
-	/**
-	 * @return the listingId
-	 */
-	public String getListingId() {
-		return listingId;
-	}
-
-	/**
-	 * @param listingId the listingId to set
-	 */
-	public void setListingId(String listingId) {
-		this.listingId = listingId;
+	
+	@Override
+	public void initializeLazyFields() {
+		this.getEclipseVersions();
+		this.getFeatureIds();
+		this.getPlatforms();
 	}
 
 	/**
@@ -79,44 +78,44 @@ public class ListingVersion {
 	/**
 	 * @return the eclipseVersions
 	 */
-	public List<String> getEclipseVersions() {
-		return new ArrayList<>(eclipseVersions);
+	public Set<String> getEclipseVersions() {
+		return new HashSet<>(eclipseVersions);
 	}
 
 	/**
 	 * @param eclipseVersions the eclipseVersions to set
 	 */
-	public void setEclipseVersions(List<String> eclipseVersions) {
+	public void setEclipseVersions(Set<String> eclipseVersions) {
 		Objects.requireNonNull(eclipseVersions);
-		this.eclipseVersions = new ArrayList<>(eclipseVersions);
+		this.eclipseVersions = new HashSet<>(eclipseVersions);
 	}
 
 	/**
 	 * @return the platforms
 	 */
-	public List<String> getPlatforms() {
-		return new ArrayList<>(platforms);
+	public Set<String> getPlatforms() {
+		return new HashSet<>(platforms);
 	}
 
 	/**
 	 * @param platforms the platforms to set
 	 */
-	public void setPlatforms(List<String> platforms) {
+	public void setPlatforms(Set<String> platforms) {
 		Objects.requireNonNull(platforms);
-		this.platforms = new ArrayList<>(platforms);
+		this.platforms = new HashSet<>(platforms);
 	}
 
 	/**
 	 * @return the minimumJavaVersion
 	 */
-	public String getMinJavaVersion() {
+	public int getMinJavaVersion() {
 		return minJavaVersion;
 	}
 
 	/**
 	 * @param minJavaVersion the minJavaVersion to set
 	 */
-	public void setMinJavaVersion(String minJavaVersion) {
+	public void setMinJavaVersion(int minJavaVersion) {
 		this.minJavaVersion = minJavaVersion;
 	}
 
@@ -137,21 +136,59 @@ public class ListingVersion {
 	/**
 	 * @return the featureIds
 	 */
-	public List<FeatureId> getFeatureIds() {
-		return new ArrayList<>(featureIds);
+	public Set<FeatureId> getFeatureIds() {
+		return new HashSet<>(featureIds);
 	}
 
 	/**
 	 * @param featureIds the featureIds to set
 	 */
-	public void setFeatureIds(List<FeatureId> featureIds) {
+	public void setFeatureIds(Set<FeatureId> featureIds) {
 		Objects.requireNonNull(featureIds);
-		this.featureIds = new ArrayList<>(featureIds);
+		this.featureIds = new HashSet<>(featureIds);
+	}
+
+	/**
+	 * @return the listingId
+	 */
+	public UUID getListingId() {
+		return listingId;
+	}
+
+	/**
+	 * @param listingId the listingId to set
+	 */
+	public void setListingId(UUID listingId) {
+		this.listingId = listingId;
 	}
 
 	public boolean validate() {
-		return version != null && listingId != null && StringUtils.isAnyBlank(minJavaVersion) && platforms.isEmpty()
+		return version != null && listingId != null && minJavaVersion < 0 && platforms.isEmpty()
 				&& !eclipseVersions.isEmpty();
 	}
 
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Objects.hash(getEclipseVersions(), getFeatureIds(), listingId, minJavaVersion,
+				getPlatforms(), updateSiteUrl, version);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		ListingVersion other = (ListingVersion) obj;
+		return Objects.equals(getEclipseVersions(), other.getEclipseVersions())
+				&& Objects.equals(getFeatureIds(), other.getFeatureIds()) && Objects.equals(listingId, other.listingId)
+				&& minJavaVersion == other.minJavaVersion && Objects.equals(getPlatforms(), other.getPlatforms())
+				&& Objects.equals(updateSiteUrl, other.updateSiteUrl) && Objects.equals(version, other.version);
+	}
+
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Market.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Market.java
index d28bf757aea8fbad03d5ff53e316fc29488d2752..539cc496d4c5441df2162464029d1c0be6715f0f 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Market.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Market.java
@@ -10,13 +10,20 @@
 package org.eclipsefoundation.marketplace.dto;
 
 import java.util.ArrayList;
-import java.util.LinkedList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.json.bind.annotation.JsonbTransient;
+import javax.persistence.Entity;
+import javax.persistence.ManyToMany;
+import javax.persistence.PostLoad;
+import javax.persistence.Table;
+import javax.persistence.Transient;
 
-import io.quarkus.runtime.annotations.RegisterForReflection;
+import org.eclipsefoundation.persistence.dto.NodeBase;
 
 /**
  * Domain object representing a Market within the marketplace.
@@ -24,32 +31,48 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
  * @author Martin Lowe
  * @since 05/2019
  */
-@RegisterForReflection
+@Entity
+@Table
 public class Market extends NodeBase {
-	private List<String> listingIds;
-	private List<Category> categories;
+	@ManyToMany
+	private Set<Listing> listingIds;
+	@Transient
+	private Set<Category> categories;
 
 	/**
 	 * Default constructor. Creates an empty linkedlist for categories, as its
 	 * unknown how many categories the market will reference.
 	 */
 	public Market() {
-		this.listingIds = new LinkedList<>();
-		this.categories = new LinkedList<>();
+		this.listingIds = new HashSet<>();
+		this.categories = new HashSet<>();
+	}
+
+	/**
+	 * Copies unique categories from all listings associated w/ Market into transient categories field
+	 */
+	@PostLoad
+	private void post() {
+		listingIds.stream().map(Listing::getCategories).flatMap(Set::stream).collect(Collectors.toList())
+				.forEach(c -> {
+					if (!categories.contains((Category) c)) {
+						this.categories.add(c);
+					}
+				});
 	}
 
 	/**
 	 * @return the listingIds
 	 */
-	public List<String> getListingIds() {
+	public List<Listing> getListingIds() {
 		return new ArrayList<>(listingIds);
 	}
 
 	/**
 	 * @param listingIds the listingIds to set
 	 */
-	public void setListingIds(List<String> listingIds) {
-		this.listingIds = new ArrayList<>(listingIds);
+	public void setListingIds(Set<Listing> listingIds) {
+		this.listingIds = new HashSet<>(listingIds);
 	}
 
 	/**
@@ -63,8 +86,8 @@ public class Market extends NodeBase {
 	 * @param categories the categories to set
 	 */
 	@JsonbTransient
-	public void setCategories(List<Category> categories) {
-		this.categories = new ArrayList<>(categories);
+	public void setCategories(Set<Category> categories) {
+		this.categories = new HashSet<>(categories);
 	}
 
 	@Override
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/MetricPeriod.java b/src/main/java/org/eclipsefoundation/marketplace/dto/MetricPeriod.java
index 187f131bdcc0161ff146eb8c707a8a701ef9c684..fd93002bb0cfb45aa870869b0e7c3d2c6c2bcf66 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/MetricPeriod.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/MetricPeriod.java
@@ -7,8 +7,15 @@
 package org.eclipsefoundation.marketplace.dto;
 
 import java.util.Date;
+import java.util.Objects;
+import java.util.UUID;
 
 import javax.json.bind.annotation.JsonbTransient;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.BareNode;
 
 /**
  * Represents a set of install metrics for a given listing in a time period.
@@ -17,9 +24,11 @@ import javax.json.bind.annotation.JsonbTransient;
  * @author Martin Lowe
  *
  */
-public class MetricPeriod {
-
-	private String listingId;
+@Entity
+@Table
+public class MetricPeriod  extends BareNode {
+	@Column(columnDefinition = "BINARY(16)")
+	private UUID listingId;
 	private Integer count;
 	private Date start;
 	private Date end;
@@ -28,14 +37,14 @@ public class MetricPeriod {
 	 * @return the listingId
 	 */
 	@JsonbTransient
-	public String getListingId() {
+	public UUID getListingId() {
 		return listingId;
 	}
 
 	/**
 	 * @param listingId the listingId to set
 	 */
-	public void setListingId(String listingId) {
+	public void setListingId(UUID listingId) {
 		this.listingId = listingId;
 	}
 
@@ -81,6 +90,27 @@ public class MetricPeriod {
 		this.end = end;
 	}
 
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Objects.hash(count, end, listingId, start);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		MetricPeriod other = (MetricPeriod) obj;
+		return Objects.equals(count, other.count) && Objects.equals(end, other.end)
+				&& Objects.equals(listingId, other.listingId) && Objects.equals(start, other.start);
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder builder = new StringBuilder();
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Organization.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Organization.java
index 77aa2209de32194bab9fc3bdb3f3c776fd5b03ab..f5d7b9ef6f331992170d3c34954203a9f272e5c3 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Organization.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Organization.java
@@ -6,43 +6,17 @@
  */
 package org.eclipsefoundation.marketplace.dto;
 
-import io.quarkus.runtime.annotations.RegisterForReflection;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.NodeBase;
 
 /**
  * Represents an associated organization in a marketplace listing
  * 
  * @author Martin Lowe
  */
-@RegisterForReflection
-public class Organization {
-	private String id;
-	private String name;
-
-	/**
-	 * @return the id
-	 */
-	public String getId() {
-		return id;
-	}
-
-	/**
-	 * @param id the id to set
-	 */
-	public void setId(String id) {
-		this.id = id;
-	}
-
-	/**
-	 * @return the name
-	 */
-	public String getName() {
-		return name;
-	}
-
-	/**
-	 * @param name the name to set
-	 */
-	public void setName(String name) {
-		this.name = name;
-	}
+@Entity
+@Table
+public class Organization extends NodeBase {
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Tab.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Tab.java
index f2a80a7f5425f088c381b826d8e0a34902d4028c..fad536a4cb27921ab5167a0907c388f7c97bfe53 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Tab.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Tab.java
@@ -6,43 +6,22 @@
  */
 package org.eclipsefoundation.marketplace.dto;
 
+import java.util.Objects;
+
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.NodeBase;
+
 /**
  * @author Martin Lowe
  *
  */
-public class Tab {
-	private String title;
-	private String url;
+@Entity
+@Table
+public class Tab extends NodeBase {
 	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
 	 */
@@ -57,4 +36,24 @@ public class Tab {
 		this.type = type;
 	}
 
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Objects.hash(type);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Tab other = (Tab) obj;
+		return Objects.equals(type, other.type);
+	}
+
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Tag.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Tag.java
index 8e5eda822c9dc67f63c46b6ac0effe673bc30477..9909fe74da92f489f5574d08ad16f1a473a425df 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/Tag.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Tag.java
@@ -6,58 +6,19 @@
  */
 package org.eclipsefoundation.marketplace.dto;
 
-import io.quarkus.runtime.annotations.RegisterForReflection;
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+import org.eclipsefoundation.persistence.dto.NodeBase;
 
 /**
- * Domain object representing a marketplace listing tag
+ * Domain object representing a marketplace listing tag. Special object in that
+ * its
  * 
  * @author Martin Lowe
  */
-@RegisterForReflection
-public class Tag {
-	private String id;
-	private String name;
-	private String url;
-
-	/**
-	 * @return the id
-	 */
-	public String getId() {
-		return id;
-	}
-
-	/**
-	 * @param id the id to set
-	 */
-	public void setId(String id) {
-		this.id = id;
-	}
-
-	/**
-	 * @return the name
-	 */
-	public String getName() {
-		return name;
-	}
-
-	/**
-	 * @param name the name to set
-	 */
-	public void setName(String name) {
-		this.name = name;
-	}
-
-	/**
-	 * @return the url
-	 */
-	public String getUrl() {
-		return url;
-	}
+@Entity
+@Table
+public class Tag extends NodeBase {
 
-	/**
-	 * @param url the url to set
-	 */
-	public void setUrl(String url) {
-		this.url = url;
-	}
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CatalogCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CatalogCodec.java
deleted file mode 100644
index cfa9cd71b7aec8358e792803193c185592c458e2..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CatalogCodec.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/* 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 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.DatabaseFieldNames;
-
-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(DatabaseFieldNames.DOCID, value.getId());
-		doc.put(DatabaseFieldNames.TITLE, value.getTitle());
-		doc.put(DatabaseFieldNames.URL, value.getUrl());
-		doc.put(DatabaseFieldNames.CATALOG_ICON, value.getIcon());
-		doc.put(DatabaseFieldNames.CATALOG_SELF_CONTAINED, value.isSelfContained());
-		doc.put(DatabaseFieldNames.CATALOG_SEARCH_ENABLED, value.isSearchEnabled());
-		doc.put(DatabaseFieldNames.CATALOG_DEPENDENCIES_REPOSITORY, value.getDependenciesRepository());
-		doc.put(DatabaseFieldNames.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(DatabaseFieldNames.DOCID));
-		out.setUrl(document.getString(DatabaseFieldNames.URL));
-		out.setTitle(document.getString(DatabaseFieldNames.TITLE));
-		out.setIcon(document.getString(DatabaseFieldNames.CATALOG_ICON));
-		out.setSelfContained(document.getBoolean(DatabaseFieldNames.CATALOG_SELF_CONTAINED));
-		out.setSearchEnabled(document.getBoolean(DatabaseFieldNames.CATALOG_SEARCH_ENABLED));
-		out.setDependenciesRepository(document.getString(DatabaseFieldNames.CATALOG_DEPENDENCIES_REPOSITORY));
-		out.setTabs(document.getList(DatabaseFieldNames.CATALOG_TABS, Document.class).stream().map(tabConverter::convert)
-				.collect(Collectors.toList()));
-		return out;
-	}
-
-	@Override
-	public Catalog generateIdIfAbsentFromDocument(Catalog document) {
-		if (!documentHasId(document)) {
-			document.setId(UUID.randomUUID().toString());
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(Catalog document) {
-		return document.getId() != null;
-	}
-
-	@Override
-	public BsonValue getDocumentId(Catalog document) {
-		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
deleted file mode 100644
index 944aedf4e73b32b0b894429f9a4f34b054e6a01b..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/CategoryCodec.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/* 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.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) {
-		if (!documentHasId(document)) {
-			document.setId(UUID.randomUUID().toString());
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(Category document) {
-		return !StringUtils.isBlank(document.getId());
-	}
-
-	@Override
-	public BsonValue getDocumentId(Category document) {
-		return new BsonString(document.getId());
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ErrorReportCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ErrorReportCodec.java
deleted file mode 100644
index e8dc9b5f7541352259f5fe751fa1ebb915b9a6af..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ErrorReportCodec.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/* 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.ErrorReport;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-
-import com.mongodb.MongoClient;
-
-
-/**
- * MongoDB codec for transcoding of {@link ErrorReport} and {@link Document}
- * objects. Used when writing or retrieving objects of given type from the
- * database.
- * 
- * @author Martin Lowe
- */
-public class ErrorReportCodec implements CollectibleCodec<ErrorReport> {
-	private final Codec<Document> documentCodec;
-
-	public ErrorReportCodec() {
-		this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);
-	}
-	
-	@Override
-	public void encode(BsonWriter writer, ErrorReport value, EncoderContext encoderContext) {
-		Document doc = new Document();
-
-		doc.put(DatabaseFieldNames.DOCID, value.getId());
-		doc.put(DatabaseFieldNames.TITLE, value.getTitle());
-		doc.put(DatabaseFieldNames.ERROR_BODY, value.getBody());
-		doc.put(DatabaseFieldNames.ERROR_DETAILED_MESSAGE, value.getDetailedMessage());
-		doc.put(DatabaseFieldNames.ERROR_READ, value.isRead());
-		doc.put(DatabaseFieldNames.ERROR_FEATURE_IDS, value.getFeatureIDs());
-		doc.put(DatabaseFieldNames.ERROR_STATUS_CODE, value.getStatus());
-		doc.put(DatabaseFieldNames.ERROR_STATUS_MESSAGE, value.getStatusMessage());
-		documentCodec.encode(writer, doc, encoderContext);
-	}
-
-	@Override
-	public Class<ErrorReport> getEncoderClass() {
-		return ErrorReport.class;
-	}
-
-	@Override
-	public ErrorReport decode(BsonReader reader, DecoderContext decoderContext) {
-		Document document = documentCodec.decode(reader, decoderContext);
-		ErrorReport out = new ErrorReport();
-		out.setId(document.getString(DatabaseFieldNames.DOCID));
-		out.setTitle(document.getString(DatabaseFieldNames.ERROR_TITLE));
-		out.setBody(document.getString(DatabaseFieldNames.ERROR_BODY));
-		out.setDetailedMessage(document.getString(DatabaseFieldNames.ERROR_DETAILED_MESSAGE));
-		out.setStatusMessage(document.getString(DatabaseFieldNames.ERROR_STATUS_MESSAGE));
-		out.setStatus(document.getString(DatabaseFieldNames.ERROR_STATUS_CODE));
-		out.setFeatureIds(document.getList(DatabaseFieldNames.ERROR_FEATURE_IDS, String.class));
-		out.setRead(document.getBoolean(DatabaseFieldNames.ERROR_READ));
-		
-		return out;
-	}
-
-	@Override
-	public ErrorReport generateIdIfAbsentFromDocument(ErrorReport document) {
-		if (!documentHasId(document)) {
-			document.setId(UUID.randomUUID().toString());
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(ErrorReport document) {
-		return !StringUtils.isBlank(document.getId());
-	}
-
-	@Override
-	public BsonValue getDocumentId(ErrorReport document) {
-		return new BsonString(document.getId());
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/InstallCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/InstallCodec.java
deleted file mode 100644
index 21099af74389b1c4f20ced2de0b59d5088ebdd57..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/InstallCodec.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/* 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.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.Install;
-import org.eclipsefoundation.marketplace.helper.JavaVersionHelper;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-
-import com.mongodb.MongoClient;
-
-/**
- * MongoDB codec for transcoding of {@link Install} and {@link Document}
- * objects. Used when writing or retrieving objects of given type from the
- * database.
- * 
- * @author Martin Lowe
- */
-public class InstallCodec implements CollectibleCodec<Install> {
-	private final Codec<Document> documentCodec;
-
-	/**
-	 * Creates the codec and initializes the codecs and converters needed to create
-	 * an install from end to end.
-	 */
-	public InstallCodec() {
-		this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);
-	}
-
-	@Override
-	public void encode(BsonWriter writer, Install value, EncoderContext encoderContext) {
-		Document doc = new Document();
-
-		doc.put(DatabaseFieldNames.DOCID, value.getId());
-		doc.put(DatabaseFieldNames.INSTALL_JAVA_VERSION, JavaVersionHelper.convertToDBSafe(value.getJavaVersion()));
-		doc.put(DatabaseFieldNames.INSTALL_VERSION, value.getVersion());
-		doc.put(DatabaseFieldNames.INSTALL_LISTING_ID, value.getListingId());
-		doc.put(DatabaseFieldNames.INSTALL_DATE, value.getInstallDate());
-		doc.put(DatabaseFieldNames.ECLIPSE_VERSION, value.getEclipseVersion());
-		doc.put(DatabaseFieldNames.OS, value.getOs());
-		doc.put(DatabaseFieldNames.LOCALE, value.getLocale());
-		documentCodec.encode(writer, doc, encoderContext);
-	}
-
-	@Override
-	public Class<Install> getEncoderClass() {
-		return Install.class;
-	}
-
-	@Override
-	public Install decode(BsonReader reader, DecoderContext decoderContext) {
-		Document document = documentCodec.decode(reader, decoderContext);
-		Install out = new Install();
-		out.setId(document.getString(DatabaseFieldNames.DOCID));
-		out.setJavaVersion(JavaVersionHelper.convertToDisplayValue(document.getString(DatabaseFieldNames.INSTALL_JAVA_VERSION)));
-		out.setVersion(document.getString(DatabaseFieldNames.INSTALL_VERSION));
-		out.setListingId(document.getString(DatabaseFieldNames.INSTALL_LISTING_ID));
-		out.setInstallDate(document.getDate(DatabaseFieldNames.INSTALL_DATE));
-		out.setEclipseVersion(document.getString(DatabaseFieldNames.ECLIPSE_VERSION));
-		out.setLocale(document.getString(DatabaseFieldNames.LOCALE));
-		out.setOs(document.getString(DatabaseFieldNames.OS));
-		return out;
-	}
-
-	@Override
-	public Install generateIdIfAbsentFromDocument(Install document) {
-		if (!documentHasId(document)) {
-			document.setId(UUID.randomUUID().toString());
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(Install document) {
-		return document.getId() != null;
-	}
-
-	@Override
-	public BsonValue getDocumentId(Install document) {
-		return new BsonString(document.getId());
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/InstallMetricsCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/InstallMetricsCodec.java
deleted file mode 100644
index ef3a33ab4e1175227b6a93ca3025ca6775f8771f..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/InstallMetricsCodec.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/* 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.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-
-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.InstallMetrics;
-import org.eclipsefoundation.marketplace.dto.MetricPeriod;
-import org.eclipsefoundation.marketplace.dto.converters.MetricPeriodConverter;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.mongodb.MongoClient;
-
-/**
- * Codec for getting and translating {@linkplain InstallMetrics} objects.
- * 
- * @author Martin Lowe
- *
- */
-public class InstallMetricsCodec implements CollectibleCodec<InstallMetrics> {
-	private static final Logger LOGGER = LoggerFactory.getLogger(InstallMetricsCodec.class);
-	private final Codec<Document> documentCodec;
-
-	private MetricPeriodConverter periodConverter;
-
-	/**
-	 * Creates the codec and initializes the codecs and converters needed to create
-	 * a listing from end to end.
-	 */
-	public InstallMetricsCodec() {
-		this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);
-		this.periodConverter = new MetricPeriodConverter();
-	}
-
-	@Override
-	public void encode(BsonWriter writer, InstallMetrics value, EncoderContext encoderContext) {
-		Document doc = new Document();
-
-		doc.put(DatabaseFieldNames.DOCID, value.getListingId());
-		doc.put(DatabaseFieldNames.PERIOD_COUNT, value.getTotal());
-
-		// get the periods and sort them
-		List<MetricPeriod> mps = value.getPeriods();
-		mps.sort((o1, o2) -> o2.getEnd().compareTo(o1.getEnd()));
-
-		LOGGER.debug("Parsing periods for {}", value.getListingId());
-		// get calendar to check months
-		Calendar c = Calendar.getInstance();
-		int curr = 0;
-		for (int i = 0; i < 12; i++) {
-			// get the next period
-			MetricPeriod period;
-			if (curr < mps.size()) {
-				period = mps.get(curr);
-				LOGGER.debug("Got period: {}", period);
-				// check that the retrieved period is the same month
-				Calendar periodCalendar = Calendar.getInstance();
-				periodCalendar.setTime(period.getEnd());
-				if (periodCalendar.get(Calendar.MONTH) != c.get(Calendar.MONTH)) {
-					LOGGER.debug("Regenerating period, {}:{}", periodCalendar.get(Calendar.MONTH),
-							c.get(Calendar.MONTH));
-					// if the month doesn't match, get a new month
-					period = generatePeriod(value.getListingId(), c);
-					LOGGER.debug("Regenerated period, {}", period);
-				} else {
-					// increment the array index pointer once its used
-					curr++;
-					// increment after we get a period
-					c.add(Calendar.MONTH, -1);
-				}
-			} else {
-				period = generatePeriod(value.getListingId(), c);
-				LOGGER.debug("Generated period: {}", period);
-			}
-			// put the period into the document
-			doc.put(DatabaseFieldNames.MONTH_OFFSET_PREFIX + i, periodConverter.convert(period));
-		}
-		documentCodec.encode(writer, doc, encoderContext);
-	}
-
-	@Override
-	public InstallMetrics decode(BsonReader reader, DecoderContext decoderContext) {
-		Document document = documentCodec.decode(reader, decoderContext);
-
-		InstallMetrics out = new InstallMetrics();
-		out.setListingId(document.getString(DatabaseFieldNames.DOCID));
-		out.setTotal(document.getInteger(DatabaseFieldNames.PERIOD_COUNT));
-
-		// get the base calendar for the documents
-		Calendar c = getBaseCalendar(document);
-		// create a list of periods
-		List<MetricPeriod> periods = new ArrayList<>(12);
-		for (int i = 0; i < 12; i++) {
-			MetricPeriod period = periodConverter.convert(document.get(DatabaseFieldNames.MONTH_OFFSET_PREFIX + i, Document.class));
-			// if there is no period, generate one. otherwise, increment c and continue
-			if (period == null) {
-				period = generatePeriod(out.getListingId(), c);
-			} else {
-				c.add(Calendar.MONTH, -1);
-			}
-			periods.add(period);
-		}
-		out.setPeriods(periods);
-
-		return out;
-	}
-
-	@Override
-	public Class<InstallMetrics> getEncoderClass() {
-		return InstallMetrics.class;
-	}
-
-	@Override
-	public InstallMetrics generateIdIfAbsentFromDocument(InstallMetrics document) {
-		if (!documentHasId(document)) {
-			throw new IllegalArgumentException(
-					"A listing ID must be set to InstallMetrics objects before writing or they are invalid");
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(InstallMetrics document) {
-		return !StringUtils.isBlank(document.getListingId());
-	}
-
-	@Override
-	public BsonValue getDocumentId(InstallMetrics document) {
-		return new BsonString(document.getListingId());
-	}
-
-	private Calendar getBaseCalendar(Document d) {
-		for (int i = 0; i < 12; i++) {
-			MetricPeriod period = periodConverter.convert(d.get(DatabaseFieldNames.MONTH_OFFSET_PREFIX + i, Document.class));
-			// if we have a period set, get its date
-			if (period != null) {
-				Calendar out = Calendar.getInstance();
-				out.setTime(period.getEnd());
-				// adjust the calendar to the base date
-				out.add(Calendar.MONTH, i);
-				return out;
-			}
-		}
-		// fall back to now as the base time as there is no date to compare to
-		return Calendar.getInstance();
-	}
-
-	private MetricPeriod generatePeriod(String listingId, Calendar c) {
-		MetricPeriod period = new MetricPeriod();
-		period.setListingId(listingId);
-		period.setCount(0);
-		period.setEnd(c.getTime());
-		c.add(Calendar.MONTH, -1);
-		period.setStart(c.getTime());
-
-		return period;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java
deleted file mode 100644
index ef2c7070044c8152877d7af86c23fee119e060b3..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/* 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 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.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.ListingVersionConverter;
-import org.eclipsefoundation.marketplace.dto.converters.TagConverter;
-import org.eclipsefoundation.marketplace.helper.DateTimeHelper;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-
-import com.mongodb.MongoClient;
-
-/**
- * MongoDB codec for transcoding of {@link Listing} and {@link Document}
- * objects. Used when writing or retrieving objects of given type from the
- * database.
- * 
- * @author Martin Lowe
- */
-public class ListingCodec implements CollectibleCodec<Listing> {
-	private final Codec<Document> documentCodec;
-
-	// converter objects for handling internal objects
-	private final AuthorConverter authorConverter;
-	private final OrganizationConverter organizationConverter;
-	private final TagConverter tagConverter;
-	private final ListingVersionConverter versionConverter;
-	private final CategoryConverter categoryConverter;
-
-	/**
-	 * Creates the codec and initializes the codecs and converters needed to create
-	 * a listing from end to end.
-	 */
-	public ListingCodec() {
-		this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);
-		this.authorConverter = new AuthorConverter();
-		this.organizationConverter = new OrganizationConverter();
-		this.tagConverter = new TagConverter();
-		this.versionConverter = new ListingVersionConverter();
-		this.categoryConverter = new CategoryConverter();
-	}
-
-	@Override
-	public void encode(BsonWriter writer, Listing value, EncoderContext encoderContext) {
-		Document doc = new Document();
-
-		// for each of the fields, get the value from the unencoded object and set it
-		doc.put(DatabaseFieldNames.DOCID, value.getId());
-		doc.put(DatabaseFieldNames.TITLE, value.getTitle());
-		doc.put(DatabaseFieldNames.URL, value.getUrl());
-		doc.put(DatabaseFieldNames.SUPPORT_PAGE_URL, value.getSupportUrl());
-		doc.put(DatabaseFieldNames.HOME_PAGE_URL, value.getHomepageUrl());
-		doc.put(DatabaseFieldNames.LISTING_BODY, value.getTeaser());
-		doc.put(DatabaseFieldNames.LISTING_TEASER, value.getBody());
-		doc.put(DatabaseFieldNames.MARKETPLACE_FAVORITES, value.getFavoriteCount());
-		doc.put(DatabaseFieldNames.LICENSE_TYPE, value.getLicense());
-		doc.put(DatabaseFieldNames.LISTING_STATUS, value.getStatus());
-		doc.put(DatabaseFieldNames.UPDATE_DATE, DateTimeHelper.toRFC3339(value.getUpdateDate()));
-		doc.put(DatabaseFieldNames.CREATION_DATE, DateTimeHelper.toRFC3339(value.getCreationDate()));
-		doc.put(DatabaseFieldNames.FOUNDATION_MEMBER_FLAG, value.isFoundationMember());
-		doc.put(DatabaseFieldNames.CATEGORY_IDS, value.getCategoryIds());
-		doc.put(DatabaseFieldNames.SCREENSHOTS, value.getScreenshots());
-		doc.put(DatabaseFieldNames.MARKET_IDS, value.getMarketIds());
-		
-		// for nested document types, use the converters to safely transform into BSON
-		// documents
-		doc.put(DatabaseFieldNames.LISTING_ORGANIZATIONS, organizationConverter.convert(value.getOrganization()));
-		doc.put(DatabaseFieldNames.LISTING_AUTHORS,
-				value.getAuthors().stream().map(authorConverter::convert).collect(Collectors.toList()));
-		doc.put(DatabaseFieldNames.LISTING_TAGS,
-				value.getTags().stream().map(tagConverter::convert).collect(Collectors.toList()));
-		documentCodec.encode(writer, doc, encoderContext);
-	}
-
-	@Override
-	public Class<Listing> getEncoderClass() {
-		return Listing.class;
-	}
-
-	@Override
-	public Listing decode(BsonReader reader, DecoderContext decoderContext) {
-		Document document = documentCodec.decode(reader, decoderContext);
-		Listing out = new Listing();
-
-		// for each field, get the value from the encoded object and set it in POJO
-		out.setId(document.getString(DatabaseFieldNames.DOCID));
-		out.setTitle(document.getString(DatabaseFieldNames.TITLE));
-		out.setUrl(document.getString(DatabaseFieldNames.URL));
-		out.setSupportUrl(document.getString(DatabaseFieldNames.SUPPORT_PAGE_URL));
-		out.setHomepageUrl(document.getString(DatabaseFieldNames.HOME_PAGE_URL));
-		out.setTeaser(document.getString(DatabaseFieldNames.LISTING_TEASER));
-		out.setBody(document.getString(DatabaseFieldNames.LISTING_BODY));
-		out.setStatus(document.getString(DatabaseFieldNames.LISTING_STATUS));
-		out.setInstallsRecent(document.getInteger(DatabaseFieldNames.RECENT_INSTALLS,0));
-		out.setInstallsTotal(document.getInteger(DatabaseFieldNames.TOTAL_INSTALLS,0));
-		out.setLicense(document.getString(DatabaseFieldNames.LICENSE_TYPE));
-		out.setFavoriteCount(document.getLong(DatabaseFieldNames.MARKETPLACE_FAVORITES));
-		out.setFoundationMember(document.getBoolean(DatabaseFieldNames.FOUNDATION_MEMBER_FLAG));
-		out.setCategoryIds(document.getList(DatabaseFieldNames.CATEGORY_IDS, String.class));
-		out.setMarketIds(document.getList(DatabaseFieldNames.MARKET_IDS, String.class));
-		out.setScreenshots(document.getList(DatabaseFieldNames.SCREENSHOTS, String.class));
-
-		// for nested document types, use the converters to safely transform into POJO
-		out.setAuthors(document.getList(DatabaseFieldNames.LISTING_AUTHORS, Document.class).stream()
-				.map(authorConverter::convert).collect(Collectors.toList()));
-		out.setOrganization(
-				organizationConverter.convert(document.get(DatabaseFieldNames.LISTING_ORGANIZATIONS, Document.class)));
-		out.setTags(document.getList(DatabaseFieldNames.LISTING_TAGS, Document.class).stream()
-				.map(tagConverter::convert).collect(Collectors.toList()));
-		out.setVersions(document.getList(DatabaseFieldNames.LISTING_VERSIONS, Document.class).stream()
-				.map(versionConverter::convert).collect(Collectors.toList()));
-		out.setCategories(document.getList(DatabaseFieldNames.LISTING_CATEGORIES, Document.class).stream()
-				.map(categoryConverter::convert).collect(Collectors.toList()));
-
-		// convert date to date string
-		out.setCreationDate(DateTimeHelper.toRFC3339(document.getDate(DatabaseFieldNames.CREATION_DATE)));
-		out.setUpdateDate(DateTimeHelper.toRFC3339(document.getDate(DatabaseFieldNames.UPDATE_DATE)));
-
-		return out;
-	}
-
-	@Override
-	public Listing generateIdIfAbsentFromDocument(Listing document) {
-		if (!documentHasId(document)) {
-			document.setId(UUID.randomUUID().toString());
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(Listing document) {
-		return document.getId() != null;
-	}
-
-	@Override
-	public BsonValue getDocumentId(Listing document) {
-		return new BsonString(document.getId());
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingVersionCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingVersionCodec.java
deleted file mode 100644
index 2bda5745771fcd0d405e83e7f119fe3ec01362f6..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingVersionCodec.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/* 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.ListingVersion;
-import org.eclipsefoundation.marketplace.dto.converters.ListingVersionConverter;
-
-import com.mongodb.MongoClient;
-
-
-/**
- * MongoDB codec for transcoding of {@link ListingVersion} and {@link Document}
- * objects. Used when writing or retrieving objects of given type from the
- * database.
- * 
- * @author Martin Lowe
- */
-public class ListingVersionCodec implements CollectibleCodec<ListingVersion> {
-	private final Codec<Document> documentCodec;
-
-	private ListingVersionConverter cc;
-
-	/**
-	 * Creates the codec and initializes the codecs and converters needed to create
-	 * a listing from end to end.
-	 */
-	public ListingVersionCodec() {
-		this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);
-		this.cc = new ListingVersionConverter();
-	}
-	
-	@Override
-	public void encode(BsonWriter writer, ListingVersion value, EncoderContext encoderContext) {
-		documentCodec.encode(writer, cc.convert(value), encoderContext);
-	}
-
-	@Override
-	public Class<ListingVersion> getEncoderClass() {
-		return ListingVersion.class;
-	}
-
-	@Override
-	public ListingVersion decode(BsonReader reader, DecoderContext decoderContext) {
-		return cc.convert(documentCodec.decode(reader, decoderContext));
-	}
-
-	@Override
-	public ListingVersion generateIdIfAbsentFromDocument(ListingVersion document) {
-		if (!documentHasId(document)) {
-			document.setId(UUID.randomUUID().toString());
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(ListingVersion document) {
-		return !StringUtils.isBlank(document.getId());
-	}
-
-	@Override
-	public BsonValue getDocumentId(ListingVersion document) {
-		return new BsonString(document.getId());
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/MarketCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/MarketCodec.java
deleted file mode 100644
index 95414a779020e4f077647e9d1f34436fda3e1237..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/MarketCodec.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/* 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 java.util.stream.Collectors;
-
-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.Market;
-import org.eclipsefoundation.marketplace.dto.converters.CategoryConverter;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-
-import com.mongodb.MongoClient;
-
-/**
- * MongoDB codec for transcoding of {@link Market} and {@link Document}
- * objects. Used when writing or retrieving objects of given type from the
- * database.
- * 
- * @author Martin Lowe
- */
-public class MarketCodec implements CollectibleCodec<Market> {
-	private final Codec<Document> documentCodec;
-	private final CategoryConverter categoryConverter;
-
-	/**
-	 * Creates the codec and initializes the codecs and converters needed to create
-	 * a listing from end to end.
-	 */
-	public MarketCodec() {
-		this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);
-		this.categoryConverter = new CategoryConverter();
-	}
-	
-	@Override
-	public void encode(BsonWriter writer, Market value, EncoderContext encoderContext) {
-		Document doc = new Document();
-
-		doc.put(DatabaseFieldNames.DOCID, value.getId());
-		doc.put(DatabaseFieldNames.URL, value.getUrl());
-		doc.put(DatabaseFieldNames.TITLE, value.getTitle());
-		doc.put(DatabaseFieldNames.LISTING_IDS, value.getListingIds());
-
-		documentCodec.encode(writer, doc, encoderContext);
-	}
-
-	@Override
-	public Class<Market> getEncoderClass() {
-		return Market.class;
-	}
-
-	@Override
-	public Market decode(BsonReader reader, DecoderContext decoderContext) {
-		Document document = documentCodec.decode(reader, decoderContext);
-
-		Market out = new Market();
-		out.setId(document.getString(DatabaseFieldNames.DOCID));
-		out.setUrl(document.getString(DatabaseFieldNames.URL));
-		out.setTitle(document.getString(DatabaseFieldNames.TITLE));
-		out.setListingIds(document.getList(DatabaseFieldNames.LISTING_IDS, String.class));
-		out.setCategories(document.getList(DatabaseFieldNames.LISTING_CATEGORIES, Document.class).stream()
-				.map(categoryConverter::convert).collect(Collectors.toList()));
-		
-		return out;
-	}
-
-	@Override
-	public Market generateIdIfAbsentFromDocument(Market document) {
-		if (!documentHasId(document)) {
-			document.setId(UUID.randomUUID().toString());
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(Market document) {
-		return StringUtils.isNotBlank(document.getId());
-	}
-
-	@Override
-	public BsonValue getDocumentId(Market document) {
-		return new BsonString(document.getId());
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/MetricPeriodCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/MetricPeriodCodec.java
deleted file mode 100644
index c4d4905cd63a386add61900e6b9fc614b68b8039..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/MetricPeriodCodec.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/* 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.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.InstallMetrics;
-import org.eclipsefoundation.marketplace.dto.MetricPeriod;
-import org.eclipsefoundation.marketplace.dto.converters.MetricPeriodConverter;
-
-import com.mongodb.MongoClient;
-
-/**
- * Codec for getting and translating {@linkplain MetricPeriod} objects. These do
- * not represent a table but a section of the {@linkplain InstallMetrics} that
- * are generated from the install table.
- * 
- * @author Martin Lowe
- *
- */
-public class MetricPeriodCodec implements CollectibleCodec<MetricPeriod> {
-	private final Codec<Document> documentCodec;
-
-	private MetricPeriodConverter periodConverter;
-
-	/**
-	 * Creates the codec and initializes the codecs and converters needed to create
-	 * a listing from end to end.
-	 */
-	public MetricPeriodCodec() {
-		this.documentCodec = MongoClient.getDefaultCodecRegistry().get(Document.class);
-		this.periodConverter = new MetricPeriodConverter();
-	}
-
-	@Override
-	public void encode(BsonWriter writer, MetricPeriod value, EncoderContext encoderContext) {
-		documentCodec.encode(writer, periodConverter.convert(value), encoderContext);
-	}
-
-	@Override
-	public Class<MetricPeriod> getEncoderClass() {
-		return MetricPeriod.class;
-	}
-
-	@Override
-	public MetricPeriod decode(BsonReader reader, DecoderContext decoderContext) {
-		return periodConverter.convert(documentCodec.decode(reader, decoderContext));
-	}
-
-	@Override
-	public MetricPeriod generateIdIfAbsentFromDocument(MetricPeriod document) {
-		if (!documentHasId(document)) {
-			throw new IllegalArgumentException(
-					"A listing ID must be set to MetricPeriod objects before writing or they are invalid");
-		}
-		return document;
-	}
-
-	@Override
-	public boolean documentHasId(MetricPeriod document) {
-		return !StringUtils.isBlank(document.getListingId());
-	}
-
-	@Override
-	public BsonValue getDocumentId(MetricPeriod document) {
-		return new BsonString(document.getListingId());
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/AuthorConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/AuthorConverter.java
deleted file mode 100644
index eedc0f71a7e71be50d54046f0729e9b5c9998e76..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/AuthorConverter.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/* 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.Author;
-
-/**
- * Converter implementation for the {@link Author} object.
- * 
- * @author Martin Lowe
- */
-public class AuthorConverter implements Converter<Author> {
-
-	@Override
-	public Author convert(Document src) {
-		Author auth = new Author();
-		auth.setFullName(src.getString("full_name"));
-		auth.setUsername(src.getString("username"));
-
-		return auth;
-	}
-
-	@Override
-	public Document convert(Author src) {
-		Document doc = new Document();
-		doc.put("full_name", src.getFullName());
-		doc.put("username", src.getUsername());
-
-		return doc;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/CategoryConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/CategoryConverter.java
deleted file mode 100644
index 14d0aaf62da8f20668f0dedff399f4d5a8743abd..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/CategoryConverter.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* 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.DatabaseFieldNames;
-
-/**
- * Converter implementation for the {@link Category} object.
- * 
- * @author Martin Lowe
- */
-public class CategoryConverter implements Converter<Category> {
-
-	@Override
-	public Category convert(Document src) {
-		Category out = new Category();
-		out.setId(src.getString(DatabaseFieldNames.DOCID));
-		out.setTitle(src.getString(DatabaseFieldNames.TITLE));
-		out.setUrl(src.getString(DatabaseFieldNames.URL));
-		return out;
-	}
-
-	@Override
-	public Document convert(Category src) {
-		Document doc = new Document();
-		doc.put(DatabaseFieldNames.DOCID, src.getId());
-		doc.put(DatabaseFieldNames.TITLE, src.getTitle());
-		doc.put(DatabaseFieldNames.URL, src.getUrl());
-		return doc;
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/Converter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/Converter.java
deleted file mode 100644
index 343b19573a3e453a532885bb041f18b703de1dff..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/Converter.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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;
-
-/**
- * Converters are used for POJOs nested in other POJOs to be written to the
- * MongoDB. In current state, nested document types aren't able to detect custom
- * codecs defined in registries.
- * 
- * @author Martin Lowe
- */
-public interface Converter<T> {
-
-	/**
-	 * Converts a BSON document into a POJO.
-	 * 
-	 * @param src BSON document to convert
-	 * @return converted POJO object
-	 */
-	T convert(Document src);
-
-	/**
-	 * Converts a POJO into a BSON document.
-	 * 
-	 * @param src object to conert
-	 * @return converted BSON document
-	 */
-	Document convert(T src);
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/FeatureIdConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/FeatureIdConverter.java
deleted file mode 100644
index 29b3fad142491094fd977aecec81d8282648cbfa..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/FeatureIdConverter.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/* 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.FeatureId;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-
-/**
- * Converter implementation for the {@link FeatureId} object.
- * 
- * @author Martin Lowe
- */
-public class FeatureIdConverter implements Converter<FeatureId> {
-
-	@Override
-	public FeatureId convert(Document src) {
-		FeatureId featureId = new FeatureId();
-
-		featureId.setName(src.getString(DatabaseFieldNames.FEATURE_ID));
-		featureId.setInstallState(src.getString(DatabaseFieldNames.INSTALL_STATE));
-
-		return featureId;
-	}
-
-	@Override
-	public Document convert(FeatureId src) {
-		Document doc = new Document();
-		doc.put(DatabaseFieldNames.FEATURE_ID, src.getName());
-		doc.put(DatabaseFieldNames.INSTALL_STATE, src.getInstallState());
-		return doc;
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/ListingVersionConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/ListingVersionConverter.java
deleted file mode 100644
index bf6eb97243a89264d96253c361aa5faeff1e7294..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/ListingVersionConverter.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/* 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 java.util.stream.Collectors;
-
-import org.bson.Document;
-import org.eclipsefoundation.marketplace.dto.ListingVersion;
-import org.eclipsefoundation.marketplace.helper.JavaVersionHelper;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-
-/**
- * Converter implementation for the {@link ListingVersion} object.
- * 
- * @author Martin Lowe
- */
-public class ListingVersionConverter implements Converter<ListingVersion> {
-
-	private final FeatureIdConverter featureIdConverter = new FeatureIdConverter();
-
-	@Override
-	public ListingVersion convert(Document src) {
-		ListingVersion version = new ListingVersion();
-		version.setId(src.getString(DatabaseFieldNames.DOCID));
-		version.setListingId(src.getString(DatabaseFieldNames.LISTING_ID));
-		version.setEclipseVersions(src.getList("compatible_versions", String.class));
-		version.setPlatforms(src.getList("platforms", String.class));
-		version.setMinJavaVersion(JavaVersionHelper.convertToDisplayValue(Integer.toString(src.getInteger("min_java_version"))));
-		version.setUpdateSiteUrl(src.getString("update_site_url"));
-		version.setVersion(src.getString("version"));
-		version.setFeatureIds(src.getList(DatabaseFieldNames.FEATURE_IDS, Document.class).stream()
-				.map(featureIdConverter::convert).collect(Collectors.toList()));
-		return version;
-	}
-
-	@Override
-	public Document convert(ListingVersion src) {
-		Document doc = new Document();
-		doc.put(DatabaseFieldNames.DOCID, src.getId());
-		doc.put(DatabaseFieldNames.LISTING_ID, src.getListingId());
-		doc.put("compatible_versions", src.getEclipseVersions());
-		doc.put("platforms", src.getPlatforms());
-		doc.put("min_java_version", Integer.valueOf(JavaVersionHelper.convertToDBSafe(src.getMinJavaVersion())));
-		doc.put("update_site_url", src.getUpdateSiteUrl());
-		doc.put("version", src.getVersion());
-		doc.put(DatabaseFieldNames.FEATURE_IDS,
-				src.getFeatureIds().stream().map(featureIdConverter::convert).collect(Collectors.toList()));
-		return doc;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/MetricPeriodConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/MetricPeriodConverter.java
deleted file mode 100644
index 9a1fe87b11a1e2fe2d5f851bc1745ae747f6f8a0..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/MetricPeriodConverter.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/* 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.MetricPeriod;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
-
-/**
- * Converter implementation for the {@link MetricPeriod} object.
- * 
- * @author Martin Lowe
- */
-public class MetricPeriodConverter implements Converter<MetricPeriod> {
-
-	@Override
-	public MetricPeriod convert(Document src) {
-		MetricPeriod out = new MetricPeriod();
-		out.setListingId(src.getString(DatabaseFieldNames.DOCID));
-		out.setStart(src.getDate(DatabaseFieldNames.PERIOD_START));
-		out.setEnd(src.getDate(DatabaseFieldNames.PERIOD_END));
-		out.setCount(src.getInteger(DatabaseFieldNames.PERIOD_COUNT));
-		return out;
-	}
-
-	@Override
-	public Document convert(MetricPeriod src) {
-		Document doc = new Document();
-		doc.put(DatabaseFieldNames.DOCID, src.getListingId());
-		doc.put(DatabaseFieldNames.PERIOD_START, src.getStart());
-		doc.put(DatabaseFieldNames.PERIOD_END, src.getEnd());
-		doc.put(DatabaseFieldNames.PERIOD_COUNT, src.getCount());
-		return doc;
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/OrganizationConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/OrganizationConverter.java
deleted file mode 100644
index 732732892998072ce2bea15d50bbe21925825cb7..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/OrganizationConverter.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/* 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.Organization;
-
-/**
- * Converter implementation for the {@link Organization} object.
- * 
- * @author Martin Lowe
- */
-public class OrganizationConverter implements Converter<Organization> {
-
-	@Override
-	public Organization convert(Document src) {
-		Organization org = new Organization();
-
-		org.setId(src.getString("id"));
-		org.setName(src.getString("name"));
-
-		return org;
-	}
-
-	@Override
-	public Document convert(Organization src) {
-		Document doc = new Document();
-		doc.put("name", src.getName());
-		doc.put("id", src.getId());
-		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
deleted file mode 100644
index 932a8bd9ab9e661ab8d11c7b7c27254c748c1336..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/TabConverter.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/* 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/converters/TagConverter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/converters/TagConverter.java
deleted file mode 100644
index 5d06635ac7ee18d7a24a51301150a270a9b14e51..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/converters/TagConverter.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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.Tag;
-
-/**
- * Converter implementation for the {@link Tag} object.
- * 
- * @author Martin Lowe
- */
-public class TagConverter implements Converter<Tag> {
-
-	@Override
-	public Tag convert(Document src) {
-		Tag tag = new Tag();
-		tag.setId(src.getString("id"));
-		tag.setName(src.getString("name"));
-		return tag;
-	}
-
-	@Override
-	public Document convert(Tag src) {
-		Document doc = new Document();
-		doc.put("name", src.getName());
-		doc.put("id", src.getId());
-		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
index 4c15814fee2cae9e232e0ed9fc20c61e20b6bdcd..13abf697322e3f7166df5c7fe10587b2b5c86a19 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CatalogFilter.java
@@ -6,21 +6,50 @@
  */
 package org.eclipsefoundation.marketplace.dto.filter;
 
+import java.util.Optional;
+import java.util.UUID;
+
 import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
 
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.dto.Catalog;
+import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
+import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
+import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 
 /**
  * Filter implementation for the {@link Catalog} class.
  * 
  * @author Martin Lowe
- * 
  */
 @ApplicationScoped
 public class CatalogFilter implements DtoFilter<Catalog> {
 
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
+	
+	@Override
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.CATALOG.getTable());
+		if (isRoot) {
+			// ID check
+			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
+			if (id.isPresent()) {
+				stmt.addClause(new ParameterizedSQLStatement.Clause(
+						DtoTableNames.CATALOG.getAlias() + "." + DatabaseFieldNames.DOCID + " = ?",
+						new Object[] { UUID.fromString(id.get()) }));
+			}
+		}
+		return stmt;
+	}
+
 	@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 7f7bfd3614b92aef175f0bf14c4eaf85cb72d563..708c81c05080278ee01fddfa396a89a1a8376aeb 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/CategoryFilter.java
@@ -6,20 +6,48 @@
  */
 package org.eclipsefoundation.marketplace.dto.filter;
 
+import java.util.Optional;
+import java.util.UUID;
+
 import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
 
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.dto.Category;
-
+import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
+import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
+import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 
 /**
- * Filter implementation for the {@link Category} class.
+ * Filter implementation for the Category class.
  * 
  * @author Martin Lowe
- * 
  */
 @ApplicationScoped
 public class CategoryFilter implements DtoFilter<Category> {
 
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
+	
+
+	@Override
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.CATEGORY.getTable());
+		if (isRoot) {
+			// ID check
+			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
+			if (id.isPresent()) {
+				stmt.addClause(new ParameterizedSQLStatement.Clause(
+						DtoTableNames.CATEGORY.getAlias() + "." + DatabaseFieldNames.DOCID + " = ?",
+						new Object[] { UUID.fromString(id.get()) }));
+			}
+		}
+		return stmt;
+	}
+
 	@Override
 	public Class<Category> getType() {
 		return Category.class;
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 6f5f4618ef2012d09b87d82740dd21cdacbafb83..f16ca52f6c267fb3694da4b6dcf73d9e0f34d693 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.QueryParameters;
+import org.eclipsefoundation.marketplace.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
 
@@ -29,50 +29,51 @@ import com.mongodb.client.model.Filters;
 @ApplicationScoped
 public class ErrorReportFilter implements DtoFilter<ErrorReport> {
 
-	@Override
-	public List<Bson> getFilters(QueryParameters params, String root) {
-		List<Bson> filters = new ArrayList<>();
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
+	
 
-		// ErrorReport ID check
-		Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
-		if (id.isPresent()) {
-			filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
+	@Override
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.ERRORREPORT.getTable());
+		if (isRoot) {
+			// ID check
+			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
+			if (id.isPresent()) {
+				stmt.addClause(new ParameterizedSQLStatement.Clause(
+						DtoTableNames.ERRORREPORT.getAlias() + "." + DatabaseFieldNames.DOCID + " = ?",
+						new Object[] { UUID.fromString(id.get()) }));
+			}
 		}
-
-		// select by multiple IDs
-		List<String> ids = params.getValues(UrlParameterNames.IDS.getParameterName());
+		// IDS
+		List<String> ids = wrap.getParams(UrlParameterNames.IDS);
 		if (!ids.isEmpty()) {
-			filters.add(Filters.in(DatabaseFieldNames.DOCID, ids));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.ERRORREPORT.getAlias() + "." + DatabaseFieldNames.DOCID + " = ?",
+					new Object[] { ids.stream().map(UUID::fromString).collect(Collectors.toList()) }));
 		}
-
-		// listing ID check
-		Optional<String> listingId = params.getFirstIfPresent(UrlParameterNames.LISTING_ID.getParameterName());
+		// listing ID filter
+		Optional<String> listingId = wrap.getFirstParam(UrlParameterNames.LISTING_ID);
 		if (listingId.isPresent()) {
-			filters.add(Filters.eq(DatabaseFieldNames.LISTING_ID, listingId.get()));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.ERRORREPORT.getAlias() + "." + DatabaseFieldNames.LISTING_ID + " = ?",
+					new Object[] { UUID.fromString(listingId.get()) }));
 		}
-
-		// listing ID check
-		Optional<String> isRead = params.getFirstIfPresent(UrlParameterNames.READ.getParameterName());
+		// read filter
+		Optional<String> isRead = wrap.getFirstParam(UrlParameterNames.READ);
 		if (isRead.isPresent()) {
-			filters.add(Filters.eq(DatabaseFieldNames.ERROR_READ, Boolean.valueOf(isRead.get())));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.ERRORREPORT.getAlias() + "." + DatabaseFieldNames.ERROR_READ + " = ?",
+					new Object[] { Boolean.valueOf(isRead.get()) }));
 		}
-		
-		// select by feature ID
-		List<String> featureId = params.getValues(UrlParameterNames.FEATURE_ID.getParameterName());
-		if (!featureId.isEmpty()) {
-			filters.add(Filters.in(DatabaseFieldNames.ERROR_FEATURE_IDS, featureId));
+		// feature IDs
+		Optional<String> featureId = wrap.getFirstParam(UrlParameterNames.FEATURE_ID);
+		if (featureId.isPresent()) {
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.ERRORREPORT.getAlias() + "." + DatabaseFieldNames.ERROR_FEATURE_IDS + " = ?",
+					new Object[] { featureId.get() }));
 		}
-		// text search
-		Optional<String> text = params.getFirstIfPresent(UrlParameterNames.QUERY_STRING.getParameterName());
-		if (text.isPresent()) {
-			filters.add(Filters.text(text.get()));
-		}
-		return filters;
-	}
-
-	@Override
-	public List<Bson> getAggregates(QueryParameters params) {
-		return Collections.emptyList();
+		return stmt;
 	}
 
 	@Override
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 2cd260db2b78ee2b7f85bccb56130b32221f09da..9cf0ef9822256ad1485251d48a5cfd0ea143cf23 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallFilter.java
@@ -11,17 +11,21 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
 
 import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
 
 import org.apache.commons.lang3.StringUtils;
-import org.bson.conversions.Bson;
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.dto.Install;
-import org.eclipsefoundation.marketplace.model.QueryParameters;
+import org.eclipsefoundation.marketplace.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
+import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
-
-import com.mongodb.client.model.Filters;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 
 /**
  * Filter implementation for the {@linkplain Install} class.
@@ -31,49 +35,59 @@ import com.mongodb.client.model.Filters;
 @ApplicationScoped
 public class InstallFilter implements DtoFilter<Install> {
 
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
+	
+
 	@Override
-	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) {
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.INSTALL.getTable());
+
+		if (isRoot) {
 			// ID check
-			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
+			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
 			if (id.isPresent()) {
-				filters.add(Filters.eq(DatabaseFieldNames.LISTING_ID, id.get()));
+				stmt.addClause(new ParameterizedSQLStatement.Clause(
+						DtoTableNames.INSTALL.getAlias() + "." + DatabaseFieldNames.LISTING_ID + " = ?",
+						new Object[] { UUID.fromString(id.get()) }));
 			}
 		}
 		// version check
-		Optional<String> version = params.getFirstIfPresent(UrlParameterNames.VERSION.getParameterName());
+		Optional<String> version = wrap.getFirstParam(UrlParameterNames.VERSION);
 		if (version.isPresent()) {
-			filters.add(Filters.eq(DatabaseFieldNames.INSTALL_VERSION, version.get()));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.INSTALL.getAlias() + "." + DatabaseFieldNames.INSTALL_VERSION + " = ?",
+					new Object[] { version.get() }));
 		}
 		// OS filter
-		Optional<String> os = params.getFirstIfPresent(UrlParameterNames.OS.getParameterName());
+		Optional<String> os = wrap.getFirstParam(UrlParameterNames.OS);
 		if (os.isPresent()) {
-			filters.add(Filters.eq(DatabaseFieldNames.OS, os.get()));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.INSTALL.getAlias() + "." + DatabaseFieldNames.OS + " = ?",
+					new Object[] { os.get() }));
 		}
 		// eclipse version
-		Optional<String> eclipseVersion = params.getFirstIfPresent(UrlParameterNames.ECLIPSE_VERSION.getParameterName());
+		Optional<String> eclipseVersion = wrap.getFirstParam(UrlParameterNames.ECLIPSE_VERSION);
 		if (eclipseVersion.isPresent()) {
-			filters.add(Filters.eq(DatabaseFieldNames.ECLIPSE_VERSION, eclipseVersion.get()));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.INSTALL.getAlias() + "." + DatabaseFieldNames.ECLIPSE_VERSION + " = ?",
+					new Object[] { 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 = params.getFirstIfPresent(UrlParameterNames.JAVA_VERSION.getParameterName());
-		if (javaVersion.isPresent()) {
-			filters.add(Filters.gte(DatabaseFieldNames.INSTALL_JAVA_VERSION, javaVersion.get()));
+		// Java version
+		Optional<String> javaVersion = wrap.getFirstParam(UrlParameterNames.JAVA_VERSION);
+		if (javaVersion.isPresent() && StringUtils.isNumeric(javaVersion.get())) {
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.INSTALL.getAlias() + "." + DatabaseFieldNames.INSTALL_JAVA_VERSION + " >= ?",
+					new Object[] { Integer.valueOf(javaVersion.get()) }));
 		}
-		Optional<String> date = params.getFirstIfPresent(UrlParameterNames.DATE_FROM.getParameterName());
+		// solution version - Java version
+		Optional<String> date = wrap.getFirstParam(UrlParameterNames.DATE_FROM);
 		if (date.isPresent() && StringUtils.isNumeric(date.get())) {
-			filters.add(Filters.gte(DatabaseFieldNames.INSTALL_DATE, new Date(Integer.valueOf(date.get()))));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.INSTALL.getAlias() + "." + DatabaseFieldNames.INSTALL_DATE + " >= ?",
+					new Object[] { Integer.valueOf(date.get()) }));
 		}
-		return filters;
-	}
-
-	@Override
-	public List<Bson> getAggregates(QueryParameters params) {
-		return Collections.emptyList();
+		return stmt;
 	}
 
 	@Override
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricFilter.java b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b64ac2ca2beab0020fed07523f695101d235991
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/InstallMetricFilter.java
@@ -0,0 +1,65 @@
+/* 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.Optional;
+import java.util.UUID;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.dto.InstallMetrics;
+import org.eclipsefoundation.marketplace.dto.MetricPeriod;
+import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
+import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
+import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
+
+/**
+ * Filter implementation for the Category class.
+ * 
+ * @author Martin Lowe
+ */
+@ApplicationScoped
+public class InstallMetricFilter implements DtoFilter<InstallMetrics> {
+
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
+	
+
+	@Inject
+	DtoFilter<MetricPeriod> metricPeriodFilter;
+
+	@Override
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.INSTALL_METRIC.getTable());
+
+		// listing ID filter
+		Optional<String> listingId = wrap.getFirstParam(UrlParameterNames.LISTING_ID);
+		if (listingId.isPresent()) {
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.INSTALL_METRIC.getAlias() + "." + DatabaseFieldNames.LISTING_ID + " = ?",
+					new Object[] { UUID.fromString(listingId.get()) }));
+		}
+		
+		// retrieve the metric period filters.
+		stmt.addJoin(new ParameterizedSQLStatement.Join(DtoTableNames.INSTALL_METRIC.getTable(), DtoTableNames.METRIC_PERIOD.getTable(),
+				"periods"));
+		stmt.combine(metricPeriodFilter.getFilters(wrap, false));
+
+		return stmt;
+	}
+
+	@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 2687c7933e1b5fc7266820c80a323c658b6be1f6..b966df476b318ceab453db83772e084db0aeeb47 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingFilter.java
@@ -9,23 +9,21 @@ package org.eclipsefoundation.marketplace.dto.filter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
 
 import javax.enterprise.context.ApplicationScoped;
 import javax.inject.Inject;
 
-import org.bson.conversions.Bson;
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.dto.Listing;
 import org.eclipsefoundation.marketplace.dto.ListingVersion;
-import org.eclipsefoundation.marketplace.model.QueryParameters;
+import org.eclipsefoundation.marketplace.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
-
-import com.mongodb.client.model.Aggregates;
-import com.mongodb.client.model.Field;
-import com.mongodb.client.model.Filters;
-import com.mongodb.client.model.Projections;
-import com.mongodb.client.model.UnwindOptions;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 
 /**
  * Filter implementation for the {@linkplain Listing} class.
@@ -35,73 +33,56 @@ import com.mongodb.client.model.UnwindOptions;
 @ApplicationScoped
 public class ListingFilter implements DtoFilter<Listing> {
 
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
 	@Inject
 	DtoFilter<ListingVersion> listingVersionFilter;
-	
+
 	@Override
-	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) {
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.LISTING.getTable());
+		if (isRoot) {
 			// ID check
-			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
+			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
 			if (id.isPresent()) {
-				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
+				stmt.addClause(new ParameterizedSQLStatement.Clause(
+						DtoTableNames.LISTING.getAlias() + "." + DatabaseFieldNames.DOCID + " = ?",
+						new Object[] { UUID.fromString(id.get()) }));
 			}
 		}
 
 		// select by multiple IDs
-		List<String> ids = params.getValues(UrlParameterNames.IDS.getParameterName());
+		List<String> ids = wrap.getParams(UrlParameterNames.IDS);
 		if (!ids.isEmpty()) {
-			filters.add(Filters.in(DatabaseFieldNames.DOCID, ids));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.LISTING.getAlias() + "." + DatabaseFieldNames.DOCID + " = ?", new Object[] { ids }));
 		}
 
 		// Listing license type check
-		Optional<String> licType = params.getFirstIfPresent(DatabaseFieldNames.LICENSE_TYPE);
+		Optional<String> licType = wrap.getFirstParam(DatabaseFieldNames.LICENSE_TYPE);
 		if (licType.isPresent()) {
-			filters.add(Filters.eq(DatabaseFieldNames.LICENSE_TYPE, licType.get()));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(DtoTableNames.LISTING.getAlias() + ".licenseType = ?",
+					new Object[] { licType.get() }));
 		}
 
+		// TODO, this might need some data structure jigging
 		// select by multiple tags
-		List<String> tags = params.getValues(UrlParameterNames.TAGS.getParameterName());
-		if (!tags.isEmpty()) {
-			filters.add(Filters.in(DatabaseFieldNames.LISTING_TAGS + ".title", tags));
-		}
+		List<String> tags = wrap.getParams(UrlParameterNames.TAGS);
+		//
+		// if (!tags.isEmpty()) {
+		// stmt.addClause(new ParameterizedSQLStatement.Clause(
+		// DtoTableNames.LISTING.getAlias() + "." + DatabaseFieldNames.DOCID + " = ?",
+		// new Object[] { ids },
+		// new JDBCType[] { JDBCType.ARRAY }));
+		// }
 
-		// text search
-		Optional<String> text = params.getFirstIfPresent(UrlParameterNames.QUERY_STRING.getParameterName());
-		if (text.isPresent()) {
-			filters.add(Filters.text(text.get()));
-		}
-		return filters;
-	}
+		// retrieve the listing version filters.
+		stmt.addJoin(
+				new ParameterizedSQLStatement.Join(DtoTableNames.LISTING.getTable(), DtoTableNames.LISTING_VERSION.getTable(), "versions"));
+		stmt.combine(listingVersionFilter.getFilters(wrap, false));
+		// TODO lookup install metric values from install_metrics table
 
-	@Override
-	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(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 = params.getValues(UrlParameterNames.MARKET_IDS.getParameterName());
-		if (!marketIds.isEmpty()) {
-			aggs.add(Aggregates.match(Filters.in("categories.market_ids", marketIds)));
-		}
-		// adds a $lookup aggregate, joining install metrics on ids as "installs"
-		aggs.add(Aggregates.lookup(DtoTableNames.INSTALL_METRIC.getTableName(), DatabaseFieldNames.DOCID,
-				DatabaseFieldNames.DOCID, "installs"));
-		// unwinds the installs out of arrays
-		aggs.add(Aggregates.unwind("$installs", new UnwindOptions().preserveNullAndEmptyArrays(true)));
-		// push the installs counts to the listing, and remove the installs merged in
-		aggs.add(Aggregates.addFields(new Field<String>(DatabaseFieldNames.RECENT_INSTALLS, "$installs.offset_0.count"),
-				new Field<String>(DatabaseFieldNames.TOTAL_INSTALLS, "$installs.count")));
-		aggs.add(Aggregates.project(Projections.exclude("installs")));
-		return aggs;
+		return stmt;
 	}
 
 	@Override
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 4334ca984077adafcdbcd33e372e5a3b6d417a01..4a3e7f46e1486c6821f391826bb81a551f38c859 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/ListingVersionFilter.java
@@ -10,17 +10,19 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
 
 import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
 
 import org.apache.commons.lang3.StringUtils;
-import org.bson.conversions.Bson;
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.dto.ListingVersion;
-import org.eclipsefoundation.marketplace.model.QueryParameters;
-import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
+import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
-
-import com.mongodb.client.model.Filters;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 
 /**
  * Filter implementation for the {@linkplain ListingVersion} class.
@@ -31,40 +33,44 @@ import com.mongodb.client.model.Filters;
 @ApplicationScoped
 public class ListingVersionFilter implements DtoFilter<ListingVersion> {
 
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
+
 	@Override
-	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) {
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.LISTING_VERSION.getTable());
+		if (isRoot) {
 			// ID check
-			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
+			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
 			if (id.isPresent()) {
-				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
+				stmt.addClause(
+						new ParameterizedSQLStatement.Clause(DtoTableNames.LISTING_VERSION.getAlias() + ".id = ?",
+								new Object[] { UUID.fromString(id.get()) }));
 			}
 		}
 
 		// solution version - OS filter
-		Optional<String> os = params.getFirstIfPresent(UrlParameterNames.OS.getParameterName());
+		Optional<String> os = wrap.getFirstParam(UrlParameterNames.OS);
 		if (os.isPresent()) {
-			filters.add(Filters.eq("platforms", os.get()));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					"? IN elements(" + DtoTableNames.LISTING_VERSION.getAlias() + ".platforms)",
+					new Object[] { os.get() }));
 		}
 		// solution version - eclipse version
-		Optional<String> eclipseVersion = params.getFirstIfPresent(UrlParameterNames.ECLIPSE_VERSION.getParameterName());
+		Optional<String> eclipseVersion = wrap.getFirstParam(UrlParameterNames.ECLIPSE_VERSION);
 		if (eclipseVersion.isPresent()) {
-			filters.add(Filters.eq("compatible_versions", eclipseVersion.get()));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					"? IN elements(" + DtoTableNames.LISTING_VERSION.getAlias() + ".eclipseVersions)",
+					new Object[] { eclipseVersion.get() }));
 		}
 		// solution version - Java version
-		Optional<String> javaVersion = params.getFirstIfPresent(UrlParameterNames.JAVA_VERSION.getParameterName());
+		Optional<String> javaVersion = wrap.getFirstParam(UrlParameterNames.JAVA_VERSION);
 		if (javaVersion.isPresent() && StringUtils.isNumeric(javaVersion.get())) {
-			filters.add(Filters.gte("min_java_version", Integer.valueOf(javaVersion.get())));
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					DtoTableNames.LISTING_VERSION.getAlias() + ".minJavaVersion >= ?",
+					new Object[] { Integer.valueOf(javaVersion.get()) }));
 		}
-
-		return filters;
-	}
-
-	@Override
-	public List<Bson> getAggregates(QueryParameters params) {
-		return Collections.emptyList();
+		return stmt;
 	}
 
 	@Override
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 f0dc7b5bff5595fe0b93fbea98750f16f43f496c..21fb37aba53a3e8739b6560414319b59ca0e121b 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MarketFilter.java
@@ -6,30 +6,20 @@
  */
 package org.eclipsefoundation.marketplace.dto.filter;
 
-import static com.mongodb.client.model.Filters.and;
-import static com.mongodb.client.model.Filters.eq;
-import static com.mongodb.client.model.Filters.expr;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
 
 import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
 
-import org.bson.BsonArray;
-import org.bson.conversions.Bson;
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.dto.Market;
-import org.eclipsefoundation.marketplace.model.QueryParameters;
 import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames;
 import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
-
-import com.mongodb.client.model.Accumulators;
-import com.mongodb.client.model.Aggregates;
-import com.mongodb.client.model.Filters;
-import com.mongodb.client.model.Projections;
-import com.mongodb.client.model.Variable;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 
 /**
  * Filter implementation for the {@linkplain Market} class.
@@ -39,66 +29,30 @@ import com.mongodb.client.model.Variable;
 @ApplicationScoped
 public class MarketFilter implements DtoFilter<Market> {
 
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
+
 	@Override
-	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) {
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.MARKET.getTable());
+		if (isRoot) {
 			// ID check
-			Optional<String> id = params.getFirstIfPresent(UrlParameterNames.ID.getParameterName());
+			Optional<String> id = wrap.getFirstParam(UrlParameterNames.ID);
 			if (id.isPresent()) {
-				filters.add(Filters.eq(DatabaseFieldNames.DOCID, id.get()));
+				stmt.addClause(new ParameterizedSQLStatement.Clause(
+						DtoTableNames.MARKET.getAlias() + "." + DatabaseFieldNames.DOCID + " = ?",
+						new Object[] { UUID.fromString(id.get()) }));
 			}
 		}
-		return filters;
-	}
-
-	@Override
-	public List<Bson> getAggregates(QueryParameters params) {
-		List<Bson> aggs = new ArrayList<>();
-
-		String tempFieldName = "tmp";
-		List<Bson> pipeline = new ArrayList<>();
-		// match the listings on the given market_id
-		pipeline.add(
-				Aggregates.match(expr(eq("$in", Arrays.asList("$" + DatabaseFieldNames.DOCID, "$$listing_ids")))));
-		// suppress all fields except category_ids
-		pipeline.add(Aggregates.project(
-				Projections.fields(Projections.excludeId(), Projections.include(DatabaseFieldNames.CATEGORY_IDS))));
-
-		// set up a var reference for the _id
-		Variable<String> id = new Variable<>("listing_ids", "$listing_ids");
-		// lookup all category IDS from listings with the given market ID
-		aggs.add(Aggregates.lookup(DtoTableNames.LISTING.getTableName(), Arrays.asList(id), pipeline, tempFieldName));
-		// explode all category IDS for collection
-		aggs.add(Aggregates.unwind("$" + tempFieldName));
-
-		// flatten categories using projection, and retain original data through data
-		// field
-		aggs.add(Aggregates.group("$_id", Accumulators.first("data", "$$ROOT"),
-				Accumulators.push(tempFieldName, "$" + tempFieldName + "." + DatabaseFieldNames.CATEGORY_IDS)));
-
-		// no reduction shortcuts in driver, build documents from scratch
-		// in operation merges multiple lists using sets to deduplicate
-		Bson inOperation = eq("$setUnion", Arrays.asList("$$value", "$$this"));
-		Bson reductionOptions = eq(tempFieldName, eq("$reduce",
-				and(eq("input", "$" + tempFieldName), eq("initialValue", new BsonArray()), eq("in", inOperation))));
-
-		// using projections, retain data-root + tmp category IDS and reduce them
-		aggs.add(Aggregates.project(Projections.fields(Projections.include("data"), reductionOptions)));
-
-		// create custom array as mergeObjects uses non-standard syntax
-		BsonArray ba = BsonArray.parse("[ '$data', {'" + tempFieldName + "': '$" + tempFieldName + "'}]");
-		// replaceRoot to restore original root data + set data for category IDs
-		aggs.add(Aggregates.replaceRoot(eq("$mergeObjects", ba)));
-
-		// adds a $lookup aggregate, joining categories on categoryIDS as "categories"
-		aggs.add(Aggregates.lookup(DtoTableNames.CATEGORY.getTableName(), tempFieldName, DatabaseFieldNames.DOCID,
-				"categories"));
+		// check for markets that contain a given listing
+		Optional<String> listingId = wrap.getFirstParam(UrlParameterNames.LISTING_ID);
+		if (listingId.isPresent()) {
+			stmt.addClause(new ParameterizedSQLStatement.Clause(
+					"? IN elements(" + DtoTableNames.MARKET.getAlias() + ".listingId)",
+					new Object[] { listingId.get() }));
+		}
 
-		// remove the unneeded temporary field
-		aggs.add(Aggregates.project(Projections.exclude(tempFieldName)));
-		return aggs;
+		return stmt;
 	}
 
 	@Override
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 59bf384535084921c92503597b5229da6c97dbe3..09aed32576b3069587a1e2889941503d261417c4 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/dto/filter/MetricPeriodFilter.java
@@ -13,20 +13,14 @@ import java.util.List;
 import java.util.Optional;
 
 import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
 
-import org.bson.BsonArray;
-import org.bson.BsonDocument;
-import org.bson.BsonString;
-import org.bson.conversions.Bson;
+import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.marketplace.dto.MetricPeriod;
-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.BsonField;
-import com.mongodb.client.model.Filters;
-import com.mongodb.client.model.Projections;
+import org.eclipsefoundation.marketplace.namespace.DtoTableNames;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
 
 /**
  * Filter implementation for the {@linkplain MetricPeriod} class.
@@ -37,54 +31,17 @@ import com.mongodb.client.model.Projections;
 @ApplicationScoped
 public class MetricPeriodFilter implements DtoFilter<MetricPeriod> {
 
-	@Override
-	public List<Bson> getFilters(QueryParameters params, String root) {
-		return Collections.emptyList();
-	}
+	@Inject
+	ParameterizedSQLStatementBuilder builder;
 
 	@Override
-	public List<Bson> getAggregates(QueryParameters params) {
-		// check that we have required fields first
-		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
-			BsonArray startDateComparison = new BsonArray();
-			startDateComparison.add(new BsonString("$" + DatabaseFieldNames.INSTALL_DATE));
-			BsonDocument startDoc = new BsonDocument();
-			startDoc.append("dateString", new BsonString(startDate.get()));
-			startDoc.append("format", new BsonString("%Y-%m-%dT%H:%M:%SZ"));
-			// build doc to convert string to date to be used in query
-			BsonDocument startDateConversion = new BsonDocument("$dateFromString", startDoc);
-			startDateComparison.add(startDateConversion);
+	public ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot) {
+		ParameterizedSQLStatement stmt = builder.build(DtoTableNames.METRIC_PERIOD.getTable());
+
+		// TODO we could do 'popular' filter by looking for metrics that have X installs
+		// in a period
 
-			// check for all listings that are before the end date
-			BsonArray endDateComparison = new BsonArray();
-			endDateComparison.add(new BsonString("$" + DatabaseFieldNames.INSTALL_DATE));
-			BsonDocument endDoc = new BsonDocument();
-			endDoc.append("dateString", new BsonString(endDate.get()));
-			endDoc.append("format", new BsonString("%Y-%m-%dT%H:%M:%SZ"));
-			// build doc to convert string to date to be used in query
-			BsonDocument endDateConversion = new BsonDocument("$dateFromString", endDoc);
-			endDateComparison.add(endDateConversion);
-			
-			// add the 2 date comparisons to the pipeline
-			aggregates.add(Aggregates.match(Filters
-					.expr(Filters.eq("$and", new BsonArray(Arrays.asList(new BsonDocument("$gte", startDateComparison),
-							new BsonDocument("$lte", endDateComparison)))))));
-			// group the results by listing ID
-			aggregates.add(Aggregates.group("$listing_id", new BsonField(DatabaseFieldNames.PERIOD_COUNT, Filters.eq("$sum", 1))));
-			// project the start + end date into the end result
-			aggregates.add(Aggregates.project(Projections.fields(Projections.include(DatabaseFieldNames.PERIOD_COUNT),
-					Projections.computed(DatabaseFieldNames.PERIOD_START, startDateConversion),
-					Projections.computed(DatabaseFieldNames.PERIOD_END, endDateConversion))));
-		} else {
-			// count all existing installs and group them by listing ID
-			aggregates.add(Aggregates.group("$listing_id", new BsonField(DatabaseFieldNames.PERIOD_COUNT, Filters.eq("$sum", 1))));
-		}
-		
-		return aggregates;
+		return stmt;
 	}
 
 	@Override
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CatalogCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CatalogCodecProvider.java
deleted file mode 100644
index 02bf75ed2df77b174d8a1cdd9fa6f48b640342f9..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CatalogCodecProvider.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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
deleted file mode 100644
index 1c9984642665af66f0f536bca697c2b274f82654..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/CategoryCodecProvider.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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/dto/providers/ErrorReportCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/ErrorReportCodecProvider.java
deleted file mode 100644
index f8b605aa8ee475e98973333a8caa03d84c6d477b..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/ErrorReportCodecProvider.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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.ErrorReport;
-import org.eclipsefoundation.marketplace.dto.codecs.ErrorReportCodec;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Provides the {@link ErrorReportCodec} to MongoDB for conversions of
- * {@link ErrorReport} objects.
- * 
- * @author Martin Lowe
- */
-public class ErrorReportCodecProvider implements CodecProvider {
-	private static final Logger LOGGER = LoggerFactory.getLogger(ErrorReportCodecProvider.class);
-
-	@SuppressWarnings("unchecked")
-	@Override
-	public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
-		if (clazz == ErrorReport.class) {
-			LOGGER.debug("Registering custom ErrorReport class MongoDB codec");
-			return (Codec<T>) new ErrorReportCodec();
-		}
-		return null;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/InstallCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/InstallCodecProvider.java
deleted file mode 100644
index 046d466cab118ed67a52f38f54a2937dfd32f035..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/InstallCodecProvider.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/* 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.Install;
-import org.eclipsefoundation.marketplace.dto.codecs.InstallCodec;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Provides the {@link InstallCodec} to MongoDB for conversions of
- * {@link Catalog} objects.
- * 
- * @author Martin Lowe
- */
-public class InstallCodecProvider implements CodecProvider {
-	private static final Logger LOGGER = LoggerFactory.getLogger(InstallCodecProvider.class);
-
-	@SuppressWarnings("unchecked")
-	@Override
-	public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
-		if (clazz == Install.class) {
-			LOGGER.debug("Registering custom Install class MongoDB codec");
-			return (Codec<T>) new InstallCodec();
-		}
-		return null;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/InstallMetricsCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/InstallMetricsCodecProvider.java
deleted file mode 100644
index b83a0c795b37eb7772a05e26284ee962e98ac8b1..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/InstallMetricsCodecProvider.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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.InstallMetrics;
-import org.eclipsefoundation.marketplace.dto.codecs.InstallMetricsCodec;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Provides the {@link InstallMetricsCodec} to MongoDB for conversions of
- * {@link InstallMetrics} objects.
- * 
- * @author Martin Lowe
- */
-public class InstallMetricsCodecProvider implements CodecProvider {
-	private static final Logger LOGGER = LoggerFactory.getLogger(InstallMetricsCodecProvider.class);
-
-	@SuppressWarnings("unchecked")
-	@Override
-	public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
-		if (clazz == InstallMetrics.class) {
-			LOGGER.debug("Registering custom InstallMetrics class MongoDB codec");
-			return (Codec<T>) new InstallMetricsCodec();
-		}
-		return null;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/ListingCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/ListingCodecProvider.java
deleted file mode 100644
index 38c3b04f3d87b018ab8b94da05ba2ed9f9ec5e7a..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/ListingCodecProvider.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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.Listing;
-import org.eclipsefoundation.marketplace.dto.codecs.ListingCodec;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Provides the {@link ListingCodec} to MongoDB for conversions of
- * {@link Listing} objects.
- * 
- * @author Martin Lowe
- */
-public class ListingCodecProvider implements CodecProvider {
-	private static final Logger LOGGER = LoggerFactory.getLogger(ListingCodecProvider.class);
-
-	@SuppressWarnings("unchecked")
-	@Override
-	public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
-		if (clazz == Listing.class) {
-			LOGGER.debug("Registering custom Listing class MongoDB codec");
-			return (Codec<T>) new ListingCodec();
-		}
-		return null;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/ListingVersionCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/ListingVersionCodecProvider.java
deleted file mode 100644
index f7b19327b4c98e0717519f14cb4e6f14449182dc..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/ListingVersionCodecProvider.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/* 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.Listing;
-import org.eclipsefoundation.marketplace.dto.ListingVersion;
-import org.eclipsefoundation.marketplace.dto.codecs.ListingCodec;
-import org.eclipsefoundation.marketplace.dto.codecs.ListingVersionCodec;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Provides the {@link ListingCodec} to MongoDB for conversions of
- * {@link Listing} objects.
- * 
- * @author Martin Lowe
- */
-public class ListingVersionCodecProvider implements CodecProvider {
-	private static final Logger LOGGER = LoggerFactory.getLogger(ListingVersionCodecProvider.class);
-
-	@SuppressWarnings("unchecked")
-	@Override
-	public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
-		if (clazz == ListingVersion.class) {
-			LOGGER.debug("Registering custom Listing class MongoDB codec");
-			return (Codec<T>) new ListingVersionCodec();
-		}
-		return null;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/MarketCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/MarketCodecProvider.java
deleted file mode 100644
index a849c57bdb19078d68937de9f0cabc1fe395d972..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/MarketCodecProvider.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/* 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.Market;
-import org.eclipsefoundation.marketplace.dto.codecs.CategoryCodec;
-import org.eclipsefoundation.marketplace.dto.codecs.MarketCodec;
-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 MarketCodecProvider implements CodecProvider {
-	private static final Logger LOGGER = LoggerFactory.getLogger(MarketCodecProvider.class);
-
-	@SuppressWarnings("unchecked")
-	@Override
-	public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
-		if (clazz == Market.class) {
-			LOGGER.debug("Registering custom Category class MongoDB codec");
-			return (Codec<T>) new MarketCodec();
-		}
-		return null;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/MetricPeriodCodecProvider.java b/src/main/java/org/eclipsefoundation/marketplace/dto/providers/MetricPeriodCodecProvider.java
deleted file mode 100644
index 445a48a6e614f942f2e2375b4b3cca7f72273fb9..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/providers/MetricPeriodCodecProvider.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/* 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.MetricPeriod;
-import org.eclipsefoundation.marketplace.dto.codecs.MetricPeriodCodec;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Provides the {@link MetricPeriodCodec} to MongoDB for conversions of
- * {@link MetricPeriod} objects.
- * 
- * @author Martin Lowe
- */
-public class MetricPeriodCodecProvider implements CodecProvider {
-	private static final Logger LOGGER = LoggerFactory.getLogger(MetricPeriodCodecProvider.class);
-
-	@SuppressWarnings("unchecked")
-	@Override
-	public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
-		if (clazz == MetricPeriod.class) {
-			LOGGER.debug("Registering custom MetricPeriod class MongoDB codec");
-			return (Codec<T>) new MetricPeriodCodec();
-		}
-		return null;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/HQLGenerator.java b/src/main/java/org/eclipsefoundation/marketplace/model/HQLGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..65768757e1b85ecb23da01b80d86ee9538dc90eb
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/marketplace/model/HQLGenerator.java
@@ -0,0 +1,89 @@
+package org.eclipsefoundation.marketplace.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import org.apache.commons.lang3.NotImplementedException;
+import org.eclipsefoundation.persistence.model.DtoTable;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement.Clause;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement.Join;
+import org.eclipsefoundation.persistence.model.SQLGenerator;
+import org.eclipsefoundation.persistence.model.SortOrder;
+
+@ApplicationScoped
+public class HQLGenerator implements SQLGenerator {
+	private static final Pattern ORDINAL_PARAMETER_PATTERN = Pattern.compile("\\?(?!\\d)");
+
+	@Override
+	public String getSelectSQL(ParameterizedSQLStatement src) {
+		DtoTable base = src.getBase();
+		// retrieve once to reduce obj churn
+		List<Join> joins = src.getJoins();
+		List<Clause> clauses = src.getClauses();
+
+		StringBuilder sb = new StringBuilder(64);
+		sb.append("SELECT ").append(base.getAlias());
+		sb.append(" FROM");
+		// handle selection of table data
+		sb.append(' ').append(base.getType().getSimpleName());
+		sb.append(' ').append(base.getAlias());
+		List<DtoTable> selectedTables = new ArrayList<>();
+		for (Join j : joins) {
+			if (base != j.getForeignTable() && !selectedTables.contains(j.getForeignTable())) {
+				selectedTables.add(j.getLocalTable());
+				sb.append(" LEFT JOIN ").append(j.getLocalTable().getAlias());
+				sb.append('.').append(j.getLocalField());
+				sb.append(" AS ").append(j.getForeignTable().getAlias());
+			}
+		}
+
+		if (!clauses.isEmpty()) {
+			sb.append(" WHERE");
+		}
+		// handle clauses
+		int ordinal = 1;
+		for (int cIdx = 0; cIdx < clauses.size(); cIdx++) {
+			if (cIdx != 0) {
+				sb.append(" AND");
+			}
+
+			// create matcher on sql clause to replace legacy parameter placeholders with
+			// ordinals
+			String sql = clauses.get(cIdx).getSql();
+			Matcher m = ORDINAL_PARAMETER_PATTERN.matcher(sql);
+			while (m.find()) {
+				sql = sql.substring(0, m.start()) + '?' + ordinal++ + sql.substring(m.end());
+			}
+			sb.append(' ').append(sql);
+		}
+
+		// add sort if set
+		if (src.getSortField() != null && !SortOrder.RANDOM.equals(src.getOrder())) {
+			sb.append("ORDER BY ").append(src.getSortField());
+			if (SortOrder.ASCENDING.equals(src.getOrder())) {
+				sb.append(" asc");
+			} else {
+				sb.append(" desc");
+			}
+		} else if (SortOrder.RANDOM.equals(src.getOrder())) {
+			sb.append(" order by RAND()");
+		}
+		return sb.toString();
+	}
+
+	@Override
+	public String getDeleteSQL(ParameterizedSQLStatement src) {
+		throw new NotImplementedException("HQL does not utilize deletion SQL logic");
+	}
+
+	@Override
+	public String getCountSQL(ParameterizedSQLStatement src) {
+		return getSelectSQL(src);
+	}
+
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java b/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java
deleted file mode 100644
index 033f3907626238a07e3b504da8b109ce7bc2e0c7..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/marketplace/model/MongoQuery.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/* 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 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;
-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.namespace.UrlParameterNames;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.mongodb.MongoClient;
-import com.mongodb.client.model.Aggregates;
-import com.mongodb.client.model.Filters;
-
-/**
- * Wrapper for initializing MongoDB BSON filters, sort clauses, and document
- * type when interacting with MongoDB.
- * 
- * @author Martin Lowe
- */
-public class MongoQuery<T> {
-	private static final Logger LOGGER = LoggerFactory.getLogger(MongoQuery.class);
-
-	private QueryParameters params;
-	private DtoFilter<T> dtoFilter;
-
-	private Bson filter;
-	private Bson sort;
-	private SortOrder order;
-	private List<Bson> aggregates;
-
-	public MongoQuery(RequestWrapper wrapper, DtoFilter<T> dtoFilter) {
-		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();
-	}
-
-	/**
-	 * Initializes the query object using the current query string parameters and
-	 * type object. This can be called again to reset the parameters if needed due
-	 * to updated fields.
-	 */
-	private void init() {
-		// 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(params, null));
-
-		// get fields that make up the required fields to enable pagination and check
-		Optional<String> sortOpt = params.getFirstIfPresent(UrlParameterNames.SORT.getParameterName());
-		if (sortOpt.isPresent()) {
-			String sortVal = sortOpt.get();
-			SortOrder ord = SortOrder.getOrderFromValue(sortOpt.get());
-			// split sort string of `<fieldName> <SortOrder>`
-			int idx = sortVal.indexOf(' ');
-			// check if the sort string matches the RANDOM sort order
-			if (SortOrder.RANDOM.equals(ord)) {
-				this.order = SortOrder.RANDOM;
-			} else if (ord != SortOrder.NONE) {
-				setSort(sortVal.substring(0, idx), sortVal.substring(idx + 1));
-			}
-		}
-
-		if (!filters.isEmpty()) {
-			this.filter = Filters.and(filters);
-		}
-		this.aggregates = dtoFilter.getAggregates(params);
-
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("MongoDB query initialized with filter: {}", this.filter);
-		}
-	}
-
-	/**
-	 * Generates a list of BSON documents representing an aggregation pipeline using
-	 * random sampling to get data.
-	 * 
-	 * @param limit the number of documents to return
-	 * @return the aggregation pipeline
-	 */
-	public List<Bson> getPipeline(int limit) {
-		if (limit < 0) {
-			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));
-		}
-		// add base aggregates (joins)
-		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
-		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);
-			}
-		}
-		
-		out.add(Aggregates.skip((page - 1) * limit));
-		// add sample if we aren't sorting
-		if ((sort == null || SortOrder.RANDOM.equals(order)) && dtoFilter.useLimit()) {
-			out.add(Aggregates.sample(limit));
-		}
-		return out;
-	}
-
-	/**
-	 * Checks the URL parameter of {@link UrlParameterNames.LIMIT} for a numeric
-	 * value and returns it if present.
-	 * 
-	 * @return the value of the URL parameter {@link UrlParameterNames.LIMIT} if
-	 *         present and numeric, otherwise returns -1.
-	 */
-	public int getLimit() {
-		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<Sortable<?>> fields = SortableHelper.getSortableFields(getDocType());
-		Optional<Sortable<?>> fieldContainer = SortableHelper.getSortableFieldByName(fields, sortField);
-		if (fieldContainer.isPresent()) {
-			this.order = SortOrder.getOrderByName(sortOrder);
-			// add sorting query if the sortOrder matches a defined order
-			switch (order) {
-			case ASCENDING:
-				this.sort = Filters.eq(sortField, order.getOrder());
-				break;
-			case DESCENDING:
-				this.sort = Filters.eq(sortField, order.getOrder());
-				break;
-			default:
-				// intentionally empty, no sort
-				break;
-			}
-		} else if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Field with name '{}' is not marked as sortable, skipping", sortField);
-		}
-	}
-
-	/**
-	 * @return the filter
-	 */
-	public Bson getFilter() {
-		return this.filter;
-	}
-
-	/**
-	 * @return the DTO filter
-	 */
-	public DtoFilter<T> getDTOFilter() {
-		return this.dtoFilter;
-	}
-
-	/**
-	 * @return the docType
-	 */
-	public Class<T> getDocType() {
-		return dtoFilter.getType();
-	}
-
-	@Override
-	public String toString() {
-		StringBuilder sb = new StringBuilder();
-		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/namespace/DatabaseFieldNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java
index b6cb1af181a5c5cd5b453a81ae4fe47d7984304f..80159c3940348305d6b66ebe5dc14ee5cebe4fb6 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/DatabaseFieldNames.java
@@ -18,7 +18,8 @@ package org.eclipsefoundation.marketplace.namespace;
 public final class DatabaseFieldNames {
 
 	// base fields
-	public static final String DOCID = "_id";
+	public static final String DOCID = "id";
+	public static final String SEED = "seed";
 	public static final String URL = "url";
 	public static final String TITLE = "title";
 	public static final String OS = "os";
diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/DtoTableNames.java
index c81d4b2eea8206ec03e44993220f93f33a4305c4..f582ad7573f81f4c789c48c564913cc94a1abc4e 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.persistence.model.DtoTable;
 import org.eclipsefoundation.marketplace.dto.Promotion;
 
 /**
@@ -24,29 +25,36 @@ import org.eclipsefoundation.marketplace.dto.Promotion;
  *
  */
 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"),PROMOTION(Promotion.class, "promotion");
+	LISTING(new DtoTable(Listing.class, "l")), CATEGORY(new DtoTable(Category.class, "ct")),
+	CATALOG(new DtoTable(Catalog.class, "cl")), MARKET(new DtoTable(Market.class, "m")),
+	ERRORREPORT(new DtoTable(ErrorReport.class, "er")), INSTALL(new DtoTable(Install.class, "i")),
+	INSTALL_METRIC(new DtoTable(InstallMetrics.class, "im")), METRIC_PERIOD(new DtoTable(MetricPeriod.class, "mp")),
+	LISTING_VERSION(new DtoTable(ListingVersion.class, "lv")), PROMOTION(new DtoTable(Promotion.class, "p"));
 
-	private Class<?> baseClass;
-	private String tableName;
+	private DtoTable table;
 
-	private DtoTableNames(Class<?> baseClass, String tableName) {
-		this.baseClass = baseClass;
-		this.tableName = tableName;
+	private DtoTableNames(DtoTable table) {
+		this.table = table;
 	}
 
-	public static String getTableName(Class<?> targetBase) {
-		for (DtoTableNames dtoPair : values()) {
-			if (dtoPair.baseClass == targetBase) {
-				return dtoPair.tableName;
+	public static DtoTableNames getTableByType(Class<?> targetBase) {
+		for (DtoTableNames dtoName : values()) {
+			if (dtoName.getType() == targetBase) {
+				return dtoName;
 			}
 		}
 		return null;
 	}
 
-	public String getTableName() {
-		return this.tableName;
+	public DtoTable getTable() {
+		return this.table;
+	}
+
+	public Class<?> getType() {
+		return this.table.getType();
+	}
+
+	public String getAlias() {
+		return this.table.getAlias();
 	}
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java b/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java
index cfd91e6a29da4ff30560af7bcdb727a2576dc697..5316dbad62be0e8fe7525af6521c5da409285454 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/namespace/UrlParameterNames.java
@@ -14,51 +14,28 @@ package org.eclipsefoundation.marketplace.namespace;
  * 
  * @author Martin Lowe
  */
-public enum UrlParameterNames {
+public final class UrlParameterNames {
 
-	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;
-	}
+	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";
+	public static final String SEED = "seed";
 	
-	/**
-	 * 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;
+	private UrlParameterNames() {
 	}
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java
index 87aa67bfd980ce0d5333638a7b7ec6f6db5a29c2..51052dab55c1869c2ba31281b58ced0e7c038328 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/CatalogResource.java
@@ -14,6 +14,7 @@ import javax.annotation.security.PermitAll;
 import javax.annotation.security.RolesAllowed;
 import javax.enterprise.context.RequestScoped;
 import javax.inject.Inject;
+import javax.persistence.NoResultException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -24,25 +25,22 @@ 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.core.helper.ResponseHelper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.marketplace.dto.Catalog;
-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.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.mongodb.client.result.DeleteResult;
-
 /**
- * @author martin
- *
+ * Resource for retrieving {@linkplain Catalog} from the DB instance.
+ * 
+ * @author Martin Lowe
  */
 @Path("/catalogs")
 @Produces(MediaType.APPLICATION_JSON)
@@ -52,7 +50,7 @@ public class CatalogResource {
 	private static final Logger LOGGER = LoggerFactory.getLogger(CatalogResource.class);
 
 	@Inject
-	MongoDao dao;
+	PersistenceDao dao;
 	@Inject
 	CachingService<List<Catalog>> cachingService;
 	@Inject
@@ -65,10 +63,9 @@ public class CatalogResource {
 	@GET
 	@PermitAll
 	public Response select() {
-		MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter);
+		RDBMSQuery<Catalog> q = new RDBMSQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
-		Optional<List<Catalog>> cachedResults = cachingService.get("all", params,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<Catalog>> cachedResults = cachingService.get("all", params, () -> dao.get(q));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached Catalogs");
 			return Response.serverError().build();
@@ -87,12 +84,8 @@ public class CatalogResource {
 	@PUT
 	@RolesAllowed({ "marketplace_catalog_put", "marketplace_admin_access" })
 	public Response putCatalog(Catalog catalog) {
-		if (catalog.getId() != null) {
-			params.addParam(UrlParameterNames.ID.getParameterName(), catalog.getId());
-		}
-		MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter);
 		// add the object, and await the result
-		StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(catalog)));
+		dao.add(new RDBMSQuery<>(params, dtoFilter), Arrays.asList(catalog));
 
 		// return the results as a response
 		return Response.ok().build();
@@ -108,16 +101,18 @@ public class CatalogResource {
 	@GET
 	@Path("/{catalogId}")
 	public Response select(@PathParam("catalogId") String catalogId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), catalogId);
+		params.addParam(UrlParameterNames.ID, catalogId);
 
-		MongoQuery<Catalog> q = new MongoQuery<>(params, dtoFilter);
+		RDBMSQuery<Catalog> q = new RDBMSQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current listing
-		Optional<List<Catalog>> cachedResults = cachingService.get(catalogId, params,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<Catalog>> cachedResults = cachingService.get(catalogId, params, () -> dao.get(q));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", catalogId);
 			return Response.serverError().build();
 		}
+		if (cachedResults.get().isEmpty()) {
+			throw new NoResultException("Could not find any documents with ID " + catalogId);
+		}
 
 		// return the results as a response
 		return responseBuider.build(catalogId, params, cachedResults.get());
@@ -134,14 +129,9 @@ public class CatalogResource {
 	@RolesAllowed({ "marketplace_catalog_delete", "marketplace_admin_access" })
 	@Path("/{catalogId}")
 	public Response delete(@PathParam("catalogId") String catalogId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), catalogId);
+		params.addParam(UrlParameterNames.ID, catalogId);
+		dao.delete(new RDBMSQuery<>(params, dtoFilter));
 
-		MongoQuery<Catalog> q = new MongoQuery<>(params, 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/resource/CategoryResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java
index f671ea1ba141e4ae536d637de9842ccb5c686e8f..c07ce489a94c9c609fb3a1fe81afd1b385effc9c 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/CategoryResource.java
@@ -14,6 +14,7 @@ import javax.annotation.security.PermitAll;
 import javax.annotation.security.RolesAllowed;
 import javax.enterprise.context.RequestScoped;
 import javax.inject.Inject;
+import javax.persistence.NoResultException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -22,27 +23,24 @@ 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.core.helper.ResponseHelper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.marketplace.dto.Category;
-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.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.mongodb.client.result.DeleteResult;
 
 /**
- * @author martin
- *
+ * Resource for retrieving {@linkplain Category} from the DB instance.
+ * 
+ * @author Martin Lowe
  */
 @Path("/categories")
 @Produces(MediaType.APPLICATION_JSON)
@@ -52,7 +50,7 @@ public class CategoryResource {
 	private static final Logger LOGGER = LoggerFactory.getLogger(CategoryResource.class);
 
 	@Inject
-	MongoDao dao;
+	PersistenceDao dao;
 	@Inject
 	CachingService<List<Category>> cachingService;
 	@Inject
@@ -65,12 +63,11 @@ public class CategoryResource {
 	@GET
 	@PermitAll
 	public Response select() {
-		MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter);
+		RDBMSQuery<Category> q = new RDBMSQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
-		Optional<List<Category>> cachedResults = cachingService.get("all", params,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<Category>> cachedResults = cachingService.get("all", params, () -> dao.get(q));
 		if (!cachedResults.isPresent()) {
-			LOGGER.error("Error while retrieving cached Categorys");
+			LOGGER.error("Error while retrieving cached Categories");
 			return Response.serverError().build();
 		}
 
@@ -85,14 +82,10 @@ public class CategoryResource {
 	 * @return response for the browser
 	 */
 	@PUT
-	@RolesAllowed({"marketplace_category_put", "marketplace_admin_access"})
+	@RolesAllowed({ "marketplace_category_put", "marketplace_admin_access" })
 	public Response putCategory(Category category) {
-		if (category.getId() != null) {
-			params.addParam(UrlParameterNames.ID.getParameterName(), category.getId());
-		}
-		MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter);
 		// add the object, and await the result
-		StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(category)));
+		dao.add(new RDBMSQuery<>(params, dtoFilter), Arrays.asList(category));
 
 		// return the results as a response
 		return Response.ok().build();
@@ -108,16 +101,18 @@ public class CategoryResource {
 	@GET
 	@Path("/{categoryId}")
 	public Response select(@PathParam("categoryId") String categoryId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), categoryId);
+		params.addParam(UrlParameterNames.ID, categoryId);
 
-		MongoQuery<Category> q = new MongoQuery<>(params, dtoFilter);
+		RDBMSQuery<Category> q = new RDBMSQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current listing
-		Optional<List<Category>> cachedResults = cachingService.get(categoryId, params,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<Category>> cachedResults = cachingService.get(categoryId, params, () -> dao.get(q));
 		if (!cachedResults.isPresent()) {
-			LOGGER.error("Error while retrieving cached listing for ID {}", categoryId);
+			LOGGER.error("Error while retrieving cached category for ID {}", categoryId);
 			return Response.serverError().build();
 		}
+		if (cachedResults.get().isEmpty()) {
+			throw new NoResultException("Could not find any documents with ID " + categoryId);
+		}
 
 		// return the results as a response
 		return responseBuider.build(categoryId, params, cachedResults.get());
@@ -134,14 +129,9 @@ public class CategoryResource {
 	@RolesAllowed({ "marketplace_category_delete", "marketplace_admin_access" })
 	@Path("/{categoryId}")
 	public Response delete(@PathParam("categoryId") String categoryId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), categoryId);
+		params.addParam(UrlParameterNames.ID, categoryId);
+		dao.delete(new RDBMSQuery<>(params, dtoFilter));
 
-		MongoQuery<Category> q = new MongoQuery<>(params, 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/resource/ErrorReportResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java
index a596dddae908b35968f0b105ff7c2a5c12437e85..0cfce420b8d4e83d000f2e4b6e4410f6feb60c49 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ErrorReportResource.java
@@ -10,9 +10,9 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 
-import javax.annotation.security.PermitAll;
 import javax.enterprise.context.RequestScoped;
 import javax.inject.Inject;
+import javax.persistence.NoResultException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
@@ -21,21 +21,20 @@ 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.core.helper.ResponseHelper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.marketplace.dto.ErrorReport;
-import org.eclipsefoundation.marketplace.dto.filter.DtoFilter;
-import org.eclipsefoundation.marketplace.helper.ResponseHelper;
-import org.eclipsefoundation.marketplace.helper.StreamHelper;
-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.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Resource for retrieving {@linkplain ErrorReport} from the MongoDB instance.
+ * Resource for retrieving {@linkplain ErrorReport} from the DB instance.
  * 
  * @author Martin Lowe
  */
@@ -47,7 +46,7 @@ public class ErrorReportResource {
 	private static final Logger LOGGER = LoggerFactory.getLogger(ErrorReportResource.class);
 
 	@Inject
-	MongoDao dao;
+	PersistenceDao dao;
 	@Inject
 	CachingService<List<ErrorReport>> cachingService;
 	@Inject
@@ -58,19 +57,17 @@ public class ErrorReportResource {
 	ResponseHelper responseBuider;
 
 	/**
-	 * Endpoint for /error/ to retrieve all ErrorReports from the database along with
-	 * the given query string parameters.
+	 * Endpoint for /error/ to retrieve all ErrorReports from the database along
+	 * with the given query string parameters.
 	 * 
 	 * @param ErrorReportId int version of the ErrorReport ID
 	 * @return response for the browser
 	 */
 	@GET
-	@PermitAll
 	public Response select() {
-		MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter);
+		RDBMSQuery<ErrorReport> q = new RDBMSQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
-		Optional<List<ErrorReport>> cachedResults = cachingService.get("all", params,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<ErrorReport>> cachedResults = cachingService.get("all", params, () -> dao.get(q));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached ErrorReports");
 			return Response.serverError().build();
@@ -87,42 +84,35 @@ public class ErrorReportResource {
 	 * @return response for the browser
 	 */
 	@POST
-	@PermitAll
 	public Response putErrorReport(ErrorReport errorReport) {
-		// attach ID if present for update
-		if (errorReport.getId() != null) {
-			params.addParam(UrlParameterNames.ID.getParameterName(), errorReport.getId());
-		}
-		MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter);
-
 		// add the object, and await the result
-		StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(errorReport)));
-
+		dao.add(new RDBMSQuery<>(params, dtoFilter), Arrays.asList(errorReport));
 		// return the results as a response
 		return Response.ok().build();
 	}
 
 	/**
-	 * Endpoint for /error/\<errorReportId\> to retrieve a specific ErrorReport from the
-	 * database.
+	 * Endpoint for /error/\<errorReportId\> to retrieve a specific ErrorReport from
+	 * the database.
 	 * 
 	 * @param errorReportId the ErrorReport ID
 	 * @return response for the browser
 	 */
 	@GET
-	@PermitAll
 	@Path("/{errorReportId}")
 	public Response select(@PathParam("errorReportId") String errorReportId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), errorReportId);
-
-		MongoQuery<ErrorReport> q = new MongoQuery<>(params, dtoFilter);
+		params.addParam(UrlParameterNames.ID, errorReportId);
+		RDBMSQuery<ErrorReport> q = new RDBMSQuery<>(params, dtoFilter);
 		// retrieve a cached version of the value for the current ErrorReport
 		Optional<List<ErrorReport>> cachedResults = cachingService.get(errorReportId, params,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				() -> dao.get(q));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached ErrorReport for ID {}", errorReportId);
 			return Response.serverError().build();
 		}
+		if (cachedResults.get().isEmpty()) {
+			throw new NoResultException("Could not find any documents with ID " + errorReportId);
+		}
 
 		// return the results as a response
 		return responseBuider.build(errorReportId, params, cachedResults.get());
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java
index 19aa49c1af412909720c76ce8a198686e29c5135..824b01d1893dd32bf33796816c688b9d0b5ccb2d 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/InstallResource.java
@@ -6,17 +6,10 @@
  */
 package org.eclipsefoundation.marketplace.resource;
 
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Calendar;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
+import java.util.UUID;
 
 import javax.annotation.security.PermitAll;
 import javax.annotation.security.RolesAllowed;
@@ -31,19 +24,18 @@ 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.core.helper.ResponseHelper;
+import org.eclipsefoundation.core.model.Error;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.marketplace.dto.Install;
 import org.eclipsefoundation.marketplace.dto.InstallMetrics;
 import org.eclipsefoundation.marketplace.dto.MetricPeriod;
-import org.eclipsefoundation.marketplace.dto.filter.DtoFilter;
-import org.eclipsefoundation.marketplace.helper.DateTimeHelper;
-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.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -58,10 +50,10 @@ import org.slf4j.LoggerFactory;
 @Consumes(MediaType.APPLICATION_JSON)
 @Path("/installs")
 public class InstallResource {
-	private static final Logger LOGGER = LoggerFactory.getLogger(ListingResource.class);
+	private static final Logger LOGGER = LoggerFactory.getLogger(InstallResource.class);
 
 	@Inject
-	MongoDao dao;
+	PersistenceDao dao;
 	@Inject
 	RequestWrapper wrapper;
 	@Inject
@@ -70,10 +62,14 @@ public class InstallResource {
 	// insert required filters for different objects + states
 	@Inject
 	DtoFilter<Install> dtoFilter;
+	
 	@Inject
 	DtoFilter<MetricPeriod> periodFilter;
 	@Inject
 	DtoFilter<InstallMetrics> metricFilter;
+	@Inject
+	CachingService<List<InstallMetrics>> installCache;
+
 
 	// Inject 2 caching service references, as we want to cache count results.
 	@Inject
@@ -92,10 +88,10 @@ public class InstallResource {
 	@PermitAll
 	@Path("/{listingId}")
 	public Response selectInstallCount(@PathParam("listingId") String listingId) {
-		wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId);
-		MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter);
+		wrapper.addParam(UrlParameterNames.ID, listingId);
+		RDBMSQuery<Install> q = new RDBMSQuery<>(wrapper, dtoFilter);
 		Optional<Long> cachedResults = countCache.get(listingId, wrapper,
-				null, () -> StreamHelper.awaitCompletionStage(dao.count(q)));
+				() -> 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 +114,11 @@ public class InstallResource {
 	@PermitAll
 	@Path("/{listingId}/{version}")
 	public Response selectInstallCount(@PathParam("listingId") String listingId, @PathParam("version") String version) {
-		wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId);
-		wrapper.addParam(UrlParameterNames.VERSION.getParameterName(), version);
-		MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter);
+		wrapper.addParam(UrlParameterNames.ID, listingId);
+		wrapper.addParam(UrlParameterNames.VERSION, version);
+		RDBMSQuery<Install> q = new RDBMSQuery<>(wrapper, dtoFilter);
 		Optional<Long> cachedResults = countCache.get(getCompositeKey(listingId, version), wrapper,
-				null, () -> StreamHelper.awaitCompletionStage(dao.count(q)));
+				() -> StreamHelper.awaitCompletionStage(dao.count(q)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", listingId);
 			return Response.serverError().build();
@@ -143,10 +139,10 @@ public class InstallResource {
 	@PermitAll
 	@Path("/{listingId}/metrics")
 	public Response selectInstallMetrics(@PathParam("listingId") String listingId) {
-		wrapper.addParam(UrlParameterNames.ID.getParameterName(), listingId);
-		MongoQuery<InstallMetrics> q = new MongoQuery<>(wrapper, metricFilter);
+		wrapper.addParam(UrlParameterNames.ID, listingId);
+		RDBMSQuery<InstallMetrics> q = new RDBMSQuery<>(wrapper, metricFilter);
 		Optional<List<InstallMetrics>> cachedResults = installCache.get(listingId, wrapper,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				() -> dao.get(q));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached install metrics for ID {}", listingId);
 			return Response.serverError().build();
@@ -187,89 +183,15 @@ public class InstallResource {
 		}
 
 		// update the install details to reflect the current request
-		record.setListingId(listingId);
+		record.setListingId(UUID.fromString(listingId));
 		record.setVersion(version);
 
 		// create the query wrapper to pass to DB dao
-		MongoQuery<Install> q = new MongoQuery<>(wrapper, dtoFilter);
+		RDBMSQuery<Install> q = new RDBMSQuery<>(wrapper, dtoFilter);
 
 		// add the object, and await the result
-		StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(record)));
-
-		// return the results as a response
-		return Response.ok().build();
-	}
-
-	/**
-	 * Regenerates the install_metrics table, using the install table as the base.
-	 * Creates 12 columns for metric periods, to provide users with a count of
-	 * installs over the past year. For months with no installs, an empty metric
-	 * period is generated to avoid gaps in the stats.
-	 * 
-	 * TODO: This should be moved to a separate job resource and be callable through
-	 * a service that tracks last run time. https://github.com/EclipseFdn/marketplace-rest-api/issues/54
-	 * 
-	 * @return an OK response when finished
-	 */
-	@GET
-	@RolesAllowed("marketplace_admin_access")
-	@Path("/generate_metrics")
-	public Response generateInstallStats() {
-		List<CompletionStage<List<MetricPeriod>>> stages = new ArrayList<>();
-		// get total install count for all listings available
-		Map<String, Integer> overallCounts = new HashMap<>();
-		CompletionStage<List<MetricPeriod>> stage = dao.get(new MongoQuery<>(wrapper, periodFilter));
-		stage.whenComplete((metrics, e) -> {
-			// if theres an error, immediately stop processing
-			if (e != null) {
-				throw new RuntimeException(e);
-			}
-			// for each metric, insert total count into the map
-			for (MetricPeriod metric : metrics) {
-				overallCounts.put(metric.getListingId(), metric.getCount());
-			}
-		});
-		stages.add(stage);
-
-		// use thread safe map impl for storing metrics
-		Map<String, List<MetricPeriod>> r = new ConcurrentHashMap<>();
-		// get the last 12 months of stats for installs asynchronously
-		Calendar c = Calendar.getInstance();
-		for (int m = 0; m < 12; m++) {
-			// set up the date ranges for the current call
-			String end = DateTimeHelper.toRFC3339(c.getTime());
-			c.add(Calendar.MONTH, -1);
-			String start = DateTimeHelper.toRFC3339(c.getTime());
-			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
-			MongoQuery<MetricPeriod> q = new MongoQuery<>(wrapper, periodFilter);
-			// run query, and set up a completion activity to record data
-			CompletionStage<List<MetricPeriod>> statStage = dao.get(q);
-			statStage.whenComplete((metrics, e) -> {
-				// if theres an error, immediately stop processing
-				if (e != null) {
-					throw new RuntimeException(e);
-				}
-				// for each metric, insert into the map
-				for (MetricPeriod metric : metrics) {
-					r.computeIfAbsent(metric.getListingId(), k -> new ArrayList<MetricPeriod>()).add(metric);
-				}
-			});
-			// keep stage reference to check when complete
-			stages.add(statStage);
-		}
-		// wrap futures and await all calls to finish
-		StreamHelper.awaitCompletionStage(CompletableFuture.allOf(stages.toArray(new CompletableFuture[] {})));
-
-		// convert the map to a list of install metric objects, adding in total count
-		List<InstallMetrics> installMetrics = r.entrySet().stream().map(entry -> new InstallMetrics(entry.getKey(),
-				entry.getValue(), overallCounts.getOrDefault(entry.getKey(), 0))).collect(Collectors.toList());
+		dao.add(q, Arrays.asList(record));
 
-		// push the content to the database, and await for it to finish
-		StreamHelper.awaitCompletionStage(dao.add(new MongoQuery<>(wrapper, metricFilter), installMetrics));
 		// 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 1674ff47e03a47ec16691ade77e502539b7f3fb1..50ac6cbb8bbedfef991c0908e5794b95ec99c120 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingResource.java
@@ -9,7 +9,6 @@
 */
 package org.eclipsefoundation.marketplace.resource;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
@@ -18,6 +17,7 @@ import javax.annotation.security.PermitAll;
 import javax.annotation.security.RolesAllowed;
 import javax.enterprise.context.RequestScoped;
 import javax.inject.Inject;
+import javax.persistence.NoResultException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -26,26 +26,19 @@ 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.core.helper.ResponseHelper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.marketplace.dto.Listing;
-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.model.SortOrder;
 import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
-import org.eclipsefoundation.marketplace.service.CachingService;
-import org.eclipsefoundation.marketplace.service.PromotionService;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.mongodb.client.result.DeleteResult;
-
 /**
  * Resource for retrieving listings from the MongoDB instance.
  * 
@@ -59,22 +52,21 @@ public class ListingResource {
 	private static final Logger LOGGER = LoggerFactory.getLogger(ListingResource.class);
 
 	@Inject
-	MongoDao dao;
-	@Inject
-	DtoFilter<Listing> dtoFilter;
+	PersistenceDao dao;
 	@Inject
 	CachingService<List<Listing>> cachingService;
-
 	@Inject
 	PromotionService promoService;
 
 	@Inject
 	RequestWrapper params;
 	@Inject
+	DtoFilter<Listing> dtoFilter;
+	@Inject
 	ResponseHelper responseBuider;
 
 	/**
-	 * Endpoint for /listings/ to retrieve all listings from the database along with
+	 * Endpoint for /listing/ to retrieve all listings from the database along with
 	 * the given query string parameters.
 	 * 
 	 * @param listingId int version of the listing ID
@@ -83,38 +75,20 @@ public class ListingResource {
 	@GET
 	@PermitAll
 	public Response select() {
-		MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
-		Optional<List<Listing>> cachedResults = cachingService.get("all", params, null,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<Listing>> cachedResults = cachingService.get("all", params,
+				() -> dao.get(new RDBMSQuery<>(params, dtoFilter)));
 		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, listings);
+		return responseBuider.build("all", params, cachedResults.get());
 	}
 
 	/**
-	 * Endpoint for /listings/ to post a new listing to the persistence layer.
+	 * Endpoint for /listing/ to post a new listing to the persistence layer.
 	 * 
 	 * @param listing the listing object to insert into the database.
 	 * @return response for the browser
@@ -122,20 +96,15 @@ public class ListingResource {
 	@PUT
 	@RolesAllowed({ "marketplace_listing_put", "marketplace_admin_access" })
 	public Response putListing(Listing listing) {
-		if (listing.getId() != null) {
-			params.addParam(UrlParameterNames.ID.getParameterName(), listing.getId());
-		}
-		MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter);
-
 		// add the object, and await the result
-		StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(listing)));
+		dao.add(new RDBMSQuery<>(params, dtoFilter), Arrays.asList(listing));
 
 		// return the results as a response
 		return Response.ok().build();
 	}
 
 	/**
-	 * Endpoint for /listings/\<listingId\> to retrieve a specific listing from the
+	 * Endpoint for /listing/\<listingId\> to retrieve a specific listing from the
 	 * database.
 	 * 
 	 * @param listingId the listing ID
@@ -145,23 +114,24 @@ public class ListingResource {
 	@PermitAll
 	@Path("/{listingId}")
 	public Response select(@PathParam("listingId") String listingId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), listingId);
+		params.addParam(UrlParameterNames.ID, 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, null,
-				() -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<Listing>> cachedResults = cachingService.get(listingId, params,
+				() -> dao.get(new RDBMSQuery<>(params, dtoFilter)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", listingId);
 			return Response.serverError().build();
 		}
-
+		if (cachedResults.get().isEmpty()) {
+			throw new NoResultException("Could not find any documents with ID " + listingId);
+		}
 		// return the results as a response
 		return responseBuider.build(listingId, params, cachedResults.get());
 	}
 
 	/**
-	 * Endpoint for /listings/\<listingId\> to delete a specific listing from the
+	 * Endpoint for /listing/\<listingId\> to delete a specific listing from the
 	 * database.
 	 * 
 	 * @param listingId the listing ID
@@ -171,13 +141,9 @@ public class ListingResource {
 	@RolesAllowed({ "marketplace_listing_delete", "marketplace_admin_access" })
 	@Path("/{listingId}")
 	public Response delete(@PathParam("listingId") String listingId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), listingId);
-		MongoQuery<Listing> q = new MongoQuery<>(params, dtoFilter);
+		params.addParam(UrlParameterNames.ID, listingId);
 		// 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();
-		}
+		dao.delete(new RDBMSQuery<>(params, dtoFilter));
 		// return the results as a response
 		return Response.ok().build();
 	}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java
index 1d312b8b373f794001f3d2a85342fce3e8a0f143..e5637baf2848fffeddf2e5aeda4d981a36df87b0 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/ListingVersionResource.java
@@ -13,6 +13,7 @@ import java.util.Optional;
 import javax.annotation.security.RolesAllowed;
 import javax.enterprise.context.RequestScoped;
 import javax.inject.Inject;
+import javax.persistence.NoResultException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -21,24 +22,19 @@ 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.core.helper.ResponseHelper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.marketplace.dto.ListingVersion;
-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.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.mongodb.client.result.DeleteResult;
-
 /**
  * Resource for retrieving {@linkplain ListingVersion}s from the MongoDB
  * instance.
@@ -53,7 +49,7 @@ public class ListingVersionResource {
 	private static final Logger LOGGER = LoggerFactory.getLogger(ListingVersionResource.class);
 
 	@Inject
-	MongoDao dao;
+	PersistenceDao dao;
 	@Inject
 	CachingService<List<ListingVersion>> cachingService;
 	@Inject
@@ -65,10 +61,9 @@ public class ListingVersionResource {
 
 	@GET
 	public Response select() {
-		MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter);
+		RDBMSQuery<ListingVersion> q = new RDBMSQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
-		Optional<List<ListingVersion>> cachedResults = cachingService.get("all", params,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<ListingVersion>> cachedResults = cachingService.get("all", params, () -> dao.get(q));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached ListingVersions");
 			return Response.serverError().build();
@@ -79,28 +74,24 @@ public class ListingVersionResource {
 	}
 
 	/**
-	 * Endpoint for /ListingVersion/ to post a new ListingVersion to the persistence layer.
+	 * Endpoint for /listing_versions/ to post a new ListingVersion to the persistence
+	 * layer.
 	 * 
 	 * @param listingVersion the ListingVersion object to insert into the database.
 	 * @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.getParameterName(), listingVersion.getId());
-		}
-		MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter);
 		// add the object, and await the result
-		StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(listingVersion)));
+		dao.add(new RDBMSQuery<>(params, dtoFilter), Arrays.asList(listingVersion));
 
 		// return the results as a response
 		return Response.ok().build();
 	}
 
 	/**
-	 * Endpoint for /listingVersions/\<listingVersionId\> to retrieve a specific ListingVersion from the
-	 * database.
+	 * Endpoint for /listing_versions/\<listingVersionId\> to retrieve a specific
+	 * ListingVersion from the database.
 	 * 
 	 * @param listingVersionId the ListingVersion ID
 	 * @return response for the browser
@@ -108,24 +99,26 @@ public class ListingVersionResource {
 	@GET
 	@Path("/{listingVersionId}")
 	public Response select(@PathParam("listingVersionId") String listingVersionId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), listingVersionId);
+		params.addParam(UrlParameterNames.ID, 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,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				() -> dao.get(new RDBMSQuery<>(params, dtoFilter)));
 		if (!cachedResults.isPresent()) {
 			LOGGER.error("Error while retrieving cached listing for ID {}", listingVersionId);
 			return Response.serverError().build();
 		}
+		if (cachedResults.get().isEmpty()) {
+			throw new NoResultException("Could not find any documents with ID " + listingVersionId);
+		}
 
 		// return the results as a response
 		return Response.ok(cachedResults.get()).build();
 	}
 
 	/**
-	 * Endpoint for /listingVersions/\<listingVersionId\> to retrieve a specific ListingVersion from the
-	 * database.
+	 * Endpoint for /listing_versions/\<listingVersionId\> to remove a specific
+	 * ListingVersion from the database.
 	 * 
 	 * @param listingVersionId the listingVersion ID
 	 * @return response for the browser
@@ -134,14 +127,9 @@ public class ListingVersionResource {
 	@RolesAllowed({ "marketplace_version_delete", "marketplace_admin_access" })
 	@Path("/{listingVersionId}")
 	public Response delete(@PathParam("listingVersionId") String listingVersionId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), listingVersionId);
-
-		MongoQuery<ListingVersion> q = new MongoQuery<>(params, dtoFilter);
+		params.addParam(UrlParameterNames.ID, listingVersionId);
 		// 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();
-		}
+		dao.delete(new RDBMSQuery<>(params, dtoFilter));
 		// return the results as a response
 		return Response.ok().build();
 	}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java b/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java
index 24110f5b82a53376bb047bf7ca93054c601acfca..c8a9d9c72422da2815dd3d7d0193cfc75f12d1e8 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/resource/MarketResource.java
@@ -10,10 +10,10 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 
-import javax.annotation.security.PermitAll;
 import javax.annotation.security.RolesAllowed;
 import javax.enterprise.context.RequestScoped;
 import javax.inject.Inject;
+import javax.persistence.NoResultException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -22,24 +22,19 @@ 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.core.helper.ResponseHelper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.marketplace.dto.Market;
-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.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import org.jboss.resteasy.annotations.jaxrs.PathParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.mongodb.client.result.DeleteResult;
-
 /**
  * @author martin
  *
@@ -52,7 +47,7 @@ public class MarketResource {
 	private static final Logger LOGGER = LoggerFactory.getLogger(MarketResource.class);
 
 	@Inject
-	MongoDao dao;
+	PersistenceDao dao;
 	@Inject
 	CachingService<List<Market>> cachingService;
 	@Inject
@@ -61,16 +56,14 @@ public class MarketResource {
 	DtoFilter<Market> dtoFilter;
 	@Inject
 	ResponseHelper responseBuider;
-	
+
 	@GET
-	@PermitAll
 	public Response select() {
-		MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter);
+		RDBMSQuery<Market> q = new RDBMSQuery<>(params, dtoFilter);
 		// retrieve the possible cached object
-		Optional<List<Market>> cachedResults = cachingService.get("all", params,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+		Optional<List<Market>> cachedResults = cachingService.get("all", params, () -> dao.get(q));
 		if (!cachedResults.isPresent()) {
-			LOGGER.error("Error while retrieving cached Categorys");
+			LOGGER.error("Error while retrieving cached markets");
 			return Response.serverError().build();
 		}
 
@@ -81,19 +74,14 @@ public class MarketResource {
 	/**
 	 * Endpoint for /markets/ to post a new Market to the persistence layer.
 	 * 
-	 * @param market the Category object to insert into the database.
+	 * @param market the market object to insert into the database.
 	 * @return response for the browser
 	 */
 	@PUT
 	@RolesAllowed({ "marketplace_market_put", "marketplace_admin_access" })
 	public Response putMarket(Market market) {
-		if (market.getId() != null) {
-			params.addParam(UrlParameterNames.ID.getParameterName(), market.getId());
-		}
-		MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter);
-
 		// add the object, and await the result
-		StreamHelper.awaitCompletionStage(dao.add(q, Arrays.asList(market)));
+		dao.add(new RDBMSQuery<>(params, dtoFilter), Arrays.asList(market));
 
 		// return the results as a response
 		return Response.ok().build();
@@ -107,19 +95,21 @@ public class MarketResource {
 	 * @return response for the browser
 	 */
 	@GET
-	@PermitAll
+
 	@Path("/{marketId}")
 	public Response select(@PathParam("marketId") String marketId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), marketId);
+		params.addParam(UrlParameterNames.ID, 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,
-				null, () -> StreamHelper.awaitCompletionStage(dao.get(q)));
+				() -> dao.get(new RDBMSQuery<>(params, dtoFilter)));
 		if (!cachedResults.isPresent()) {
-			LOGGER.error("Error while retrieving cached listing for ID {}", marketId);
+			LOGGER.error("Error while retrieving cached market for ID {}", marketId);
 			return Response.serverError().build();
 		}
+		if (cachedResults.get().isEmpty()) {
+			throw new NoResultException("Could not find any documents with ID " + marketId);
+		}
 
 		// return the results as a response
 		return responseBuider.build(marketId, params, cachedResults.get());
@@ -136,14 +126,9 @@ public class MarketResource {
 	@RolesAllowed({ "marketplace_market_delete", "marketplace_admin_access" })
 	@Path("/{marketId}")
 	public Response delete(@PathParam("marketId") String marketId) {
-		params.addParam(UrlParameterNames.ID.getParameterName(), marketId);
-
-		MongoQuery<Market> q = new MongoQuery<>(params, dtoFilter);
+		params.addParam(UrlParameterNames.ID, marketId);
 		// 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();
-		}
+		dao.delete(new RDBMSQuery<>(params, dtoFilter));
 		// return the results as a response
 		return Response.ok().build();
 	}
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 6addf934f7f8b821c57178228dd88970d8ce2a3a..df09e084bd6526f3444764037669015a2f5c2cb3 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java
+++ b/src/main/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingService.java
@@ -7,7 +7,6 @@
 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,9 +18,9 @@ import javax.annotation.PostConstruct;
 import javax.enterprise.context.ApplicationScoped;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.marketplace.namespace.MicroprofilePropertyNames;
-import org.eclipsefoundation.marketplace.service.CachingService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -130,5 +129,4 @@ public class GuavaCachingService<T> implements CachingService<T> {
 	public long getMaxAge() {
 		return ttlWrite;
 	}
-
 }
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dao/MongoDao.java b/src/main/java/org/eclipsefoundation/persistence/dao/PersistenceDao.java
similarity index 72%
rename from src/main/java/org/eclipsefoundation/marketplace/dao/MongoDao.java
rename to src/main/java/org/eclipsefoundation/persistence/dao/PersistenceDao.java
index 507b50dbb27e889f555b314e89f1f0d0ccc2bd50..afd88bfc4da2b42941d8ed689539902021ace36d 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dao/MongoDao.java
+++ b/src/main/java/org/eclipsefoundation/persistence/dao/PersistenceDao.java
@@ -4,15 +4,18 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.dao;
+package org.eclipsefoundation.persistence.dao;
 
 import java.util.List;
 import java.util.concurrent.CompletionStage;
 
-import org.eclipsefoundation.marketplace.health.BeanHealth;
-import org.eclipsefoundation.marketplace.model.MongoQuery;
+import javax.enterprise.event.Observes;
 
-import com.mongodb.client.result.DeleteResult;
+import org.eclipsefoundation.core.health.BeanHealth;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+
+import io.quarkus.runtime.StartupEvent;
 
 /**
  * Interface for classes communicating with MongoDB. Assumes that reactive
@@ -20,7 +23,7 @@ import com.mongodb.client.result.DeleteResult;
  * 
  * @author Martin Lowe
  */
-public interface MongoDao extends BeanHealth {
+public interface PersistenceDao extends BeanHealth {
 
 	/**
 	 * Retrieves a list of typed results given the query passed.
@@ -28,7 +31,7 @@ public interface MongoDao extends BeanHealth {
 	 * @param q the query object for the current operation
 	 * @return a future result set of objects of type set in query
 	 */
-	<T> CompletionStage<List<T>> get(MongoQuery<T> q);
+	<T extends BareNode> List<T> get(RDBMSQuery<T> q);
 
 	/**
 	 * Adds a list of typed documents to the currently active database and schema,
@@ -39,7 +42,7 @@ public interface MongoDao extends BeanHealth {
 	 * @param documents the list of typed documents to add to the database instance.
 	 * @return a future Void result indicating success on return.
 	 */
-	<T> CompletionStage<Void> add(MongoQuery<T> q, List<T> documents);
+	<T extends BareNode> void add(RDBMSQuery<T> q, List<T> documents);
 
 	/**
 	 * Deletes documents that match the given query.
@@ -49,7 +52,7 @@ public interface MongoDao extends BeanHealth {
 	 * @return a future deletion result indicating whether the operation was
 	 *         successful
 	 */
-	<T> CompletionStage<DeleteResult> delete(MongoQuery<T> q);
+	<T extends BareNode> void delete(RDBMSQuery<T> q);
 
 	/**
 	 * Counts the number of filtered results of the given document type present.
@@ -59,5 +62,9 @@ public interface MongoDao extends BeanHealth {
 	 * @return a future long result representing the number of results available for
 	 *         the given query and docuement type.
 	 */
-	<T> CompletionStage<Long> count(MongoQuery<T> q);
+	<T extends BareNode> CompletionStage<Long> count(RDBMSQuery<T> q);
+
+	default void startup(@Observes StartupEvent event) {
+		// intentionally empty
+	}
 }
diff --git a/src/main/java/org/eclipsefoundation/persistence/dto/BareNode.java b/src/main/java/org/eclipsefoundation/persistence/dto/BareNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..df87a8d6f03ae11c355c9e6f72d868c82c2c7c46
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/persistence/dto/BareNode.java
@@ -0,0 +1,95 @@
+package org.eclipsefoundation.persistence.dto;
+
+import java.util.Objects;
+import java.util.Random;
+import java.util.UUID;
+
+import javax.json.bind.annotation.JsonbTransient;
+import javax.persistence.Column;
+import javax.persistence.Id;
+import javax.persistence.MappedSuperclass;
+
+import org.hibernate.id.uuid.StandardRandomStrategy;
+import org.hibernate.search.annotations.Field;
+
+/**
+ * Represents a bare node with just ID for sake of persistence.
+ * 
+ * @author Martin Lowe
+ */
+@MappedSuperclass
+public abstract class BareNode {
+	private static final Random RND = new Random();
+
+	@Id
+	@Column(unique = true, nullable = false, columnDefinition = "BINARY(16)")
+	@Field
+	private UUID id;
+	@JsonbTransient
+	@Column
+	private float seed;
+
+	/**
+	 * Use auto-generated value internally, rather than generator. Currently
+	 * generators are incredibly finnicky around pre-set string identifiers for
+	 * whatever reason.
+	 */
+	public BareNode() {
+		this.id = StandardRandomStrategy.INSTANCE.generateUUID(null);
+		this.seed = RND.nextFloat();
+	}
+
+	/**
+	 * Initializes lazy fields through access. This is used to avoid issues with JPA
+	 * sessions closing before access.
+	 */
+	public void initializeLazyFields() {
+		// intentionally empty
+	}
+
+	/**
+	 * @return the id
+	 */
+	public UUID getId() {
+		return id;
+	}
+
+	/**
+	 * @param id the id to set
+	 */
+	public void setId(UUID id) {
+		this.id = id;
+	}
+
+	/**
+	 * @return the seed
+	 */
+	public float getSeed() {
+		return seed;
+	}
+
+	/**
+	 * @param seed the seed to set
+	 */
+	public void setSeed(float seed) {
+		this.seed = seed;
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(id, seed);
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		BareNode other = (BareNode) obj;
+		return Objects.equals(id, other.id);
+	}
+
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/NodeBase.java b/src/main/java/org/eclipsefoundation/persistence/dto/NodeBase.java
similarity index 76%
rename from src/main/java/org/eclipsefoundation/marketplace/dto/NodeBase.java
rename to src/main/java/org/eclipsefoundation/persistence/dto/NodeBase.java
index f6f6bf6772781e515aaee31b788fbe5ca8c6703d..9b246f28fe4427fca82b2ad13d5388e84faf1a41 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/dto/NodeBase.java
+++ b/src/main/java/org/eclipsefoundation/persistence/dto/NodeBase.java
@@ -4,10 +4,12 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.dto;
+package org.eclipsefoundation.persistence.dto;
 
 import java.util.Objects;
 
+import javax.persistence.MappedSuperclass;
+
 import org.apache.commons.lang3.StringUtils;
 
 /**
@@ -15,25 +17,11 @@ import org.apache.commons.lang3.StringUtils;
  * 
  * @author Martin Lowe
  */
-public abstract class NodeBase {
-	private String id;
+@MappedSuperclass
+public abstract class NodeBase extends BareNode {
 	private String title;
 	private String url;
 
-	/**
-	 * @return the id
-	 */
-	public String getId() {
-		return id;
-	}
-
-	/**
-	 * @param id the id to set
-	 */
-	public void setId(String id) {
-		this.id = id;
-	}
-
 	/**
 	 * @return the title
 	 */
@@ -73,7 +61,7 @@ public abstract class NodeBase {
 
 	@Override
 	public int hashCode() {
-		return Objects.hash(id, title, url);
+		return Objects.hash(super.hashCode(), title, url);
 	}
 
 	@Override
@@ -88,6 +76,6 @@ public abstract class NodeBase {
 			return false;
 		}
 		NodeBase other = (NodeBase) obj;
-		return Objects.equals(id, other.id) && Objects.equals(title, other.title) && Objects.equals(url, other.url);
+		return super.equals(obj) && Objects.equals(title, other.title) && Objects.equals(url, other.url);
 	}
 }
diff --git a/src/main/java/org/eclipsefoundation/persistence/dto/filter/DtoFilter.java b/src/main/java/org/eclipsefoundation/persistence/dto/filter/DtoFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..6bc26f7802708cee0da92eba830ce981883f43fb
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/persistence/dto/filter/DtoFilter.java
@@ -0,0 +1,45 @@
+/* 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.persistence.dto.filter;
+
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+
+/**
+ * Filter interface for usage when querying data.
+ * 
+ * @author Martin Lowe
+ */
+public interface DtoFilter<T> {
+	
+	/**
+	 * Retrieve filter objects for the current arguments.
+	 * 
+	 * @param wrap       wrapper for the current request
+	 * @param nestedPath current path for nesting of filters
+	 * @return list of filters for the current request, or empty if there are no
+	 *         applicable filters.
+	 */
+	ParameterizedSQLStatement getFilters(RequestWrapper wrap, boolean isRoot);
+
+	/**
+	 * Returns the type of data this object will filter for.
+	 * 
+	 * @return class of object to filter
+	 */
+	Class<T> getType();
+
+	/**
+	 * Whether this type of data should be restrained to a limited set, or return
+	 * all data that is found.
+	 * 
+	 * @return true if limit should be used, false otherwise.
+	 */
+	default boolean useLimit() {
+		return true;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/health/MongoDBHealthCheck.java b/src/main/java/org/eclipsefoundation/persistence/health/PersistenceDaoHealthCheck.java
similarity index 72%
rename from src/main/java/org/eclipsefoundation/marketplace/health/MongoDBHealthCheck.java
rename to src/main/java/org/eclipsefoundation/persistence/health/PersistenceDaoHealthCheck.java
index 85ef6e05cecc5b97c6a33aa5165364dc0e02ec17..f4acdcd6d051122e2ed02f6241895ded54a18691 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/health/MongoDBHealthCheck.java
+++ b/src/main/java/org/eclipsefoundation/persistence/health/PersistenceDaoHealthCheck.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.health;
+package org.eclipsefoundation.persistence.health;
 
 import javax.enterprise.context.ApplicationScoped;
 import javax.inject.Inject;
@@ -12,19 +12,19 @@ import javax.inject.Inject;
 import org.eclipse.microprofile.health.HealthCheck;
 import org.eclipse.microprofile.health.HealthCheckResponse;
 import org.eclipse.microprofile.health.Liveness;
-import org.eclipsefoundation.marketplace.dao.MongoDao;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
 
 /**
- * Liveness check implementation for the MongoDB DAO layer.
+ * Liveness check implementation for the DB DAO layer.
  * 
  * @author Martin Lowe
  */
 @Liveness
 @ApplicationScoped
-public class MongoDBHealthCheck implements HealthCheck {
+public class PersistenceDaoHealthCheck implements HealthCheck {
 
 	@Inject
-	MongoDao dao;
+	PersistenceDao dao;
 
 	@Override
 	public HealthCheckResponse call() {
diff --git a/src/main/java/org/eclipsefoundation/marketplace/helper/SortableHelper.java b/src/main/java/org/eclipsefoundation/persistence/helper/SortableHelper.java
similarity index 94%
rename from src/main/java/org/eclipsefoundation/marketplace/helper/SortableHelper.java
rename to src/main/java/org/eclipsefoundation/persistence/helper/SortableHelper.java
index 2e04209faf82403d279dc5b04f093475f8fd62de..a1e70aae484c7a846b245a4896bc7a08e524c2ac 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/helper/SortableHelper.java
+++ b/src/main/java/org/eclipsefoundation/persistence/helper/SortableHelper.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.helper;
+package org.eclipsefoundation.persistence.helper;
 
 import java.lang.reflect.Field;
 import java.util.HashMap;
@@ -15,9 +15,8 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Function;
 
-import org.eclipsefoundation.marketplace.model.SortableField;
-
-import com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy;
+import org.eclipsefoundation.persistence.model.SortableField;
+import org.hibernate.boot.model.naming.Identifier;
 
 /**
  * Reflection based helper that reads in a type and reads annotations present on
@@ -29,8 +28,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy;
 public class SortableHelper {
 	private static final int MAX_DEPTH = 2;
 
-	// 
-	private static final SnakeCaseStrategy NAMING_STRATEGY = new SnakeCaseStrategy();
 	// set up the internal conversion functions
 	private static final Map<Class<?>, Function<String, ?>> CONVERSION_FUNCTIONS = new HashMap<>();
 	static {
@@ -80,7 +77,7 @@ public class SortableHelper {
 		for (Field f : tgt.getDeclaredFields()) {
 			// create new container for field
 			Sortable<?> c = new Sortable<>(f.getType());
-			c.name = NAMING_STRATEGY.translate(f.getName());
+			c.name = Identifier.toIdentifier(f.getName()).getText();
 			c.path = c.name;
 
 			// if annotation exists, get values from it
diff --git a/src/main/java/org/eclipsefoundation/persistence/model/DtoTable.java b/src/main/java/org/eclipsefoundation/persistence/model/DtoTable.java
new file mode 100644
index 0000000000000000000000000000000000000000..27fe017c57e58a125680086ee874028032578b52
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/persistence/model/DtoTable.java
@@ -0,0 +1,29 @@
+package org.eclipsefoundation.persistence.model;
+
+import java.util.Objects;
+
+/**
+ * Represents an object in the database. This should be deprecated to instead
+ * read persistence annotations from the javax namespace.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public class DtoTable {
+
+	private Class<?> baseClass;
+	private String alias;
+
+	public DtoTable(Class<?> baseClass, String alias) {
+		this.baseClass = Objects.requireNonNull(baseClass);
+		this.alias = Objects.requireNonNull(alias);
+	}
+
+	public Class<?> getType() {
+		return this.baseClass;
+	}
+
+	public String getAlias() {
+		return this.alias;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatement.java b/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatement.java
new file mode 100644
index 0000000000000000000000000000000000000000..3dae1cf3b9b89f545b29217672bfe335c2485d49
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatement.java
@@ -0,0 +1,239 @@
+package org.eclipsefoundation.persistence.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+/**
+ * In-context wrapper for prepared statement
+ * 
+ * @author Martin Lowe
+ *
+ */
+public class ParameterizedSQLStatement {
+	private static final Random RND = new Random();
+	private DtoTable base;
+
+	private List<Clause> clauses;
+	private List<Join> joins;
+
+	private String sortField;
+	private SortOrder order;
+	private SQLGenerator gen;
+	private float seed;
+
+	/**
+	 * Builds a loaded parameterized statement to be used in querying dataset.
+	 */
+	ParameterizedSQLStatement(DtoTable base, SQLGenerator gen) {
+		this.base = Objects.requireNonNull(base);
+		this.clauses = new ArrayList<>();
+		this.joins = new ArrayList<>();
+		this.gen = Objects.requireNonNull(gen);
+		this.seed = RND.nextFloat();
+	}
+
+	public ParameterizedSQLStatement combine(ParameterizedSQLStatement other) {
+		this.clauses.addAll(other.clauses);
+		this.joins.addAll(other.joins);
+
+		return this;
+	}
+
+	/**
+	 * @return the sql
+	 */
+	public String getSelectSql() {
+		return gen.getSelectSQL(this);
+	}
+
+	/**
+	 * @return the base
+	 */
+	public DtoTable getBase() {
+		return base;
+	}
+
+	/**
+	 * @param base the base to set
+	 */
+	public void setBase(DtoTable base) {
+		this.base = base;
+	}
+
+	/**
+	 * @return the sortField
+	 */
+	public String getSortField() {
+		return sortField;
+	}
+
+	/**
+	 * @param sortField the sortField to set
+	 */
+	public void setSortField(String sortField) {
+		this.sortField = sortField;
+	}
+
+	/**
+	 * @return the order
+	 */
+	public SortOrder getOrder() {
+		return order;
+	}
+
+	/**
+	 * @param order the order to set
+	 */
+	public void setOrder(SortOrder order) {
+		this.order = order;
+	}
+
+	/**
+	 * @return the params
+	 */
+	public Object[] getParams() {
+		return clauses.stream().map(Clause::getParams).flatMap(Arrays::stream).collect(Collectors.toList()).toArray();
+	}
+
+	public void addClause(Clause c) {
+		this.clauses.add(c);
+	}
+
+	public void addJoin(Join j) {
+		this.joins.add(j);
+	}
+
+	public List<Clause> getClauses() {
+		return new ArrayList<>(clauses);
+	}
+
+	public List<Join> getJoins() {
+		return new ArrayList<>(joins);
+	}
+	
+	public float getSeed() {
+		return this.seed;
+	}
+
+	/**
+	 * Represents a clause for an SQL query
+	 * 
+	 * @author Martin Lowe
+	 *
+	 */
+	public static class Clause {
+		private String sql;
+		private Object[] params;
+
+		public Clause(String sql, Object[] params) {
+			this.sql = sql;
+			this.params = params;
+		}
+
+		/**
+		 * @return the sql
+		 */
+		public String getSql() {
+			return sql;
+		}
+
+		/**
+		 * @param sql the sql to set
+		 */
+		public void setSql(String sql) {
+			this.sql = sql;
+		}
+
+		/**
+		 * @return the params
+		 */
+		public Object[] getParams() {
+			return params;
+		}
+
+		/**
+		 * @param params the params to set
+		 */
+		public void setParams(Object[] params) {
+			this.params = params;
+		}
+	}
+
+	public static class Join {
+		private DtoTable localTable;
+		private DtoTable foreignTable;
+		private String localField;
+		private String foreignField;
+
+		public Join(DtoTable localTable, DtoTable foreignTable, String localField) {
+			this(localTable, foreignTable, localField, null);
+		}
+
+		public Join(DtoTable localTable, DtoTable foreignTable, String localField, String foreignField) {
+			this.localTable = localTable;
+			this.foreignTable = foreignTable;
+			this.localField = localField;
+			this.foreignField = foreignField;
+		}
+
+		/**
+		 * @return the localTable
+		 */
+		public DtoTable getLocalTable() {
+			return localTable;
+		}
+
+		/**
+		 * @param localTable the localTable to set
+		 */
+		public void setLocalTable(DtoTable localTable) {
+			this.localTable = localTable;
+		}
+
+		/**
+		 * @return the foreignTable
+		 */
+		public DtoTable getForeignTable() {
+			return foreignTable;
+		}
+
+		/**
+		 * @param foreignTable the foreignTable to set
+		 */
+		public void setForeignTable(DtoTable foreignTable) {
+			this.foreignTable = foreignTable;
+		}
+
+		/**
+		 * @return the localField
+		 */
+		public String getLocalField() {
+			return localField;
+		}
+
+		/**
+		 * @param localField the localField to set
+		 */
+		public void setLocalField(String localField) {
+			this.localField = localField;
+		}
+
+		/**
+		 * @return the foreignField
+		 */
+		public String getForeignField() {
+			return foreignField;
+		}
+
+		/**
+		 * @param foreignField the foreignField to set
+		 */
+		public void setForeignField(String foreignField) {
+			this.foreignField = foreignField;
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatementBuilder.java b/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatementBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..94ae0340ee080fc79cd4567208e705dca5e83023
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatementBuilder.java
@@ -0,0 +1,16 @@
+package org.eclipsefoundation.persistence.model;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+@ApplicationScoped
+public class ParameterizedSQLStatementBuilder {
+
+	@Inject
+	SQLGenerator generator;
+	
+	public ParameterizedSQLStatement build(DtoTable base) {
+		return new ParameterizedSQLStatement(base, generator);
+	}
+	
+}
diff --git a/src/main/java/org/eclipsefoundation/persistence/model/RDBMSQuery.java b/src/main/java/org/eclipsefoundation/persistence/model/RDBMSQuery.java
new file mode 100644
index 0000000000000000000000000000000000000000..76ea43f5d08af7ed99978eb3145a5254d10dd809
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/persistence/model/RDBMSQuery.java
@@ -0,0 +1,165 @@
+/* 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.persistence.model;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.marketplace.namespace.UrlParameterNames;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.helper.SortableHelper;
+import org.eclipsefoundation.persistence.helper.SortableHelper.Sortable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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 RDBMSQuery<T extends BareNode> {
+	private static final Logger LOGGER = LoggerFactory.getLogger(RDBMSQuery.class);
+
+	private RequestWrapper wrapper;
+	private DtoFilter<T> dtoFilter;
+
+	private ParameterizedSQLStatement filter;
+	private SortOrder order;
+	private int page = 1;
+
+	public RDBMSQuery(RequestWrapper wrapper, DtoFilter<T> dtoFilter) {
+		this.wrapper = wrapper;
+		this.dtoFilter = dtoFilter;
+		init();
+	}
+
+	/**
+	 * Initializes the query object using the current query string parameters and
+	 * type object. This can be called again to reset the parameters if needed due
+	 * to updated fields.
+	 */
+	public void init() {
+		// clear old values if set to default
+		this.filter = null;
+		this.order = SortOrder.NONE;
+
+		// get the filters for the current DTO
+		this.filter = dtoFilter.getFilters(wrapper, true);
+
+		// get fields that make up the required fields to enable pagination and check
+		Optional<String> sortOpt = wrapper.getFirstParam(UrlParameterNames.SORT);
+		if (sortOpt.isPresent()) {
+			String sortVal = sortOpt.get();
+			SortOrder ord = SortOrder.getOrderFromValue(sortOpt.get());
+			// split sort string of `<fieldName> <SortOrder>`
+			int idx = sortVal.indexOf(' ');
+			// check if the sort string matches the RANDOM sort order
+			if (SortOrder.RANDOM.equals(ord)) {
+				filter.setOrder(SortOrder.RANDOM);
+				this.order = SortOrder.RANDOM;
+			} else if (ord != SortOrder.NONE) {
+				setSort(sortVal.substring(0, idx), sortVal.substring(idx + 1));
+			}
+		}
+		// 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;
+		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);
+			}
+		}
+	}
+
+	/**
+	 * Checks the URL parameter of {@link UrlParameterNames.LIMIT} for a numeric
+	 * value and returns it if present.
+	 * 
+	 * @return the value of the URL parameter {@link UrlParameterNames.LIMIT} if
+	 *         present and numeric, otherwise returns -1.
+	 */
+	public int getLimit() {
+		Optional<String> limitVal = wrapper.getFirstParam(UrlParameterNames.LIMIT);
+		if (limitVal.isPresent() && StringUtils.isNumeric(limitVal.get())) {
+			return Integer.parseInt(limitVal.get());
+		}
+		return -1;
+	}
+
+	private void setSort(String sortField, String sortOrder) {
+		List<Sortable<?>> fields = SortableHelper.getSortableFields(getDocType());
+		Optional<Sortable<?>> fieldContainer = SortableHelper.getSortableFieldByName(fields, sortField);
+		if (fieldContainer.isPresent()) {
+			this.order = SortOrder.getOrderByName(sortOrder);
+			// add sorting query if the sortOrder matches a defined order
+			switch (order) {
+			case ASCENDING:
+			case DESCENDING:
+				this.filter.setOrder(order);
+				this.filter.setSortField(sortField);
+				break;
+			case RANDOM:
+				this.filter.setOrder(order);
+				break;
+			default:
+				// intentionally empty, no sort
+				break;
+			}
+		} else if (LOGGER.isDebugEnabled()) {
+			LOGGER.debug("Field with name '{}' is not marked as sortable, skipping", sortField);
+		}
+	}
+
+	/**
+	 * @return the page of results to retrieve for query
+	 */
+	public int getPage() {
+		return this.page;
+	}
+
+	/**
+	 * @return the filter
+	 */
+	public ParameterizedSQLStatement getFilter() {
+		return this.filter;
+	}
+
+	/**
+	 * @return the DTO filter
+	 */
+	public DtoFilter<T> getDTOFilter() {
+		return this.dtoFilter;
+	}
+
+	/**
+	 * @return the docType
+	 */
+	public Class<T> getDocType() {
+		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;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/persistence/model/SQLGenerator.java b/src/main/java/org/eclipsefoundation/persistence/model/SQLGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..d2e885da1afdd7a1ae7679a80814192527f002db
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/persistence/model/SQLGenerator.java
@@ -0,0 +1,10 @@
+package org.eclipsefoundation.persistence.model;
+
+public interface SQLGenerator {
+
+	String getSelectSQL(ParameterizedSQLStatement src);
+	
+	String getDeleteSQL(ParameterizedSQLStatement src);
+	
+	String getCountSQL(ParameterizedSQLStatement src);
+}
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/SortOrder.java b/src/main/java/org/eclipsefoundation/persistence/model/SortOrder.java
similarity index 97%
rename from src/main/java/org/eclipsefoundation/marketplace/model/SortOrder.java
rename to src/main/java/org/eclipsefoundation/persistence/model/SortOrder.java
index f850948d0dd2c3d97015973cbd03ebd0ea33f338..3f9f93ad26d64ffe13d8adcc2b33b527a1bea348 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/model/SortOrder.java
+++ b/src/main/java/org/eclipsefoundation/persistence/model/SortOrder.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.model;
+package org.eclipsefoundation.persistence.model;
 
 import java.util.Objects;
 
diff --git a/src/main/java/org/eclipsefoundation/marketplace/model/SortableField.java b/src/main/java/org/eclipsefoundation/persistence/model/SortableField.java
similarity index 93%
rename from src/main/java/org/eclipsefoundation/marketplace/model/SortableField.java
rename to src/main/java/org/eclipsefoundation/persistence/model/SortableField.java
index 68d9ab53d7b266051a2b9f76e8949962f281e9ed..ccdd136b45be48758c3d8d5c2833ffd4120dcf1e 100644
--- a/src/main/java/org/eclipsefoundation/marketplace/model/SortableField.java
+++ b/src/main/java/org/eclipsefoundation/persistence/model/SortableField.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.model;
+package org.eclipsefoundation.persistence.model;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
diff --git a/src/main/java/org/eclipsefoundation/persistence/resource/mapper/NoResultMapper.java b/src/main/java/org/eclipsefoundation/persistence/resource/mapper/NoResultMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..0b034b44adff51691f0681c584c34838d3a10274
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/persistence/resource/mapper/NoResultMapper.java
@@ -0,0 +1,38 @@
+/*
+ * 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 https://www.eclipse.org/legal/epl-2.0/
+ * 
+ * SPDX-License-Identifier: EPL-2.0
+*/
+package org.eclipsefoundation.persistence.resource.mapper;
+
+import javax.persistence.NoResultException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.eclipsefoundation.core.model.Error;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Creates human legible error responses in the case of no result exceptions. By
+ * catching the NoResultException, we handle exceptions thrown when there are no
+ * results when one is required.
+ * 
+ * @author Martin Lowe
+ */
+@Provider
+public class NoResultMapper implements ExceptionMapper<NoResultException> {
+	private static final Logger LOGGER = LoggerFactory.getLogger(NoResultMapper.class);
+
+	@Override
+	public Response toResponse(NoResultException exception) {
+		LOGGER.error(exception.getMessage(), exception);
+		return new Error(Status.NOT_FOUND, exception.getMessage()).asResponse();
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/search/dao/SearchIndexDAO.java b/src/main/java/org/eclipsefoundation/search/dao/SearchIndexDAO.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab8928712a9f8f55b1a16037c1f17aafb07f910b
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/search/dao/SearchIndexDAO.java
@@ -0,0 +1,48 @@
+package org.eclipsefoundation.search.dao;
+
+import java.io.Closeable;
+import java.util.List;
+
+import org.apache.lucene.document.Document;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.search.model.IndexerResponse;
+
+/**
+ * Interface for interacting with a search indexing engine. This DAO should be
+ * able to write new entries, as well as retrieve, update or delete existing
+ * entries.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public interface SearchIndexDAO extends Closeable {
+
+	/**
+	 * Retrieves indexed and ranked information for the given query. This
+	 * information
+	 * 
+	 * @param <T> the type of entity to be searched
+	 * @param q   the current RDBMS query to get ranked indexed documents for
+	 * @return an ordered list of bare documents
+	 */
+	<T extends BareNode> List<Document> get(RDBMSQuery<T> q);
+
+	/**
+	 * Update or create entries in the search indexer for the given entities.
+	 * 
+	 * @param <T>      the type of entities that will be persisted to the service.
+	 * @param entities the list of new or updated database entities to be indexed.
+	 * @return 
+	 */
+	<T extends BareNode> IndexerResponse createOrUpdate(List<T> entities, Class<T> docType);
+
+	/**
+	 * Remove the given list of entities from the search index.
+	 * 
+	 * @param <T>      the type of entities that will be removed from the index.
+	 * @param entities the list of (potentially) existing indexed entities to remove
+	 *                 from the index.
+	 */
+	<T extends BareNode> IndexerResponse remove(List<T> entities);
+}
diff --git a/src/main/java/org/eclipsefoundation/search/dao/impl/SolrIndexDAO.java b/src/main/java/org/eclipsefoundation/search/dao/impl/SolrIndexDAO.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd29ea0b1751caa9618978a4351dd88a088ebb81
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/search/dao/impl/SolrIndexDAO.java
@@ -0,0 +1,185 @@
+package org.eclipsefoundation.search.dao.impl;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+
+import org.apache.lucene.document.Document;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.ConcurrentUpdateSolrClient;
+import org.apache.solr.client.solrj.response.UpdateResponse;
+import org.apache.solr.common.SolrInputDocument;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.search.dao.SearchIndexDAO;
+import org.eclipsefoundation.search.model.IndexerResponse;
+import org.eclipsefoundation.search.model.SolrDocumentConverter;
+import org.eclipsefoundation.search.namespace.IndexerResponseStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+public class SolrIndexDAO implements SearchIndexDAO {
+	private static final Logger LOGGER = LoggerFactory.getLogger(SolrIndexDAO.class);
+
+	// DAO settings
+	@ConfigProperty(name = "eclipse.solr.enabled", defaultValue = "false")
+	boolean solrEnabled;
+	@ConfigProperty(name = "eclipse.solr.maintenance", defaultValue = "false")
+	boolean solrMaintenance;
+
+	// Solr settings
+	@ConfigProperty(name = "eclipse.solr.host", defaultValue = "")
+	String solrURL;
+	@ConfigProperty(name = "eclipse.solr.core", defaultValue = "")
+	String core;
+	@ConfigProperty(name = "eclipse.solr.timeout", defaultValue = "10000")
+	int solrTimeout;
+	@ConfigProperty(name = "eclipse.solr.queue", defaultValue = "20")
+	int queueSize;
+	@ConfigProperty(name = "eclipse.solr.threads", defaultValue = "10")
+	int threadCount;
+
+	// internal state members
+	private ConcurrentUpdateSolrClient solrServer;
+	private Map<Class, SolrDocumentConverter> converters;
+
+	@PostConstruct
+	public void init() {
+		if (!solrEnabled) {
+			LOGGER.info("Eclipse Solr server not started, it is not currently enabled");
+		} else {
+			// create solr server
+			ConcurrentUpdateSolrClient.Builder b = new ConcurrentUpdateSolrClient.Builder(solrURL + '/' + core)
+					.withConnectionTimeout(solrTimeout)
+					.withQueueSize(queueSize)
+					.withThreadCount(threadCount);
+			this.solrServer = b.build();
+			this.converters = new HashMap<>();
+			LOGGER.debug("Started Solr server for index processing");
+		}
+	}
+
+	@Override
+	public void close() {
+		// shut down threads to solr server
+		this.solrServer.close();
+		LOGGER.error("Closed Solr server for index processing");
+	}
+
+	@Override
+	public <T extends BareNode> List<Document> get(RDBMSQuery<T> q) {
+		// check whether call should proceed
+		if (!stateCheck()) {
+			return Collections.emptyList();
+		}
+
+		// TODO Auto-generated method stub
+
+		return Collections.emptyList();
+	}
+
+	@Override
+	public <T extends BareNode> IndexerResponse createOrUpdate(List<T> entities, Class<T> docType) {
+		// check whether call should proceed
+		if (!stateCheck()) {
+			return IndexerResponse.getMaintenanceResponse();
+		}
+
+		// only way of setting value, so type is known and is safe
+		@SuppressWarnings("unchecked")
+		SolrDocumentConverter<T> converter = (SolrDocumentConverter<T>) converters.computeIfAbsent(docType,
+				SolrDocumentConverter::new);
+
+		// convert the documents
+		List<SolrInputDocument> docs = entities.stream().map(converter::convert).collect(Collectors.toList());
+		// attempt to update + commit the changes
+		long now = System.currentTimeMillis();
+		try {
+			// attempting to add documents to solr core
+			solrServer.add(docs);
+			return generateResponse(solrServer.commit(false, false, true),
+					"Success! Indexed " + entities.size() + " documents",
+					"Non-fatal error encountered while indexing documents");
+		} catch (SolrServerException | IOException e) {
+			LOGGER.error("Error while adding indexed documents to Solr server", e);
+			return new IndexerResponse("Error while creating/updating index documents", IndexerResponseStatus.FAILED,
+					System.currentTimeMillis() - now, e);
+		}
+	}
+
+	@Override
+	public <T extends BareNode> IndexerResponse remove(List<T> entities) {
+		// check whether call should proceed
+		if (!stateCheck()) {
+			return IndexerResponse.getMaintenanceResponse();
+		}
+
+		if (entities == null || entities.isEmpty()) {
+			return new IndexerResponse();
+		}
+		// retrieve the list of IDs of indexed documents to delete
+		List<String> ids = entities.stream().map(BareNode::getId).map(UUID::toString).collect(Collectors.toList());
+
+		// attempt to update + commit the changes
+		long now = System.currentTimeMillis();
+		try {
+			// run the update to remove responses
+			solrServer.deleteById(ids);
+			return generateResponse(solrServer.commit(false, false, true),
+					"Success! Removed " + entities.size() + " documents",
+					"Non-fatal error encountered while removing documents");
+		} catch (SolrServerException | IOException e) {
+			LOGGER.error("Error while removing indexed documents from Solr server", e);
+			return new IndexerResponse("Error while removing indexed documents from Solr server",
+					IndexerResponseStatus.FAILED, System.currentTimeMillis() - now, e);
+		}
+	}
+
+	/**
+	 * Generate a generic indexer response object based on the Solr update response
+	 * document and a set of messages that will be set into the response depending
+	 * on operation success or failure..
+	 * 
+	 * @param solrResponse   the response object from Solr on commit
+	 * @param successMessage the message to include for successful calls
+	 * @param failureMessage the message to include for failed calls
+	 * @return a populated generic indexer response object
+	 */
+	private IndexerResponse generateResponse(UpdateResponse solrResponse, String successMessage,
+			String failureMessage) {
+		// create output response
+		IndexerResponse r = new IndexerResponse();
+		r.setElapsedTimeMS(solrResponse.getElapsedTime());
+
+		// check for an error, and update the response
+		if (solrResponse.getException() != null) {
+			r.setException(solrResponse.getException());
+			r.setStatus(IndexerResponseStatus.FAILED);
+			r.setMessage(failureMessage);
+		} else {
+			r.setStatus(IndexerResponseStatus.SUCCESSFUL);
+			r.setMessage(successMessage);
+		}
+		return r;
+	}
+
+	private boolean stateCheck() {
+		// check state to ensure call should run
+		if (!solrEnabled) {
+			return false;
+		} else if (solrMaintenance) {
+			LOGGER.warn("Solr DAO set to maintenance, not indexing content");
+			return false;
+		}
+		return true;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/search/model/Indexed.java b/src/main/java/org/eclipsefoundation/search/model/Indexed.java
new file mode 100644
index 0000000000000000000000000000000000000000..e333c09354cbdece2ad6e0710cb54f0fb8aea69f
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/search/model/Indexed.java
@@ -0,0 +1,22 @@
+package org.eclipsefoundation.search.model;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({ TYPE, FIELD })
+public @interface Indexed {
+
+	String fieldName() default "";
+
+	/**
+	 * Boost value for the given field.
+	 * 
+	 * @return the boost value for the field, or the default value of 1
+	 */
+	int boost() default 1;
+}
diff --git a/src/main/java/org/eclipsefoundation/search/model/IndexedClassDescriptor.java b/src/main/java/org/eclipsefoundation/search/model/IndexedClassDescriptor.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e39115598757b57206c6c044050b129139da82c
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/search/model/IndexedClassDescriptor.java
@@ -0,0 +1,55 @@
+package org.eclipsefoundation.search.model;
+
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipsefoundation.persistence.dto.BareNode;
+
+public class IndexedClassDescriptor<T extends BareNode> {
+	private List<IndexedDescriptor> internal;
+
+	public IndexedClassDescriptor(Class<T> clazz) {
+		try {
+			for (Field f : clazz.getFields()) {
+				Indexed annotation = f.getAnnotation(Indexed.class);
+				if (annotation != null) {
+					String name = f.getName();
+					Optional<PropertyDescriptor> propertyOpt = Arrays
+							.asList(Introspector.getBeanInfo(clazz).getPropertyDescriptors()).stream()
+							.filter(pd -> name.equals(pd.getName())).findFirst();
+					if (!propertyOpt.isPresent()) {
+						throw new RuntimeException("Could not generate SolrDocumentConverter for " + clazz.getName());
+					}
+					PropertyDescriptor property = propertyOpt.get();
+					internal.add(
+							new IndexedDescriptor(property.getName(), property.getReadMethod(), annotation.boost()));
+				}
+			}
+		} catch (IntrospectionException e) {
+			throw new RuntimeException("Could not generate SolrDocumentConverter for " + clazz.getName(), e);
+		}
+	}
+
+	public List<IndexedDescriptor> getDescriptors() {
+		return new ArrayList<>(internal);
+	}
+	
+	public static class IndexedDescriptor {
+		String name;
+		Method getter;
+		int boost;
+
+		private IndexedDescriptor(String name, Method getter, int boost) {
+			this.name = name;
+			this.getter = getter;
+			this.boost = boost;
+		}
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/search/model/IndexerResponse.java b/src/main/java/org/eclipsefoundation/search/model/IndexerResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..e5e4a746eedac41b2b19b4c982f161042ae97d78
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/search/model/IndexerResponse.java
@@ -0,0 +1,89 @@
+package org.eclipsefoundation.search.model;
+
+import org.eclipsefoundation.search.namespace.IndexerResponseStatus;
+
+public class IndexerResponse {
+	private String message;
+	private IndexerResponseStatus status;
+	private Exception exception;
+	private long elapsedTimeMS;
+
+	public IndexerResponse() {
+		this("", null, 0);
+	}
+
+	public IndexerResponse(String message, IndexerResponseStatus status, long elapsedTimeMS) {
+		this(message, status, elapsedTimeMS, null);
+	}
+
+	public IndexerResponse(String message, IndexerResponseStatus status, long elapsedTimeMS, Exception exception) {
+		this.message = message;
+		this.status = status;
+		this.elapsedTimeMS = elapsedTimeMS;
+		this.setException(exception);
+	}
+
+	/**
+	 * @return the message
+	 */
+	public String getMessage() {
+		return message;
+	}
+
+	/**
+	 * @param message the message to set
+	 */
+	public void setMessage(String message) {
+		this.message = message;
+	}
+
+	/**
+	 * @return the status
+	 */
+	public IndexerResponseStatus getStatus() {
+		return status;
+	}
+
+	/**
+	 * @param status the status to set
+	 */
+	public void setStatus(IndexerResponseStatus status) {
+		this.status = status;
+	}
+
+	/**
+	 * @return the exception
+	 */
+	public Exception getException() {
+		return exception;
+	}
+
+	/**
+	 * @param exception the exception to set
+	 */
+	public void setException(Exception exception) {
+		this.exception = exception;
+	}
+
+	/**
+	 * @return the elapsedTimeMS
+	 */
+	public long getElapsedTimeMS() {
+		return elapsedTimeMS;
+	}
+
+	/**
+	 * @param elapsedTimeMS the elapsedTimeMS to set
+	 */
+	public void setElapsedTimeMS(long elapsedTimeMS) {
+		this.elapsedTimeMS = elapsedTimeMS;
+	}
+
+	public static IndexerResponse getMaintenanceResponse() {
+		IndexerResponse out = new IndexerResponse();
+		out.message = "";
+		out.elapsedTimeMS = 0;
+		out.setStatus(IndexerResponseStatus.MAINTENANCE);
+		return out;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/search/model/SolrDocumentConverter.java b/src/main/java/org/eclipsefoundation/search/model/SolrDocumentConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..c241f7393476cc82eaec865dccc4d1ebd5d0cf31
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/search/model/SolrDocumentConverter.java
@@ -0,0 +1,38 @@
+package org.eclipsefoundation.search.model;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.apache.solr.common.SolrInputDocument;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.search.model.IndexedClassDescriptor.IndexedDescriptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SolrDocumentConverter<T extends BareNode> {
+	private static final Logger LOGGER = LoggerFactory.getLogger(SolrDocumentConverter.class);
+
+	private Class<T> clazz;
+	private IndexedClassDescriptor<T> internal;
+
+	public SolrDocumentConverter(Class<T> clazz) {
+		this.clazz = clazz;
+		this.internal = new IndexedClassDescriptor<>(clazz);
+	}
+
+	public SolrInputDocument convert(T entity) {
+		SolrInputDocument in = new SolrInputDocument();
+		try {
+			for (IndexedDescriptor c : internal.getDescriptors()) {
+				in.addField(c.name, c.getter.invoke(entity));
+			}
+		} catch (IllegalAccessException e) {
+			LOGGER.error("Could not invoke getter while converting entity of type {}", clazz.getName(), e);
+		} catch (IllegalArgumentException e) {
+			LOGGER.error("Unexpected argument encountered in getter while converting entity of type {}",
+					clazz.getName(), e);
+		} catch (InvocationTargetException e) {
+			LOGGER.error("Unknown exception while converting entity of type {}", clazz.getName(), e);
+		}
+		return in;
+	}
+}
diff --git a/src/main/java/org/eclipsefoundation/search/namespace/IndexerResponseStatus.java b/src/main/java/org/eclipsefoundation/search/namespace/IndexerResponseStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a68ac15a0ba2f897a45a24048294fd054ca9c6a
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/search/namespace/IndexerResponseStatus.java
@@ -0,0 +1,5 @@
+package org.eclipsefoundation.search.namespace;
+
+public enum IndexerResponseStatus {
+	SUCCESSFUL, FAILED, MAINTENANCE;
+}
diff --git a/src/main/node/index.js b/src/main/node/index.js
index 201316f141414f74fdb708fba947dbec17717244..06bc4aef2d574eb4bbfb5407cbbe5df0dd099dc2 100644
--- a/src/main/node/index.js
+++ b/src/main/node/index.js
@@ -1,7 +1,7 @@
 const axios = require('axios');
 const time = require('moment');
 const instance = axios.create({
-  timeout: 5000,
+  timeout: 10000,
   headers: {'User-Agent': 'mpc/0.0.0'}
 });
 const randomWords = require('random-words');
@@ -13,6 +13,12 @@ const argv = require('yargs')
     default: 1000,
     nargs: 1
   })
+  .option('b', {
+    description: 'Number of listings to generate per "batch"',
+    alias: 'batch',
+    default: 25,
+    nargs: 1
+  })
   .option('i', {
     description: 'Number of installs to generate',
     alias: 'installs',
@@ -26,12 +32,13 @@ const argv = require('yargs')
     demandOption: true
   }).argv;
 
-let max = argv.c;
+const max = argv.c;
+const batch = argv.b;
 var moment = require('moment-timezone');
-const lic_types = ["EPL-2.0","EPL-1.0","GPL"];
+const lic_types = ["EPL-2.0", "EPL-1.0", "GPL", "MIT"];
 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 eclipseVs = ["4.6","4.7","4.8","4.9","4.10","4.11","4.12", "4.13"];
+const javaVs = [5, 6, 7, 8, 9, 10, 11];
 const categoryIds = [];
 for (var i=0;i<200;i++) {
   categoryIds.push(uuid.v4());
@@ -44,7 +51,29 @@ for (var i=0;i<5;i++) {
 run();
 
 async function run() {
-  var a = await createListing(0);
+  var a = await createCategory(0);
+  
+  console.log("Hold on to your pants, its about to get wild")
+  // wait for ~5s
+  var time = Date.now() + 5000;
+  while (time > Date.now()) ;
+  
+  var count = 0;
+  while (count < max) {
+	  var listings = [];
+	  for (var i = 0; i < batch; i++) {
+		  var result = await createListing(count++);
+		  if (result != null) {
+			  listings.push(result);
+		  }
+	  }
+	  for (var listingIdx in listings) {
+		  await listingCallback(listings[listingIdx]);
+	  }
+  }
+
+  // create markets once listings are all generated
+  await createMarket(0);
 }
 
 function shuff(arr) {
@@ -70,41 +99,39 @@ function splice(arr) {
 }
 
 async function createListing(count) {
-  if (count >= max) {
-    createCategory(0);
-    return;
-  }
-  
   console.log(`Generating listing ${count} of ${max}`);
   var json = generateJSON(uuid.v4());
-  instance.put(argv.s+"/listings/", json)
-    .then(await listingCallback(json, count))
+  return instance.put(argv.s+"/listings/", json)
+    .then(function() { return json; })
     .catch(err => console.log(err));
 }
 
-async function listingCallback(json, count) {
+async function listingCallback(json) {
   var installs = Math.floor(Math.random()*argv.i);
-  var solsCount = Math.floor(Math.random()*5) + 1;
+  var solsCount = Math.floor(Math.random()*5) + 2;
+  console.log(`Generating ${solsCount} version records for listing '${json.id}'`);
   var versions = [];
-  for (var j=0;j<solsCount;j++) {
+  for (var j=1;j<solsCount;j++) {
     var v = await createVersion(j, solsCount, json.id);
     if (v != null) {
       versions.push(v);
     }
   }
-  
+  if (versions.length == 0) {
+	  return;
+  }
   console.log(`Generating ${installs} install records for listing '${json.id}'`);
-  createInstall(0, installs, json, versions, () => createListing(count+1));
+  createInstall(0, installs, json, versions);
 }
 
-function createCategory(count) {
+async function createCategory(count) {
   if (count >= categoryIds.length) {
-    createMarket(0);
     return;
   }
 
-  instance.put(argv.s+"/categories/", generateCategoryJSON(categoryIds[count++]))
-    .then(() => createCategory(count))
+  console.log(`Generating category record with ID '${categoryIds[count]}'`);
+  await instance.put(argv.s+"/categories/", generateCategoryJSON(categoryIds[count]))
+    .then(async function() { await createCategory(count + 1); })
     .catch(err => console.log(err));
 }
 
@@ -118,13 +145,13 @@ function createMarket(count) {
     .catch(err => console.log(err));
 }
 
-function createInstall(curr, max, listing, versions, callback) {
-  if (curr >= max) {
-    return callback();
+function createInstall(curr, maxInstall, listing, versions) {
+  if (curr >= maxInstall) {
+    return;
   }
   var json = generateInstallJSON(listing, versions);
   instance.post(`${argv.s}/installs/${json['listing_id']}/${json.version}`, json)
-    .then(createInstall(curr+1,max,listing,versions,callback))
+    .then(createInstall(curr+1,maxInstall,listing,versions))
     .catch(err => console.log(err));
 }
 
@@ -144,16 +171,21 @@ function generateJSON(id) {
     var currUuid = markets[marketIdx].uuid;
     for (var actualMarketIdx in marketIds) {
       if (marketIds[actualMarketIdx].uuid === currUuid) {
-        marketIds[actualMarketIdx].listings.push(id);
+        marketIds[actualMarketIdx].listings.push({'id':id});
         break;
       }
     }
   }
+  
+  var catIdsRaw = splice(categoryIds).splice(0,Math.ceil(Math.random()*5)+1);
+  var cats = [];
+  for (var categoryIdIdx in catIdsRaw) {
+	  cats.push({'id': catIdsRaw[categoryIdIdx]});
+  }
   return {
     "id": id,
   	"title": "Sample",
   	"url": "https://jakarta.ee",
-  	"foundation_member": false,
   	"teaser": randomWords({exactly:1, wordsPerString:Math.floor(Math.random()*100)})[0],
   	"body": randomWords({exactly:1, wordsPerString:Math.floor(Math.random()*300)})[0],
     "status": "draft",
@@ -168,17 +200,15 @@ function generateJSON(id) {
   		}
   	],
     "organization": {
-			"name": "Eclipse Foundation",
-			"id": 1
+			"name": "Eclipse Foundation"
 		},
   	"tags": [
   		{
   			"name": "Build tools",
-  			"id": "1",
   			"url": ""
   		}
   	],
-  "category_ids": splice(categoryIds).splice(0,Math.ceil(Math.random()*5)+1),
+  "categories": cats,
 	"screenshots": ["http://www.example.com/img/sample.png"]
   };
 }
@@ -220,9 +250,11 @@ function generateInstallJSON(listing,versions) {
 function generateVersionJSON(name, listingId) {
   return {
     "version": name,
+    "update_site_url": "",
     "listing_id": listingId,
     "eclipse_versions": splice(eclipseVs),
     "min_java_version": javaVs[Math.floor(Math.random()*javaVs.length)],
-    "platforms": splice(platforms)
+    "platforms": splice(platforms),
+    "feature_ids":[]
   };
 }
diff --git a/src/main/resources/META-INF/resources/favicon.ico b/src/main/resources/META-INF/resources/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..f2014a925df90a23017f9229b8d884d20222508b
Binary files /dev/null and b/src/main/resources/META-INF/resources/favicon.ico differ
diff --git a/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource b/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
index da76aec0e189c10687e8a007661684b94351041c..8a41de60a9d5a89941f0a8425fceccd36bf32e9f 100644
--- a/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
+++ b/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
@@ -1 +1 @@
-org.eclipsefoundation.marketplace.config.SecretConfigSource
\ No newline at end of file
+org.eclipsefoundation.core.config.SecretConfigSource
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index ed44390400f4aa9e1293b483df725bc8cfb3d355..a244642591890ba936ab5fe6ebf51a440f0860f5 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,6 +1,6 @@
 ## OAUTH CONFIG
 quarkus.oauth2.enabled=true
-quarkus.oauth2.introspection-url=https://accounts.eclipse.org/oauth2/introspect
+quarkus.oauth2.introspection-url=http://accounts.eclipse.org/oauth2/introspect
 
 ## LOGGER CONFIG
 quarkus.log.file.enable=true
@@ -8,16 +8,21 @@ quarkus.log.file.level=DEBUG
 quarkus.log.file.path=/tmp/logs/quarkus.log
 
 ## DATASOURCE CONFIG
-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
+quarkus.datasource.url = jdbc:mariadb://mariadb/mpc_db
+quarkus.datasource.driver = org.mariadb.jdbc.Driver
+quarkus.datasource.username = root
+quarkus.datasource.min-size = 5
+quarkus.datasource.max-size = 15
+eclipse.db.default.limit=25
+eclipse.db.default.limit.max=100
+#eclipse.solr.host=http://solr.dev.docker/solr
+#eclipse.solr.core=mpc_dev
+
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.hibernate-orm.dialect=org.eclipsefoundation.marketplace.config.CustomMariaDBDialect
+##quarkus.hibernate-orm.log.sql=true
 
 # MISC
 quarkus.resteasy.gzip.enabled=true
-quarkus.http.port=8090
\ No newline at end of file
+quarkus.http.port=8090
+eclipse.oauth.override=true
\ No newline at end of file
diff --git a/src/main/sql/mysql.sql b/src/main/sql/mysql.sql
new file mode 100644
index 0000000000000000000000000000000000000000..ada0a44a50c99c9924ef68b4a588a10c83204c25
--- /dev/null
+++ b/src/main/sql/mysql.sql
@@ -0,0 +1,83 @@
+DROP FUNCTION IF EXISTS mpc_sb.UUID_TO_BIN;
+DROP PROCEDURE IF EXISTS InstallMetrics_proc;
+
+DELIMITER //
+-- Port over UUID_TO_BIN functionality with swap flag enabled. SRC: https://mariadb.com/kb/en/guiduuid-performance/
+CREATE DEFINER=`admin`@`%` FUNCTION `mpc_sb`.`UUID_TO_BIN`(_uuid BINARY(36)) RETURNS binary(16)
+    DETERMINISTIC
+    SQL SECURITY INVOKER 
+BEGIN
+	RETURN
+        UNHEX(CONCAT(
+            SUBSTR(_uuid, 15, 4),
+            SUBSTR(_uuid, 10, 4),
+            SUBSTR(_uuid,  1, 8),
+            SUBSTR(_uuid, 20, 4),
+            SUBSTR(_uuid, 25) ));
+END//
+
+CREATE DEFINER=`admin`@`%` PROCEDURE `mpc_sb`.`InstallMetrics_PROC`()
+BEGIN
+  DROP TEMPORARY TABLE IF EXISTS metrics_temp;
+  CREATE TEMPORARY TABLE metrics_temp(
+    listingId BINARY(16) NOT NULL,
+    total INT NOT NULL DEFAULT 0,
+    start_date date NOT NULL,
+    end_date date
+  );
+  SET @CurrentMonth = EXTRACT(MONTH FROM NOW());
+  SET @CurrentYear = EXTRACT(YEAR FROM NOW());
+  SET @FirstDate = CONCAT(@CurrentYear, "-", @CurrentMonth, "-1"); 
+  SET @CurrentDate = CAST(@FirstDate as date);
+
+  SET @i = 0;
+  main: LOOP
+    IF (@i > 12) THEN
+      LEAVE main;
+    END IF;
+    SET @i = @i + 1;
+
+    -- Retrieve counts for current install period
+    IF @PreviousDate = '' THEN
+      INSERT INTO metrics_temp (listingId, total, start_date)
+        SELECT listingId, COUNT(*) as total, @CurrentDate as start_date
+          FROM Install
+          	WHERE installDate >= @CurrentDate
+          		GROUP BY listingId;
+    ELSE
+      INSERT INTO metrics_temp (listingId, total, start_date, end_date)
+        SELECT listingId, COUNT(*) as total, @CurrentDate as start_date, @PreviousDate as end_date
+          FROM Install
+          	WHERE installDate >= @CurrentDate
+              AND installDate < @PreviousDate
+          		  GROUP BY listingId;
+    END IF;
+
+    -- Update the date vars for next iteration
+    SET @PreviousDate = @CurrentDate;
+    SET @CurrentDate = SUBDATE(@CurrentDate, INTERVAL 1 MONTH);
+  END LOOP main;
+
+  -- Remove old install metrics values (should cascade to metrics)
+  DELETE FROM InstallMetrics;
+  INSERT INTO InstallMetrics (id, listing_Id, total)
+    SELECT UUID_TO_BIN(UUID()) as id, listingId, COUNT(*) as total
+      FROM Install
+        GROUP BY listingId;
+
+  -- Bulk update the metric period table
+  INSERT INTO MetricPeriod (id, listingId, count, start, end)
+    SELECT UUID_TO_BIN(UUID()) as id, listingId, total, start_date, end_date
+      FROM metrics_temp;
+
+  -- Update the many-to-many associative table 
+  INSERT INTO InstallMetrics_MetricPeriod
+    SELECT im.id as install_metrics_id, mp.id as periods_id
+      FROM MetricPeriod mp
+        INNER JOIN InstallMetrics im
+          ON mp.listingId = im.listing_id;
+         
+END
+//
+DELIMITER ;
+
diff --git a/src/test/java/org/eclipsefoundation/core/config/RoleAugmentor.java b/src/test/java/org/eclipsefoundation/core/config/RoleAugmentor.java
new file mode 100644
index 0000000000000000000000000000000000000000..f990118b8e65358b3e2a6c9b158b1447e762a7d8
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/core/config/RoleAugmentor.java
@@ -0,0 +1,44 @@
+package org.eclipsefoundation.core.config;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import io.quarkus.security.identity.AuthenticationRequestContext;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.SecurityIdentityAugmentor;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+
+/**
+ * Custom override for test classes that ignores current login state and sets
+ * all users as admin always. This should only ever be used in testing.
+ * 
+ * @author Martin Lowe
+ */
+@ApplicationScoped
+public class RoleAugmentor implements SecurityIdentityAugmentor {
+
+	@Override
+	public int priority() {
+		return 0;
+	}
+
+	@Override
+	public CompletionStage<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
+
+		// create a new builder and copy principal, attributes, credentials and roles
+		// from the original
+		QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder()
+				.setPrincipal(identity.getPrincipal()).addAttributes(identity.getAttributes())
+				.addCredentials(identity.getCredentials()).addRoles(identity.getRoles());
+
+		// add custom role source here
+		builder.addRole("marketplace_admin_access");
+
+		CompletableFuture<SecurityIdentity> cs = new CompletableFuture<>();
+		cs.complete(builder.build());
+
+		return cs;
+	}
+}
\ No newline at end of file
diff --git a/src/test/java/org/eclipsefoundation/marketplace/helper/DateTimeHelperTest.java b/src/test/java/org/eclipsefoundation/core/helper/DateTimeHelperTest.java
similarity index 97%
rename from src/test/java/org/eclipsefoundation/marketplace/helper/DateTimeHelperTest.java
rename to src/test/java/org/eclipsefoundation/core/helper/DateTimeHelperTest.java
index 084893bede63eb7bd65eef1ff8656ca4605f8727..32317af7b19b7b5f6fe10b2dab3a90341440f653 100644
--- a/src/test/java/org/eclipsefoundation/marketplace/helper/DateTimeHelperTest.java
+++ b/src/test/java/org/eclipsefoundation/core/helper/DateTimeHelperTest.java
@@ -4,7 +4,7 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.helper;
+package org.eclipsefoundation.core.helper;
 
 import java.time.ZoneId;
 import java.util.Calendar;
@@ -12,6 +12,7 @@ import java.util.Date;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 
+import org.eclipsefoundation.core.helper.DateTimeHelper;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
diff --git a/src/test/java/org/eclipsefoundation/marketplace/model/RequestWrapperMock.java b/src/test/java/org/eclipsefoundation/core/model/RequestWrapperMock.java
similarity index 80%
rename from src/test/java/org/eclipsefoundation/marketplace/model/RequestWrapperMock.java
rename to src/test/java/org/eclipsefoundation/core/model/RequestWrapperMock.java
index 7549e7f90e8566b9ff4ce57c37212ead77a631bb..73ede91689f0ce8c179ed27ce0eeb57153ff22fa 100644
--- a/src/test/java/org/eclipsefoundation/marketplace/model/RequestWrapperMock.java
+++ b/src/test/java/org/eclipsefoundation/core/model/RequestWrapperMock.java
@@ -4,9 +4,9 @@
  * which is available at http://www.eclipse.org/legal/epl-v20.html,
  * SPDX-License-Identifier: EPL-2.0
  */
-package org.eclipsefoundation.marketplace.model;
+package org.eclipsefoundation.core.model;
 
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
+import org.eclipsefoundation.core.model.RequestWrapper;
 
 /**
  * Wraps RequestWrapper for access outside of request scope in tests.
diff --git a/src/test/java/org/eclipsefoundation/marketplace/dao/impl/MockHibernateDao.java b/src/test/java/org/eclipsefoundation/marketplace/dao/impl/MockHibernateDao.java
new file mode 100644
index 0000000000000000000000000000000000000000..14da028af7a1fe70f42c616a7a4092b1a318b498
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/marketplace/dao/impl/MockHibernateDao.java
@@ -0,0 +1,119 @@
+package org.eclipsefoundation.marketplace.dao.impl;
+
+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.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+import javax.persistence.NoResultException;
+
+import org.eclipsefoundation.marketplace.dto.Catalog;
+import org.eclipsefoundation.marketplace.dto.Category;
+import org.eclipsefoundation.marketplace.dto.ErrorReport;
+import org.eclipsefoundation.marketplace.dto.Listing;
+import org.eclipsefoundation.marketplace.dto.ListingVersion;
+import org.eclipsefoundation.marketplace.dto.Market;
+import org.eclipsefoundation.persistence.dto.BareNode;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+
+import com.google.common.collect.Sets;
+
+import io.quarkus.test.Mock;
+
+/**
+ * To keep tests separate from datastore, set up a dummy endpoint that returns
+ * copies of static data.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Mock
+@ApplicationScoped
+public class MockHibernateDao extends DefaultHibernateDao {
+	private Map<Class<?>, Object> mockData;
+
+	// allow query to be captured and exposed for test validation
+	public RDBMSQuery<?> capturedQuery;
+	
+	@PostConstruct
+	public void init() {
+		this.mockData = new HashMap<>();
+		
+		// create the listing
+		Listing l = new Listing();
+		l.setId(UUID.randomUUID());
+		l.setBody("");
+		mockData.put(Listing.class, l);
+		
+		// create a listing version
+		ListingVersion lv = new ListingVersion();
+		lv.setId(UUID.randomUUID());
+		lv.setVersion("sample");
+		mockData.put(ListingVersion.class, lv);
+		l.setVersions(Sets.newHashSet(lv));
+		
+		Market m = new Market();
+		m.setId(UUID.randomUUID());
+		mockData.put(Market.class, m);
+		
+		Category c = new Category();
+		mockData.put(Category.class, c);
+
+		Catalog cl = new Catalog();
+		mockData.put(Catalog.class, cl);
+		
+		ErrorReport er = new ErrorReport();
+		mockData.put(ErrorReport.class, er);
+		
+	}
+	
+	@Override
+	public <T extends BareNode> List<T> get(RDBMSQuery<T> q) {
+		capturedQuery = q;
+		Optional<String> useTestData = q.getWrapper().getFirstParam("test-data-exists");
+		if (useTestData.isPresent() && "false".equals(useTestData.get())) {
+			return Collections.emptyList();
+		}
+		// if this is ever wrong, then there was bad mock data
+		@SuppressWarnings("unchecked")
+		T o = (T) mockData.get(q.getDocType());
+		if (o != null) {
+			return Arrays.asList(o);
+		}
+		return Collections.emptyList();
+	}
+
+	@Override
+	public <T extends BareNode> void add(RDBMSQuery<T> q, List<T> documents) {
+		capturedQuery = q;
+		// do nothing for add events, return
+		return;
+	}
+
+	@Override
+	public <T extends BareNode> CompletionStage<Long> count(RDBMSQuery<T> q) {
+		capturedQuery = q;
+		// Complete with an empty default value
+		CompletableFuture<Long> future = new CompletableFuture<Long>();
+		future.complete(0L);
+
+		return future;
+	}
+
+	@Override
+	public <T extends BareNode> void delete(RDBMSQuery<T> q) {
+		capturedQuery = q;
+		// throw the same exception as the main would
+		Optional<String> useTestData = q.getWrapper().getFirstParam("test-data-exists");
+		if (useTestData.isPresent() && "false".equals(useTestData.get())) {
+			throw new NoResultException("Could not find any documents with given filters");
+		}
+	}
+}
diff --git a/src/test/java/org/eclipsefoundation/marketplace/dao/impl/MockMongoDao.java b/src/test/java/org/eclipsefoundation/marketplace/dao/impl/MockMongoDao.java
deleted file mode 100644
index 34be07f46be445130c558e2cd36f2fa373e0d74d..0000000000000000000000000000000000000000
--- a/src/test/java/org/eclipsefoundation/marketplace/dao/impl/MockMongoDao.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/* 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.dao.impl;
-
-import java.lang.reflect.Constructor;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-
-import javax.enterprise.context.ApplicationScoped;
-
-import org.eclipse.microprofile.health.HealthCheckResponse;
-import org.eclipsefoundation.marketplace.dao.MongoDao;
-import org.eclipsefoundation.marketplace.model.MongoQuery;
-
-import com.mongodb.MongoException;
-import com.mongodb.client.result.DeleteResult;
-
-import io.quarkus.test.Mock;
-
-/**
- * Dummy service for MongoDB that spoofs all returned results.
- * 
- * @author Martin Lowe
- */
-@Mock
-@ApplicationScoped
-public class MockMongoDao implements MongoDao {
-
-	private boolean completeExceptionally = false;
-	private Throwable exception = new MongoException("");
-
-	@Override
-	public <T> CompletionStage<List<T>> get(MongoQuery<T> q) {
-		try {
-			// use reflection to create an object
-			Constructor<T> con = q.getDocType().getConstructor();
-			T out = con.newInstance();
-
-			// return the object in a list after a short wait.
-			CompletableFuture<List<T>> cf = CompletableFuture.supplyAsync(() -> {
-				return Arrays.asList(out);
-			});
-			// if flag is set, complete exceptionally
-			if (completeExceptionally) {
-				cf.completeExceptionally(exception);
-			}
-			return cf;
-		} catch (Exception e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	@Override
-	public <T> CompletionStage<Void> add(MongoQuery<T> q, List<T> documents) {
-		try {
-			// return the object in a list after a short wait.
-			CompletableFuture<Void> cf = CompletableFuture.supplyAsync(() -> {
-				return null;
-			});
-			// if flag is set, complete exceptionally
-			if (completeExceptionally) {
-				cf.completeExceptionally(exception);
-			}
-			return cf;
-		} catch (Exception e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	@Override
-	public <T> CompletionStage<DeleteResult> delete(MongoQuery<T> q) {
-		try {
-			// return the object in a list after a short wait.
-			CompletableFuture<DeleteResult> cf = CompletableFuture.supplyAsync(() -> {
-				return DeleteResult.acknowledged(1);
-			});
-			// if flag is set, complete exceptionally
-			if (completeExceptionally) {
-				cf.completeExceptionally(exception);
-			}
-			return cf;
-		} catch (Exception e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	@Override
-	public <T> CompletionStage<Long> count(MongoQuery<T> q) {
-		try {
-			// return the object in a list after a short wait.
-			CompletableFuture<Long> cf = CompletableFuture.supplyAsync(() -> {
-				return Math.round(Math.random() * 100);
-			});
-			// if flag is set, complete exceptionally
-			if (completeExceptionally) {
-				cf.completeExceptionally(exception);
-			}
-			return cf;
-		} catch (Exception e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	public void setCompleteExceptionally(boolean completeExceptionally) {
-		this.completeExceptionally = completeExceptionally;
-	}
-
-	public void setException(Throwable exception) {
-		this.exception = exception;
-	}
-
-	@Override
-	public HealthCheckResponse health() {
-		return HealthCheckResponse.named("MOCK MongoDB readiness").up().build();
-	}
-
-}
diff --git a/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java b/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java
index 41d65df20003061de18bcdd6534a2750422078dd..e508c9f00a7cd3463270b2dfa3c6e5e823e05130 100644
--- a/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java
+++ b/src/test/java/org/eclipsefoundation/marketplace/helper/SortableHelperTest.java
@@ -9,17 +9,20 @@ package org.eclipsefoundation.marketplace.helper;
 import java.util.List;
 import java.util.Optional;
 
-import org.eclipsefoundation.marketplace.helper.SortableHelper.Sortable;
-import org.eclipsefoundation.marketplace.model.SortableField;
+import org.eclipsefoundation.persistence.helper.SortableHelper;
+import org.eclipsefoundation.persistence.helper.SortableHelper.Sortable;
+import org.eclipsefoundation.persistence.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/helper/TestHelper.java b/src/test/java/org/eclipsefoundation/marketplace/helper/TestHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..e126a820f5ea57c773e86b50f23d8d9ec83fe639
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/marketplace/helper/TestHelper.java
@@ -0,0 +1,16 @@
+package org.eclipsefoundation.marketplace.helper;
+
+/**
+ * Class containing useful or widely used test parameters to keep data
+ * centralized
+ * 
+ * @author Martin Lowe
+ *
+ */
+public class TestHelper {
+	public static final String SAMPLE_UUID = "123e4567-e89b-12d3-a456-426655440000";
+	public static final String DATA_EXISTS_PARAM_NAME = "test-data-exists";
+
+	private TestHelper() {
+	}
+}
diff --git a/src/test/java/org/eclipsefoundation/marketplace/resource/CatalogResourceTest.java b/src/test/java/org/eclipsefoundation/marketplace/resource/CatalogResourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5de2b8deda6ccfb31018abb9e27163f077d5da29
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/marketplace/resource/CatalogResourceTest.java
@@ -0,0 +1,137 @@
+/* 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 static io.restassured.RestAssured.given;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.marketplace.dao.impl.MockHibernateDao;
+import org.eclipsefoundation.marketplace.helper.TestHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+
+/**
+ * Test the catalog resource endpoint, using fake data points to test solely the
+ * responsiveness of the endpoint.
+ * 
+ * @author Martin Lowe
+ */
+@QuarkusTest
+public class CatalogResourceTest {
+
+	// explicitly use the mock DAO to avoid potential issues with standard DAO
+	@Inject
+	private MockHibernateDao dao;
+
+	@BeforeEach
+	public void cleanDao() {
+		dao.init();
+	}
+
+	@Test
+	public void testCatalogs() {
+		given()
+			.when().get("/catalogs")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testCatalogIdEndpoint() {
+		given()
+			.when().get("/catalogs/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+
+	@Test
+	public void testCatalogIdEndpointNoResults() {
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().get("/catalogs/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testCatalogIdEndpointInvalidUUID() {
+		given()
+			.when().get("/catalogs/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testDeletion() {
+		given()
+			.when().delete("/catalogs/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testDeletionNotExists() {
+		// pass param that tells the mock service that no data should be returned for
+		// this call
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().delete("/catalogs/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testDeletionInvalidUUID() {
+		given()
+			.when().delete("/catalogs/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testPutJSON() {
+		// JSON string for a catalog
+		String json = 
+			"{\n" + 
+			"	\"title\": \"Sample\",\n" + 
+			"	\"url\": \"https://www.eclipse.org\",\n" + 
+			"	\"self_contained\": true,\n" + 
+			"	\"search_enabled\": true,\n" + 
+			"	\"icon\": \"1.png\",\n" + 
+			"	\"description\": \"Lorem ipsum\",\n" + 
+			"	\"dependencies_repository\": \"https://maven.org\",\n" + 
+			"	\"tabs\": [\n" + 
+			"		{\n" + 
+			"			\"title\": \"Sample\",\n" + 
+			"			\"url\": \"https://www.eclipse.org/#catalog-1\",\n" + 
+			"			\"type\": \"default\"\n" + 
+			"		}\n" + 
+			"	]\n" + 
+			"}";
+		given()
+			.body(json)
+			.contentType(ContentType.JSON)
+			.when().put("/catalogs")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testPutInvalidJSON() {
+		// expect bad request response as whole object needs to be posted
+		given()
+			.body("{'id':'" + TestHelper.SAMPLE_UUID + "'}")
+			.contentType(ContentType.JSON)
+			.when().put("/catalogs")
+				.then()
+					.statusCode(400);
+	}
+}
diff --git a/src/test/java/org/eclipsefoundation/marketplace/resource/CategoryResourceTest.java b/src/test/java/org/eclipsefoundation/marketplace/resource/CategoryResourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b17082846e85bcf7f28d93ba94e18dcffbc33881
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/marketplace/resource/CategoryResourceTest.java
@@ -0,0 +1,126 @@
+/* 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 static io.restassured.RestAssured.given;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.marketplace.dao.impl.MockHibernateDao;
+import org.eclipsefoundation.marketplace.helper.TestHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+
+/**
+ * Test the catalog resource endpoint, using fake data points to test solely the
+ * responsiveness of the endpoint.
+ * 
+ * @author Martin Lowe
+ */
+@QuarkusTest
+public class CategoryResourceTest {
+
+	// explicitly use the mock DAO to avoid potential issues with standard DAO
+	@Inject
+	private MockHibernateDao dao;
+
+	@BeforeEach
+	public void cleanDao() {
+		dao.init();
+	}
+
+	@Test
+	public void testCategorys() {
+		given()
+			.when().get("/categories")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testCategoryIdEndpoint() {
+		given()
+			.when().get("/categories/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+
+	@Test
+	public void testCategoryIdEndpointNoResults() {
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().get("/categories/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testCategoryIdEndpointInvalidUUID() {
+		given()
+			.when().get("/categories/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testDeletion() {
+		given()
+			.when().delete("/categories/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testDeletionNotExists() {
+		// pass param that tells the mock service that no data should be returned for
+		// this call
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().delete("/categories/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testDeletionInvalidUUID() {
+		given()
+			.when().delete("/categories/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	// TODO this test is broken, not sure why
+	public void testPutJSON() {
+		// JSON string for a category
+		String json = 
+			"{" + 
+			"   \"id\": \"7478a5e3-b76d-4463-894d-bb6547ea1333\",\n" +
+			"	\"title\": \"Sample\",\n" + 
+			"	\"url\": \"https://www.eclipse.org\",\n" +
+			"}";
+		given()
+			.body(json)
+			.contentType(ContentType.JSON)
+			.when().put("/categories")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testPutInvalidJSON() {
+		// expect bad request response as whole object needs to be posted
+		given()
+			.body("{'id':'" + TestHelper.SAMPLE_UUID + "'}")
+			.contentType(ContentType.JSON)
+			.when().put("/categories")
+				.then()
+					.statusCode(400);
+	}
+}
diff --git a/src/test/java/org/eclipsefoundation/marketplace/resource/ErrorReportResourceTest.java b/src/test/java/org/eclipsefoundation/marketplace/resource/ErrorReportResourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f44d7a3376748c743d42b5ad3303e40557c6bbe
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/marketplace/resource/ErrorReportResourceTest.java
@@ -0,0 +1,105 @@
+/* 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 static io.restassured.RestAssured.given;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.marketplace.dao.impl.MockHibernateDao;
+import org.eclipsefoundation.marketplace.helper.TestHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+
+/**
+ * Test the error report resource endpoint, using fake data points to test solely the
+ * responsiveness of the endpoint.
+ * 
+ * @author Martin Lowe
+ */
+@QuarkusTest
+public class ErrorReportResourceTest {
+
+	// explicitly use the mock DAO to avoid potential issues with standard DAO
+	@Inject
+	private MockHibernateDao dao;
+
+	@BeforeEach
+	public void cleanDao() {
+		dao.init();
+	}
+
+	@Test
+	public void testErrorReports() {
+		given()
+			.when().get("/error_reports")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testErrorReportIdEndpoint() {
+		given()
+			.when().get("/error_reports/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+
+	@Test
+	public void testErrorReportIdEndpointNoResults() {
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().get("/error_reports/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testErrorReportIdEndpointInvalidUUID() {
+		given()
+			.when().get("/error_reports/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testPostJSON() {
+		// JSON string for an error report
+		String json = 
+			"{\n" + 
+			"	\"id\": \"c8014575-a2a2-4a5f-ae1b-14b9c1746ead\",\n" + 
+			"	\"title\": \"Goodbye\",\n" + 
+			"	\"body\": \"friend.\",\n" + 
+			"	\"detailed_message\": \"Hello world you suck!\",\n" + 
+			"	\"ip_address\": \"192.168.0.1\",\n" + 
+			"	\"is_read\": true,\n" + 
+			"	\"listing_id\": \"7478a5e3-b76d-4463-894d-bb6547ea1333\",\n" + 
+			"	\"feature_ids\": null,\n" + 
+			"	\"status\": \"0\"\n" + 
+			"}";
+		given()
+			.body(json)
+			.contentType(ContentType.JSON)
+			.when().post("/error_reports")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testPostInvalidJSON() {
+		// expect bad request response as whole object needs to be posted
+		given()
+			.body("{'id':'" + TestHelper.SAMPLE_UUID + "'}")
+			.contentType(ContentType.JSON)
+			.when().post("/error_reports")
+				.then()
+					.statusCode(400);
+	}
+}
diff --git a/src/test/java/org/eclipsefoundation/marketplace/resource/ListingResourceTest.java b/src/test/java/org/eclipsefoundation/marketplace/resource/ListingResourceTest.java
index cb9254dcaf8547b440531504bdfa6f5da1ad2035..cad09957e056b826e36aa70bf6e817b25afc711b 100644
--- a/src/test/java/org/eclipsefoundation/marketplace/resource/ListingResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/marketplace/resource/ListingResourceTest.java
@@ -8,9 +8,15 @@ package org.eclipsefoundation.marketplace.resource;
 
 import static io.restassured.RestAssured.given;
 
+import javax.inject.Inject;
+
+import org.eclipsefoundation.marketplace.dao.impl.MockHibernateDao;
+import org.eclipsefoundation.marketplace.helper.TestHelper;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
 
 /**
  * Test the listing resource endpoint, using fake data points to test solely the
@@ -21,13 +27,145 @@ import io.quarkus.test.junit.QuarkusTest;
 @QuarkusTest
 public class ListingResourceTest {
 
+	// explicitly use the mock DAO to avoid potential issues with standard DAO
+	@Inject
+	private MockHibernateDao dao;
+
+	@BeforeEach
+	public void cleanDao() {
+		dao.init();
+	}
+
+	@Test
+	public void testListings() {
+		given()
+			.when().get("/listings")
+				.then()
+					.statusCode(200);
+	}
+	
 	@Test
 	public void testListingIdEndpoint() {
-		given().when().get("/listings/abc-123").then().statusCode(200);
+		given()
+			.when().get("/listings/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+
+	@Test
+	public void testListingIdEndpointNoResults() {
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().get("/listings/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
 	}
 	
 	@Test
-	public void testListings() {
-		given().when().get("/listings").then().statusCode(200);
+	public void testListingIdEndpointInvalidUUID() {
+		given()
+			.when().get("/listings/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testDeletion() {
+		given()
+			.when().delete("/listings/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testDeletionNotExists() {
+		// pass param that tells the mock service that no data should be returned for
+		// this call
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().delete("/listings/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testDeletionInvalidUUID() {
+		given()
+			.when().delete("/listings/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testPutJSON() {
+		// JSON string for a listing
+		String json = 
+			"{" + 
+			"   \"id\":\"d348733d-c7ab-4867-83b0-4a2f601a6843\"," + 
+			"   \"title\":\"Sample\"," + 
+			"   \"url\":\"https://jakarta.ee\"," + 
+			"   \"teaser\":\"birth refused iron replace stretch exciting anyone extra stop toy vowel land group zero voyage hair tell taste thumb own son full hour growth how handle product blew example ranch present affect original let captured older require environment spread ranch wing tiny aware mostly independent market nobody sharp frighten vegetable bring liquid must\"," + 
+			"   \"body\":\"flag sheet attention drive boy dear copper two stop driver orange garage broad stream blue again alphabet composed fruit hole possibly consider purple catch steel front according pan selection upper model scared pull doll magic usually character studied use company worry over announced pond handsome recall thought flow term settle practice plenty bone union familiar horn well gold direct brought paid itself consider hunt someone wheat station occur brave respect heart ago thousand house voyage simplest train welcome stop other like colony swimming hospital doll fed central steady wait farm community wore shine settle proper movement product plenty nervous teach total guide trip after audience rising toward honor place hollow wall doubt bare desk branch entirely wave tell hello bat law birth ill biggest dish oil till sense difficult make easier gasoline pig bowl whenever state plus seldom widely writer explore bill understanding sold limited would solid horse common shallow men saved corner pull blue officer heard had provide sick sold noted situation silver official hunt adjective sort son though pattern twice none triangle fresh master quite meat cream end walk factor remarkable him themselves tribe string\"," + 
+			"   \"status\":\"draft\"," + 
+			"   \"support_url\":\"https://jakarta.ee/about/faq\"," + 
+			"   \"license_type\":\"EPL-2.0\"," + 
+			"   \"created\":\"2020-03-04T14:46:52-05:00\"," + 
+			"   \"changed\":\"2020-03-04T14:46:52-05:00\"," + 
+			"   \"authors\":[" + 
+			"      {" + 
+			"         \"full_name\":\"Martin Lowe\"," + 
+			"         \"username\":\"autumnfound\"" + 
+			"      }" + 
+			"   ]," + 
+			"   \"organization\":{" + 
+			"      \"name\":\"Eclipse Foundation\"" + 
+			"   }," + 
+			"   \"tags\":[" + 
+			"      {" + 
+			"         \"name\":\"Build tools\"," + 
+			"         \"url\":\"\"" + 
+			"      }" + 
+			"   ]," + 
+			"   \"categories\":[" + 
+			"      {" + 
+			"         \"id\":\"e763f904-c51d-462f-bd94-a1594f4367d4\"" + 
+			"      }," + 
+			"      {" + 
+			"         \"id\":\"14059954-8ca9-4d04-8d85-cbb7f4da60ed\"" + 
+			"      }," + 
+			"      {" + 
+			"         \"id\":\"2d9b20a0-6c90-4708-9b82-0ed2100a995c\"" + 
+			"      }," + 
+			"      {" + 
+			"         \"id\":\"0dcebdda-85b7-43b5-b985-fe54bc5967db\"" + 
+			"      }," + 
+			"      {" + 
+			"         \"id\":\"93e475a8-0c2e-41c0-86a7-dbc8b8dda33d\"" + 
+			"      }," + 
+			"      {" + 
+			"         \"id\":\"d8dd18d0-9d2c-4223-9b25-d7fefee4b4af\"" + 
+			"      }" + 
+			"   ]," + 
+			"   \"screenshots\":[" + 
+			"      \"http://www.example.com/img/sample.png\"" + 
+			"   ]" + 
+			"}";
+		given()
+			.body(json)
+			.contentType(ContentType.JSON)
+			.when().put("/listings")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testPutInvalidJSON() {
+		// expect bad request response as whole object needs to be posted
+		given()
+			.body("{'id':'" + TestHelper.SAMPLE_UUID + "'}")
+			.contentType(ContentType.JSON)
+			.when().put("/listings")
+				.then()
+					.statusCode(400);
 	}
 }
diff --git a/src/test/java/org/eclipsefoundation/marketplace/resource/ListingVersionResourceTest.java b/src/test/java/org/eclipsefoundation/marketplace/resource/ListingVersionResourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c954b9d284c9806612c398852e04dffaad5efba1
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/marketplace/resource/ListingVersionResourceTest.java
@@ -0,0 +1,130 @@
+/* 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 static io.restassured.RestAssured.given;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.marketplace.dao.impl.MockHibernateDao;
+import org.eclipsefoundation.marketplace.helper.TestHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+
+/**
+ * Test the listing resource endpoint, using fake data points to test solely the
+ * responsiveness of the endpoint.
+ * 
+ * @author Martin Lowe
+ */
+@QuarkusTest
+public class ListingVersionResourceTest {
+
+	// explicitly use the mock DAO to avoid potential issues with standard DAO
+	@Inject
+	private MockHibernateDao dao;
+
+	@BeforeEach
+	public void cleanDao() {
+		dao.init();
+	}
+
+	@Test
+	public void testListingVersions() {
+		given()
+			.when().get("/listing_versions")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testListingVersionIdEndpoint() {
+		given()
+			.when().get("/listing_versions/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+
+	@Test
+	public void testListingVersionIdEndpointNoResults() {
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().get("/listing_versions/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testListingVersionIdEndpointInvalidUUID() {
+		given()
+			.when().get("/listing_versions/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testDeletion() {
+		given()
+			.when().delete("/listing_versions/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testDeletionNotExists() {
+		// pass param that tells the mock service that no data should be returned for
+		// this call
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().delete("/listing_versions/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testDeletionInvalidUUID() {
+		given()
+			.when().delete("/listing_versions/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testPutJSON() {
+		// JSON string for a listing version
+		String json = 
+			"{" +
+			"\"listing_id\":\"7478a5e3-b76d-4463-894d-bb6547ea1333\"," + 
+			"\"min_java_version\":\"8\"," + 
+			"\"update_site_url\":\"sample url\"," + 
+			"\"version\":\"1.3\"," + 
+			"\"eclipse_versions\":[\"4.14\"]," + 
+			"\"platforms\":[\"windows\", \"macos\"]," + 
+			"\"featureIds\":[]"+
+			"}";
+		given()
+			.body(json)
+			.contentType(ContentType.JSON)
+			.when().put("/listing_versions")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testPutInvalidJSON() {
+		// expect bad request response as whole object needs to be posted
+		given()
+			.body("{'id':'" + TestHelper.SAMPLE_UUID + "'}")
+			.contentType(ContentType.JSON)
+			.when().put("/listing_versions")
+				.then()
+					.statusCode(400);
+	}
+}
diff --git a/src/test/java/org/eclipsefoundation/marketplace/resource/MarketResourceTest.java b/src/test/java/org/eclipsefoundation/marketplace/resource/MarketResourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d449ed2c01ee8202ac7d79ea247d9c55cdbfdeb
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/marketplace/resource/MarketResourceTest.java
@@ -0,0 +1,126 @@
+/* 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 static io.restassured.RestAssured.given;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.marketplace.dao.impl.MockHibernateDao;
+import org.eclipsefoundation.marketplace.helper.TestHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+
+/**
+ * Test the market resource endpoint, using fake data points to test solely the
+ * responsiveness of the endpoint.
+ * 
+ * @author Martin Lowe
+ */
+@QuarkusTest
+public class MarketResourceTest {
+
+	// explicitly use the mock DAO to avoid potential issues with standard DAO
+	@Inject
+	private MockHibernateDao dao;
+
+	@BeforeEach
+	public void cleanDao() {
+		dao.init();
+	}
+
+	@Test
+	public void testMarkets() {
+		given()
+			.when().get("/markets")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testMarketIdEndpoint() {
+		given()
+			.when().get("/markets/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+
+	@Test
+	public void testMarketIdEndpointNoResults() {
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().get("/markets/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testMarketIdEndpointInvalidUUID() {
+		given()
+			.when().get("/markets/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testDeletion() {
+		given()
+			.when().delete("/markets/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testDeletionNotExists() {
+		// pass param that tells the mock service that no data should be returned for
+		// this call
+		given()
+			.param(TestHelper.DATA_EXISTS_PARAM_NAME, false)
+			.when().delete("/markets/" + TestHelper.SAMPLE_UUID)
+				.then()
+					.statusCode(404);
+	}
+	
+	@Test
+	public void testDeletionInvalidUUID() {
+		given()
+			.when().delete("/markets/invalid-uuid-string")
+				.then()
+					.statusCode(500);
+	}
+	
+	@Test
+	public void testPutJSON() {
+		// JSON string for a market
+		String json = 
+			"{" + 
+			"	\"title\": \"Sample\",\n" + 
+			"	\"url\": \"https://www.eclipse.org\",\n" + 
+			"	\"listing_ids\": [{\"id\":\"7478a5e3-b76d-4463-894d-bb6547ea1333\"}]\n" + 
+			"}";
+		given()
+			.body(json)
+			.contentType(ContentType.JSON)
+			.when().put("/markets")
+				.then()
+					.statusCode(200);
+	}
+	
+	@Test
+	public void testPutInvalidJSON() {
+		// expect bad request response as whole object needs to be posted
+		given()
+			.body("{'id':'" + TestHelper.SAMPLE_UUID + "'}")
+			.contentType(ContentType.JSON)
+			.when().put("/markets")
+				.then()
+					.statusCode(400);
+	}
+}
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 197951446084e4abea7e9a0aa3e4c316ff63ef0e..9c6c68188e9123b4857e96732e97e2da995d3b6a 100644
--- a/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java
+++ b/src/test/java/org/eclipsefoundation/marketplace/service/impl/GuavaCachingServiceTest.java
@@ -13,8 +13,8 @@ import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.UriInfo;
 
-import org.eclipsefoundation.marketplace.model.RequestWrapper;
-import org.eclipsefoundation.marketplace.model.RequestWrapperMock;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.model.RequestWrapperMock;
 import org.jboss.resteasy.core.ResteasyContext;
 import org.jboss.resteasy.specimpl.ResteasyUriInfo;
 import org.junit.jupiter.api.Assertions;
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index b4ffee9e0fa2c862efe1e81792ab78ded2dbaefa..3e2d2e91e138136e454048273fe32598fe428481 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -1,26 +1,28 @@
 ## OAUTH CONFIG
-quarkus.oauth2.enabled=true
-quarkus.oauth2.introspection-url=http://accounts.php56.dev.docker/oauth2/introspect
+quarkus.oauth2.enabled=false
+quarkus.oauth2.introspection-url=http://accounts.eclipse.org/oauth2/introspect
 
 ## LOGGER CONFIG
-quarkus.log.file.enable=true
-quarkus.log.file.level=DEBUG
-quarkus.log.level=TRACE
-quarkus.log.file.path=/tmp/logs/quarkus.log
+quarkus.log.console.enable=true
+quarkus.log.console.level=TRACE
 
 ## DATASOURCE CONFIG
-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
+quarkus.datasource.url = jdbc:mariadb://mariadb/mpc_sb
+quarkus.datasource.driver = org.mariadb.jdbc.Driver
+quarkus.datasource.username = root
+quarkus.datasource.password = my-secret-pw
+quarkus.datasource.min-size = 5
+quarkus.datasource.max-size = 15
+quarkus.hibernate-orm.physical-naming-strategy=org.eclipsefoundation.marketplace.config.DatabaseNamingStrategy
+eclipse.db.default.limit=25
+eclipse.db.default.limit.max=100
 
 # TEST PROPERTIES
-sample.secret.property=application-value
\ No newline at end of file
+sample.secret.property=application-value
+quarkus.oauth2.client-id=sample
+quarkus.oauth2.client-secret=sample
+
+eclipse.secret.token=1cd66cc0-dbcd-11e9-8a34-2a2ae2dbcce4
+
+%test.sample.secret.property=secret-value
+%test.eclipse.secret.token=example