Merge "Refactor ncmp request handlers (fix async issue)"
authorSourabh Sourabh <sourabh.sourabh@est.tech>
Thu, 20 Jul 2023 15:29:14 +0000 (15:29 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 20 Jul 2023 15:29:14 +0000 (15:29 +0000)
52 files changed:
checkstyle/pom.xml
cps-application/pom.xml
cps-bom/pom.xml
cps-dependencies/pom.xml
cps-events/pom.xml
cps-ncmp-events/pom.xml
cps-ncmp-rest-stub/cps-ncmp-rest-stub-app/pom.xml
cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/pom.xml
cps-ncmp-rest-stub/pom.xml
cps-ncmp-rest/pom.xml
cps-ncmp-service/pom.xml
cps-parent/pom.xml
cps-path-parser/pom.xml
cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBooleanOperatorType.java [deleted file]
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathComparativeOperator.java [deleted file]
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy
cps-rest/pom.xml
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-ri/pom.xml
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java
cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java
cps-ri/src/main/java/org/onap/cps/spi/utils/EscapeUtils.java
cps-ri/src/main/resources/changelog/changelog-master.yaml
cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-forward.sql [new file with mode: 0644]
cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-rollback.sql [new file with mode: 0644]
cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath.yaml [new file with mode: 0644]
cps-ri/src/test/groovy/org/onap/cps/spi/utils/EscapeUtilsSpec.groovy
cps-service/pom.xml
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/utils/YangUtils.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/resources/bookstore.json
cps-service/src/test/resources/bookstore.yang
dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-app/pom.xml
dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/pom.xml
dmi-plugin-demo-and-csit-stub/pom.xml
docs/cps-path.rst
docs/release-notes.rst
integration-test/pom.xml
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy
integration-test/src/test/resources/data/bookstore/bookstore.yang
integration-test/src/test/resources/data/bookstore/bookstoreData.json
jacoco-report/pom.xml
pom.xml
releases/3.3.4-container.yaml [new file with mode: 0644]
releases/3.3.4.yaml [new file with mode: 0644]
spotbugs/pom.xml
version.properties

index a6fa570..34775c0 100644 (file)
@@ -26,7 +26,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>checkstyle</artifactId>
-    <version>3.3.4-SNAPSHOT</version>
+    <version>3.3.5-SNAPSHOT</version>
 
     <profiles>
         <profile>
index 4b28469..699ea45 100755 (executable)
@@ -28,7 +28,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 7374b4c..fcf98ee 100644 (file)
@@ -25,7 +25,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-bom</artifactId>
-    <version>3.3.4-SNAPSHOT</version>
+    <version>3.3.5-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <description>This artifact contains dependencyManagement declarations of all published CPS components.</description>
index 48e7044..05b7747 100755 (executable)
@@ -27,7 +27,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-dependencies</artifactId>
-    <version>3.3.4-SNAPSHOT</version>
+    <version>3.3.5-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <name>${project.groupId}:${project.artifactId}</name>
index 6eb8b50..3ff539c 100644 (file)
@@ -24,7 +24,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index cd08e56..e11ab11 100644 (file)
@@ -23,7 +23,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 9254086..3d5b595 100644 (file)
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-ncmp-rest-stub</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
     </parent>
 
     <artifactId>cps-ncmp-rest-stub-app</artifactId>
index 9f3e904..c05f610 100644 (file)
@@ -21,7 +21,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-ncmp-rest-stub</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
     </parent>
     <artifactId>cps-ncmp-rest-stub-service</artifactId>
 
index 6d1cd5a..84e7dac 100644 (file)
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 7444619..c4c6e19 100644 (file)
@@ -27,7 +27,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 1573034..0698932 100644 (file)
@@ -27,7 +27,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 913120d..49395e2 100755 (executable)
@@ -32,7 +32,7 @@
 
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-parent</artifactId>
-    <version>3.3.4-SNAPSHOT</version>
+    <version>3.3.5-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <properties>
index 15a2719..c1330ab 100644 (file)
@@ -23,7 +23,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index c88a822..3aef120 100644 (file)
@@ -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");
  *  ============LICENSE_END=========================================================
  */
 
+/*
+ * Parser Rules
+ * Some of the parser rules below are inspired by
+ * https://github.com/antlr/grammars-v4/blob/master/xpath/xpath31/XPath31Parser.g4
+ */
+
 grammar CpsPath ;
 
 cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? containsFunctionCondition? ancestorAxis? invalidPostFix?;
@@ -60,7 +66,7 @@ invalidPostFix : (AT | CB | COLONCOLON | comparativeOperators ).+ ;
 /*
  * Lexer Rules
  * Most of the lexer rules below are inspired by
- * https://raw.githubusercontent.com/antlr/grammars-v4/master/xpath/xpath31/XPath31.g4
+ * https://github.com/antlr/grammars-v4/blob/master/xpath/xpath31/XPath31Lexer.g4
  */
 
 AT : '@' ;
@@ -89,9 +95,9 @@ IntegerLiteral : FragDigits ;
 // Add below type definitions for leafvalue comparision in https://jira.onap.org/browse/CPS-440
 DecimalLiteral : ('.' FragDigits) | (FragDigits '.' [0-9]*) ;
 DoubleLiteral : (('.' FragDigits) | (FragDigits ('.' [0-9]*)?)) [eE] [+-]? FragDigits ;
-StringLiteral : ('"' (FragEscapeQuot | ~[^"])*? '"') | ('\'' (FragEscapeApos | ~['])*? '\'') ;
+StringLiteral : '"' (~["] | FragEscapeQuot)* '"' | '\'' (~['] | FragEscapeApos)* '\'' ;
 fragment FragEscapeQuot : '""' ;
-fragment FragEscapeApos : '\';
+fragment FragEscapeApos : '\'\'';
 fragment FragDigits : [0-9]+ ;
 
 QName  : FragQName ;
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 (file)
index b2f1ddd..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*\r
- *  ============LICENSE_START=======================================================\r
- *  Copyright (C) 2023 TechMahindra Ltd\r
- *  ================================================================================\r
- *  Licensed under the Apache License, Version 2.0 (the "License");\r
- *  you may not use this file except in compliance with the License.\r
- *  You may obtain a copy of the License at\r
- *\r
- *        http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- *  Unless required by applicable law or agreed to in writing, software\r
- *  distributed under the License is distributed on an "AS IS" BASIS,\r
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- *  See the License for the specific language governing permissions and\r
- *  limitations under the License.\r
- *\r
- *  SPDX-License-Identifier: Apache-2.0\r
- *  ============LICENSE_END=========================================================\r
- */\r
-\r
-package org.onap.cps.cpspath.parser;\r
-\r
-public enum CpsPathBooleanOperatorType {\r
-    AND("and"),\r
-    OR("or");\r
-\r
-    private final String operatorValue;\r
-\r
-    CpsPathBooleanOperatorType(final String operatorValue) {\r
-        this.operatorValue = operatorValue;\r
-    }\r
-\r
-    public String getValues() {\r
-        return this.operatorValue;\r
-    }\r
-\r
-    /**\r
-     * Finds the value of the given enumeration.\r
-     *\r
-     * @param operatorValue value of the enum\r
-     * @return a booleanOperatorType\r
-     */\r
-    public static CpsPathBooleanOperatorType fromString(final String operatorValue) {\r
-        for (final CpsPathBooleanOperatorType booleanOperatorType : CpsPathBooleanOperatorType.values()) {\r
-            if (booleanOperatorType.operatorValue.equalsIgnoreCase(operatorValue)) {\r
-                return booleanOperatorType;\r
-            }\r
-        }\r
-        return null;\r
-    }\r
-}\r
index 5c47127..de261e6 100644 (file)
@@ -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<String, Object> leavesData = new LinkedHashMap<>();
+    private final List<CpsPathQuery.DataLeaf> 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<String> containerNames = new ArrayList<>();
+    private final List<String> containerNames = new ArrayList<>();
 
-    final List<String> booleanOperators = new ArrayList<>();
+    private final List<String> booleanOperators = new ArrayList<>();
 
-    final List<String> comparativeOperators = new ArrayList<>();
+    private final List<String> comparativeOperators = new ArrayList<>();
 
     @Override
     public void exitInvalidPostFix(final CpsPathParser.InvalidPostFixContext ctx) {
@@ -81,34 +79,25 @@ public class CpsPathBuilder extends CpsPathBaseListener {
 
     @Override
     public void exitLeafCondition(final LeafConditionContext ctx) {
-        Object comparisonValue;
+        final Object comparisonValue;
         if (ctx.IntegerLiteral() != null) {
             comparisonValue = Integer.valueOf(ctx.IntegerLiteral().getText());
         } else if (ctx.StringLiteral() != null) {
-            final boolean wasWrappedInDoubleQuote = ctx.StringLiteral().getText().startsWith("\"");
-            comparisonValue = stripFirstAndLastCharacter(ctx.StringLiteral().getText());
-            if (wasWrappedInDoubleQuote) {
-                comparisonValue = String.valueOf(comparisonValue).replace("'", "\\'");
-            }
+            comparisonValue = unwrapQuotedString(ctx.StringLiteral().getText());
         } else {
-            throw new PathParsingException(
-                    "Unsupported comparison value encountered in expression" + ctx.getText());
+            throw new PathParsingException("Unsupported comparison value encountered in expression" + ctx.getText());
         }
         leafContext(ctx.leafName(), comparisonValue);
     }
 
     @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 +111,8 @@ public class CpsPathBuilder extends CpsPathBaseListener {
     public void enterMultipleLeafConditions(final MultipleLeafConditionsContext ctx)  {
         normalizedXpathBuilder.append(OPEN_BRACKET);
         leavesData.clear();
+        booleanOperators.clear();
+        comparativeOperators.clear();
     }
 
     @Override
@@ -144,13 +135,13 @@ public class CpsPathBuilder extends CpsPathBaseListener {
     @Override
     public void exitTextFunctionCondition(final TextFunctionConditionContext ctx) {
         cpsPathQuery.setTextFunctionConditionLeafName(ctx.leafName().getText());
-        cpsPathQuery.setTextFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText()));
+        cpsPathQuery.setTextFunctionConditionValue(unwrapQuotedString(ctx.StringLiteral().getText()));
     }
 
     @Override
     public void exitContainsFunctionCondition(final CpsPathParser.ContainsFunctionConditionContext ctx) {
         cpsPathQuery.setContainsFunctionConditionLeafName(ctx.leafName().getText());
-        cpsPathQuery.setContainsFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText()));
+        cpsPathQuery.setContainsFunctionConditionValue(unwrapQuotedString(ctx.StringLiteral().getText()));
     }
 
     @Override
@@ -177,10 +168,6 @@ public class CpsPathBuilder extends CpsPathBaseListener {
         return cpsPathQuery;
     }
 
-    private static String stripFirstAndLastCharacter(final String wrappedString) {
-        return wrappedString.substring(1, wrappedString.length() - 1);
-    }
-
     @Override
     public void exitContainerName(final CpsPathParser.ContainerNameContext ctx) {
         final String containerName = ctx.getText();
@@ -193,7 +180,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);
@@ -211,11 +198,25 @@ public class CpsPathBuilder extends CpsPathBaseListener {
                                     .append(name)
                                     .append(getLastElement(comparativeOperators))
                                     .append("'")
-                                    .append(value)
+                                    .append(value.toString().replace("'", "''"))
                                     .append("'");
     }
 
-    private String getLastElement(final List<String> listOfStrings) {
+    private static String getLastElement(final List<String> listOfStrings) {
         return listOfStrings.get(listOfStrings.size() - 1);
     }
+
+    private static String unwrapQuotedString(final String wrappedString) {
+        final boolean wasWrappedInSingleQuote = wrappedString.startsWith("'");
+        final String value = stripFirstAndLastCharacter(wrappedString);
+        if (wasWrappedInSingleQuote) {
+            return value.replace("''", "'");
+        } else {
+            return value.replace("\"\"", "\"");
+        }
+    }
+
+    private static String stripFirstAndLastCharacter(final String wrappedString) {
+        return wrappedString.substring(1, wrappedString.length() - 1);
+    }
 }
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 (file)
index c7ffd0d..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*\r
- *  ============LICENSE_START=======================================================\r
- *  Copyright (C) 2023 Tech Mahindra Ltd\r
- *  ================================================================================\r
- *  Licensed under the Apache License, Version 2.0 (the "License");\r
- *  you may not use this file except in compliance with the License.\r
- *  You may obtain a copy of the License at\r
- *\r
- *        http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- *  Unless required by applicable law or agreed to in writing, software\r
- *  distributed under the License is distributed on an "AS IS" BASIS,\r
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- *  See the License for the specific language governing permissions and\r
- *  limitations under the License.\r
- *\r
- *  SPDX-License-Identifier: Apache-2.0\r
- *  ============LICENSE_END=========================================================\r
- */\r
-\r
-package org.onap.cps.cpspath.parser;\r
-\r
-import java.util.HashMap;\r
-import java.util.Map;\r
-\r
-public enum CpsPathComparativeOperator {\r
-    EQ("="),\r
-    GT(">"),\r
-    LT("<"),\r
-    GE(">="),\r
-    LE("<=");\r
-\r
-    private final String label;\r
-\r
-    CpsPathComparativeOperator(final String label) {\r
-        this.label = label;\r
-    }\r
-\r
-    public final String getLabel() {\r
-        return this.label;\r
-    }\r
-\r
-    private static final Map<String, CpsPathComparativeOperator> cpsPathComparativeOperatorPerLabel = new HashMap<>();\r
-\r
-    static {\r
-        for (final CpsPathComparativeOperator cpsPathComparativeOperator : CpsPathComparativeOperator.values()) {\r
-            cpsPathComparativeOperatorPerLabel.put(cpsPathComparativeOperator.label, cpsPathComparativeOperator);\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Finds the value of the given enumeration.\r
-     *\r
-     * @param label value of the enum\r
-     * @return a comparativeOperatorType\r
-     */\r
-    public static CpsPathComparativeOperator fromString(final String label) {\r
-        if (!cpsPathComparativeOperatorPerLabel.containsKey(label)) {\r
-            throw new PathParsingException("Incomplete leaf condition (no operator)");\r
-        }\r
-        return cpsPathComparativeOperatorPerLabel.get(label);\r
-    }\r
-}\r
-\r
index 3c3cbcc..f98df05 100644 (file)
@@ -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<String> containerNames;
     private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE;
     private String descendantName;
-    private Map<String, Object> leavesData;
+    private List<DataLeaf> 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;
+    }
 }
index 9ab5491..0017242 100644 (file)
@@ -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'
@@ -46,6 +46,10 @@ class CpsPathQuerySpec extends Specification {
             'spaces around ='      | '/parent/child[@common-leaf-name-int = 5]'                 || '/parent/child'                                 | 'common-leaf-name-int' | 5
             'key in top container' | '/parent[@common-leaf-name-int=5]'                         || '/parent'                                       | 'common-leaf-name-int' | 5
             'parent list'          | '/shops/shop[@id=1]/categories[@id=1]/book[@title="Dune"]' || "/shops/shop[@id='1']/categories[@id='1']/book" | 'title'                | 'Dune'
+            "' in double quote"    | '/parent[@common-leaf-name="leaf\'value"]'                 || '/parent'                                       | 'common-leaf-name'     | "leaf'value"
+            "' in single quote"    | "/parent[@common-leaf-name='leaf''value']"                 || '/parent'                                       | 'common-leaf-name'     | "leaf'value"
+            '" in double quote'    | '/parent[@common-leaf-name="leaf""value"]'                 || '/parent'                                       | 'common-leaf-name'     | 'leaf"value'
+            '" in single quote'    | '/parent[@common-leaf-name=\'leaf"value\']'                || '/parent'                                       | 'common-leaf-name'     | 'leaf"value'
     }
 
     def 'Parse cps path of type ends with a #scenario.'() {
@@ -80,8 +84,8 @@ class CpsPathQuerySpec extends Specification {
             'parent leaf of type Integer & child'                         | '/parent/child[@code=1]/child2'                                || "/parent/child[@code='1']/child2"
             'parent leaf with double quotes'                              | '/parent/child[@code="1"]/child2'                              || "/parent/child[@code='1']/child2"
             'parent leaf with double quotes inside single quotes'         | '/parent/child[@code=\'"1"\']/child2'                          || "/parent/child[@code='\"1\"']/child2"
-            'parent leaf with single quotes inside double quotes'         | '/parent/child[@code="\'1\'"]/child2'                          || "/parent/child[@code='\\\'1\\\'']/child2"
-            'leaf with single quotes inside double quotes'                | '/parent/child[@code="\'1\'"]'                                 || "/parent/child[@code='\\\'1\\\'']"
+            'parent leaf with single quotes inside double quotes'         | '/parent/child[@code="\'1\'"]/child2'                          || "/parent/child[@code='''1''']/child2"
+            'leaf with single quotes inside double quotes'                | '/parent/child[@code="\'1\'"]'                                 || "/parent/child[@code='''1''']"
             'leaf with more than one attribute'                           | '/parent/child[@key1=1 and @key2="abc"]'                       || "/parent/child[@key1='1' and @key2='abc']"
             'parent & child with more than one attribute'                 | '/parent/child[@key1=1 and @key2="abc"]/child2'                || "/parent/child[@key1='1' and @key2='abc']/child2"
             'leaf with more than one attribute has OR operator'           | '/parent/child[@key1=1 or @key2="abc"]'                        || "/parent/child[@key1='1' or @key2='abc']"
@@ -103,31 +107,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 +217,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'
+    }
+
 }
index e40aa91..6eb1a92 100755 (executable)
@@ -28,7 +28,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index d88a9cd..81262c8 100755 (executable)
@@ -179,6 +179,29 @@ class DataRestControllerSpec extends Specification {
             'without observed-timestamp XML'  | null                           | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
     }
 
+    def 'save list elements under root node #scenario.'() {
+        given: 'root node xpath '
+            def rootNodeXpath = '/'
+        when: 'list-node endpoint is invoked with post (create) operation'
+            def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
+                .contentType(MediaType.APPLICATION_JSON)
+                .param('xpath', rootNodeXpath )
+                .content(requestBodyJson)
+            if (observedTimestamp != null)
+                postRequestBuilder.param('observed-timestamp', observedTimestamp)
+            def response = mvc.perform(postRequestBuilder).andReturn().response
+        then: 'a created response is returned'
+            response.status == expectedHttpStatus.value()
+        then: 'the java API was called with the correct parameters'
+            expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, rootNodeXpath, expectedJsonData,
+                { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+        where:
+            scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
+            'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.CREATED
+            'without observed-timestamp'      | null                           || 1                | HttpStatus.CREATED
+            'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
+    }
+
     def 'Save list elements #scenario.'() {
         given: 'parent node xpath '
             def parentNodeXpath = 'parent node xpath'
index 1adca2f..9ed8be5 100644 (file)
@@ -26,7 +26,7 @@
     <parent>\r
         <groupId>org.onap.cps</groupId>\r
         <artifactId>cps-parent</artifactId>\r
-        <version>3.3.4-SNAPSHOT</version>\r
+        <version>3.3.5-SNAPSHOT</version>\r
         <relativePath>../cps-parent/pom.xml</relativePath>\r
     </parent>\r
 \r
index 7b5c0c6..e371035 100644 (file)
@@ -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<String> booleanOperatorsQueue = new LinkedList<>(cpsPathQuery.getBooleanOperators());
         final Queue<String> 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(EscapeUtils.escapeForSqlStringLiteral(leafValueAsText));
                     sqlStringBuilder.append("'");
                 } else {
                     throw new CpsPathException(" can use only " + nextComparativeOperator + " with integer ");
index 139a8b3..4c7971e 100644 (file)
@@ -31,6 +31,7 @@ import javax.persistence.EntityManager;
 import javax.persistence.PersistenceContext;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.spi.utils.EscapeUtils;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -86,7 +87,7 @@ public class TempTableCreator {
         final Collection<String> sqlInserts = new HashSet<>(sqlData.size());
         for (final Collection<String> rowValues : sqlData) {
             final Collection<String> escapedValues =
-                rowValues.stream().map(it -> escapeSingleQuotesByDoublingThem(it)).collect(Collectors.toList());
+                rowValues.stream().map(EscapeUtils::escapeForSqlStringLiteral).collect(Collectors.toList());
             sqlInserts.add("('" + String.join("','", escapedValues) + "')");
         }
         sqlStringBuilder.append("INSERT INTO ");
@@ -98,8 +99,4 @@ public class TempTableCreator {
         sqlStringBuilder.append(";");
     }
 
-    private static String escapeSingleQuotesByDoublingThem(final String value) {
-        return value.replace("'", "''");
-    }
-
 }
index 3092b79..2b61d39 100644 (file)
@@ -26,8 +26,12 @@ import lombok.NoArgsConstructor;
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class EscapeUtils {
 
-    public static String escapeForSqlLike(final String text) {
-        return text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
+    public static String escapeForSqlLike(final String value) {
+        return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
+    }
+
+    public static String escapeForSqlStringLiteral(final String value) {
+        return value.replace("'", "''");
     }
 
 }
index 4e6986e..f76c5ba 100644 (file)
@@ -56,3 +56,5 @@ databaseChangeLog:
       file: changelog/db/changes/19-delete-not-required-dataspace-id-from-fragment.yaml
   - include:
       file: changelog/db/changes/20-change-foreign-key-id-types-to-integer.yaml
+  - include:
+      file: changelog/db/changes/21-escape-quotes-in-xpath.yaml
diff --git a/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-forward.sql b/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-forward.sql
new file mode 100644 (file)
index 0000000..9bf7f9a
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+   ============LICENSE_START=======================================================
+    Copyright (C) 2023 Nordix Foundation.
+   ================================================================================
+   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=========================================================
+*/
+
+-- replace \' with '' and "" with "
+UPDATE fragment SET xpath = replace(replace(xpath, $$\'$$, $$''$$), '""', '"');
\ No newline at end of file
diff --git a/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-rollback.sql b/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-rollback.sql
new file mode 100644 (file)
index 0000000..0fd1633
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+   ============LICENSE_START=======================================================
+    Copyright (C) 2023 Nordix Foundation.
+   ================================================================================
+   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=========================================================
+*/
+
+-- replace '' with \' and " with ""
+UPDATE fragment SET xpath = replace(replace(xpath, $$''$$, $$\'$$), '"', '""');
\ No newline at end of file
diff --git a/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath.yaml b/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath.yaml
new file mode 100644 (file)
index 0000000..7b5b1db
--- /dev/null
@@ -0,0 +1,29 @@
+# ============LICENSE_START=======================================================
+# Copyright (C) 2023 Nordix Foundation.
+# ================================================================================
+# 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=========================================================
+
+databaseChangeLog:
+
+  - changeSet:
+      id: 21
+      author: cps
+      changes:
+        - sqlFile:
+            path: changelog/db/changes/21-escape-quotes-in-xpath-forward.sql
+      rollback:
+        - sqlFile:
+            path: changelog/db/changes/21-escape-quotes-in-xpath-rollback.sql
index 7de9b97..52330e6 100644 (file)
@@ -24,7 +24,7 @@ import spock.lang.Specification
 
 class EscapeUtilsSpec extends Specification {
 
-    def 'Escape text for using in SQL LIKE operation'() {
+    def 'Escape text for use in SQL LIKE operation.'() {
         expect: 'SQL LIKE special characters to be escaped with forward-slash'
             assert EscapeUtils.escapeForSqlLike(unescapedText) == escapedText
         where:
@@ -33,4 +33,9 @@ class EscapeUtilsSpec extends Specification {
             'Others (./?$) are not special' || 'Others (./?$) are not special'
     }
 
+    def 'Escape text for use in SQL string literal.'() {
+        expect: 'single quotes to be doubled'
+            assert EscapeUtils.escapeForSqlStringLiteral("I'm escaping!") == "I''m escaping!"
+    }
+
 }
index dffc38d..06d93d3 100644 (file)
@@ -29,7 +29,7 @@
   <parent>
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-parent</artifactId>
-    <version>3.3.4-SNAPSHOT</version>
+    <version>3.3.5-SNAPSHOT</version>
     <relativePath>../cps-parent/pom.xml</relativePath>
   </parent>
 
index 6e7c164..7db87e8 100755 (executable)
@@ -116,8 +116,12 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> listElementDataNodeCollection =
             buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON);
-        cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
-            listElementDataNodeCollection);
+        if (isRootNodeXpath(parentNodeXpath)) {
+            cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, listElementDataNodeCollection);
+        } else {
+            cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
+                                                      listElementDataNodeCollection);
+        }
         processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp);
     }
 
@@ -391,6 +395,10 @@ public class CpsDataServiceImpl implements CpsDataService {
             .get(anchor.getDataspaceName(), anchor.getSchemaSetName()).getSchemaContext();
     }
 
+    private static boolean isRootNodeXpath(final String xpath) {
+        return ROOT_NODE_XPATH.equals(xpath);
+    }
+
     private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) {
         cpsDataPersistenceService.batchUpdateDataLeaves(anchor.getDataspaceName(), anchor.getName(),
                 Collections.singletonMap(dataNodeUpdate.getXpath(), dataNodeUpdate.getLeaves()));
index 7da4024..f00f944 100644 (file)
@@ -253,7 +253,7 @@ public class YangUtils {
         final List<String> keyAttributes = nodeIdentifier.entrySet().stream().map(
                 entry -> {
                     final String name = entry.getKey().getLocalName();
-                    final String value = String.valueOf(entry.getValue()).replace("'", "\\'");
+                    final String value = String.valueOf(entry.getValue()).replace("'", "''");
                     return String.format("@%s='%s'", name, value);
                 }
         ).collect(Collectors.toList());
index db86640..ba43849 100644 (file)
@@ -110,6 +110,28 @@ class CpsDataServiceImplSpec extends Specification {
             noExceptionThrown()
     }
 
+    def 'Saving list element data fragment under Root node.'() {
+        given: 'schema set for given anchor and dataspace references bookstore model'
+            setupSchemaSetMocks('bookstore.yang')
+        when: 'save data method is invoked with list element json data'
+            def jsonData = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Banana","price": "100","stock": True}]}'
+            objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
+        then: 'the persistence service method is invoked with correct parameters'
+            1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
+                { dataNodeCollection ->
+                    {
+                        assert dataNodeCollection.size() == 1
+                        assert dataNodeCollection.collect { it.getXpath() }
+                            .containsAll(['/invoice[@ProductID=\'2\']'])
+                    }
+                }
+            )
+        and: 'the CpsValidator is called on the dataspaceName and AnchorName'
+            1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
+        and: 'data updated event is sent to notification service'
+            1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.UPDATE, observedTimestamp)
+    }
+
     def 'Saving child data fragment under existing node.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
index 459908b..4b8ed3d 100644 (file)
@@ -1,4 +1,12 @@
 {
+   "multiple-data-tree:invoice": [
+      {
+         "ProductID": "1",
+         "ProductName": "Apple",
+         "price": "100",
+         "stock": false
+      }
+   ],
    "test:bookstore":{
       "bookstore-name": "Chapters/Easons",
       "categories": [
index 2179fb9..b7a52e2 100644 (file)
@@ -15,6 +15,34 @@ module stores {
         }
     }
 
+    list invoice {
+        key "ProductID";
+        leaf ProductID {
+            type uint64;
+            mandatory "true";
+            description
+            "Unique product ID. Example: 001";
+        }
+        leaf ProductName {
+            type string;
+            mandatory "true";
+            description
+            "Name of the Product";
+        }
+        leaf price {
+            type uint64;
+            mandatory "true";
+            description
+            "Price of book";
+        }
+        leaf stock {
+            type boolean;
+            default "false";
+            description
+            "Book in stock or not. Example value: true";
+        }
+    }
+
     container bookstore {
 
         leaf bookstore-name {
index 71dcec8..534729e 100644 (file)
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>dmi-plugin-demo-and-csit-stub</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
     </parent>
 
     <artifactId>dmi-plugin-demo-and-csit-stub-app</artifactId>
@@ -30,7 +30,7 @@
     <properties>
         <app>org.onap.cps.ncmp.dmi.rest.stub.DmiDemoApplication</app>
         <maven.build.timestamp.format>yyyyMMdd'T'HHmmss'Z'</maven.build.timestamp.format>
-        <base.image>${docker.pull.registry}/onap/integration-java11:8.0.0</base.image>
+        <base.image>${docker.pull.registry}/onap/integration-java17:12.0.0</base.image>
         <image.tag>${project.version}-${maven.build.timestamp}</image.tag>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
index a9e3827..d5bc8a8 100644 (file)
@@ -21,7 +21,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>dmi-plugin-demo-and-csit-stub</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
     </parent>
     <artifactId>dmi-plugin-demo-and-csit-stub-service</artifactId>
 
index e8dd4c0..e32911c 100644 (file)
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 796eb7f..6611789 100644 (file)
@@ -177,6 +177,7 @@ General Notes
 =============
 
 - String values must be wrapped in quotation marks ``"`` (U+0022) or apostrophes ``'`` (U+0027).
+- Quotations marks and apostrophes can be escaped by doubling them in their respective quotes, for example ``'CPS ''Path'' Query' -> CPS 'Path' Query``
 - String comparisons are case sensitive.
 
 Query Syntax
@@ -247,7 +248,6 @@ leaf-conditions
   - The key should be supplied with correct data type for it to be queried from DB. In the last example above the attribute code is of type
     Integer so the cps query will not work if the value is passed as string.
     eg: ``//categories[@code="1"]`` or ``//categories[@code='1']`` will not work because the key attribute code is treated a string.
-  - Having '[' token in any index in any list will have a negative impact on this function.
 
 **Notes**
   - For performance reasons it does not make sense to query using key leaf as attribute. If the key value is known it is better to execute a get request with the complete xpath.
@@ -272,7 +272,6 @@ The text()-condition  can be added to any CPS path query.
   - Only string and integer values are supported, boolean and float values are not supported.
   - Since CPS cannot return individual leaves it will always return the container with all its leaves. Ancestor-axis can be used to specify a parent higher up the tree.
   - When querying a leaf value (instead of leaf-list) it is better, more performant to use a text value condition use @<leaf-name> as described above.
-  - Having '[' token in any index in any list will have a negative impact on this function.
 
 contains()-condition
 --------------------
index d9033a0..25f6d22 100755 (executable)
@@ -16,6 +16,34 @@ CPS Release Notes
 ..      * * *   MONTREAL   * * *
 ..      ========================
 
+Version: 3.3.5
+==============
+
+Release Data
+------------
+
++--------------------------------------+--------------------------------------------------------+
+| **CPS Project**                      |                                                        |
+|                                      |                                                        |
++--------------------------------------+--------------------------------------------------------+
+| **Docker images**                    | onap/cps-and-ncmp:3.3.5                                |
+|                                      |                                                        |
++--------------------------------------+--------------------------------------------------------+
+| **Release designation**              | 3.3.5 Montreal                                         |
+|                                      |                                                        |
++--------------------------------------+--------------------------------------------------------+
+| **Release date**                     | Not yet released                                       |
+|                                      |                                                        |
++--------------------------------------+--------------------------------------------------------+
+
+Bug Fixes
+---------
+3.3.5
+
+Features
+--------
+    - `CPS-1760 <https://jira.onap.org/browse/CPS-1760>`_ Improve handling of special characters in Cps Paths
+
 Version: 3.3.4
 ==============
 
@@ -32,7 +60,7 @@ Release Data
 | **Release designation**              | 3.3.4 Montreal                                         |
 |                                      |                                                        |
 +--------------------------------------+--------------------------------------------------------+
-| **Release date**                     | Not yet released                                       |
+| **Release date**                     | 2023 July 19                                           |
 |                                      |                                                        |
 +--------------------------------------+--------------------------------------------------------+
 
@@ -42,6 +70,7 @@ Bug Fixes
 
 Features
 --------
+    - `CPS-1767 <https://jira.onap.org/browse/CPS-1767>`_ Upgrade CPS to java 17
 
 Version: 3.3.3
 ==============
index 04ce7cc..18b660f 100644 (file)
@@ -23,7 +23,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 351f310..a3f1439 100644 (file)
@@ -43,11 +43,13 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
 
     CpsDataService objectUnderTest
     def originalCountBookstoreChildNodes
+    def originalCountBookstoreTopLevelListNodes
     def now = OffsetDateTime.now()
 
     def setup() {
         objectUnderTest = cpsDataService
         originalCountBookstoreChildNodes = countDataNodesInBookstore()
+        originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
     }
 
     def 'Read bookstore top-level container(s) using #fetchDescendantsOption.'() {
@@ -64,18 +66,18 @@ 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.'() {
         when: 'get data nodes for bookstore container'
             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, root, OMIT_DESCENDANTS)
         then: 'the tree consist ouf of one data node'
-            assert countDataNodesInTree(result) == 1
+            assert countDataNodesInTree(result) == 2
         and: 'the top level data node has the expected attribute and value'
-            assert result.leaves['bookstore-name'] == ['Easons']
+            assert result.leaves.size() == 2
         where: 'the following variations of "root" are used'
             root << [ '/', '' ]
     }
@@ -179,6 +181,21 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             thrown(DataNodeNotFoundExceptionBatch)
     }
 
+    def 'Add and Delete top-level list (element) data nodes with root node.'() {
+        given: 'a new (multiple-data-tree:invoice) datanodes'
+            def json = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Mango","price": "150","stock": true}]}'
+        when: 'the new list elements are saved'
+            objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/', json, now)
+        then: 'they can be retrieved by their xpaths'
+            objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', INCLUDE_ALL_DESCENDANTS)
+        and: 'there is one extra datanode'
+            assert originalCountBookstoreTopLevelListNodes + 1 == countTopLevelListDataNodesInBookstore()
+        when: 'the new elements are deleted'
+            objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', now)
+        then: 'the original number of datanodes is restored'
+            assert originalCountBookstoreTopLevelListNodes == countTopLevelListDataNodesInBookstore()
+    }
+
     def 'Add and Delete list (element) data nodes.'() {
         given: 'two new (categories) data nodes'
             def json = '{"categories": [ {"code":"new1"}, {"code":"new2" } ] }'
@@ -368,4 +385,8 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
     def countDataNodesInBookstore() {
         return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
     }
+
+    def countTopLevelListDataNodesInBookstore() {
+        return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
+    }
 }
index a736ab0..74496d3 100644 (file)
@@ -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
@@ -372,4 +356,23 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             'text-condition'     || "/bookstore/categories[@code='1']/books/title[text()='[@hello=world]']"
             'contains-condition' || "/bookstore/categories[@code='1']/books[contains(@title, '[@hello=world]')]"
     }
+
+    def 'Cps Path get and query can handle apostrophe inside #quotes.'() {
+        given: 'a book with special characters in title'
+            cpsDataService.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, "/bookstore/categories[@code='1']",
+                    '{"books": [ {"title":"I\'m escaping"} ] }', OffsetDateTime.now())
+        when: 'a query is executed'
+            def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, OMIT_DESCENDANTS)
+        then: 'the node is returned'
+            assert result.size() == 1
+            assert result[0].xpath == "/bookstore/categories[@code='1']/books[@title='I''m escaping']"
+        cleanup: 'the new datanode'
+            cpsDataService.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, "/bookstore/categories[@code='1']/books[@title='I''m escaping']", OffsetDateTime.now())
+        where:
+            quotes               || cpsPath
+            'single quotes'      || "/bookstore/categories[@code='1']/books[@title='I''m escaping']"
+            'double quotes'      || '/bookstore/categories[@code="1"]/books[@title="I\'m escaping"]'
+            'text-condition'     || "/bookstore/categories[@code='1']/books/title[text()='I''m escaping']"
+            'contains-condition' || "/bookstore/categories[@code='1']/books[contains(@title, 'I''m escaping')]"
+    }
 }
index e592a9c..ab384de 100644 (file)
@@ -15,6 +15,34 @@ module stores {
         }
     }
 
+    list invoice {
+        key "ProductID";
+        leaf ProductID {
+            type uint64;
+            mandatory "true";
+            description
+            "Unique product ID. Example: 001";
+        }
+        leaf ProductName {
+            type string;
+            mandatory "true";
+            description
+            "Name of the Product";
+        }
+        leaf price {
+            type uint64;
+            mandatory "true";
+            description
+            "Price of book";
+        }
+        leaf stock {
+            type boolean;
+            default "false";
+            description
+            "Book in stock or not. Example value: true";
+        }
+    }
+
     container bookstore {
 
         leaf bookstore-name {
index 12df20e..5f66a1d 100644 (file)
@@ -1,4 +1,12 @@
 {
+  "multiple-data-tree:invoice": [
+    {
+      "ProductID": "1",
+      "ProductName": "Apple",
+      "price": "100",
+      "stock": false
+    }
+  ],
   "bookstore": {
     "bookstore-name": "Easons",
     "premises": {
@@ -27,7 +35,7 @@
             "lang": "English",
             "authors": ["Roald Dahl"],
             "editions": [1988, 2000],
-            "price": 10
+            "price": 20
           },
           {
             "title": "The Gruffalo",
             "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
+          }
+        ]
       }
     ]
   }
index 623f2a0..7bd6c9b 100644 (file)
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.3.4-SNAPSHOT</version>
+        <version>3.3.5-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
diff --git a/pom.xml b/pom.xml
index 910afa2..119b14b 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
 \r
     <groupId>org.onap.cps</groupId>\r
     <artifactId>cps-aggregator</artifactId>\r
-    <version>3.3.4-SNAPSHOT</version>\r
+    <version>3.3.5-SNAPSHOT</version>\r
     <packaging>pom</packaging>\r
 \r
     <name>cps</name>\r
diff --git a/releases/3.3.4-container.yaml b/releases/3.3.4-container.yaml
new file mode 100644 (file)
index 0000000..ee2a0d4
--- /dev/null
@@ -0,0 +1,8 @@
+distribution_type: container
+container_release_tag: 3.3.4
+project: cps
+log_dir: cps-maven-docker-stage-master/923/
+ref: 6b31279b2122ff9add6696b5eacfbeea8bb31cef
+containers:
+  - name: 'cps-and-ncmp'
+    version: '3.3.4-20230718T101218Z'
diff --git a/releases/3.3.4.yaml b/releases/3.3.4.yaml
new file mode 100644 (file)
index 0000000..073bd42
--- /dev/null
@@ -0,0 +1,4 @@
+distribution_type: maven
+log_dir: cps-maven-stage-master/931/
+project: cps
+version: 3.3.4
\ No newline at end of file
index 20a10d2..6e84c3f 100644 (file)
@@ -25,7 +25,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>spotbugs</artifactId>
-    <version>3.3.4-SNAPSHOT</version>
+    <version>3.3.5-SNAPSHOT</version>
 
     <properties>
         <nexusproxy>https://nexus.onap.org</nexusproxy>
index 9456209..5445d99 100755 (executable)
@@ -22,7 +22,7 @@
 
 major=3
 minor=3
-patch=4
+patch=5
 
 base_version=${major}.${minor}.${patch}