Fix Absolute Path to list with Integer/String key 38/128238/16
authorLathish <lathishbabu.ganesan@est.tech>
Thu, 31 Mar 2022 16:29:22 +0000 (17:29 +0100)
committerLathish <lathishbabu.ganesan@est.tech>
Mon, 25 Apr 2022 17:33:28 +0000 (18:33 +0100)
 - Introduced normalizedXpath to normalize the xpath and cpspath
 - Introduced normalizedAncestorpath to normalize the ancestor path in xpath and cpspath
 - Added new condition in the ANTLR Grammar to capture the invalid path in the xpath
 - Introduced PathParsingException to replace the IllegalStateException

Issue-ID: CPS-961
Change-Id: Ie10f5c6cfc466387e79eec184b933297d1d79587
Signed-off-by: Lathish <lathishbabu.ganesan@est.tech>
13 files changed:
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/CpsPathBuilder.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java [new file with mode: 0644]
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java [new file with mode: 0755]
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-ri/src/test/resources/data/cps-path-query.sql
cps-ri/src/test/resources/data/fragment.sql

index cefeac4..40ad410 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Nordix Foundation
+ *  Copyright (C) 2021-2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
 
 grammar CpsPath ;
 
-cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? ;
+cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? invalidPostFix?;
 
 ancestorAxis : SLASH KW_ANCESTOR COLONCOLON ancestorPath ;
 
@@ -46,6 +46,8 @@ leafCondition : AT leafName EQ ( IntegerLiteral | StringLiteral) ;
 
 leafName : QName ;
 
