From: Lathish Date: Thu, 31 Mar 2022 16:29:22 +0000 (+0100) Subject: Fix Absolute Path to list with Integer/String key X-Git-Tag: 3.1.0~138^2 X-Git-Url: https://gerrit.onap.org/r/gitweb?p=cps.git;a=commitdiff_plain;h=2f09266fd3231529e41ce97b02577bc5b82a8c03 Fix Absolute Path to list with Integer/String key - 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 --- diff --git a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 index cefeac438..40ad410a0 100644 --- a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 +++ b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 @@ -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 diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java index ebf6fd3c9..21f5173a9 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 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 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("'"); + } } diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java index de7adf2b7..53490f3a2 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java @@ -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 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 index 000000000..97d7d1d76 --- /dev/null +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java @@ -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 index 000000000..4a67167c9 --- /dev/null +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/PathParsingException.java @@ -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; + } +} diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy index bfec574eb..b837a64fe 100644 --- a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy +++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 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 } - } diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy index 2aa4ddd1e..d4c68c30a 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy @@ -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 diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index bb3c2d07d..847a1d129 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -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 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); diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy index ae88d302b..36b378a77 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy @@ -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' || [] } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy index ab290051a..6f780fc50 100755 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy @@ -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]) diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy index c50876205..52f2309cc 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy @@ -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) } diff --git a/cps-ri/src/test/resources/data/cps-path-query.sql b/cps-ri/src/test/resources/data/cps-path-query.sql index 8f525df6b..d1a62209e 100644 --- a/cps-ri/src/test/resources/data/cps-path-query.sql +++ b/cps-ri/src/test/resources/data/cps-path-query.sql @@ -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"}'); diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index fb4a5f77c..410654106 100755 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -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"}');