From e9c475edd6b3352ce3cd06b8a866fd0eefeb33f6 Mon Sep 17 00:00:00 2001 From: Andrew Gauld Date: Thu, 21 Feb 2019 14:05:03 +0000 Subject: [PATCH] Add catalogId field to catalog search selector Change-Id: I72859ca300077d9f6cbfed595634c33c34cb25b9 Issue-ID: ACUMOS-2285 Signed-off-by: Andrew Gauld --- docs/design.rst | 2 +- docs/developer-guide.rst | 2 +- docs/index.rst | 1 + docs/release-notes.rst | 5 + docs/selectors.rst | 95 +++++++++ gateway/pom.xml | 2 +- .../acumos/federation/gateway/cds/Solution.java | 1 + .../gateway/service/impl/CatalogServiceImpl.java | 46 +++-- .../service/impl/CatalogServiceLocalImpl.java | 7 +- .../gateway/service/impl/ServiceImpl.java | 219 ++++++++++++--------- .../gateway/test/CatalogServiceTest.java | 48 ++++- 11 files changed, 315 insertions(+), 113 deletions(-) create mode 100644 docs/selectors.rst diff --git a/docs/design.rst b/docs/design.rst index ea26924..7cfe658 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -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. diff --git a/docs/developer-guide.rst b/docs/developer-guide.rst index 40e559e..6d2100c 100644 --- a/docs/developer-guide.rst +++ b/docs/developer-guide.rst @@ -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} diff --git a/docs/index.rst b/docs/index.rst index 2636639..efe6ca9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,5 +27,6 @@ Federation Gateway overview.rst design.rst developer-guide.rst + selectors.rst config.rst release-notes.rst diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 010db2d..e262190 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -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 `_) + Version 2.0.0, 2019-02-20 ------------------------- diff --git a/docs/selectors.rst b/docs/selectors.rst new file mode 100644 index 0000000..3bcfd5b --- /dev/null +++ b/docs/selectors.rst @@ -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. diff --git a/gateway/pom.xml b/gateway/pom.xml index 51e3006..0d929f7 100644 --- a/gateway/pom.xml +++ b/gateway/pom.xml @@ -25,7 +25,7 @@ 4.0.0 org.acumos.federation gateway - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT Federation Gateway Federated Acumos Interface for inter-acumos and ONAP communication diff --git a/gateway/src/main/java/org/acumos/federation/gateway/cds/Solution.java b/gateway/src/main/java/org/acumos/federation/gateway/cds/Solution.java index 8bf0a4e..b515c1b 100644 --- a/gateway/src/main/java/org/acumos/federation/gateway/cds/Solution.java +++ b/gateway/src/main/java/org/acumos/federation/gateway/cds/Solution.java @@ -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 revisions; diff --git a/gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceImpl.java b/gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceImpl.java index 1f3e2ec..b265fc6 100644 --- a/gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceImpl.java +++ b/gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceImpl.java @@ -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 allATs = new ArrayList(); + + 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 pageResponse = null; List solutions = new ArrayList(), - pageSolutions = null; + pageSolutions = null; ICommonDataServiceRestClient cdsClient = getClient(theContext); try { + Predicate matcher = ServiceImpl.compileSelector(selector); + String catid = (String)selector.get(Solution.Fields.catalogId); + Function> 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)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)) diff --git a/gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceLocalImpl.java b/gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceLocalImpl.java index 24941e9..75bd770 100644 --- a/gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceLocalImpl.java +++ b/gateway/src/main/java/org/acumos/federation/gateway/service/impl/CatalogServiceLocalImpl.java @@ -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 getSolutions(Map 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 diff --git a/gateway/src/main/java/org/acumos/federation/gateway/service/impl/ServiceImpl.java b/gateway/src/main/java/org/acumos/federation/gateway/service/impl/ServiceImpl.java index e8c325c..e59b470 100644 --- a/gateway/src/main/java/org/acumos/federation/gateway/service/impl/ServiceImpl.java +++ b/gateway/src/main/java/org/acumos/federation/gateway/service/impl/ServiceImpl.java @@ -21,12 +21,18 @@ 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 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 and(Predicate ... preds) { + Predicate ret = null; + for (Predicate 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 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 contains(Map theSelector, String key, Function> 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 has(Map theSelector, String key, Function 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 compileSelector(Map 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 ret = new HashSet(); + for (MLPTag tag: arg.getTags()) { + ret.add(tag.getTag()); + } + return(ret); + }, true), + has(theSelector, Solution.Fields.name, arg ->arg.getName(), false) + )); + } } diff --git a/gateway/src/test/java/org/acumos/federation/gateway/test/CatalogServiceTest.java b/gateway/src/test/java/org/acumos/federation/gateway/test/CatalogServiceTest.java index 1137419..c17470d 100644 --- a/gateway/src/test/java/org/acumos/federation/gateway/test/CatalogServiceTest.java +++ b/gateway/src/test/java/org/acumos/federation/gateway/test/CatalogServiceTest.java @@ -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 selector(Object... flds) { + Map ret = new HashMap(); + 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 solutions = catalog.getSolutions(Collections.EMPTY_MAP, selfService); + List 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); -- 2.16.6