+invalidPostFix : (AT | CB | COLONCOLON | EQ ).+ ;
+
 /*
  * Lexer Rules
  * Most of the lexer rules below are inspired by
index ebf6fd3..21f5173 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Nordix Foundation
+ *  Copyright (C) 2021-2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT;
 import java.util.HashMap;
 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;
 import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.DescendantContext;
 import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.IncorrectPrefixContext;
@@ -35,18 +36,33 @@ import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.TextFunctionConditionCon
 
 public class CpsPathBuilder extends CpsPathBaseListener {
 
+    private static final String OPEN_BRACKET = "[";
+
+    private static final String CLOSE_BRACKET = "]";
+
     final CpsPathQuery cpsPathQuery = new CpsPathQuery();
 
     final Map<String, Object> leavesData = new HashMap<>();
 
+    final StringBuilder normalizedXpathBuilder = new StringBuilder();
+
+    final StringBuilder normalizedAncestorPathBuilder = new StringBuilder();
+
+    boolean processingAncestorAxis = false;
+
+    @Override
+    public void exitInvalidPostFix(final CpsPathParser.InvalidPostFixContext ctx) {
+        throw new PathParsingException(ctx.getText());
+    }
+
     @Override
     public void exitPrefix(final PrefixContext ctx) {
-        cpsPathQuery.setXpathPrefix(ctx.getText());
+        cpsPathQuery.setXpathPrefix(normalizedXpathBuilder.toString());
     }
 
     @Override
     public void exitIncorrectPrefix(final IncorrectPrefixContext ctx) {
-        throw new IllegalStateException("CPS path can only start with one or two slashes (/)");
+        throw new PathParsingException("CPS path can only start with one or two slashes (/)");
     }
 
     @Override
@@ -56,32 +72,49 @@ public class CpsPathBuilder extends CpsPathBaseListener {
             comparisonValue = Integer.valueOf(ctx.IntegerLiteral().getText());
         }
         if (ctx.StringLiteral() != null) {
+            final boolean wasWrappedInDoubleQuote  = ctx.StringLiteral().getText().startsWith("\"");
             comparisonValue = stripFirstAndLastCharacter(ctx.StringLiteral().getText());
+            if (wasWrappedInDoubleQuote) {
+                comparisonValue = String.valueOf(comparisonValue).replace("'", "\\'");
+            }
         } else if (comparisonValue == null) {
-            throw new IllegalStateException("Unsupported comparison value encountered in expression" + ctx.getText());
+            throw new PathParsingException("Unsupported comparison value encountered in expression" + ctx.getText());
         }
         leavesData.put(ctx.leafName().getText(), comparisonValue);
+        appendCondition(normalizedXpathBuilder, ctx.leafName().getText(), comparisonValue);
+        if (processingAncestorAxis) {
+            appendCondition(normalizedAncestorPathBuilder, ctx.leafName().getText(), comparisonValue);
+        }
     }
 
     @Override
     public void exitDescendant(final DescendantContext ctx) {
         cpsPathQuery.setCpsPathPrefixType(DESCENDANT);
-        cpsPathQuery.setDescendantName(ctx.getText().substring(2));
+        cpsPathQuery.setDescendantName(normalizedXpathBuilder.substring(1));
+        normalizedXpathBuilder.insert(0, "/");
     }
 
     @Override
     public void enterMultipleLeafConditions(final MultipleLeafConditionsContext ctx)  {
+        normalizedXpathBuilder.append(OPEN_BRACKET);
         leavesData.clear();
     }
 
     @Override
     public void exitMultipleLeafConditions(final MultipleLeafConditionsContext ctx) {
+        normalizedXpathBuilder.append(CLOSE_BRACKET);
         cpsPathQuery.setLeavesData(leavesData);
     }
 
+    @Override
+    public void enterAncestorAxis(final AncestorAxisContext ctx) {
+        processingAncestorAxis = true;
+    }
+
     @Override
     public void exitAncestorAxis(final AncestorAxisContext ctx) {
-        cpsPathQuery.setAncestorSchemaNodeIdentifier(ctx.ancestorPath().getText());
+        cpsPathQuery.setAncestorSchemaNodeIdentifier(normalizedAncestorPathBuilder.substring(1));
+        processingAncestorAxis = false;
     }
 
     @Override
@@ -90,7 +123,24 @@ public class CpsPathBuilder extends CpsPathBaseListener {
         cpsPathQuery.setTextFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText()));
     }
 
+    @Override
+    public void enterListElementRef(final CpsPathParser.ListElementRefContext ctx) {
+        normalizedXpathBuilder.append(OPEN_BRACKET);
+        if (processingAncestorAxis) {
+            normalizedAncestorPathBuilder.append(OPEN_BRACKET);
+        }
+    }
+
+    @Override
+    public void exitListElementRef(final CpsPathParser.ListElementRefContext ctx) {
+        normalizedXpathBuilder.append(CLOSE_BRACKET);
+        if (processingAncestorAxis) {
+            normalizedAncestorPathBuilder.append(CLOSE_BRACKET);
+        }
+    }
+
     CpsPathQuery build() {
+        cpsPathQuery.setNormalizedXpath(normalizedXpathBuilder.toString());
         return cpsPathQuery;
     }
 
@@ -98,4 +148,23 @@ public class CpsPathBuilder extends CpsPathBaseListener {
         return wrappedString.substring(1, wrappedString.length() - 1);
     }
 
+    @Override
+    public void exitContainerName(final CpsPathParser.ContainerNameContext ctx) {
+        normalizedXpathBuilder.append("/")
+                .append(ctx.getText());
+        if (processingAncestorAxis) {
+            normalizedAncestorPathBuilder.append("/").append(ctx.getText());
+        }
+    }
+
+    private void appendCondition(final StringBuilder currentNormalizedPathBuilder, final String name,
+                                final Object value) {
+        final char lastCharacter = currentNormalizedPathBuilder.charAt(currentNormalizedPathBuilder.length() - 1);
+        currentNormalizedPathBuilder.append(lastCharacter == '[' ? "" : " and ")
+                .append("@")
+                .append(name)
+                .append("='")
+                .append(value)
+                .append("'");
+    }
 }
index de7adf2..53490f3 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Nordix Foundation
+ *  Copyright (C) 2021-2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -26,19 +26,13 @@ import java.util.Map;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Setter;
-import org.antlr.v4.runtime.BaseErrorListener;
-import org.antlr.v4.runtime.CharStreams;
-import org.antlr.v4.runtime.CommonTokenStream;
-import org.antlr.v4.runtime.RecognitionException;
-import org.antlr.v4.runtime.Recognizer;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathLexer;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathParser;
 
 @Getter
 @Setter(AccessLevel.PACKAGE)
 public class CpsPathQuery {
 
     private String xpathPrefix;
+    private String normalizedXpath;
     private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE;
     private String descendantName;
     private Map<String, Object> leavesData;
@@ -53,20 +47,7 @@ public class CpsPathQuery {
      * @return a CpsPathQuery object.
      */
     public static CpsPathQuery createFrom(final String cpsPathSource) {
-        final var inputStream = CharStreams.fromString(cpsPathSource);
-        final var cpsPathLexer = new CpsPathLexer(inputStream);
-        final var cpsPathParser = new CpsPathParser(new CommonTokenStream(cpsPathLexer));
-        cpsPathParser.addErrorListener(new BaseErrorListener() {
-            @Override
-            public void syntaxError(final Recognizer<?, ?> recognizer, final Object offendingSymbol, final int line,
-                                    final int charPositionInLine, final String msg, final RecognitionException e) {
-                throw new IllegalStateException("failed to parse at line " + line + " due to " + msg, e);
-            }
-        });
-        final var cpsPathBuilder = new CpsPathBuilder();
-        cpsPathParser.addParseListener(cpsPathBuilder);
-        cpsPathParser.cpsPath();
-        return cpsPathBuilder.build();
+        return CpsPathUtil.getCpsPathQuery(cpsPathSource);
     }
 
     /**
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java
new file mode 100644 (file)
index 0000000..97d7d1d
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 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=========================================================
+ */
+
+package org.onap.cps.cpspath.parser;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathLexer;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathParser;
+
+@Getter
+@Setter
+@NoArgsConstructor(access = AccessLevel.PACKAGE)
+public class CpsPathUtil {
+
+    /**
+     * Returns a normalized xpath path query.
+     *
+     * @param xpathSource xpath
+     * @return a normalized xpath String.
+     */
+    public static String getNormalizedXpath(final String xpathSource) {
+        final CpsPathBuilder cpsPathBuilder = getCpsPathBuilder(xpathSource);
+        return cpsPathBuilder.build().getNormalizedXpath();
+    }
+
+    /**
+     * Returns a cps path query.
+     *
+     * @param cpsPathSource cps path
+     * @return a CpsPathQuery object.
+     */
+
+    public static CpsPathQuery getCpsPathQuery(final String cpsPathSource) {
+        final CpsPathBuilder cpsPathBuilder = getCpsPathBuilder(cpsPathSource);
+        return cpsPathBuilder.build();
+    }
+
+    private static CpsPathBuilder getCpsPathBuilder(final String cpsPathSource) {
+        final CharStream inputStream = CharStreams.fromString(cpsPathSource);
+        final CpsPathLexer cpsPathLexer = new CpsPathLexer(inputStream);
+        final CpsPathParser cpsPathParser = new CpsPathParser(new CommonTokenStream(cpsPathLexer));
+        cpsPathParser.addErrorListener(new BaseErrorListener() {
+            @Override
+            public void syntaxError(final Recognizer<?, ?> recognizer, final Object offendingSymbol, final int line,
+                                    final int charPositionInLine, final String msg, final RecognitionException e) {
+                throw new PathParsingException("failed to parse at line " + line + " due to " + msg,
+                        e == null ? "" : e.getMessage());
+            }
+        });
+        final CpsPathBuilder cpsPathBuilder = new CpsPathBuilder();
+        cpsPathParser.addParseListener(cpsPathBuilder);
+        cpsPathParser.cpsPath();
+        return cpsPathBuilder;
+    }
+}
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java
new file mode 100755 (executable)
index 0000000..4a67167
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 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=========================================================
+ */
+
+package org.onap.cps.cpspath.parser;
+
+import lombok.Getter;
+
+/**
+ * XPath Parsing Exception.
+ */
+public class PathParsingException extends RuntimeException {
+
+    private static final long serialVersionUID = 7072864354925271894L;
+
+    @Getter
+    final String details;
+
+    /**
+     * Constructor.
+     *
+     * @param details the error details
+     */
+    public PathParsingException(final String details) {
+        super("Error while parsing xpath expression");
+        this.details = details;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param message the error message
+     * @param details the error details
+     */
+    public PathParsingException(final String message, final String details) {
+        super(message);
+        this.details = details;
+    }
+}
index bfec574..b837a64 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Nordix Foundation
+ *  Copyright (C) 2021-2022 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -34,17 +34,17 @@ class CpsPathQuerySpec extends Specification {
             result.cpsPathPrefixType == ABSOLUTE
         and: 'the right query parameters are set'
             result.xpathPrefix == expectedXpathPrefix
-            result.hasLeafConditions() == true
-            result.leavesData.containsKey(expectedLeafName) == true
+            result.hasLeafConditions()
+            result.leavesData.containsKey(expectedLeafName)
             result.leavesData.get(expectedLeafName) == 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'
-            'leaf of type String'  | '/parent/child[@common-leaf-name=\'common-leaf-value\']'   || '/parent/child'                             | 'common-leaf-name'     | 'common-leaf-value'
-            'leaf of type Integer' | '/parent/child[@common-leaf-name-int=5]'                   || '/parent/child'                             | 'common-leaf-name-int' | 5
-            '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'
+            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'
+            'leaf of type String'  | '/parent/child[@common-leaf-name=\'common-leaf-value\']'   || '/parent/child'                                 | 'common-leaf-name'     | 'common-leaf-value'
+            'leaf of type Integer' | '/parent/child[@common-leaf-name-int=5]'                   || '/parent/child'                                 | 'common-leaf-name-int' | 5
+            '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'
     }
 
     def 'Parse cps path of type ends with a #scenario.'() {
@@ -60,6 +60,38 @@ class CpsPathQuerySpec extends Specification {
             'parent & child' | '//parent/child' || 'parent/child'
     }
 
+    def 'Parse cps path to form the Normalized cps path containing #scenario.'() {
+        when: 'the given cps path is parsed'
+            def result = CpsPathUtil.getCpsPathQuery(cpsPath)
+        then: 'the query has the right normalized xpath type'
+            assert result.normalizedXpath == expectedNormalizedXPath
+        where: 'the following data is used'
+            scenario                                              | cpsPath                                         || expectedNormalizedXPath
+            'yang container'                                      | '/cps-path'                                     || '/cps-path'
+            'descendant anywhere'                                 | '//cps-path'                                    || '//cps-path'
+            'descendant with leaf condition'                      | '//cps-path[@key=1]'                            || "//cps-path[@key='1']"
+            'descendant with leaf value and ancestor'             | '//cps-path[@key=1]/ancestor:parent[@key=1]'    || "//cps-path[@key='1']/ancestor:parent[@key='1']"
+            'parent & child'                                      | '/parent/child'                                 || '/parent/child'
+            '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\\\'']"
+            '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"
+    }
+
+    def 'Parse xpath to form the Normalized xpath containing #scenario.'() {
+        when: 'the given xpath is parsed'
+            def result = CpsPathUtil.getNormalizedXpath(xPath)
+        then: 'the query has the right normalized xpath type'
+            assert result == expectedNormalizedXPath
+        where: 'the following data is used'
+            scenario               | xPath      || expectedNormalizedXPath
+            'yang container'       | '/xpath'   || '/xpath'
+            'descendant anywhere'  | '//xpath'  || '//xpath'
+    }
+
     def 'Parse cps path that ends with a yang list containing #scenario.'() {
         when: 'the given cps path is parsed'
             def result = CpsPathQuery.createFrom(cpsPath)
@@ -99,7 +131,7 @@ class CpsPathQuerySpec extends Specification {
         when: 'the given cps path is parsed'
             CpsPathQuery.createFrom(cpsPath)
         then: 'a CpsPathException is thrown'
-            thrown(IllegalStateException)
+            thrown(PathParsingException)
         where: 'the following data is used'
             scenario                                                            | cpsPath
             'no / at the start'                                                 | 'invalid-cps-path/child'
@@ -110,7 +142,9 @@ class CpsPathQuerySpec extends Specification {
             'end with descendant and more than one attribute separated by "or"' | '//child[@int-leaf=5 or @leaf-name="leaf value"]'
             'missing attribute value'                                           | '//child[@int-leaf=5 and @name]'
             'incomplete ancestor value'                                         | '//books/ancestor::'
-//  DISCUSS WITH TEAM :           'unsupported postfix after value condition (JIRA CPS-450)'          | '/parent/child[@id=1]/somePostFix'
+            'invalid list element with missing ['                               | '/parent-206/child-206/grand-child-206@key="A"]'
+            'invalid list element with incorrect ]'                             | '/parent-206/child-206/grand-child-206]@key="A"]'
+            'invalid list element with incorrect ::'                            | '/parent-206/child-206/grand-child-206::@key"A"]'
     }
 
     def 'Parse cps path using ancestor by schema node identifier with a #scenario.'() {
@@ -125,11 +159,12 @@ class CpsPathQuerySpec extends Specification {
         and: 'there are no leaves conditions'
             result.hasLeafConditions() == false
         where:
-            scenario                  | ancestorPath
-            'basic container'         | 'someContainer'
-            'container with parent'   | 'parent/child'
-            'ancestor that is a list' | 'categories[@code=1]'
-            'parent that is a list'   | 'parent[@id=1]/child'
+            scenario                                    | ancestorPath
+            'basic container'                           | 'someContainer'
+            'container with parent'                     | 'parent/child'
+            'ancestor that is a list'                   | "categories[@code='1']"
+            'ancestor that is a list with compound key' | "categories[@key1='1' and @key2='2']"
+            'parent that is a list'                     | "parent[@id='1']/child"
     }
 
     def 'Combinations #scenario.'() {
@@ -145,11 +180,10 @@ class CpsPathQuerySpec extends Specification {
             result.ancestorSchemaNodeIdentifier == 'someAncestor'
             result.descendantName == expectedDescendantName
         where:
-            scenario                     | cpsPath                               || expectedDescendantName | expectLeafConditions
-            'basic container'            | '//someContainer'                     || 'someContainer'        | false
-            'container with parent'      | '//parent/child'                      || 'parent/child'         | false
-            '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
+            scenario                     | cpsPath                               || expectedDescendantName   | expectLeafConditions
+            'basic container'            | '//someContainer'                     || 'someContainer'          | false
+            'container with parent'      | '//parent/child'                      || 'parent/child'           | false
+            '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
     }
-
 }
index 2aa4ddd..d4c68c3 100644 (file)
@@ -24,7 +24,6 @@ package org.onap.cps.rest.exceptions
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import groovy.json.JsonSlurper
-import org.mapstruct.factory.Mappers
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsModuleService
index bb3c2d0..847a1d1 100644 (file)
@@ -41,6 +41,8 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.hibernate.StaleStateException;
 import org.onap.cps.cpspath.parser.CpsPathQuery;
+import org.onap.cps.cpspath.parser.CpsPathUtil;
+import org.onap.cps.cpspath.parser.PathParsingException;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.entities.AnchorEntity;
@@ -174,8 +176,14 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         if (isRootXpath(xpath)) {
             return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity);
         } else {
+            final String normalizedXpath;
+            try {
+                normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath);
+            } catch (final PathParsingException e) {
+                throw new CpsPathException(e.getMessage());
+            }
             return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity,
-                xpath);
+                    normalizedXpath);
         }
     }
 
