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