Add catalogId field to catalog search selector 25/3825/5
authorAndrew Gauld <agauld@att.com>
Thu, 21 Feb 2019 14:05:03 +0000 (14:05 +0000)
committerAndrew Gauld <agauld@att.com>
Fri, 22 Feb 2019 17:00:05 +0000 (17:00 +0000)
Change-Id: I72859ca300077d9f6cbfed595634c33c34cb25b9
Issue-ID: ACUMOS-2285
Signed-off-by: Andrew Gauld <agauld@att.com>
docs/design.rst
docs/developer-guide.rst
docs/index.rst
docs/release-notes.rst
docs/selectors.rst [new file with mode: 0644]
gateway/pom.xml
gateway/src/main/java/org/acumos/federation/gateway/cds/Solution.java
gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceImpl.java
gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceLocalImpl.java
gateway/src/main/java/org/acumos/federation/gateway/service/impl/ServiceImpl.java
gateway/src/test/java/org/acumos/federation/gateway/test/CatalogServiceTest.java

index ea26924..7cfe658 100644 (file)
@@ -49,7 +49,7 @@ This interface assumes a pull-based mechanism.
 As such, only the â€˜server’ side is defined by E5.
 
 The client side is based on a set of subscriptions, where each subscription defines a set of solutions
-the client is interested in  (through a selector), and employs periodic polling to detect new material.
+the client is interested in, through a selector (see :ref:`selecting`), and employs periodic polling to detect new material.
 This interface defines no shared state, nothing to synchronize; all responsibility resides with the interested party.
 Requires a pre-provisioned peer on the server side, and uses both client and server authentication (CA based),
 principal to certificate matching.
index 40e559e..6d2100c 100644 (file)
@@ -81,7 +81,7 @@ The following endpoints are defined:
 
 * /solutions
 
-  List all public solutions. Accepts a query parameter, 'selector', which contains a JSON object with selection criteria, base64 encoded. Acceptable selection criteria are the solution object attributes. The entries are ANDed.
+  List all public solutions. Accepts a query parameter, 'selector', which contains a JSON object with selection criteria, base64 encoded. Acceptable selection criteria are the solution object attributes. The entries are ANDed (see :ref:`selecting`).
 
 * /solutions/{solutionId}
 
index 2636639..efe6ca9 100644 (file)
@@ -27,5 +27,6 @@ Federation Gateway
        overview.rst
        design.rst
        developer-guide.rst
+       selectors.rst
        config.rst
        release-notes.rst
index 010db2d..e262190 100644 (file)
@@ -23,6 +23,11 @@ Federation Gateway Release Notes
 This server is available as a Docker image in a Docker registry at the Linux Foundation.
 The image name is "federation-gateway" and the tag is a version string as shown below. 
 
+Version 2.0.1, 2019-02-21
+-------------------------
+
+* Add catalogId field in solution search selector (`ACUMOS-2285 <https://jira.acumos.org/browse/ACUMOS-2285>`_)
+
 Version 2.0.0, 2019-02-20
 -------------------------
 