@@ -186,8 +194,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
         final CpsPathQuery cpsPathQuery;
         try {
-            cpsPathQuery = CpsPathQuery.createFrom(cpsPath);
-        } catch (final IllegalStateException e) {
+            cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+        } catch (final PathParsingException e) {
             throw new CpsPathException(e.getMessage());
         }
         List<FragmentEntity> fragmentEntities =
@@ -378,12 +386,13 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     }
 
     private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
-        if (parentFragmentEntity.getXpath().equals(targetXpath)) {
+        final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath);
+        if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) {
             fragmentRepository.delete(parentFragmentEntity);
             return true;
         }
         if (parentFragmentEntity.getChildFragments()
-            .removeIf(fragment -> fragment.getXpath().equals(targetXpath))) {
+            .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) {
             fragmentRepository.save(parentFragmentEntity);
             return true;
         }
@@ -391,7 +400,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     }
 
     private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
-        final String deleteTargetXpathPrefix = listXpath + "[";
+        final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath);
+        final String deleteTargetXpathPrefix = normalizedListXpath + "[";
         if (parentFragmentEntity.getChildFragments()
             .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
             fragmentRepository.save(parentFragmentEntity);
index ae88d30..36b378a 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Nordix Foundation
+ *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021 Bell Canada.
  *  ================================================================================
@@ -92,15 +92,15 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
             }
         where: 'the following data is used'
             scenario                                                 | cpsPath                                 || expectedXPaths
