From: Toine Siebelink Date: Thu, 20 Jul 2023 08:52:40 +0000 (+0000) Subject: Merge "Allow duplicate leaf names in Cps Path leaf condition" X-Git-Tag: 3.3.5~5 X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=6a2eca2859d8b2ab88ff04663902eb7cc74b4fc1;hp=6c9621a3ec2334ccf5cee8db23e425b301a53e0c;p=cps.git Merge "Allow duplicate leaf names in Cps Path leaf condition" --- diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBooleanOperatorType.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBooleanOperatorType.java deleted file mode 100644 index b2f1dddb1..000000000 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBooleanOperatorType.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2023 TechMahindra Ltd - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License 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. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.cpspath.parser; - -public enum CpsPathBooleanOperatorType { - AND("and"), - OR("or"); - - private final String operatorValue; - - CpsPathBooleanOperatorType(final String operatorValue) { - this.operatorValue = operatorValue; - } - - public String getValues() { - return this.operatorValue; - } - - /** - * Finds the value of the given enumeration. - * - * @param operatorValue value of the enum - * @return a booleanOperatorType - */ - public static CpsPathBooleanOperatorType fromString(final String operatorValue) { - for (final CpsPathBooleanOperatorType booleanOperatorType : CpsPathBooleanOperatorType.values()) { - if (booleanOperatorType.operatorValue.equalsIgnoreCase(operatorValue)) { - return booleanOperatorType; - } - } - return null; - } -} diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java index 5c4712737..99135962f 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2022 Nordix Foundation + * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,9 +24,7 @@ package org.onap.cps.cpspath.parser; import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import org.onap.cps.cpspath.parser.antlr4.CpsPathBaseListener; import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.AncestorAxisContext; @@ -43,21 +41,21 @@ public class CpsPathBuilder extends CpsPathBaseListener { private static final String CLOSE_BRACKET = "]"; - final CpsPathQuery cpsPathQuery = new CpsPathQuery(); + private final CpsPathQuery cpsPathQuery = new CpsPathQuery(); - final Map leavesData = new LinkedHashMap<>(); + private final List leavesData = new ArrayList<>(); - final StringBuilder normalizedXpathBuilder = new StringBuilder(); + private final StringBuilder normalizedXpathBuilder = new StringBuilder(); - final StringBuilder normalizedAncestorPathBuilder = new StringBuilder(); + private final StringBuilder normalizedAncestorPathBuilder = new StringBuilder(); - boolean processingAncestorAxis = false; + private boolean processingAncestorAxis = false; - private List containerNames = new ArrayList<>(); + private final List containerNames = new ArrayList<>(); - final List booleanOperators = new ArrayList<>(); + private final List booleanOperators = new ArrayList<>(); - final List comparativeOperators = new ArrayList<>(); + private final List comparativeOperators = new ArrayList<>(); @Override public void exitInvalidPostFix(final CpsPathParser.InvalidPostFixContext ctx) { @@ -99,16 +97,12 @@ public class CpsPathBuilder extends CpsPathBaseListener { @Override public void exitBooleanOperators(final CpsPathParser.BooleanOperatorsContext ctx) { - final CpsPathBooleanOperatorType cpsPathBooleanOperatorType = CpsPathBooleanOperatorType.fromString( - ctx.getText()); - booleanOperators.add(cpsPathBooleanOperatorType.getValues()); + booleanOperators.add(ctx.getText()); } @Override public void exitComparativeOperators(final CpsPathParser.ComparativeOperatorsContext ctx) { - final CpsPathComparativeOperator cpsPathComparativeOperator = CpsPathComparativeOperator.fromString( - ctx.getText()); - comparativeOperators.add(cpsPathComparativeOperator.getLabel()); + comparativeOperators.add(ctx.getText()); } @Override @@ -122,6 +116,8 @@ public class CpsPathBuilder extends CpsPathBaseListener { public void enterMultipleLeafConditions(final MultipleLeafConditionsContext ctx) { normalizedXpathBuilder.append(OPEN_BRACKET); leavesData.clear(); + booleanOperators.clear(); + comparativeOperators.clear(); } @Override @@ -193,7 +189,7 @@ public class CpsPathBuilder extends CpsPathBaseListener { } private void leafContext(final CpsPathParser.LeafNameContext ctx, final Object comparisonValue) { - leavesData.put(ctx.getText(), comparisonValue); + leavesData.add(new CpsPathQuery.DataLeaf(ctx.getText(), comparisonValue)); appendCondition(normalizedXpathBuilder, ctx.getText(), comparisonValue); if (processingAncestorAxis) { appendCondition(normalizedAncestorPathBuilder, ctx.getText(), comparisonValue); diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathComparativeOperator.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathComparativeOperator.java deleted file mode 100644 index c7ffd0d7e..000000000 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathComparativeOperator.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2023 Tech Mahindra Ltd - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License 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. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.cpspath.parser; - -import java.util.HashMap; -import java.util.Map; - -public enum CpsPathComparativeOperator { - EQ("="), - GT(">"), - LT("<"), - GE(">="), - LE("<="); - - private final String label; - - CpsPathComparativeOperator(final String label) { - this.label = label; - } - - public final String getLabel() { - return this.label; - } - - private static final Map cpsPathComparativeOperatorPerLabel = new HashMap<>(); - - static { - for (final CpsPathComparativeOperator cpsPathComparativeOperator : CpsPathComparativeOperator.values()) { - cpsPathComparativeOperatorPerLabel.put(cpsPathComparativeOperator.label, cpsPathComparativeOperator); - } - } - - /** - * Finds the value of the given enumeration. - * - * @param label value of the enum - * @return a comparativeOperatorType - */ - public static CpsPathComparativeOperator fromString(final String label) { - if (!cpsPathComparativeOperatorPerLabel.containsKey(label)) { - throw new PathParsingException("Incomplete leaf condition (no operator)"); - } - return cpsPathComparativeOperatorPerLabel.get(label); - } -} - diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java index 3c3cbccf7..f98df05a2 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java @@ -24,8 +24,9 @@ package org.onap.cps.cpspath.parser; import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE; import java.util.List; -import java.util.Map; import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -39,7 +40,7 @@ public class CpsPathQuery { private List containerNames; private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE; private String descendantName; - private Map leavesData; + private List leavesData; private String ancestorSchemaNodeIdentifier = ""; private String textFunctionConditionLeafName; private String textFunctionConditionValue; @@ -103,4 +104,11 @@ public class CpsPathQuery { return cpsPathPrefixType == ABSOLUTE && hasLeafConditions(); } + @Getter + @EqualsAndHashCode + @AllArgsConstructor + public static class DataLeaf { + private final String name; + private final Object value; + } } diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy index 9ab5491b5..78963033d 100644 --- a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy +++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2022 Nordix Foundation + * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,12 +32,12 @@ class CpsPathQuerySpec extends Specification { when: 'the given cps path is parsed' def result = CpsPathQuery.createFrom(cpsPath) then: 'the query has the right xpath type' - result.cpsPathPrefixType == ABSOLUTE + assert result.cpsPathPrefixType == ABSOLUTE and: 'the right query parameters are set' - result.xpathPrefix == expectedXpathPrefix - result.hasLeafConditions() - result.leavesData.containsKey(expectedLeafName) - result.leavesData.get(expectedLeafName) == expectedLeafValue + assert result.xpathPrefix == expectedXpathPrefix + assert result.hasLeafConditions() + assert result.leavesData[0].name == expectedLeafName + assert result.leavesData[0].value == expectedLeafValue where: 'the following data is used' scenario | cpsPath || expectedXpathPrefix | expectedLeafName | expectedLeafValue 'leaf of type String' | '/parent/child[@common-leaf-name="common-leaf-value"]' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value' @@ -103,31 +103,24 @@ class CpsPathQuerySpec extends Specification { 'descendant anywhere' | '//xpath' || '//xpath' } - def 'Parse cps path that ends with a yang list containing #scenario.'() { + def 'Parse cps path that ends with a yang list containing multiple leaf conditions.'() { when: 'the given cps path is parsed' def result = CpsPathQuery.createFrom(cpsPath) - then: 'the query has the right xpath type' - result.cpsPathPrefixType == DESCENDANT - and: 'the right parameters are set' - result.descendantName == "child" + then: 'the expected number of leaves are returned' result.leavesData.size() == expectedNumberOfLeaves and: 'the given operator(s) returns in the correct order' result.booleanOperators == expectedOperators and: 'the given comparativeOperator(s) returns in the correct order' result.comparativeOperators == expectedComparativeOperator where: 'the following data is used' - scenario | cpsPath || expectedNumberOfLeaves || expectedOperators || expectedComparativeOperator - 'one attribute' | '//child[@common-leaf-name-int=5]' || 1 || [] || ['='] - 'more than one attribute has AND operator' | '//child[@int-leaf=5 and @leaf-name="leaf value"]' || 2 || ['and'] || ['=', '='] - 'more than one attribute has OR operator' | '//child[@int-leaf=5 or @leaf-name="leaf value"]' || 2 || ['or'] || ['=', '='] - 'more than one attribute has combinations AND operators' | '//child[@int-leaf=5 and @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['and', 'and'] || ['=', '=', '='] - 'more than one attribute has combinations OR operators' | '//child[@int-leaf=5 or @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['or', 'or'] || ['=', '=', '='] - 'more than one attribute has combinations AND/OR combination' | '//child[@int-leaf=5 and @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['and', 'or'] || ['=', '=', '='] - 'more than one attribute has combinations OR/AND combination' | '//child[@int-leaf=5 or @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['or', 'and'] || ['=', '=', '='] - 'more than one attribute has AND/> operators' | '//child[@int-leaf>15 and @leaf-name="leaf value"]' || 2 || ['and'] || ['>', '='] - 'more than one attribute has OR/< operators' | '//child[@int-leaf<5 or @leaf-name="leaf value"]' || 2 || ['or'] || ['<', '='] - 'more than one attribute has combinations AND/>= operators' | '//child[@int-leaf>=18 and @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['and', 'and'] || ['>=', '=', '='] - 'more than one attribute has combinations OR/<= operators' | '//child[@int-leaf<=25 or @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['or', 'or'] || ['<=', '=', '='] + cpsPath || expectedNumberOfLeaves || expectedOperators || expectedComparativeOperator + '/parent[@code=1]/child[@common-leaf-name-int=5]' || 1 || [] || ['='] + '//child[@int-leaf>15 and @leaf-name="leaf value"]' || 2 || ['and'] || ['>', '='] + '//child[@int-leaf<5 or @leaf-name="leaf value"]' || 2 || ['or'] || ['<', '='] + '//child[@int-leaf=5 and @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['and', 'or'] || ['=', '=', '='] + '//child[@int-leaf=5 or @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['or', 'and'] || ['=', '=', '='] + '//child[@int-leaf>=18 and @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['and', 'and'] || ['>=', '=', '='] + '//child[@int-leaf<=25 or @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['or', 'or'] || ['<=', '=', '='] } def 'Parse #scenario cps path with text function condition'() { @@ -220,4 +213,28 @@ class CpsPathQuerySpec extends Specification { 'container with list-parent' | '//parent[@id=1]/child' || "parent[@id='1']/child" | false 'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || "parent[@id='1']/child" | true } + + def 'Parse cps path with multiple conditions on same leaf.'() { + when: 'the given cps path is parsed using multiple conditions on same leaf' + def result = CpsPathQuery.createFrom('/test[@same-name="value1" or @same-name="value2"]') + then: 'two leaves are present with correct values' + assert result.leavesData.size() == 2 + assert result.leavesData[0].name == "same-name" + assert result.leavesData[0].value == "value1" + assert result.leavesData[1].name == "same-name" + assert result.leavesData[1].value == "value2" + } + + def 'Ordering of data leaves is preserved.'() { + when: 'the given cps path is parsed' + def result = CpsPathQuery.createFrom(cpsPath) + then: 'the order of the data leaves is preserved' + assert result.leavesData[0].name == expectedFirstLeafName + assert result.leavesData[1].name == expectedSecondLeafName + where: 'the following data is used' + cpsPath || expectedFirstLeafName | expectedSecondLeafName + '/test[@name1="value1" and @name2="value2"]' || 'name1' | 'name2' + '/test[@name2="value2" and @name1="value1"]' || 'name2' | 'name1' + } + } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java index 7b5c0c693..be06ebac0 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java @@ -37,7 +37,6 @@ import org.onap.cps.spi.entities.DataspaceEntity; import org.onap.cps.spi.entities.FragmentEntity; import org.onap.cps.spi.exceptions.CpsPathException; import org.onap.cps.spi.utils.EscapeUtils; -import org.onap.cps.utils.JsonObjectMapper; import org.springframework.stereotype.Component; @RequiredArgsConstructor @@ -49,8 +48,6 @@ public class FragmentQueryBuilder { @PersistenceContext private EntityManager entityManager; - private final JsonObjectMapper jsonObjectMapper; - /** * Create a sql query to retrieve by anchor(id) and cps path. * @@ -128,18 +125,18 @@ public class FragmentQueryBuilder { sqlStringBuilder.append(" AND ("); final Queue booleanOperatorsQueue = new LinkedList<>(cpsPathQuery.getBooleanOperators()); final Queue comparativeOperatorQueue = new LinkedList<>(cpsPathQuery.getComparativeOperators()); - cpsPathQuery.getLeavesData().entrySet().forEach(entry -> { + cpsPathQuery.getLeavesData().forEach(leaf -> { final String nextComparativeOperator = comparativeOperatorQueue.poll(); - if (entry.getValue() instanceof Integer) { - sqlStringBuilder.append("(attributes ->> "); - sqlStringBuilder.append("'").append(entry.getKey()).append("')\\:\\:int"); - sqlStringBuilder.append(" ").append(nextComparativeOperator).append(" "); - sqlStringBuilder.append("'").append(jsonObjectMapper.asJsonString(entry.getValue())).append("'"); + if (leaf.getValue() instanceof Integer) { + sqlStringBuilder.append("(attributes ->> '").append(leaf.getName()).append("')\\:\\:int"); + sqlStringBuilder.append(nextComparativeOperator); + sqlStringBuilder.append(leaf.getValue()); } else { if ("=".equals(nextComparativeOperator)) { - sqlStringBuilder.append(" attributes @> "); - sqlStringBuilder.append("'"); - sqlStringBuilder.append(jsonObjectMapper.asJsonString(entry)); + final String leafValueAsText = leaf.getValue().toString(); + sqlStringBuilder.append("attributes ->> '").append(leaf.getName()).append("'"); + sqlStringBuilder.append(" = '"); + sqlStringBuilder.append(leafValueAsText); sqlStringBuilder.append("'"); } else { throw new CpsPathException(" can use only " + nextComparativeOperator + " with integer "); diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy index 351f3106f..750deb1b3 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy @@ -64,9 +64,9 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase { where: 'the following option is used' fetchDescendantsOption || expectNumberOfDataNodes OMIT_DESCENDANTS || 1 - DIRECT_CHILDREN_ONLY || 6 - INCLUDE_ALL_DESCENDANTS || 17 - new FetchDescendantsOption(2) || 17 + DIRECT_CHILDREN_ONLY || 7 + INCLUDE_ALL_DESCENDANTS || 28 + new FetchDescendantsOption(2) || 28 } def 'Read bookstore top-level container(s) using "root" path variations.'() { diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy index a736ab0c0..53737fba8 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy @@ -54,52 +54,30 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { 'the AND is used where result does not exist' | '//books[@lang="English" and @price=1000]' || 0 | [] } - def 'Cps Path query using combinations of OR operator #scenario.'() { + def 'Cps Path query using comparative and boolean operators.'() { + given: 'a cps path query in the discount category' + def cpsPath = "/bookstore/categories[@code='5']/books" + leafCondition when: 'a query is executed to get response by the given cps path' - def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, OMIT_DESCENDANTS) - then: 'the result contains expected number of nodes' - assert result.size() == expectedResultSize - and: 'the cps-path of queryDataNodes has the expectedLeaves' - assert result.leaves.sort() == expectedLeaves.sort() - where: 'the following data is used' - scenario | cpsPath || expectedResultSize | expectedLeaves - 'the "OR" condition' | '//books[@lang="English" or @price=15]' || 6 | [[lang: "English", price: 15, title: "Annihilation", authors: ["Jeff VanderMeer"], editions: [2014]], - [lang: "English", price: 15, title: "The Gruffalo", authors: ["Julia Donaldson"], editions: [1999]], - [lang: "English", price: 14, title: "The Light Fantastic", authors: ["Terry Pratchett"], editions: [1986]], - [lang: "English", price: 13, title: "Good Omens", authors: ["Terry Pratchett", "Neil Gaiman"], editions: [2006]], - [lang: "English", price: 12, title: "The Colour of Magic", authors: ["Terry Pratchett"], editions: [1983]], - [lang: "English", price: 10, title: "Matilda", authors: ["Roald Dahl"], editions: [1988, 2000]]] - 'the "OR" condition with non-json data' | '//books[@title="xyz" or @price=15]' || 2 | [[lang: "English", price: 15, title: "Annihilation", authors: ["Jeff VanderMeer"], editions: [2014]], - [lang: "English", price: 15, title: "The Gruffalo", authors: ["Julia Donaldson"], editions: [1999]]] - 'combination of multiple AND' | '//books[@lang="English" and @price=15 and @edition=1983]' || 0 | [] - 'combination of multiple OR' | '//books[ @title="Matilda" or @price=15 or @edition=1983]' || 3 | [[lang: "English", price: 15, title: "Annihilation", authors: ["Jeff VanderMeer"], editions: [2014]], - [lang: "English", price: 10, title: "Matilda", authors: ["Roald Dahl"], editions: [1988, 2000]], - [lang: "English", price: 15, title: "The Gruffalo", authors: ["Julia Donaldson"], editions: [1999]]] - 'combination of AND/OR' | '//books[@edition=1983 and @price=15 or @title="Good Omens"]' || 1 | [[lang: "English", price: 13, title: "Good Omens", authors: ["Terry Pratchett", "Neil Gaiman"], editions: [2006]]] - 'combination of OR/AND' | '//books[@title="Annihilation" or @price=39 and @lang="arabic"]' || 1 | [[lang: "English", price: 15, title: "Annihilation", authors: ["Jeff VanderMeer"], editions: [2014]]] - } - - def 'cps-path query using combinations of Comparative Operators #scenario.'() { - when: 'a query is executed to get response by the given cpsPath' - def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, OMIT_DESCENDANTS) - then: 'the result contains expected number of nodes' - assert result.size() == expectedResultSize - and: 'xpaths of the retrieved data nodes are as expected' - def bookTitles = result.collect { it.getLeaves().get('title') } - assert bookTitles.sort() == expectedBookTitles.sort() + def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, + cpsPath, OMIT_DESCENDANTS) + then: 'the cps-path of queryDataNodes has the expectedLeaves' + def bookPrices = result.collect { it.getLeaves().get('price') } + assert bookPrices.sort() == expectedBookPrices.sort() where: 'the following data is used' - scenario | cpsPath || expectedResultSize | expectedBookTitles - 'the ">" condition' | '//books[@price>13 ]' || 5 | ['A Book with No Language', 'Annihilation', 'Debian GNU/Linux', 'The Gruffalo', 'The Light Fantastic'] - 'the "<" condition ' | '//books[@price<15]' || 5 | ['Good Omens', 'Logarithm tables', 'Matilda', 'The Colour of Magic', 'The Light Fantastic'] - 'the "<=" condition' | '//books[@price<=15]' || 7 | ['Annihilation', 'Good Omens', 'Logarithm tables', 'Matilda', 'The Colour of Magic', 'The Gruffalo', 'The Light Fantastic'] - 'the ">=" condition' | '//books[@price>=20]' || 2 | ['A Book with No Language', 'Debian GNU/Linux'] - 'the "<" condition where result does not exist' | '//books[@price<5]' || 0 | [] - 'the ">" condition where result does not exist' | '//books[@price>1000]' || 0 | [] - 'the ">" condition with AND condition' | '//books[@price>13 and @title="A Book with No Language"]' || 1 | ['A Book with No Language'] - 'the "<" condition with OR condition' | '//books[@price<10 or @lang="German"]' || 1 | ['Debian GNU/Linux'] - 'the "<=" condition with AND/OR condition' | '//books[@price<=15 and @title="Annihilation" or @lang="Spanish"]' || 1 | ['Annihilation'] - 'the ">=" condition with OR/AND condition' | '//books[@price>=13 or @lang="Spanish" and @title="Good Omens"]' || 6 | ['A Book with No Language', 'Annihilation', 'Good Omens', 'Debian GNU/Linux', 'The Gruffalo', 'The Light Fantastic'] - 'Mix of integer and string condition ' | '//books[@lang="German" and @price>38]' || 1 | ['Debian GNU/Linux'] + leafCondition || expectedBookPrices + '[@price = 5]' || [5] + '[@price < 5]' || [1, 2, 3, 4] + '[@price > 5]' || [6, 7, 8, 9, 10] + '[@price <= 5]' || [1, 2, 3, 4, 5] + '[@price >= 5]' || [5, 6, 7, 8, 9, 10] + '[@price > 10]' || [] + '[@price = 3 or @price = 7]' || [3, 7] + '[@price = 3 and @price = 7]' || [] + '[@price > 3 and @price <= 6]' || [4, 5, 6] + '[@price < 3 or @price > 8]' || [1, 2, 9, 10] + '[@price = 1 or @price = 3 or @price = 5]' || [1, 3, 5] + '[@price = 1 or @price >= 8 and @price < 10]' || [1, 8, 9] + '[@price >= 3 and @price <= 5 or @price > 9]' || [3, 4, 5, 10] } def 'Cps Path query for leaf value(s) with #scenario.'() { @@ -113,9 +91,9 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { scenario | cpsPath | fetchDescendantsOption || expectedNumberOfParentNodes | expectedTotalNumberOfNodes 'string and no descendants' | '/bookstore/categories[@code="1"]/books[@title="Matilda"]' | OMIT_DESCENDANTS || 1 | 1 'integer and descendants' | '/bookstore/categories[@code="1"]/books[@price=15]' | INCLUDE_ALL_DESCENDANTS || 1 | 1 - 'no condition and no descendants' | '/bookstore/categories' | OMIT_DESCENDANTS || 4 | 4 - 'no condition and level 1 descendants' | '/bookstore' | new FetchDescendantsOption(1) || 1 | 6 - 'no condition and level 2 descendants' | '/bookstore' | new FetchDescendantsOption(2) || 1 | 17 + 'no condition and no descendants' | '/bookstore/categories' | OMIT_DESCENDANTS || 5 | 5 + 'no condition and level 1 descendants' | '/bookstore' | new FetchDescendantsOption(1) || 1 | 7 + 'no condition and level 2 descendants' | '/bookstore' | new FetchDescendantsOption(2) || 1 | 28 } def 'Query for attribute by cps path with cps paths that return no data because of #scenario.'() { @@ -146,7 +124,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { when: 'a query is executed to get all books' def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '//books', OMIT_DESCENDANTS) then: 'the expected number of books are returned' - assert result.size() == 9 + assert result.size() == 19 } def 'Cps Path query using descendant anywhere with #scenario.'() { @@ -160,7 +138,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { 'string leaf condition' | '//books[@title="Matilda"]' || ["Matilda"] 'text condition on leaf' | '//books/title[text()="Matilda"]' || ["Matilda"] 'text condition case mismatch' | '//books/title[text()="matilda"]' || [] - 'text condition on int leaf' | '//books/price[text()="10"]' || ["Matilda"] + 'text condition on int leaf' | '//books/price[text()="20"]' || ["A Book with No Language", "Matilda"] 'text condition on leaf-list' | '//books/authors[text()="Terry Pratchett"]' || ["Good Omens", "The Colour of Magic", "The Light Fantastic"] 'text condition partial match' | '//books/authors[text()="Terry"]' || [] 'text condition (existing) empty string' | '//books/lang[text()=""]' || ["A Book with No Language"] @@ -182,7 +160,13 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { 'contains condition with leaf' | '//books[contains(@title,"Mat")]' || ["Matilda"] 'contains condition with case-sensitive' | '//books[contains(@title,"Ti")]' || [] 'contains condition with Integer Value' | '//books[contains(@price,"15")]' || ["Annihilation", "The Gruffalo"] - 'contains condition with No-value' | '//books[contains(@title,"")]' || ["A Book with No Language", "Annihilation", "Debian GNU/Linux", "Good Omens", "Logarithm tables", "Matilda", "The Colour of Magic", "The Gruffalo", "The Light Fantastic"] + } + + def 'Query for attribute by cps path using contains condition with no value.'() { + when: 'a query is executed to get response by the given cps path' + def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '//books[contains(@title,"")]', OMIT_DESCENDANTS) + then: 'all books are returned' + assert result.size() == 19 } def 'Cps Path query using descendant anywhere with #scenario condition for a container element.'() { @@ -194,7 +178,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { where: 'the following data is used' scenario | cpsPath || expectedBookTitles 'one leaf' | '//books[@price=14]' || ['The Light Fantastic'] - 'one leaf with ">" condition' | '//books[@price>14]' || ['A Book with No Language', 'Annihilation', 'Debian GNU/Linux', 'The Gruffalo'] + 'one leaf with ">" condition' | '//books[@price>14]' || ['A Book with No Language', 'Annihilation', 'Debian GNU/Linux', 'Matilda', 'The Gruffalo'] 'one text' | '//books/authors[text()="Terry Pratchett"]' || ['Good Omens', 'The Colour of Magic', 'The Light Fantastic'] 'more than one leaf' | '//books[@price=12 and @lang="English"]' || ['The Colour of Magic'] 'more than one leaf has "OR" condition' | '//books[@lang="English" or @price=15]' || ['Annihilation', 'Good Omens', 'Matilda', 'The Colour of Magic', 'The Gruffalo', 'The Light Fantastic'] @@ -228,11 +212,11 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { assert result.xpath.sort() == expectedXPaths.sort() where: 'the following data is used' scenario | cpsPath || expectedXPaths - 'multiple list-ancestors' | '//books/ancestor::categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"] + 'multiple list-ancestors' | '//books/ancestor::categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"] 'one ancestor with list value' | '//books/ancestor::categories[@code="1"]' || ["/bookstore/categories[@code='1']"] 'top ancestor' | '//books/ancestor::bookstore' || ["/bookstore"] 'list with index value in the xpath prefix' | '//categories[@code="1"]/books/ancestor::bookstore' || ["/bookstore"] - 'ancestor with parent list' | '//books/ancestor::bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"] + 'ancestor with parent list' | '//books/ancestor::bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"] 'ancestor with parent' | '//books/ancestor::bookstore/categories[@code="2"]' || ["/bookstore/categories[@code='2']"] 'ancestor combined with text condition' | '//books/title[text()="Matilda"]/ancestor::bookstore' || ["/bookstore"] 'ancestor with parent that does not exist' | '//books/ancestor::parentDoesNoExist/categories' || [] @@ -248,8 +232,8 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { where: 'the following data is used' scenario | fetchDescendantsOption || expectedNumberOfNodes 'no' | OMIT_DESCENDANTS || 1 - 'direct' | DIRECT_CHILDREN_ONLY || 6 - 'all' | INCLUDE_ALL_DESCENDANTS || 17 + 'direct' | DIRECT_CHILDREN_ONLY || 7 + 'all' | INCLUDE_ALL_DESCENDANTS || 28 } def 'Cps Path query with #scenario throws a CPS Path Exception.'() { @@ -277,13 +261,13 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { where: 'the following data is used' scenario | cpsPath || expectedXpathsPerAnchor 'container node' | '/bookstore' || ["/bookstore"] - 'list node' | '/bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"] + 'list node' | '/bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"] 'string leaf-condition' | '/bookstore[@bookstore-name="Easons"]' || ["/bookstore"] 'integer leaf-condition' | '/bookstore/categories[@code="1"]/books[@price=15]' || ["/bookstore/categories[@code='1']/books[@title='The Gruffalo']"] - 'multiple list-ancestors' | '//books/ancestor::categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"] + 'multiple list-ancestors' | '//books/ancestor::categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"] 'one ancestor with list value' | '//books/ancestor::categories[@code="1"]' || ["/bookstore/categories[@code='1']"] 'list with index value in the xpath prefix' | '//categories[@code="1"]/books/ancestor::bookstore' || ["/bookstore"] - 'ancestor with parent list' | '//books/ancestor::bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"] + 'ancestor with parent list' | '//books/ancestor::bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"] 'ancestor with parent list element' | '//books/ancestor::bookstore/categories[@code="2"]' || ["/bookstore/categories[@code='2']"] 'ancestor combined with text condition' | '//books/title[text()="Matilda"]/ancestor::bookstore' || ["/bookstore"] } @@ -298,8 +282,8 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { where: 'the following data is used' scenario | fetchDescendantsOption || expectedNumberOfNodesPerAnchor 'no' | OMIT_DESCENDANTS || 1 - 'direct' | DIRECT_CHILDREN_ONLY || 6 - 'all' | INCLUDE_ALL_DESCENDANTS || 17 + 'direct' | DIRECT_CHILDREN_ONLY || 7 + 'all' | INCLUDE_ALL_DESCENDANTS || 28 } def 'Cps Path query across anchors with ancestors and #scenario descendants.'() { @@ -312,8 +296,8 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { where: 'the following data is used' scenario | fetchDescendantsOption || expectedNumberOfNodesPerAnchor 'no' | OMIT_DESCENDANTS || 1 - 'direct' | DIRECT_CHILDREN_ONLY || 6 - 'all' | INCLUDE_ALL_DESCENDANTS || 17 + 'direct' | DIRECT_CHILDREN_ONLY || 7 + 'all' | INCLUDE_ALL_DESCENDANTS || 28 } def 'Cps Path query across anchors with syntax error throws a CPS Path Exception.'() { @@ -330,10 +314,10 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { assert countDataNodesInTree(result) == expectedNumberOfDataNodes where: scenario | cpsPath || expectedNumberOfDataNodes - 'absolute path all list entries' | '/bookstore/categories' || 13 + 'absolute path all list entries' | '/bookstore/categories' || 24 'absolute path 1 list entry by key' | '/bookstore/categories[@code="3"]' || 5 'absolute path 1 list entry by name' | '/bookstore/categories[@name="Comedy"]' || 5 - 'relative path all list entries' | '//categories' || 13 + 'relative path all list entries' | '//categories' || 24 'relative path 1 list entry by key' | '//categories[@code="3"]' || 5 'relative path 1 list entry by leaf' | '//categories[@name="Comedy"]' || 5 'incomplete absolute path' | '/categories' || 0 diff --git a/integration-test/src/test/resources/data/bookstore/bookstoreData.json b/integration-test/src/test/resources/data/bookstore/bookstoreData.json index 12df20e55..5d22f6d67 100644 --- a/integration-test/src/test/resources/data/bookstore/bookstoreData.json +++ b/integration-test/src/test/resources/data/bookstore/bookstoreData.json @@ -27,7 +27,7 @@ "lang": "English", "authors": ["Roald Dahl"], "editions": [1988, 2000], - "price": 10 + "price": 20 }, { "title": "The Gruffalo", @@ -104,6 +104,82 @@ "price": 11 } ] + }, + { + "code": 5, + "name": "Discount books", + "books" : [ + { + "title": "Book 1", + "lang": "blah", + "authors": [], + "editions": [], + "price": 1 + }, + { + "title": "Book 2", + "lang": "blah", + "authors": [], + "editions": [], + "price": 2 + }, + { + "title": "Book 3", + "lang": "blah", + "authors": [], + "editions": [], + "price": 3 + }, + { + "title": "Book 4", + "lang": "blah", + "authors": [], + "editions": [], + "price": 4 + }, + { + "title": "Book 5", + "lang": "blah", + "authors": [], + "editions": [], + "price": 5 + }, + { + "title": "Book 6", + "lang": "blah", + "authors": [], + "editions": [], + "price": 6 + }, + { + "title": "Book 7", + "lang": "blah", + "authors": [], + "editions": [], + "price": 7 + }, + { + "title": "Book 8", + "lang": "blah", + "authors": [], + "editions": [], + "price": 8 + }, + { + "title": "Book 9", + "lang": "blah", + "authors": [], + "editions": [], + "price": 9 + }, + { + "title": "Book 10", + "lang": "blah", + "authors": [], + "editions": [], + "price": 10 + } + ] } ] }