diff --git a/docs/selectors.rst b/docs/selectors.rst
new file mode 100644 (file)
index 0000000..3bcfd5b
--- /dev/null
@@ -0,0 +1,95 @@
+.. ===============LICENSE_START=======================================================
+.. Acumos CC-BY-4.0
+.. ===================================================================================
+.. Copyright (C) 2019 AT&T Intellectual Property & Tech Mahindra. All rights reserved.
+.. ===================================================================================
+.. This Acumos documentation file is distributed by AT&T and Tech Mahindra
+.. under the Creative Commons Attribution 4.0 International License (the "License");
+.. you may not use this file except in compliance with the License.
+.. You may obtain a copy of the License at
+..
+.. http://creativecommons.org/licenses/by/4.0
+..
+.. This file is distributed on an "AS IS" BASIS,
+.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.. See the License for the specific language governing permissions and
+.. limitations under the License.
+.. ===============LICENSE_END=========================================================
+
+.. _selecting:
+
+Selectors and Finding Solutions
+-------------------------------
+
+The Acumos federation gateway supports retrieving solutions from a peer
+instance of Acumos.  Usually, though, what is desired is to retrieve a subset
+of the solutions, and this is done by specifying a "selector" as a query
+parameter on the HTTP GET from the peer federation gateway.  The value of the
+selector is the Base64 encoding of a JSON object, with keys specifying
+constraints on the set of solutions to be returned.  For example, to specify
+that the name of the solution must be "hello," the JSON object might look like::
+
+    {"name":"hello"}
+
+The Base64 encoding of this is::
+
+    eyJuYW1lIjoiaGVsbG8ifQ==
+
+And the URL for an HTTP GET with this selector might be::
+
+    https://example.org/solutions?selector=eyJuYW1lIjoiaGVsbG8ifQ%3D%3D
+
+The keys supported in the selector object are:
+
+* active
+
+  Boolean, either true or false.  Defaults to true.  If true, only active
+  solutions will be returned.  If false, only inactive solutions will be
+  returned.
+
+* catalogId
+
+  String.  If specified, only solutions from the specified catalog will be
+  returned.
+
+* modelTypeCode
+
+  String or array of strings.  If specified, only solutions with one of the
+  specified modelTypeCodes will be returned.
+
+* modified
+
+  Integer.  Defaults to 1.  A timestamp specified as the number of seconds since
+  January 1, 1970, 00:00:00 GMT.  Only solutions modified at or after the
+  specified timestamp will be returned.
+
+* name
+
+  String.  If specified, only solutions with the specified name will be
+  returned.
+
+* solutionId
+
+  String.  If specified, only the specified solution will be returned.
+
+* toolkitTypeCode
+
+  String or array of strings.  If specified, only solutions with one of the
+  specified toolkitTypeCodes will be returned.
+
+* tags
+
+  String or array of strings.  If specified, only solutions that have at
+  least one of the specified tags will be returned.
+
+Note: String comparison uses an exact match.
+
+Note: Only solutions that meet all of the specified constraints will be returned.
+
+Note: A federation gateway can be configured with additional default values as
+well as overrides of user specified values.
+
+Note: To get "all" solutions, don't specify a selector on the HTTP request (or
+specify one without any of the above keys).  Default constraints will still
+be applied, so only active solutions modified after
+January 1st, 1970 at 00:00:00 GMT will be returned.
index 51e3006..0d929f7 100644 (file)
@@ -25,7 +25,7 @@
        <modelVersion>4.0.0</modelVersion>
        <groupId>org.acumos.federation</groupId>
        <artifactId>gateway</artifactId>
-       <version>2.0.0-SNAPSHOT</version>
+       <version>2.0.1-SNAPSHOT</version>
        <name>Federation Gateway</name>
        <description>Federated Acumos Interface for inter-acumos and ONAP communication</description>
 
index 8bf0a4e..b515c1b 100644 (file)
@@ -47,6 +47,7 @@ public class Solution extends MLPSolution {
                public static final String validationStatusCode = "validationStatusCode";
                public static final String modified = "modified";
                public static final String sourceId = "sourceId";
+               public static final String catalogId = "catalogId";
        };
 
        private List<? extends MLPSolutionRevision>             revisions;
index 1f3e2ec..b265fc6 100644 (file)
@@ -32,6 +32,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import javax.annotation.PostConstruct;
@@ -73,6 +75,14 @@ public class CatalogServiceImpl extends AbstractServiceImpl
 
        private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+       private static final List<String> allATs = new ArrayList<String>();
+
+       static {
+               for (AccessType atc: AccessType.values()) {
+                       allATs.add(atc.code());
+               }
+       }
+
        @Autowired
        private CatalogServiceConfiguration config;
 
@@ -95,31 +105,41 @@ public class CatalogServiceImpl extends AbstractServiceImpl
                RestPageRequest pageRequest = new RestPageRequest(0, this.cdsConfig.getPageSize());
                RestPageResponse<MLPSolution> pageResponse = null;
                List<MLPSolution> solutions = new ArrayList<MLPSolution>(),