-            'fully unique descendant name'                           | '//categories[@code=2]'                 || ['/shops/shop[@id=1]/categories[@code=2]', '/shops/shop[@id=2]/categories[@code=1]', '/shops/shop[@id=2]/categories[@code=2]']
-            'descendant name match end of other node'                | '//book'                                || ['/shops/shop[@id=1]/categories[@code=1]/book', '/shops/shop[@id=1]/categories[@code=2]/book']
-            'descendant with text condition on leaf'                 | '//book/title[text()="Chapters"]'       || ['/shops/shop[@id=1]/categories[@code=2]/book']
+            'fully unique descendant name'                           | '//categories[@code=2]'                 || ["/shops/shop[@id='1']/categories[@code='2']", "/shops/shop[@id='2']/categories[@code='1']", "/shops/shop[@id='2']/categories[@code='2']"]
+            'descendant name match end of other node'                | '//book'                                || ["/shops/shop[@id='1']/categories[@code='1']/book", "/shops/shop[@id='1']/categories[@code='2']/book"]
+            'descendant with text condition on leaf'                 | '//book/title[text()="Chapters"]'       || ["/shops/shop[@id='1']/categories[@code='2']/book"]
             'descendant with text condition case mismatch'           | '//book/title[text()="chapters"]'       || []
-            'descendant with text condition on int leaf'             | '//book/price[text()="5"]'              || ['/shops/shop[@id=1]/categories[@code=1]/book']
-            'descendant with text condition on leaf-list'            | '//book/labels[text()="special offer"]' || ['/shops/shop[@id=1]/categories[@code=1]/book']
+            'descendant with text condition on int leaf'             | '//book/price[text()="5"]'              || ["/shops/shop[@id='1']/categories[@code='1']/book"]
+            'descendant with text condition on leaf-list'            | '//book/labels[text()="special offer"]' || ["/shops/shop[@id='1']/categories[@code='1']/book"]
             'descendant with text condition partial match'           | '//book/labels[text()="special"]'       || []
-            'descendant with text condition (existing) empty string' | '//book/labels[text()=""]'              || ['/shops/shop[@id=1]/categories[@code=1]/book']
-            'descendant with text condition on int leaf-list'        | '//book/editions[text()="2000"]'        || ['/shops/shop[@id=1]/categories[@code=2]/book']
+            'descendant with text condition (existing) empty string' | '//book/labels[text()=""]'              || ["/shops/shop[@id='1']/categories[@code='1']/book"]
+            'descendant with text condition on int leaf-list'        | '//book/editions[text()="2000"]'        || ["/shops/shop[@id='1']/categories[@code='2']/book"]
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -115,10 +115,10 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
             }
         where: 'the following data is used'
             scenario                   | cpsPath                                               || expectedXPaths
-            'one leaf'                 | '//author[@FirstName="Joe"]'                          || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]']
-            'more than one leaf'       | '//author[@FirstName="Joe" and @Surname="Bloggs"]'    || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
-            'leaves reversed in order' | '//author[@Surname="Bloggs" and @FirstName="Joe"]'    || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
-            'leaf and text condition'  | '//author[@FirstName="Joe"]/Surname[text()="Bloggs"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
+            'one leaf'                 | '//author[@FirstName="Joe"]'                          || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']", "/shops/shop[@id='1']/categories[@code='2']/book/author[@FirstName='Joe' and @Surname='Smith']"]
+            'more than one leaf'       | '//author[@FirstName="Joe" and @Surname="Bloggs"]'    || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
+            'leaves reversed in order' | '//author[@Surname="Bloggs" and @FirstName="Joe"]'    || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
+            'leaf and text condition'  | '//author[@FirstName="Joe"]/Surname[text()="Bloggs"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -133,9 +133,9 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
             }
         where: 'the following data is used'
             scenario                              | cpsPath                                        || expectedXPaths
-            'one partial key leaf'                | '//author[@FirstName="Joe"]'                   || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]']
-            'one non key leaf'                    | '//author[@title="Dune"]'                      || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
-            'mix of partial key and non key leaf' | '//author[@FirstName="Joe" and @title="Dune"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]']
+            'one partial key leaf'                | '//author[@FirstName="Joe"]'                   || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']", "/shops/shop[@id='1']/categories[@code='2']/book/author[@FirstName='Joe' and @Surname='Smith']"]
+            'one non key leaf'                    | '//author[@title="Dune"]'                      || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
+            'mix of partial key and non key leaf' | '//author[@FirstName="Joe" and @title="Dune"]' || ["/shops/shop[@id='1']/categories[@code='1']/book/author[@FirstName='Joe' and @Surname='Bloggs']"]
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -149,13 +149,13 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
             }
         where: 'the following data is used'
             scenario                                    | cpsPath                                              || expectedXPaths
-            'multiple list-ancestors'                   | '//book/ancestor::categories'                        || ['/shops/shop[@id=1]/categories[@code=1]', '/shops/shop[@id=1]/categories[@code=2]']
-            'one ancestor with list value'              | '//book/ancestor::categories[@code=1]'               || ['/shops/shop[@id=1]/categories[@code=1]']
+            'multiple list-ancestors'                   | '//book/ancestor::categories'                        || ["/shops/shop[@id='1']/categories[@code='1']", "/shops/shop[@id='1']/categories[@code='2']"]
+            'one ancestor with list value'              | '//book/ancestor::categories[@code=1]'               || ["/shops/shop[@id='1']/categories[@code='1']"]
             'top ancestor'                              | '//shop[@id=1]/ancestor::shops'                      || ['/shops']
-            'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]'   || ['/shops/shop[@id=1]']
-            'ancestor with parent list'                 | '//book/ancestor::shop[@id=1]/categories[@code=2]'   || ['/shops/shop[@id=1]/categories[@code=2]']
-            'ancestor with parent'                      | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ['/shops/shop[@id=3]/info/contact']
-            'ancestor combined with text condition'     | '//book/title[text()="Dune"]/ancestor::shop'         || ['/shops/shop[@id=1]']
+            'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]'   || ["/shops/shop[@id='1']"]
+            'ancestor with parent list'                 | '//book/ancestor::shop[@id=1]/categories[@code=2]'   || ["/shops/shop[@id='1']/categories[@code='2']"]
+            'ancestor with parent'                      | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ["/shops/shop[@id='3']/info/contact"]
+            'ancestor combined with text condition'     | '//book/title[text()="Dune"]/ancestor::shop'         || ["/shops/shop[@id='1']"]
             'ancestor with parent that does not exist'  | '//book/ancestor::parentDoesNoExist/categories'      || []
             'ancestor does not exist'                   | '//book/ancestor::ancestorDoesNotExist'              || []
     }
index ab29005..6f780fc 100755 (executable)
@@ -23,11 +23,13 @@ package org.onap.cps.spi.impl
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.google.common.collect.ImmutableSet
+import org.onap.cps.cpspath.parser.PathParsingException
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.entities.FragmentEntity
 import org.onap.cps.spi.exceptions.AlreadyDefinedException
 import org.onap.cps.spi.exceptions.AnchorNotFoundException
 import org.onap.cps.spi.exceptions.CpsAdminException
+import org.onap.cps.spi.exceptions.CpsPathException
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
 import org.onap.cps.spi.model.DataNode
@@ -150,7 +152,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             thrown(expectedException)
         where: 'the following data is used'
             scenario                 | parentXpath                      | dataNode              || expectedException
-            'parent does not exist'  | 'unknown'                        | newDataNode           || DataNodeNotFoundException
+            'parent does not exist'  | '/unknown'                       | newDataNode           || DataNodeNotFoundException
             'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException
     }
 