-                                                                                       pageSolutions = null;
+                                                                                               pageSolutions = null;
                ICommonDataServiceRestClient cdsClient = getClient(theContext);
                try {
+                       Predicate<MLPSolution> matcher = ServiceImpl.compileSelector(selector);
+                       String catid = (String)selector.get(Solution.Fields.catalogId);
+                       Function<RestPageRequest, RestPageResponse<MLPSolution>> pager = null;
+                       if (catid != null) {
+                               pager = page -> cdsClient.getSolutionsInCatalog(catid, page);
+                       } else {
+                               boolean active = (Boolean)selector.getOrDefault(Solution.Fields.active, Boolean.TRUE);
+                               Object o = selector.getOrDefault(Solution.Fields.accessTypeCode, allATs);
+                               String[] codes = null;
+                               if (o instanceof String) {
+                                       codes = new String[] { (String)o };
+                               } else {
+                                       codes = ((List<String>)o).toArray(new String[0]);
+                               }
+                               String[] xcodes = codes;
+                               Instant since = Instant.ofEpochSecond((Long)selector.get(Solution.Fields.modified));
+                               pager = page -> cdsClient.findSolutionsByDate(active, xcodes, since, page);
+                       }
                        do {
                                log.debug("getSolutions page {}", pageResponse);
-                               pageResponse =
-                                       cdsClient.findSolutionsByDate(
-                                               (Boolean)selector.getOrDefault(Solution.Fields.active, Boolean.TRUE),
-                                               selector.containsKey(Solution.Fields.accessTypeCode) ?
-                                                       new String[] {selector.get(Solution.Fields.accessTypeCode).toString()} :
-                                                       Arrays.stream(AccessType.values()).map(at -> at.code()).toArray(String[]::new),
-                                               Instant.ofEpochSecond ((Long)selector.get(Solution.Fields.modified)),
-                                               pageRequest);
+                               pageResponse = pager.apply(pageRequest);
                        
                                log.debug("getSolutions page response {}", pageResponse);
                                //we need to post-process all other selection criteria
                                pageSolutions = pageResponse.getContent().stream()
-                                                                                                       .filter(solution -> ServiceImpl.isSelectable(solution, selector))
-                                                                                                       .collect(Collectors.toList());
+                                                                                                                       .filter(matcher)
+                                                                                                                       .collect(Collectors.toList());
                                log.debug("getSolutions page selection {}", pageSolutions);
                
                                pageRequest.setPage(pageResponse.getNumber() + 1);
                                solutions.addAll(pageSolutions);
-                       }
-                       while (!pageResponse.isLast());
+                       } while (!pageResponse.isLast());
                }
                catch (HttpStatusCodeException restx) {
                        if (Errors.isCDSNotFound(restx))
index 24941e9..75bd770 100644 (file)
@@ -23,9 +23,11 @@ package org.acumos.federation.gateway.service.impl;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.net.URISyntaxException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import javax.annotation.PostConstruct;
@@ -106,10 +108,7 @@ public class CatalogServiceLocalImpl extends AbstractServiceLocalImpl implements
        public List<MLPSolution> getSolutions(Map<String, ?> theSelector, ServiceContext theContext) throws ServiceException {
 
                log.debug("getSolutions, selector {}", theSelector);
-
-               return solutions.stream()
-                       .filter(solution -> ServiceImpl.isSelectable(solution, theSelector))
-                       .collect(Collectors.toList());
+               return(solutions.stream().filter(ServiceImpl.compileSelector(theSelector)).collect(Collectors.toList()));
        }
 
        @Override
index e8c325c..e59b470 100644 (file)
 package org.acumos.federation.gateway.service.impl;
 
 import java.lang.invoke.MethodHandles;
+import java.time.Instant;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
 
 import org.acumos.cds.domain.MLPSolution;
 import org.acumos.cds.domain.MLPTag;
+import org.acumos.federation.gateway.cds.Solution;
+import org.acumos.federation.gateway.service.ServiceException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,108 +47,141 @@ public abstract class ServiceImpl {
        private ServiceImpl() {
        }
 
+
        /**
-        * Bit of a primitive implementation
-        * @param theSolution solution
-        * @param theSelector selector
-        * @return Boolean
+        * Returns a predicate equivalent to the logical AND of any non-null argument predicates.
+        * @param preds predicates to combine
+        * @return a predicate computing the logical AND of any non-null arguments or a predicate returning true, if there are none
         */
-       public static boolean isSelectable(MLPSolution theSolution, Map<String, ?> theSelector) /*throws ServiceException*/ {
-               boolean res = true;
-
-               log.trace("isSelectable {}", theSolution);
-               if (theSelector == null || theSelector.isEmpty())
-                       return true;
-
-               Object solutionId = theSelector.get("solutionId");
-               if (solutionId != null) {
-                       log.trace("using solutionId based selection {}", solutionId);
-                       if (solutionId instanceof String) {
-                               res &= theSolution.getSolutionId().equals(solutionId);
-                       }
-                       else {
-                               log.debug("unknown solutionId criteria representation {}", solutionId.getClass().getName());
-                               return false;
+       private static Predicate<MLPSolution> and(Predicate<MLPSolution> ... preds) {
+               Predicate<MLPSolution> ret = null;
+               for (Predicate<MLPSolution> x: preds) {
+                       if (ret == null) {
+                               ret = x;
+                       } else if (x != null) {
+                               ret = ret.and(x);
                        }
                }
-
-               Object modelTypeCode = theSelector.get("modelTypeCode");
-               if (modelTypeCode != null) {
-                       log.trace("using modelTypeCode based selection {}", modelTypeCode);
-                       String solutionModelTypeCode = theSolution.getModelTypeCode();
-                       if (solutionModelTypeCode == null) {
-                               return false;
-                       }
-                       else {
-                               if (modelTypeCode instanceof String) {
-                                       res &= solutionModelTypeCode.equals(modelTypeCode);
-                               }
-                               else if (modelTypeCode instanceof List) {
-                                       res &= ((List)modelTypeCode).contains(solutionModelTypeCode);
-                               }
-                               else {
-                                       log.debug("unknown modelTypeCode criteria representation {}", modelTypeCode.getClass().getName());
-                                       return false;
-                               }
-                       }
+               if (ret == null) {
+                       ret = (arg) -> true;
                }
+               return(ret);
+       }
 
-               Object toolkitTypeCode = theSelector.get("toolkitTypeCode");
-               if (toolkitTypeCode != null) {
-                       log.trace("using toolkitTypeCode based selection {}", toolkitTypeCode);
-                       String solutionToolkitTypeCode = theSolution.getToolkitTypeCode();
-                       if (solutionToolkitTypeCode == null) {
-                               return false;
-                       }
-                       else {
-                               if (toolkitTypeCode instanceof String) {
-                                       res &= solutionToolkitTypeCode.equals(toolkitTypeCode);
-                               }
-                               else if (toolkitTypeCode instanceof List) {
-                                       res &= ((List)toolkitTypeCode).contains(solutionToolkitTypeCode);
-                               }
-                               else {
-                                       log.debug("unknown toolkitTypeCode criteria representation {}", toolkitTypeCode.getClass().getName());
-                                       return false;
-                               }
-                       }
-               }
 
-               Object tags = theSelector.get("tags");
-               if (tags != null) {
-                       log.trace("using tags based selection {}", tags);
-                       Set<MLPTag> solutionTags = theSolution.getTags();
-                       if (solutionTags == null) {
-                               return false;
-                       }
-                       else {
-                               if (tags instanceof String) {
-                                       res &= solutionTags.stream().filter(solutionTag -> tags.equals(solutionTag.getTag())).findAny().isPresent();
-                               }
-                               else if (tags instanceof List) {
-                                       res &= solutionTags.stream().filter(solutionTag -> ((List)tags).contains(solutionTag.getTag())).findAny().isPresent();
-                               }
-                               else {
-                                       log.debug("unknown tags criteria representation {}", tags.getClass().getName());
-                                       return false; 
+       /**
+        * Returns a predicate determining matching against a multi-valued field.
+        * The returned predicate will be called with an MLPSolution.  The
+        * Function specified by field will be invoked on it, to extract a Set
+        * of Strings, which will be tested against the value, in theSelector,
+        * corresponding to key.
+        * If theSelector does not contain key, this just returns null.
+        * Otherwise, if the value is a String, this returns a predicate
+        * computing whether the value is in the extracted Set of Strings.
+        * Otherwise, if the value is a List, this returns a predicate
+        * computing whether any of the values in the List is contained in
+        * the Set of Strings.
+        * @param theSelector a map of field names to expected values
+        * @param key the field name to be handled by this predicate
+        * @param field the Function to extract the field value from the Solution
+        * @param listok whether this field supports a list of values in the selector
+        * @return the predicate for testing the field value
+        * @throws ServiceException if the value of key, in theSelector is neither a String nor a List.
+        */
+
+       private static Predicate<MLPSolution> contains(Map<String, ?> theSelector, String key, Function<MLPSolution, Set<String>> field, boolean listok) throws ServiceException {
+               Object o = theSelector.get(key);
+               if (o == null) {
+                       return(null);
+               }
+               log.trace("using {} based selection {}", key, o);
+               if (o instanceof String) {
+                       String s = (String)o;
+                       return(arg-> field.apply(arg).contains(s));
+               }
+               if (listok && o instanceof List) {
+                       List l = (List)o;
+                       return(arg -> {
+                               for (Object val: field.apply(arg)) {
+                                       if (l.contains(val)) {
+                                               return(true);
+                                       }
                                }
-                       }
+                               return(false);
+                       });
                }
+               log.debug("unknown {} criteria representation {}", key, o.getClass().getName());
+               throw new ServiceException("Invalid Selector");
+       }
 
-               Object name = theSelector.get("name");
-               if (name != null) {
-                       log.debug("using name based selection {}", name);
-                       String solutionName = theSolution.getName();
-                       if (solutionName == null) {
-                               return false;
-                       }
-                       else {
-                               res &= solutionName.contains(name.toString());
-                       }
-               }
 
-               return res;
+       /**
+        * Returns a predicate determining matching against a field.
+        * The returned predicate will be called with an MLPSolution.  The
+        * Function specified by field will be invoked on it, to extract its
+        * value, which will be tested against the value, in theSelector,
+        * corresponding to key.
+        * If theSelector does not contain key, this just returns null.
+        * Otherwise, if theSelector contains a String, this returns a predicate
+        * computing whether the value equals the extracted value.
+        * Otherwise, if the value is a List, this returns a predicate
+        * computing whether the extracted value is contained in the List.
+        * @param theSelector a map of field names to expected values
+        * @param key the field name to be handled by this predicate
+        * @param field the Function to extract the field value from the Solution
+        * @param listok whether this field supports a list of values in the selector
+        * @return the predicate for testing the field value
+        * @throws ServiceException if the value of key, in theSelector is neither a String nor a List.
+        */
+
+       private static Predicate<MLPSolution> has(Map<String, ?> theSelector, String key, Function<MLPSolution, String> field, boolean listok) throws ServiceException {
+               Object o = theSelector.get(key);
+               if (o == null) {
+                       return(null);
+               }
+               log.trace("using {} based selection {}", key, o);
+               if (o instanceof String) {
+                       String s = (String)o;
+                       return(arg -> s.equals(field.apply(arg)));
+               }
+               if (listok && o instanceof List) {
+                       List l = (List)o;
+                       return(arg -> l.contains(field.apply(arg)));
+               }
+               log.debug("unknown {} criteria representation {}", key, o.getClass().getName());
+               throw new ServiceException("Invalid Selector");
        }
 
 
+       /**
+        * Returns a predicate for testing an MLPSolution against a selector.
+        * @param theSelector the criteria to be met in a matching solution
+        * @return a predicate for checking for matching solutions
+        * @throws ServiceException if theSelector is malformed
+        */
+
+       public static Predicate<MLPSolution> compileSelector(Map<String, ?> theSelector) throws ServiceException {
+               if (theSelector == null) {
+                       return(arg -> true);
+               }
+               log.trace("compileSelector {}", theSelector);
+               Boolean ao = (Boolean)theSelector.get(Solution.Fields.active);
+               boolean active = ao == null? true: ao.booleanValue();
+               Instant since = Instant.ofEpochSecond((Long)theSelector.get(Solution.Fields.modified));
+               return(and(
+                       arg -> arg.isActive() == active,
+                       arg -> arg.getModified().compareTo(since) >= 0,
+                       has(theSelector, Solution.Fields.solutionId, arg -> arg.getSolutionId(), false),
+                       has(theSelector, Solution.Fields.modelTypeCode, arg -> arg.getModelTypeCode(), true),
+                       has(theSelector, Solution.Fields.toolkitTypeCode, arg -> arg.getToolkitTypeCode(), true),
+                       contains(theSelector, Solution.Fields.tags, arg -> {
+                               Set<String> ret = new HashSet<String>();
+                               for (MLPTag tag: arg.getTags()) {
+                                       ret.add(tag.getTag());
+                               }
+                               return(ret);
+                       }, true),
+                       has(theSelector, Solution.Fields.name, arg ->arg.getName(), false)
+               ));
+       }
 }
index 1137419..c17470d 100644 (file)
@@ -22,7 +22,10 @@ package org.acumos.federation.gateway.test;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.List;
 
 import org.acumos.cds.domain.MLPArtifact;
@@ -81,11 +84,20 @@ public class CatalogServiceTest extends ServiceTest {
        @Autowired
        private CatalogService catalog;
 
+       private static Map<String, Object> selector(Object... flds) {
+               Map<String, Object> ret = new HashMap<String, Object>();
+               for (int i = 0; i < flds.length; i += 2) {
+                       ret.put((String)flds[i], flds[i + 1]);
+               }
+               return(ret);
+       }
+
        protected void initMockResponses() {
 
+               registerMockResponse("GET /ccds/catalog/mycatalog/solution?page=0&size=100", MockResponse.success("mockCDSPortalSolutionsResponse.json"));
                registerMockResponse("GET /ccds/solution/search/date?atc=PB&inst=1000&active=true&page=0&size=100", MockResponse.success("mockCDSPortalSolutionsResponse.json"));
-               registerMockResponse("GET /ccds/solution/search/date?atc=PB&datems=1531747662&active=true&page=0&size=100", MockResponse.success("mockCDSDateSolutionsResponsePage0.json"));
-               registerMockResponse("GET /ccds/solution/search/date?atc=PB&datems=1531747662&active=true&page=1&size=100", MockResponse.success("mockCDSDateSolutionsResponsePage1.json"));
+               registerMockResponse("GET /ccds/solution/search/date?atc=PB&inst=1531747662000&active=true&page=0&size=100", MockResponse.success("mockCDSDateSolutionsResponsePage0.json"));
+               registerMockResponse("GET /ccds/solution/search/date?atc=PB&inst=1531747662000&active=true&page=1&size=100", MockResponse.success("mockCDSDateSolutionsResponsePage1.json"));
                registerMockResponse("GET /ccds/solution/10101010-1010-1010-1010-101010101010", MockResponse.success("mockCDSSolutionResponse.json"));
                registerMockResponse("GET /ccds/solution/10101010-1010-1010-1010-101010101010/revision", MockResponse.success("mockCDSSolutionRevisionsResponse.json"));
                registerMockResponse("GET /ccds/revision/a0a0a0a0-a0a0-a0a0-a0a0-a0a0a0a0a0a0/artifact", MockResponse.success("mockCDSSolutionRevisionArtifactsResponse.json"));
@@ -109,7 +121,37 @@ public class CatalogServiceTest extends ServiceTest {
                        ServiceContext selfService = 
                                ServiceContext.forPeer(new Peer(new MLPPeer("acumosa", "gateway.acumosa.org", "https://gateway.acumosa.org:9084", false, false, "admin@acumosa.org", "AC"), Role.SELF));
 
-                       List<MLPSolution> solutions = catalog.getSolutions(Collections.EMPTY_MAP, selfService);
+                       List<MLPSolution> solutions = catalog.getSolutions(selector("catalogId", "mycatalog"), selfService);
+                       assertTrue("Unexpected solutions count: " + solutions.size(), solutions.size() == 5);
+                       solutions = catalog.getSolutions(selector("catalogId", "mycatalog", "modelTypeCode", "RG"), selfService);
+                       assertTrue("Unexpected solutions count: " + solutions.size(), solutions.size() == 2);
+                       solutions = catalog.getSolutions(selector("catalogId", "mycatalog", "toolkitTypeCode", new CopyOnWriteArrayList(new String[] {"CP", "TF" })), selfService);
+                       assertTrue("Unexpected solutions count: " + solutions.size(), solutions.size() == 3);
+                       solutions = catalog.getSolutions(selector("catalogId", "mycatalog", "tags", "subtract"), selfService);
+                       assertTrue("Unexpected solutions count: " + solutions.size(), solutions.size() == 1);
+                       solutions = catalog.getSolutions(selector("catalogId", "mycatalog", "tags", new CopyOnWriteArrayList(new String[] { "subtract", "poutput"})), selfService);
+                       assertTrue("Unexpected solutions count: " + solutions.size(), solutions.size() == 2);
+                       solutions = catalog.getSolutions(selector("catalogId", "mycatalog", "solutionId", "38efeef1-e4f4-4298-9f68-6f0052d6ade9"), selfService);
+                       assertTrue("Unexpected solutions count: " + solutions.size(), solutions.size() == 1);
+                       try {
+                               catalog.getSolutions(selector("catalogId", "mycatalog", "name", new CopyOnWriteArrayList(new String[] { "A", "B" })), selfService);
+                               assertTrue("Expected service exception, got none", 1 == 0);
+                       }
+                       catch (ServiceException sx) {
+                       }
+                       try {
+                               catalog.getSolutions(selector("catalogId", "mycatalog", "name", true), selfService);
+                               assertTrue("Expected service exception, got none", 1 == 0);
+                       }
+                       catch (ServiceException sx) {
+                       }
+                       try {
+                               catalog.getSolutions(selector("catalogId", "mycatalog", "tags", true), selfService);
+                               assertTrue("Expected service exception, got none", 1 == 0);
+                       }
+                       catch (ServiceException sx) {
+                       }
+                       solutions = catalog.getSolutions(Collections.EMPTY_MAP, selfService);
                        assertTrue("Unexpected solutions count: " + solutions.size(), solutions.size() == 5);
                
                        Solution solution = catalog.getSolution("10101010-1010-1010-1010-101010101010", selfService);