@@ -185,9 +187,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'following parameters were used'
-            scenario                     | parentNodeXpath | listElementXpaths                   || expectedException
-            'parent node does not exist' | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
-            'already existing fragment'  | '/parent-201'   | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
+            scenario                        | parentNodeXpath | listElementXpaths                   || expectedException
+            'parent node does not exist'    | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
+            'data fragment already exists'  | '/parent-201'   | ["/parent-201/child-204[@key='A']"] || AlreadyDefinedException
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -207,6 +209,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             'empty xpath' | ''
     }
 
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Cps Path query with syntax error throws a CPS Path Exception.'() {
+        when: 'trying to execute a query with a syntax (parsing) error'
+            objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS)
+        then: 'exception is thrown'
+            def exceptionThrown = thrown(CpsPathException)
+            assert exceptionThrown.getDetails().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'')
+    }
+
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Get data node by xpath with all descendants.'() {
         when: 'data node is requested with all descendants'
@@ -235,10 +246,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
-            scenario                 | dataspaceName  | anchorName                        | xpath          || expectedException
-            'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant' || DataspaceNotFoundException
-            'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant' || AnchorNotFoundException
-            'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH'     || DataNodeNotFoundException
+            scenario                 | dataspaceName  | anchorName                        | xpath           || expectedException
+            'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | '/not relevant' || DataspaceNotFoundException
+            'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | '/not relevant' || AnchorNotFoundException
+            'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO XPATH'     || DataNodeNotFoundException
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -265,10 +276,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
-            scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
-            'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
-            'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
-            'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
+            scenario                 | dataspaceName  | anchorName                        | xpath                 || expectedException
+            'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | '/not relevant'       || DataspaceNotFoundException
+            'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | '/not relevant'       || AnchorNotFoundException
+            'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING XPATH' || DataNodeNotFoundException
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -359,10 +370,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
-            scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
-            'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
-            'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
-            'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
+            scenario                 | dataspaceName  | anchorName                        | xpath                 || expectedException
+            'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | '/not relevant'       || DataspaceNotFoundException
+            'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | '/not relevant'       || AnchorNotFoundException
+            'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING XPATH' || DataNodeNotFoundException
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -468,10 +479,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
         where: 'following parameters were used'
             scenario                          | targetXpaths                                                 | parentFragmentId                     || expectedRemainingChildXpaths
-            'list element with key'           | '/parent-203/child-204[@key="A"]'                            | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="B"]']
-            'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]'              | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]']
+            'list element with key'           | '/parent-203/child-204[@key="A"]'                            | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ["/parent-203/child-203", "/parent-203/child-204[@key='B']"]
+            'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]'              | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ["/parent-202/child-206[@key='A']"]
             'whole list'                      | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
-            'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="A"]', '/parent-203/child-204[@key="B"]']
+            'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -510,9 +521,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             'child of target'                       | '/parent-206/child-206'                            | '/parent-206/child-206'                           || null
             'child data node, parent still exists'  | '/parent-206/child-206'                            | '/parent-206'                                     || '/parent-206'
             'list element'                          | '/parent-206/child-206/grand-child-206[@key="A"]'  | '/parent-206/child-206/grand-child-206[@key="A"]' || null
-            'list element, sibling still exists'    | '/parent-206/child-206/grand-child-206[@key="A"]'  | '/parent-206/child-206/grand-child-206[@key="X"]' || '/parent-206/child-206/grand-child-206[@key="X"]'
+            'list element, sibling still exists'    | '/parent-206/child-206/grand-child-206[@key="A"]'  | '/parent-206/child-206/grand-child-206[@key="X"]' || "/parent-206/child-206/grand-child-206[@key='X']"
             'container node'                        | '/parent-206'                                      | '/parent-206'                                     || null
-            'container list node'                   | '/parent-206[@key="A"]'                            | '/parent-206[@key="B"]'                           || '/parent-206[@key="B"]'
+            'container list node'                   | '/parent-206[@key="A"]'                            | '/parent-206[@key="B"]'                           || "/parent-206[@key='B']"
             'root node with xpath /'                | '/'                                                | '/'                                               || null
             'root node with xpath passed as blank'  | ''                                                 | ''                                                || null
 
@@ -523,11 +534,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         when: 'data node is deleted'
             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
         then: 'a #expectedException is thrown'
-            thrown(DataNodeNotFoundException)
+            thrown(expectedException)
         where: 'the following parameters were used'
-            scenario                                        | datanodeXpath
-            'valid data node, non existent child node'      | '/parent-203/child-non-existent'
-            'invalid list element'                          | '/parent-206/child-206/grand-child-206@key="A"]'
+            scenario                                        | datanodeXpath                                    | expectedException
+            'valid data node, non existent child node'      | '/parent-203/child-non-existent'                 | DataNodeNotFoundException
+            'invalid list element'                          | '/parent-206/child-206/grand-child-206@key="A"]' | PathParsingException
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
index c508762..52f2309 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * ============LICENSE_START=======================================================
  * Copyright (c) 2021 Bell Canada.
+ * Modifications Copyright (C) 2021-2022 Nordix Foundation
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -45,7 +46,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def 'Handling of StaleStateException (caused by concurrent updates) during data node tree update.'() {
 
-        def parentXpath = 'parent-01'
+        def parentXpath = '/parent-01'
         def myDataspaceName = 'my-dataspace'
         def myAnchorName = 'my-anchor'
 
@@ -82,7 +83,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
         }
         when: 'getting the data node represented by this fragment'
         def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
-                'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+                '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
         then: 'the leaf is of the correct value and data type'
         def attributeValue = dataNode.leaves.get('some attribute')
         assert attributeValue == expectedValue
@@ -108,7 +109,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
         }
         when: 'getting the data node represented by this fragment'
         def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
-                'parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+                '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
         then: 'a data validation exception is thrown'
         thrown(DataValidationException)
     }
index 8f525df..d1a6220 100644 (file)
@@ -1,6 +1,6 @@
 /*
    ============LICENSE_START=======================================================
-    Copyright (C) 2021 Nordix Foundation.
+    Copyright (C) 2021-2022 Nordix Foundation.
     Modifications Copyright (C) 2021 Bell Canada.
    ================================================================================
    Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,25 +30,25 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES
 
 INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
     (1, 1001, 1003, null, '/shops', null),
-    (2, 1001, 1003, 1, '/shops/shop[@id=1]', '{"id" : 1, "type" : "bookstore"}'),
-    (3, 1001, 1003, 2, '/shops/shop[@id=1]/categories[@code=1]', '{"code" : 1, "type" : "bookstore", "name": "SciFi"}'),
-    (4, 1001, 1003, 2, '/shops/shop[@id=1]/categories[@code=2]', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'),
-    (5, 1001, 1003, 3, '/shops/shop[@id=1]/categories[@code=1]/book', '{"price" :  5, "title" : "Dune", "labels" : ["special offer","classics",""]}'),
-    (6, 1001, 1003, 4, '/shops/shop[@id=1]/categories[@code=2]/book', '{"price" : 15, "title" : "Chapters", "editions" : [2000,2010,2020]}'),
-    (7, 1001, 1003, 5, '/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '{"FirstName" : "Joe", "Surname": "Bloggs","title": "Dune"}'),
-    (8, 1001, 1003, 6, '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]', '{"FirstName" : "Joe", "Surname": "Smith","title": "Chapters"}');
+    (2, 1001, 1003, 1, '/shops/shop[@id=''1'']', '{"id" : 1, "type" : "bookstore"}'),
+    (3, 1001, 1003, 2, '/shops/shop[@id=''1'']/categories[@code=''1'']', '{"code" : 1, "type" : "bookstore", "name": "SciFi"}'),
+    (4, 1001, 1003, 2, '/shops/shop[@id=''1'']/categories[@code=''2'']', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'),
+    (5, 1001, 1003, 3, '/shops/shop[@id=''1'']/categories[@code=''1'']/book', '{"price" :  5, "title" : "Dune", "labels" : ["special offer","classics",""]}'),
+    (6, 1001, 1003, 4, '/shops/shop[@id=''1'']/categories[@code=''2'']/book', '{"price" : 15, "title" : "Chapters", "editions" : [2000,2010,2020]}'),
+    (7, 1001, 1003, 5, '/shops/shop[@id=''1'']/categories[@code=''1'']/book/author[@FirstName=''Joe'' and @Surname=''Bloggs'']', '{"FirstName" : "Joe", "Surname": "Bloggs","title": "Dune"}'),
+    (8, 1001, 1003, 6, '/shops/shop[@id=''1'']/categories[@code=''2'']/book/author[@FirstName=''Joe'' and @Surname=''Smith'']', '{"FirstName" : "Joe", "Surname": "Smith","title": "Chapters"}');
 
     INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
-    (9, 1001, 1003, 1, '/shops/shop[@id=2]', '{"type" : "bookstore"}'),
-    (10, 1001, 1003, 9, '/shops/shop[@id=2]/categories[@code=1]', '{"code" : 2, "type" : "bookstore", "name": "Kids"}'),
-    (11, 1001, 1003, 10, '/shops/shop[@id=2]/categories[@code=2]', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}');
+    (9, 1001, 1003, 1, '/shops/shop[@id=''2'']', '{"type" : "bookstore"}'),
+    (10, 1001, 1003, 9, '/shops/shop[@id=''2'']/categories[@code=''1'']', '{"code" : 2, "type" : "bookstore", "name": "Kids"}'),
+    (11, 1001, 1003, 10, '/shops/shop[@id=''2'']/categories[@code=''2'']', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}');
 
     INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
-    (12, 1001, 1003, 1, '/shops/shop[@id=3]', '{"type" : "garden centre"}'),
-    (13, 1001, 1003, 12, '/shops/shop[@id=3]/categories[@code=1]', '{"id" : 1, "type" : "garden centre", "name": "indoor plants"}'),
-    (14, 1001, 1003, 12, '/shops/shop[@id=3]/categories[@code=2]', '{"id" : 2, "type" : "garden centre", "name": "outdoor plants"}'),
-    (16, 1001, 1003, 1, '/shops/shop[@id=3]/info', null),
-    (17, 1001, 1003, 1, '/shops/shop[@id=3]/info/contact', null),
-    (18, 1001, 1003, 1, '/shops/shop[@id=3]/info/contact/website', '{"address" : "myshop.ie"}'),
-    (19, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="mob"]', '{"type" : "mob", "number" : "123123456"}'),
-    (20, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="landline"]', '{"type" : "landline", "number" : "012123456"}');
+    (12, 1001, 1003, 1, '/shops/shop[@id=''3'']', '{"type" : "garden centre"}'),
+    (13, 1001, 1003, 12, '/shops/shop[@id=''3'']/categories[@code=''1'']', '{"id" : 1, "type" : "garden centre", "name": "indoor plants"}'),
+    (14, 1001, 1003, 12, '/shops/shop[@id=''3'']/categories[@code=''2'']', '{"id" : 2, "type" : "garden centre", "name": "outdoor plants"}'),
+    (16, 1001, 1003, 1, '/shops/shop[@id=''3'']/info', null),
+    (17, 1001, 1003, 1, '/shops/shop[@id=''3'']/info/contact', null),
+    (18, 1001, 1003, 1, '/shops/shop[@id=''3'']/info/contact/website', '{"address" : "myshop.ie"}'),
+    (19, 1001, 1003, 12, '/shops/shop[@id=''3'']/info/contact/phonenumbers[@type=''mob'']', '{"type" : "mob", "number" : "123123456"}'),
+    (20, 1001, 1003, 12, '/shops/shop[@id=''3'']/info/contact/phonenumbers[@type=''landline'']', '{"type" : "landline", "number" : "012123456"}');
index fb4a5f7..4106541 100755 (executable)
@@ -52,32 +52,32 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
     (4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'),
     (4206, 1001, 3003, null, '/parent-201', '{"leaf-value": "original"}'),
     (4207, 1001, 3003, 4206, '/parent-201/child-203', '{}'),
-    (4208, 1001, 3003, 4206, '/parent-201/child-204[@key="A"]', '{"key": "A"}'),
-    (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="B"]', '{"key": "B"}'),
+    (4208, 1001, 3003, 4206, '/parent-201/child-204[@key=''A'']', '{"key": "A"}'),
+    (4209, 1001, 3003, 4206, '/parent-201/child-204[@key=''B'']', '{"key": "B"}'),
     (4211, 1001, 3003, null, '/parent-202', '{"leaf-value": "original"}'),
-    (4212, 1001, 3003, 4211, '/parent-202/child-205[@key="A" and @key2="B"]', '{"key": "A", "key2": "B"}'),
-    (4213, 1001, 3003, 4211, '/parent-202/child-206[@key="A"]', '{"key": "A"}'),
+    (4212, 1001, 3003, 4211, '/parent-202/child-205[@key=''A'' and @key2=''B'']', '{"key": "A", "key2": "B"}'),
+    (4213, 1001, 3003, 4211, '/parent-202/child-206[@key=''A'']', '{"key": "A"}'),
     (4214, 1001, 3003, null, '/parent-203', '{"leaf-value": "original"}'),
     (4215, 1001, 3003, 4214, '/parent-203/child-203', '{}'),
-    (4216, 1001, 3003, 4214, '/parent-203/child-204[@key="A"]', '{"key": "A"}'),
-    (4217, 1001, 3003, 4214, '/parent-203/child-204[@key="B"]', '{"key": "B"}'),
-    (4218, 1001, 3003, 4217, '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]', '{"key": "B", "key2": "Y"}'),
+    (4216, 1001, 3003, 4214, '/parent-203/child-204[@key=''A'']', '{"key": "A"}'),
+    (4217, 1001, 3003, 4214, '/parent-203/child-204[@key=''B'']', '{"key": "B"}'),
+    (4218, 1001, 3003, 4217, '/parent-203/child-204[@key=''B'']/grand-child-204[@key2=''Y'']', '{"key": "B", "key2": "Y"}'),
     (4226, 1001, 3003, null, '/parent-206', '{"leaf-value": "original"}'),
     (4227, 1001, 3003, 4226, '/parent-206/child-206', '{}'),
     (4228, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206', '{}'),
-    (4229, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="A"]', '{"key": "A"}'),
-    (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="X"]', '{"key": "X"}'),
-    (4231, 1001, 3003, null, '/parent-206[@key="A"]', '{"key": "A"}'),
-    (4232, 1001, 3003, 4231, '/parent-206[@key="A"]/child-206', '{}'),
-    (4233, 1001, 3003, null, '/parent-206[@key="B"]', '{"key": "B"}');
+    (4229, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key=''A'']', '{"key": "A"}'),
+    (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key=''X'']', '{"key": "X"}'),
+    (4231, 1001, 3003, null, '/parent-206[@key=''A'']', '{"key": "A"}'),
+    (4232, 1001, 3003, 4231, '/parent-206[@key=''A'']/child-206', '{}'),
+    (4233, 1001, 3003, null, '/parent-206[@key=''B'']', '{"key": "B"}');
 
 INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
-    (5000, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo"]', '{"id": "PNFDemo", "dmi-service-name": "http://172.21.235.14:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
-    (5001, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo2"]', '{"id": "PNFDemo2", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
-    (5002, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo3"]', '{"id": "PNFDemo3", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
-    (5003, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo4"]', '{"id": "PNFDemo4", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
-    (5004, 1002, 3004, 5000, '/dmi-registry/cm-handles[@id="PNFDemo"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
-    (5005, 1002, 3004, 5001, '/dmi-registry/cm-handles[@id="PNFDemo2"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
-    (5006, 1002, 3004, 5002, '/dmi-registry/cm-handles[@id="PNFDemo3"]/public-properties[@name="Contact"]', '{"name": "Contact3", "value": "PNF3@bookstore.com"}'),
-    (5007, 1002, 3004, 5003, '/dmi-registry/cm-handles[@id="PNFDemo4"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
-    (5008, 1002, 3004, 5004, '/dmi-registry/cm-handles[@id="PNFDemo4"]/public-properties[@name="Contact2"]', '{"name": "Contact2", "value": "newemailforstore2@bookstore.com"}');
\ No newline at end of file
+    (5000, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo'']', '{"id": "PNFDemo", "dmi-service-name": "http://172.21.235.14:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+    (5001, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo2'']', '{"id": "PNFDemo2", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+    (5002, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo3'']', '{"id": "PNFDemo3", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+    (5003, 1002, 3004, null, '/dmi-registry/cm-handles[@id=''PNFDemo4'']', '{"id": "PNFDemo4", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+    (5004, 1002, 3004, 5000, '/dmi-registry/cm-handles[@id=''PNFDemo'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+    (5005, 1002, 3004, 5001, '/dmi-registry/cm-handles[@id=''PNFDemo2'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+    (5006, 1002, 3004, 5002, '/dmi-registry/cm-handles[@id=''PNFDemo3'']/public-properties[@name=''Contact'']', '{"name": "Contact3", "value": "PNF3@bookstore.com"}'),
+    (5007, 1002, 3004, 5003, '/dmi-registry/cm-handles[@id=''PNFDemo4'']/public-properties[@name=''Contact'']', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+    (5008, 1002, 3004, 5004, '/dmi-registry/cm-handles[@id=''PNFDemo4'']/public-properties[@name=''Contact2'']', '{"name": "Contact2", "value": "newemailforstore2@bookstore.com"}');