aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToineSiebelink <toine.siebelink@est.tech>2021-07-06 13:03:03 +0100
committerToineSiebelink <toine.siebelink@est.tech>2021-07-20 16:19:37 +0100
commitf0527c58c17963d940535d0ce0eb934c2b4c635c (patch)
treef2d8df5b92f3b3e50655fb9a0d685672bf010f0a
parent6355b212de77e658b16614eb775f03c7713c8460 (diff)
Support text() condition
- Added Antlr parsing of text() condition (as an optional additional to any query) - Implemented text-condition combined with descendants - Refactor descendants queries into using one more flexible Custom (native) Query builder - Refactor ALL cpsPath queries to now use FragmentRepositoryCpsPathQuery (custom query builder) - Refactor Antrl code to simply parsing of cpsPath and allow all combinations (no more query types, addresses CPS-436) - Minor clean up of some minor convention issues in CpsAdminServiceImplSpec.groovy (found during groovy demo) - Update .rst documentation of xPaths - Fixed incorrect matching of additional list indexes using more precise SIMILAR-TO regex in postgreSQL - Documented special chararter limitation (CPS-500) - Checked for consistent use of term 'CPS path' in documentation and error message - Included (updated) copyright in all .SQL test files Issue-ID: CPS-452 Issue-ID: CPS-436 Issue-ID: CPS-500 Signed-off-by: ToineSiebelink <toine.siebelink@est.tech> Change-Id: If422d25cafd2850d25c9a28dea16ba7a5f93dddb
-rw-r--r--cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g454
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java52
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathPrefixType.java (renamed from cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java)17
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java28
-rw-r--r--cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy63
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java17
-rwxr-xr-xcps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java41
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java29
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java120
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy46
-rwxr-xr-xcps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java6
-rw-r--r--cps-ri/src/test/resources/data/anchor.sql24
-rw-r--r--cps-ri/src/test/resources/data/clear-all.sql22
-rw-r--r--cps-ri/src/test/resources/data/cps-path-query.sql27
-rwxr-xr-xcps-ri/src/test/resources/data/fragment.sql24
-rw-r--r--cps-ri/src/test/resources/data/schemaset.sql22
-rwxr-xr-xcps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy10
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy14
-rw-r--r--cps-service/src/test/resources/test-tree.json8
-rw-r--r--cps-service/src/test/resources/test-tree.yang12
-rw-r--r--docs/cps-path.rst153
-rwxr-xr-xdocs/release-notes.rst6
22 files changed, 555 insertions, 240 deletions
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 198cede0d1..cefeac4387 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
@@ -20,56 +20,50 @@
grammar CpsPath ;
-cpsPath: (cpsPathWithSingleLeafCondition | cpsPathWithDescendant | cpsPathWithDescendantAndLeafConditions) ancestorAxis? ;
+cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? ;
-ancestorAxis: SLASH KW_ANCESTOR COLONCOLON ancestorPath ;
+ancestorAxis : SLASH KW_ANCESTOR COLONCOLON ancestorPath ;
-ancestorPath: yangElement (SLASH yangElement)* ;
+ancestorPath : yangElement ( SLASH yangElement)* ;
-cpsPathWithSingleLeafCondition: prefix singleValueCondition postfix? ;
+textFunctionCondition : SLASH leafName OB KW_TEXT_FUNCTION EQ StringLiteral CB ;
-/*
-No need to ditinguish between cpsPathWithDescendant | cpsPathWithDescendantAndLeafConditions really!
-See https://jira.onap.org/browse/CPS-436
-*/
-
-cpsPathWithDescendant: descendant ;
-
-cpsPathWithDescendantAndLeafConditions: descendant multipleValueConditions ;
+prefix : ( SLASH yangElement)* SLASH containerName ;
-descendant: SLASH prefix ;
+descendant : SLASH prefix ;
-prefix: (SLASH yangElement)* SLASH containerName ;
+incorrectPrefix : SLASH SLASH SLASH+ ;
-postfix: (SLASH yangElement)+ ;
+yangElement : containerName listElementRef? ;
-yangElement: containerName listElementRef? ;
+containerName : QName ;
-containerName: QName ;
+listElementRef : OB leafCondition ( KW_AND leafCondition)* CB ;
-listElementRef: multipleValueConditions ;
+multipleLeafConditions : OB leafCondition ( KW_AND leafCondition)* CB ;
-singleValueCondition: '[' leafCondition ']' ;
+leafCondition : AT leafName EQ ( IntegerLiteral | StringLiteral) ;
-multipleValueConditions: '[' leafCondition (' and ' leafCondition)* ']' ;
-
-leafCondition: '@' leafName '=' (IntegerLiteral | StringLiteral ) ;
-
-//To Confirm: defintion of Lefname with external xPath grammar
-leafName: QName ;
+leafName : QName ;
/*
* Lexer Rules
- * Most of the lexer rules below are 'imporetd' from
+ * Most of the lexer rules below are inspired by
* https://raw.githubusercontent.com/antlr/grammars-v4/master/xpath/xpath31/XPath31.g4
*/
-SLASH : '/';
+AT : '@' ;
+CB : ']' ;
COLONCOLON : '::' ;
+EQ : '=' ;
+OB : '[' ;
+SLASH : '/' ;
// KEYWORDS
KW_ANCESTOR : 'ancestor' ;
+KW_AND : 'and' ;
+KW_TEXT_FUNCTION: 'text()' ;
IntegerLiteral : FragDigits ;
// Add below type definitions for leafvalue comparision in https://jira.onap.org/browse/CPS-440
@@ -77,7 +71,7 @@ DecimalLiteral : ('.' FragDigits) | (FragDigits '.' [0-9]*) ;
DoubleLiteral : (('.' FragDigits) | (FragDigits ('.' [0-9]*)?)) [eE] [+-]? FragDigits ;
StringLiteral : ('"' (FragEscapeQuot | ~[^"])*? '"') | ('\'' (FragEscapeApos | ~['])*? '\'') ;
fragment FragEscapeQuot : '""' ;
-fragment FragEscapeApos : '\'';
+fragment FragEscapeApos : '\'' ;
fragment FragDigits : [0-9]+ ;
QName : FragQName ;
@@ -109,7 +103,7 @@ fragment FragNCNameChar
| '\u00B7' | '\u0300'..'\u036F'
| '\u203F'..'\u2040'
;
-fragment FragmentNCName : FragNCNameStartChar FragNCNameChar* ;
+fragment FragmentNCName : FragNCNameStartChar FragNCNameChar* ;
// https://www.w3.org/TR/REC-xml/#NT-Char
@@ -117,7 +111,7 @@ fragment FragChar : '\u0009' | '\u000a' | '\u000d'
| '\u0020'..'\ud7ff'
| '\ue000'..'\ufffd'
| '\u{10000}'..'\u{10ffff}'
- ;
+ ;
// Skip all Whitespace
Whitespace : ('\u000d' | '\u000a' | '\u0020' | '\u0009')+ -> skip ;
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 b9d0c25b13..ebf6fd3c91 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
@@ -20,18 +20,18 @@
package org.onap.cps.cpspath.parser;
+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.AncestorAxisContext;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.CpsPathWithDescendantAndLeafConditionsContext;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.CpsPathWithDescendantContext;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.CpsPathWithSingleLeafConditionContext;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.DescendantContext;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.IncorrectPrefixContext;
import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.LeafConditionContext;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.MultipleValueConditionsContext;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.PostfixContext;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.MultipleLeafConditionsContext;
import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.PrefixContext;
-import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.SingleValueConditionContext;
+import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.TextFunctionConditionContext;
public class CpsPathBuilder extends CpsPathBaseListener {
@@ -45,8 +45,8 @@ public class CpsPathBuilder extends CpsPathBaseListener {
}
@Override
- public void exitPostfix(final PostfixContext ctx) {
- throw new IllegalStateException(String.format("Unsupported postfix %s encountered in CpsPath.", ctx.getText()));
+ public void exitIncorrectPrefix(final IncorrectPrefixContext ctx) {
+ throw new IllegalStateException("CPS path can only start with one or two slashes (/)");
}
@Override
@@ -64,38 +64,18 @@ public class CpsPathBuilder extends CpsPathBaseListener {
}
@Override
- public void enterSingleValueCondition(final SingleValueConditionContext ctx) {
- leavesData.clear();
+ public void exitDescendant(final DescendantContext ctx) {
+ cpsPathQuery.setCpsPathPrefixType(DESCENDANT);
+ cpsPathQuery.setDescendantName(ctx.getText().substring(2));
}
@Override
- public void enterMultipleValueConditions(final MultipleValueConditionsContext ctx) {
+ public void enterMultipleLeafConditions(final MultipleLeafConditionsContext ctx) {
leavesData.clear();
}
@Override
- public void exitSingleValueCondition(final SingleValueConditionContext ctx) {
- final String leafName = ctx.leafCondition().leafName().getText();
- cpsPathQuery.setLeafName(leafName);
- cpsPathQuery.setLeafValue(leavesData.get(leafName));
- }
-
- @Override
- public void exitCpsPathWithSingleLeafCondition(final CpsPathWithSingleLeafConditionContext ctx) {
- cpsPathQuery.setCpsPathQueryType(CpsPathQueryType.XPATH_LEAF_VALUE);
- }
-
- @Override
- public void exitCpsPathWithDescendant(final CpsPathWithDescendantContext ctx) {
- cpsPathQuery.setCpsPathQueryType(CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE);
- cpsPathQuery.setDescendantName(cpsPathQuery.getXpathPrefix().substring(1));
- }
-
- @Override
- public void exitCpsPathWithDescendantAndLeafConditions(
- final CpsPathWithDescendantAndLeafConditionsContext ctx) {
- cpsPathQuery.setCpsPathQueryType(CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES);
- cpsPathQuery.setDescendantName(cpsPathQuery.getXpathPrefix().substring(1));
+ public void exitMultipleLeafConditions(final MultipleLeafConditionsContext ctx) {
cpsPathQuery.setLeavesData(leavesData);
}
@@ -104,6 +84,12 @@ public class CpsPathBuilder extends CpsPathBaseListener {
cpsPathQuery.setAncestorSchemaNodeIdentifier(ctx.ancestorPath().getText());
}
+ @Override
+ public void exitTextFunctionCondition(final TextFunctionConditionContext ctx) {
+ cpsPathQuery.setTextFunctionConditionLeafName(ctx.leafName().getText());
+ cpsPathQuery.setTextFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText()));
+ }
+
CpsPathQuery build() {
return cpsPathQuery;
}
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathPrefixType.java
index ac3e313340..dfac9b0c89 100644
--- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java
+++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathPrefixType.java
@@ -22,19 +22,16 @@
package org.onap.cps.cpspath.parser;
/**
- * The enum Cps path query type.
+ * The enum Cps path prefix type.
*/
-public enum CpsPathQueryType {
+public enum CpsPathPrefixType {
/**
- * Xpath descendant anywhere type e.g. //nodeName .
+ * Fully qualified Xpath starting from root with single slash e.g. /parent/child .
*/
- XPATH_HAS_DESCENDANT_ANYWHERE,
- /**
- * Xpath descendant anywhere type e.g. //nodeName[@leafName="value"] .
- */
- XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES,
+ ABSOLUTE,
+
/**
- * Xpath leaf value cps path query type e.g. /cps-path[@leaf1="leafValue" and @leaf2=123] .
+ * Xpath descendant anywhere starting with double slash type e.g. //child/grandchild .
*/
- XPATH_LEAF_VALUE
+ DESCENDANT
}
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 107bfa3e3a..de7adf2b71 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
@@ -20,6 +20,8 @@
package org.onap.cps.cpspath.parser;
+import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE;
+
import java.util.Map;
import lombok.AccessLevel;
import lombok.Getter;
@@ -36,13 +38,13 @@ import org.onap.cps.cpspath.parser.antlr4.CpsPathParser;
@Setter(AccessLevel.PACKAGE)
public class CpsPathQuery {
- private CpsPathQueryType cpsPathQueryType;
private String xpathPrefix;
- private String leafName;
- private Object leafValue;
+ private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE;
private String descendantName;
private Map<String, Object> leavesData;
private String ancestorSchemaNodeIdentifier = "";
+ private String textFunctionConditionLeafName;
+ private String textFunctionConditionValue;
/**
* Returns a cps path query.
@@ -68,7 +70,7 @@ public class CpsPathQuery {
}
/**
- * Has ancestor axis been populated.
+ * Has ancestor axis been included in cpsPath.
*
* @return boolean value.
*/
@@ -76,4 +78,22 @@ public class CpsPathQuery {
return !(ancestorSchemaNodeIdentifier.isEmpty());
}
+ /**
+ * Have leaf value conditions been included in cpsPath.
+ *
+ * @return boolean value.
+ */
+ public boolean hasLeafConditions() {
+ return leavesData != null;
+ }
+
+ /**
+ * Has text function condition been included in cpsPath.
+ *
+ * @return boolean value.
+ */
+ public boolean hasTextFunctionCondition() {
+ return textFunctionConditionLeafName != null;
+ }
+
}
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 b7826f67bb..bfec574eba 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
@@ -22,17 +22,21 @@ package org.onap.cps.cpspath.parser
import spock.lang.Specification
+import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE
+import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT
+
class CpsPathQuerySpec extends Specification {
def 'Parse cps path with valid cps path and a filter with #scenario.'() {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath)
- then: 'the query has the right type'
- result.cpsPathQueryType == CpsPathQueryType.XPATH_LEAF_VALUE
+ then: 'the query has the right xpath type'
+ result.cpsPathPrefixType == ABSOLUTE
and: 'the right query parameters are set'
result.xpathPrefix == expectedXpathPrefix
- result.leafName == expectedLeafName
- result.leafValue == expectedLeafValue
+ result.hasLeafConditions() == true
+ result.leavesData.containsKey(expectedLeafName) == true
+ 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'
@@ -46,8 +50,8 @@ class CpsPathQuerySpec extends Specification {
def 'Parse cps path of type ends with a #scenario.'() {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath)
- then: 'the query has the right type'
- result.cpsPathQueryType == CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE
+ then: 'the query has the right xpath type'
+ result.cpsPathPrefixType == DESCENDANT
and: 'the right ends with parameters are set'
result.descendantName == expectedDescendantName
where: 'the following data is used'
@@ -59,9 +63,9 @@ class CpsPathQuerySpec extends Specification {
def 'Parse cps path that ends with a yang list containing #scenario.'() {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath)
- then: 'the query has the right type'
- result.cpsPathQueryType == CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES
- and: 'the right ends with parameters are set'
+ then: 'the query has the right xpath type'
+ result.cpsPathPrefixType == DESCENDANT
+ and: 'the right parameters are set'
result.descendantName == "child"
result.leavesData.size() == expectedNumberOfLeaves
where: 'the following data is used'
@@ -70,6 +74,27 @@ class CpsPathQuerySpec extends Specification {
'more than one attribute' | '//child[@int-leaf=5 and @leaf-name="leaf value"]' || 2
}
+ def 'Parse #scenario cps path with text function condition'() {
+ when: 'the given cps path is parsed'
+ def result = CpsPathQuery.createFrom(cpsPath)
+ then: 'the query has the right xpath type'
+ result.cpsPathPrefixType == DESCENDANT
+ and: 'leaf conditions are only present when expected'
+ result.hasLeafConditions() == expectLeafConditions
+ and: 'the right text function condition is set'
+ result.hasTextFunctionCondition()
+ result.textFunctionConditionLeafName == 'leaf-name'
+ result.textFunctionConditionValue == 'search'
+ and: 'the ancestor is only present when expected'
+ assert result.hasAncestorAxis() == expectHasAncestorAxis
+ where: 'the following data is used'
+ scenario | cpsPath || expectLeafConditions | expectHasAncestorAxis
+ 'descendant anywhere' | '//someContainer/leaf-name[text()="search"]' || false | false
+ 'descendant with leaf value' | '//child[@other-leaf=1]/leaf-name[text()="search"]' || true | false
+ 'descendant anywhere and ancestor' | '//someContainer/leaf-name[text()="search"]/ancestor::parent' || false | true
+ 'descendant with leaf value and ancestor' | '//child[@other-leaf=1]/leaf-name[text()="search"]/ancestor::parent' || true | true
+ }
+
def 'Parse cps path with error: #scenario.'() {
when: 'the given cps path is parsed'
CpsPathQuery.createFrom(cpsPath)
@@ -85,18 +110,20 @@ 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::'
- 'unsupported postfix after single value condition (JIRA CPS-450)' | '/parent/child[@id=1]/somePostFix'
+// DISCUSS WITH TEAM : 'unsupported postfix after value condition (JIRA CPS-450)' | '/parent/child[@id=1]/somePostFix'
}
def 'Parse cps path using ancestor by schema node identifier with a #scenario.'() {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom('//descendant/ancestor::' + ancestorPath)
then: 'the query has the right type'
- result.cpsPathQueryType == CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE
+ result.cpsPathPrefixType == DESCENDANT
and: 'the result has ancestor axis'
result.hasAncestorAxis()
and: 'the correct ancestor schema node identifier is set'
result.ancestorSchemaNodeIdentifier == ancestorPath
+ and: 'there are no leaves conditions'
+ result.hasLeafConditions() == false
where:
scenario | ancestorPath
'basic container' | 'someContainer'
@@ -109,18 +136,20 @@ class CpsPathQuerySpec extends Specification {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath + '/ancestor::someAncestor')
then: 'the query has the right type'
- result.cpsPathQueryType == expectedCpsPathQueryType
+ result.cpsPathPrefixType == DESCENDANT
+ and: 'leaf conditions are only present when expected'
+ result.hasLeafConditions() == expectLeafConditions
and: 'the result has ancestor axis'
result.hasAncestorAxis()
and: 'the correct ancestor schema node identifier is set'
result.ancestorSchemaNodeIdentifier == 'someAncestor'
result.descendantName == expectedDescendantName
where:
- scenario | cpsPath || expectedDescendantName | expectedCpsPathQueryType
- 'basic container' | '//someContainer' || 'someContainer' | CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE
- 'container with parent' | '//parent/child' || 'parent/child' | CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE
- 'container with list-parent' | '//parent[@id=1]/child' || 'parent[@id=1]/child' | CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE
- 'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || 'parent[@id=1]/child' | CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES
+ 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-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 844ad84744..6e12d06011 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
@@ -38,7 +38,6 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.transaction.Transactional;
import org.onap.cps.cpspath.parser.CpsPathQuery;
-import org.onap.cps.cpspath.parser.CpsPathQueryType;
import org.onap.cps.spi.CpsDataPersistenceService;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.entities.AnchorEntity;
@@ -179,20 +178,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
} catch (final IllegalStateException e) {
throw new CpsPathException(e.getMessage());
}
- List<FragmentEntity> fragmentEntities;
- if (CpsPathQueryType.XPATH_LEAF_VALUE.equals(cpsPathQuery.getCpsPathQueryType())) {
- fragmentEntities = fragmentRepository
- .getByAnchorAndXpathAndLeafAttributes(anchorEntity.getId(), cpsPathQuery.getXpathPrefix(),
- cpsPathQuery.getLeafName(), cpsPathQuery.getLeafValue());
- } else if (CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES.equals(cpsPathQuery.getCpsPathQueryType())) {
- final String leafDataAsJson = GSON.toJson(cpsPathQuery.getLeavesData());
- fragmentEntities = fragmentRepository
- .getByAnchorAndDescendentNameAndLeafValues(anchorEntity.getId(), cpsPathQuery.getDescendantName(),
- leafDataAsJson);
- } else {
- fragmentEntities = fragmentRepository
- .getByAnchorAndXpathEndsInDescendantName(anchorEntity.getId(), cpsPathQuery.getDescendantName());
- }
+ List<FragmentEntity> fragmentEntities =
+ fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
if (cpsPathQuery.hasAncestorAxis()) {
final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
fragmentEntities = ancestorXpaths.isEmpty()
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
index 4d7e7ff547..c48c79ef65 100755
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020-201 Nordix Foundation.
+ * Copyright (C) 2020-2021 Nordix Foundation.
* Modifications Copyright (C) 2020-2021 Bell Canada.
* Modifications Copyright (C) 2020-2021 Pantheon.tech.
* ================================================================================
@@ -38,13 +38,15 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
-public interface FragmentRepository extends JpaRepository<FragmentEntity, Long> {
+public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery {
Optional<FragmentEntity> findByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity,
- @NonNull AnchorEntity anchorEntity, @NonNull String xpath);
+ @NonNull AnchorEntity anchorEntity,
+ @NonNull String xpath);
default FragmentEntity getByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity,
- @NonNull AnchorEntity anchorEntity, @NonNull String xpath) {
+ @NonNull AnchorEntity anchorEntity,
+ @NonNull String xpath) {
return findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, xpath)
.orElseThrow(() -> new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath));
}
@@ -52,42 +54,19 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>
@Query(
value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND dataspace_id = :dataspace AND parent_id is NULL",
nativeQuery = true)
- List<FragmentEntity> findRootsByDataspaceAndAnchor(
- @Param("dataspace") int dataspaceId, @Param("anchor") int anchorId);
+ List<FragmentEntity> findRootsByDataspaceAndAnchor(@Param("dataspace") int dataspaceId,
+ @Param("anchor") int anchorId);
default FragmentEntity findFirstRootByDataspaceAndAnchor(@NonNull DataspaceEntity dataspaceEntity,
- @NonNull AnchorEntity anchorEntity) {
+ @NonNull AnchorEntity anchorEntity) {
return findRootsByDataspaceAndAnchor(dataspaceEntity.getId(), anchorEntity.getId()).stream().findFirst()
.orElseThrow(() -> new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName()));
}
List<FragmentEntity> findAllByAnchorAndXpathIn(@NonNull AnchorEntity anchorEntity,
- @NonNull Collection<String> xpath);
+ @NonNull Collection<String> xpath);
@Modifying
@Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)")
void deleteByAnchorIn(@NotNull @Param("anchors") Collection<AnchorEntity> anchorEntities);
-
- @Query(value =
- "SELECT * FROM FRAGMENT WHERE (anchor_id = :anchor) AND (xpath = (:xpath) OR xpath LIKE "
- + "CONCAT(:xpath,'\\[@%]')) AND attributes @> jsonb_build_object(:leafName , :leafValue)",
- nativeQuery = true)
- // Above query will match an xpath with or without the index for a list [@key=value] and match anchor id,
- // leaf name and leaf value
- List<FragmentEntity> getByAnchorAndXpathAndLeafAttributes(@Param("anchor") int anchorId, @Param("xpath")
- String xpathPrefix, @Param("leafName") String leafName, @Param("leafValue") Object leafValue);
-
- @Query(value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND xpath LIKE CONCAT('%/',:descendantName)",
- nativeQuery = true)
- // Above query will match the anchor id and last descendant name
- List<FragmentEntity> getByAnchorAndXpathEndsInDescendantName(@Param("anchor") int anchorId,
- @Param("descendantName") String descendantName);
-
- @Query(value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND (xpath LIKE CONCAT('%/',:descendantName) OR "
- + "xpath LIKE CONCAT('%/', :descendantName,'\\[@%]')) AND attributes @> :leafDataAsJson\\:\\:jsonb",
- nativeQuery = true)
- // Above query will match the anchor id, last descendant name and all parameters passed into leafDataASJson with the
- // attribute values of the requested data node eg: {"leaf_name":"value", "another_leaf_name":"another value"}​​​​​​
- List<FragmentEntity> getByAnchorAndDescendentNameAndLeafValues(@Param("anchor") int anchorId,
- @Param("descendantName") String descendantName, @Param("leafDataAsJson") String leafDataAsJson);
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java
new file mode 100644
index 0000000000..04138ecc64
--- /dev/null
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java
@@ -0,0 +1,29 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 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.spi.repository;
+
+import java.util.List;
+import org.onap.cps.cpspath.parser.CpsPathQuery;
+import org.onap.cps.spi.entities.FragmentEntity;
+
+public interface FragmentRepositoryCpsPathQuery {
+ List<FragmentEntity> findByAnchorAndCpsPath(int anchorId, CpsPathQuery cpsPathQuery);
+}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
new file mode 100644
index 0000000000..4aa3e5fb36
--- /dev/null
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
@@ -0,0 +1,120 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 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.spi.repository;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.persistence.Query;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.onap.cps.cpspath.parser.CpsPathPrefixType;
+import org.onap.cps.cpspath.parser.CpsPathQuery;
+import org.onap.cps.spi.entities.FragmentEntity;
+
+public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCpsPathQuery {
+
+ public static final String SIMILAR_TO_ABSOLUTE_PATH_PREFIX = "%/";
+ public static final String SIMILAR_TO_OPTIONAL_LIST_INDEX_POSTFIX = "(\\[[^/]*])?";
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ private static final Gson GSON = new GsonBuilder().create();
+
+ @Override
+ public List<FragmentEntity> findByAnchorAndCpsPath(final int anchorId, final CpsPathQuery cpsPathQuery) {
+ final var sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE anchor_id = :anchorId");
+ final Map<String, Object> queryParameters = new HashMap<>();
+ queryParameters.put("anchorId", anchorId);
+ sqlStringBuilder.append(" AND xpath SIMILAR TO :xpathRegex");
+ final String xpathRegex = getSimilarToXpathSqlRegex(cpsPathQuery);
+ queryParameters.put("xpathRegex", xpathRegex);
+ if (cpsPathQuery.hasLeafConditions()) {
+ sqlStringBuilder.append(" AND attributes @> :leafDataAsJson\\:\\:jsonb");
+ queryParameters.put("leafDataAsJson", GSON.toJson(cpsPathQuery.getLeavesData()));
+ }
+
+ addTextFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters);
+ final var query = entityManager.createNativeQuery(sqlStringBuilder.toString(), FragmentEntity.class);
+ setQueryParameters(query, queryParameters);
+ return query.getResultList();
+ }
+
+ @NotNull
+ private static String getSimilarToXpathSqlRegex(final CpsPathQuery cpsPathQuery) {
+ final var xpathRegexBuilder = new StringBuilder();
+ if (CpsPathPrefixType.ABSOLUTE.equals(cpsPathQuery.getCpsPathPrefixType())) {
+ xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getXpathPrefix()));
+ } else {
+ xpathRegexBuilder.append(SIMILAR_TO_ABSOLUTE_PATH_PREFIX);
+ xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getDescendantName()));
+ }
+ xpathRegexBuilder.append(SIMILAR_TO_OPTIONAL_LIST_INDEX_POSTFIX);
+ return xpathRegexBuilder.toString();
+ }
+
+ @NotNull
+ private static String escapeXpath(final String xpath) {
+ // See https://jira.onap.org/browse/CPS-500 for limitations of this basic escape mechanism
+ return xpath.replace("[@", "\\[@");
+ }
+
+ @Nullable
+ private static Integer getTextValueAsInt(final CpsPathQuery cpsPathQuery) {
+ try {
+ return Integer.parseInt(cpsPathQuery.getTextFunctionConditionValue());
+ } catch (final NumberFormatException e) {
+ return null;
+ }
+ }
+
+ private static void addTextFunctionCondition(final CpsPathQuery cpsPathQuery, final StringBuilder sqlStringBuilder,
+ final Map<String, Object> queryParameters) {
+ if (cpsPathQuery.hasTextFunctionCondition()) {
+ sqlStringBuilder.append(" AND (");
+ sqlStringBuilder.append("attributes @> jsonb_build_object(:textLeafName, :textValue)");
+ sqlStringBuilder
+ .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValue))");
+ queryParameters.put("textLeafName", cpsPathQuery.getTextFunctionConditionLeafName());
+ queryParameters.put("textValue", cpsPathQuery.getTextFunctionConditionValue());
+ final var textValueAsInt = getTextValueAsInt(cpsPathQuery);
+ if (textValueAsInt != null) {
+ sqlStringBuilder.append(" OR attributes @> jsonb_build_object(:textLeafName, :textValueAsInt)");
+ sqlStringBuilder
+ .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValueAsInt))");
+ queryParameters.put("textValueAsInt", textValueAsInt);
+ }
+ sqlStringBuilder.append(")");
+ }
+ }
+
+ private static void setQueryParameters(final Query query, final Map<String, Object> queryParameters) {
+ for (final Map.Entry<String, Object> queryParameter : queryParameters.entrySet()) {
+ query.setParameter(queryParameter.getKey(), queryParameter.getValue());
+ }
+ }
+
+}
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 8dc9b7f3cb..ae88d302bb 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
@@ -23,7 +23,6 @@ package org.onap.cps.spi.impl
import org.onap.cps.spi.CpsDataPersistenceService
import org.onap.cps.spi.exceptions.CpsPathException
-import org.onap.cps.spi.model.DataNode
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.jdbc.Sql
@@ -38,23 +37,25 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
static final String SET_DATA = '/data/cps-path-query.sql'
@Sql([CLEAR_DATA, SET_DATA])
- def 'Cps Path query for single leaf value with type: #type.'() {
+ def 'Cps Path query for leaf value(s) with : #scenario.'() {
when: 'a query is executed to get a data node by the given cps path'
def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_SHOP_EXAMPLE, cpsPath, includeDescendantsOption)
+ then: 'the correct number of parent nodes are returned'
+ result.size() == expectedNumberOfParentNodes
then: 'the correct data is returned'
- def leaves = '[price:15.0, title:Dune]'
- DataNode dataNode = result.stream().findFirst().get()
- dataNode.getLeaves().toString() == leaves
- dataNode.getChildDataNodes().size() == expectedNumberOfChidlNodes
+ result.each {
+ assert it.getChildDataNodes().size() == expectedNumberOfChildNodes
+ }
where: 'the following data is used'
- type | cpsPath | includeDescendantsOption || expectedNumberOfChidlNodes
- 'String and no descendants' | '/shops/shop[@id=1]/categories[@code=1]/book[@title="Dune"]' | OMIT_DESCENDANTS || 0
- 'Integer and descendants' | '/shops/shop[@id=1]/categories[@code=1]/book[@price=15]' | INCLUDE_ALL_DESCENDANTS || 1
+ scenario | cpsPath | includeDescendantsOption || expectedNumberOfParentNodes | expectedNumberOfChildNodes
+ 'String and no descendants' | '/shops/shop[@id=1]/categories[@code=1]/book[@title="Dune"]' | OMIT_DESCENDANTS || 1 | 0
+ 'Integer and descendants' | '/shops/shop[@id=1]/categories[@code=1]/book[@price=5]' | INCLUDE_ALL_DESCENDANTS || 1 | 1
+ 'No condition no descendants' | '/shops/shop[@id=1]/categories' | OMIT_DESCENDANTS || 2 | 0
}
@Sql([CLEAR_DATA, SET_DATA])
def 'Query for attribute by cps path with cps paths that return no data because of #scenario.'() {
- when: 'a query is executed to get datanodes for the given cps path'
+ when: 'a query is executed to get data nodes for the given cps path'
def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_SHOP_EXAMPLE, cpsPath, OMIT_DESCENDANTS)
then: 'no data is returned'
result.isEmpty()
@@ -71,7 +72,7 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
def cpsPath = '//categories[@code=1]'
def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_SHOP_EXAMPLE, cpsPath, includeDescendantsOption)
then: 'the data node has the correct number of children'
- DataNode dataNode = result.stream().findFirst().get()
+ def dataNode = result.stream().findFirst().get()
dataNode.getChildDataNodes().size() == expectedNumberOfChildNodes
where: 'the following data is used'
type | includeDescendantsOption || expectedNumberOfChildNodes
@@ -90,9 +91,16 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
assert result[i].getXpath() == expectedXPaths[i]
}
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']
+ 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']
+ '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 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']
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -106,10 +114,11 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
assert result[i].getXpath() == expectedXPaths[i]
}
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"]']
+ 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"]']
}
@Sql([CLEAR_DATA, SET_DATA])
@@ -146,6 +155,7 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
'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/java/org/onap/cps/DatabaseTestContainer.java b/cps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java
index d3908ea807..10f8de422d 100755
--- a/cps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java
+++ b/cps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java
@@ -23,8 +23,10 @@ import org.testcontainers.containers.PostgreSQLContainer;
/**
* The Postgresql database test container wrapper.
- * Singleton implementation allows saving time on database initialization which
- * otherwise would occur on each test.
+ * Singleton implementation allows saving time on database initialization which otherwise would occur on each test.
+ * for debugging/developing purposes you can suspend any test and connect to this database:
+ * docker exec -it {container-id} sh
+ * psql -d test -U test
*/
public class DatabaseTestContainer extends PostgreSQLContainer<DatabaseTestContainer> {
private static final String IMAGE_VERSION = "postgres:13.2";
diff --git a/cps-ri/src/test/resources/data/anchor.sql b/cps-ri/src/test/resources/data/anchor.sql
index a7d3e67154..dbf1a6a47d 100644
--- a/cps-ri/src/test/resources/data/anchor.sql
+++ b/cps-ri/src/test/resources/data/anchor.sql
@@ -1,3 +1,25 @@
+/*
+ ============LICENSE_START=======================================================
+ Copyright (C) 2020 Pantheon.tech
+ Modifications Copyright (C) 2020 Nordix Foundation.
+ Modifications Copyright (C) 2021 Bell Canada.
+ ================================================================================
+ 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=========================================================
+*/
+
INSERT INTO DATASPACE (ID, NAME) VALUES
(1001, 'DATASPACE-001'), (1002, 'DATASPACE-002');
@@ -9,4 +31,4 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES
(3002, 'ANCHOR-002', 1001, 2002);
INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
- (4001, 1001, 3001, null, '/xpath', '{}'); \ No newline at end of file
+ (4001, 1001, 3001, null, '/xpath', '{}');
diff --git a/cps-ri/src/test/resources/data/clear-all.sql b/cps-ri/src/test/resources/data/clear-all.sql
index 9aee604a31..8a5e8444e7 100644
--- a/cps-ri/src/test/resources/data/clear-all.sql
+++ b/cps-ri/src/test/resources/data/clear-all.sql
@@ -1,3 +1,25 @@
+/*
+ ============LICENSE_START=======================================================
+ Copyright (C) 2020-2021 Pantheon.tech
+ Modifications Copyright (C) 2020 Nordix Foundation.
+ Modifications Copyright (C) 2020 Bell Canada.
+ ================================================================================
+ 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=========================================================
+*/
+
DELETE FROM FRAGMENT;
DELETE FROM ANCHOR;
DELETE FROM DATASPACE;
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 67558456eb..8f525df6bd 100644
--- a/cps-ri/src/test/resources/data/cps-path-query.sql
+++ b/cps-ri/src/test/resources/data/cps-path-query.sql
@@ -1,3 +1,24 @@
+/*
+ ============LICENSE_START=======================================================
+ Copyright (C) 2021 Nordix Foundation.
+ Modifications Copyright (C) 2021 Bell Canada.
+ ================================================================================
+ 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=========================================================
+*/
+
INSERT INTO DATASPACE (ID, NAME) VALUES
(1001, 'DATASPACE-001');
@@ -12,8 +33,8 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
(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" : 15, "title": "Dune"}'),
- (6, 1001, 1003, 4, '/shops/shop[@id=1]/categories[@code=2]/book', '{"price" : 15, "title": "Chapters"}'),
+ (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"}');
@@ -30,4 +51,4 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
(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"}'); \ No newline at end of file
+ (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 1897185fa0..d7109f20b2 100755
--- a/cps-ri/src/test/resources/data/fragment.sql
+++ b/cps-ri/src/test/resources/data/fragment.sql
@@ -1,3 +1,25 @@
+/*
+ ============LICENSE_START=======================================================
+ Copyright (C) 2021 Nordix Foundation.
+ Modifications Copyright (C) 2021 Pantheon.tech
+ Modifications Copyright (C) 2021 Bell Canada.
+ ================================================================================
+ 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=========================================================
+*/
+
INSERT INTO DATASPACE (ID, NAME) VALUES
(1001, 'DATASPACE-001');
@@ -31,4 +53,4 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
(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="X"]', '{"key": "X"}'); \ No newline at end of file
+ (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="X"]', '{"key": "X"}');
diff --git a/cps-ri/src/test/resources/data/schemaset.sql b/cps-ri/src/test/resources/data/schemaset.sql
index e6306d0d0f..adfcfa172e 100644
--- a/cps-ri/src/test/resources/data/schemaset.sql
+++ b/cps-ri/src/test/resources/data/schemaset.sql
@@ -1,3 +1,25 @@
+/*
+ ============LICENSE_START=======================================================
+ Copyright (C) 2020-2021 Pantheon.tech
+ Modifications Copyright (C) 2020 Nordix Foundation.
+ Modifications Copyright (C) 2020-2021 Bell Canada.
+ ================================================================================
+ 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=========================================================
+*/
+
INSERT INTO DATASPACE (ID, NAME) VALUES
(1001, 'DATASPACE-001'), (1002, 'DATASPACE-002');
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
index 532f442036..be213c0fa0 100755
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
@@ -35,16 +35,16 @@ class CpsAdminServiceImplSpec extends Specification {
}
def 'Create dataspace method invokes persistence service.'() {
- when: 'Create dataspace method is invoked'
+ when: 'create dataspace method is invoked'
objectUnderTest.createDataspace('someDataspace')
- then: 'The persistence service method is invoked with same parameters'
+ then: 'the persistence service method is invoked with same parameters'
1 * mockCpsAdminPersistenceService.createDataspace('someDataspace')
}
def 'Create anchor method invokes persistence service.'() {
- when: 'Create anchor method is invoked'
+ when: 'create anchor method is invoked'
objectUnderTest.createAnchor('someDataspace', 'someSchemaSet', 'someAnchorName')
- then: 'The persistence service method is invoked with same parameters'
+ then: 'the persistence service method is invoked with same parameters'
1 * mockCpsAdminPersistenceService.createAnchor('someDataspace', 'someSchemaSet', 'someAnchorName')
}
@@ -61,7 +61,7 @@ class CpsAdminServiceImplSpec extends Specification {
Anchor anchor = new Anchor()
mockCpsAdminPersistenceService.getAnchor('someDataspace','someAnchor') >> anchor
expect: 'the anchor provided by persistence service is returned as result'
- objectUnderTest.getAnchor('someDataspace','someAnchor') == anchor
+ assert objectUnderTest.getAnchor('someDataspace','someAnchor') == anchor
}
def 'Delete anchor.'() {
diff --git a/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy
index 7f50f7f524..2751d55075 100644
--- a/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2021 Pantheon.tech
+ * Modifications Copyright (C) 2021 Nordix Foundation.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,11 +29,12 @@ import spock.lang.Specification
class DataNodeBuilderSpec extends Specification {
Map<String, Map<String, Object>> expectedLeavesByXpathMap = [
- '/test-tree' : [],
- '/test-tree/branch[@name=\'Left\']' : [name: 'Left'],
- '/test-tree/branch[@name=\'Left\']/nest' : [name: 'Small', birds: ['Sparrow', 'Robin', 'Finch']],
- '/test-tree/branch[@name=\'Right\']' : [name: 'Right'],
- '/test-tree/branch[@name=\'Right\']/nest': [name: 'Big', birds: ['Owl', 'Raven', 'Crow']]
+ '/test-tree' : [],
+ '/test-tree/branch[@name=\'Left\']' : [name: 'Left'],
+ '/test-tree/branch[@name=\'Left\']/nest' : [name: 'Small', birds: ['Sparrow', 'Robin', 'Finch']],
+ '/test-tree/branch[@name=\'Right\']' : [name: 'Right'],
+ '/test-tree/branch[@name=\'Right\']/nest' : [name: 'Big', birds: ['Owl', 'Raven', 'Crow']],
+ '/test-tree/fruit[@color=\'Green\' and @name=\'Apple\']': [color: 'Green', name: 'Apple']
]
String[] networkTopologyModelRfc8345 = [
@@ -55,7 +57,7 @@ class DataNodeBuilderSpec extends Specification {
def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build()
def mappedResult = TestUtils.getFlattenMapByXpath(result)
then: '5 DataNode objects with unique xpath were created in total'
- mappedResult.size() == 5
+ mappedResult.size() == 6
and: 'all expected xpaths were built'
mappedResult.keySet().containsAll(expectedLeavesByXpathMap.keySet())
and: 'each data node contains the expected attributes'
diff --git a/cps-service/src/test/resources/test-tree.json b/cps-service/src/test/resources/test-tree.json
index bc9cbd7cea..e1789abc1d 100644
--- a/cps-service/src/test/resources/test-tree.json
+++ b/cps-service/src/test/resources/test-tree.json
@@ -23,6 +23,12 @@
]
}
}
+ ],
+ "fruit": [
+ {
+ "name": "Apple",
+ "color": "Green"
+ }
]
}
-} \ No newline at end of file
+}
diff --git a/cps-service/src/test/resources/test-tree.yang b/cps-service/src/test/resources/test-tree.yang
index faba8a11d4..63100657ef 100644
--- a/cps-service/src/test/resources/test-tree.yang
+++ b/cps-service/src/test/resources/test-tree.yang
@@ -20,5 +20,17 @@ module test-tree {
}
}
}
+ list fruit {
+ key "name color";
+
+ leaf name {
+ type string;
+ }
+
+ leaf color {
+ type string;
+ }
+
+ }
}
}
diff --git a/docs/cps-path.rst b/docs/cps-path.rst
index aec87cd436..0271d07e1d 100644
--- a/docs/cps-path.rst
+++ b/docs/cps-path.rst
@@ -14,8 +14,8 @@ CPS Path
Introduction
============
-Several CPS APIs use the cps-path (or cpsPath in Java API) parameter.
-The CPS Path parameter is used for querying xpaths. CPS Path is insprired by the `XML Path Language (XPath) 3.1. <https://www.w3.org/TR/2017/REC-xpath-31-20170321/>`_
+Several CPS APIs use the CPS path (or cpsPath in Java API) parameter.
+The CPS path parameter is used for querying xpaths. CPS path is inspired by the `XML Path Language (XPath) 3.1. <https://www.w3.org/TR/2017/REC-xpath-31-20170321/>`_
This section describes the functionality currently supported by CPS Path.
@@ -28,104 +28,137 @@ The xml below describes some basic data to be used to illustrate the CPS Path fu
<shops>
<bookstore name="Chapters">
- <bookstore-name>Chapters</bookstore-name>
- <categories code="1" name="SciFi" numberOfBooks="2">
- <books>
- <book name="Space Odyssee"/>
- <book name="Dune"/>
- </books>
- </categories>
- <categories code="2" name="Kids" numberOfBooks="1">
- <books>
- <book name="Matilda"/>
- </books>
- </categories>
+ <bookstore-name>Chapters</bookstore-name>
+ <categories code="1" name="SciFi" numberOfBooks="2">
+ <books>
+ <book title="2001: A Space Odyssey" price="5">
+ <label>sale</label>
+ <label>classic</label>
+ <edition>1968</edition>
+ <edition>2018</edition>
+ </book>
+ <book title="Dune" price="5">
+ <label>classic</label>
+ <edition>1965</edition>
+ </book>
+ </books>
+ </categories>
+ <categories code="2" name="Kids" numberOfBooks="1">
+ <books>
+ <book title="Matilda" />
+ </books>
+ </categories>
</bookstore>
</shops>
-**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers
+**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers. 'label' and 'edition' are both leaf-lists.
General Notes
=============
-- String values must be wrapped in quotation marks (U+0022) or apostrophes (U+0027).
+- String values must be wrapped in quotation marks ``"`` (U+0022) or apostrophes ``'`` (U+0027).
- String comparisons are case sensitive.
+- List key-fields containing ``\`` or ``@[`` will not be processed correctly when being referenced with such key values in absolute or descendant paths. This means such entries will be omitted from any query result. See `CPS-500 <https://jira.onap.org/browse/CPS-500>`_ Special Character Limitations of cpsPath Queries
-Supported Queries
-=================
+Query Syntax
+============
-Get List Elements by Any Attribute Value
-----------------------------------------
+``( <absolute-path> | <descendant-path> ) [ <leaf-conditions> ] [ <text()-condition> ] [ <ancestor-axis> ]``
-**Syntax**: ``<xpath>/<target-node>[@<leaf-name>=<leaf-value>]``
- - ``xpath``: The xpath to the parent of the target node including all ancestors.
- - ``target-node``: The name of the (list) node which elements will queried.
- - ``leaf-name``: The name of the leaf which value needs to be compared.
- - ``leaf-value``: The required value of the leaf.
+Each CPS path expression need to start with an 'absolute' or 'descendant' xpath.
+
+absolute-path
+-------------
+
+**Syntax**: ``'/' <container-name> ( '[' <list-key> ']' )? ( '/' <containerName> ( '[' <list-key> ']' )? )*``
+
+ - ``container name``: Any yang container or list.
+ - ``list-key``: One or more key-value pairs, each preceded by the ``@`` symbol, combined using the ``and`` keyword.
+ - The above van repeated any number of times.
**Examples**
- - ``/shops/bookstore/categories[@numberOfBooks=1]``
- - ``/shops/bookstore/categories[@name="Kids"]``
- - ``/shops/bookstore/categories[@name='Kids']``
+ - ``/shops/bookstore``
+ - ``/shops/bookstore/categories[@code=1]``
+ - ``/shops/bookstore/categories[@code=1]/book``
**Limitations**
- - Only one list (last descendant) can be queried for a non-key value. Any ancestor list will have to be referenced by its key name-value pair(s).
- - Only one attribute can be queried.
- - Only string and integer values are supported (boolean and float values are not supported).
+ - Absolute paths must start with the top element (data node) as per the model tree.
+ - Each list reference must include a valid instance reference to the key for that list. Except when it is the last element.
-**Notes**
- - For performance reasons it does not make sense to query using key leaf as attribute. If the key value is known it is better to execute a get request with the complete xpath.
+descendant-path
+---------------
-Get Any Descendant
-------------------
+**Syntax**: ``'//' <container-name> ( '[' <list-key> ']' )? ( '/' <containerName> ( '[' <list-key> ']' )? )*``
-**Syntax**: ``//<direct-ancestors><target-node>``
- - ``direct-ancestors``: Optional path to direct ancestors of the target node. This can contain zero to many ancestor nodes separated by a /.
- - ``target-node``: The name of the (list) node from which element will be selected. If the target node is a Yang List he element needs to be specified using the key as normal e.g. ``categories[@code=1]``.
+ - The syntax of a descendant path is identical to a absolute path except that it is preceded by a double slash ``//``.
**Examples**
- - ``//book``
- - ``//books/book``
- - ``//categories[@code=1]``
- - ``//categories[@code=1]/books``
+ - ``//bookstore``
+ - ``//categories[@code=1]/book``
+ - ``//bookstore/categories``
**Limitations**
- - List elements can only be addressed using the list key leaf.
+ - Each list reference must include a valid instance reference to the key for that list. Except when it is the last element.
-Get Any Descendant by Any Attribute Value
------------------------------------------
+leaf-conditions
+---------------
-**Syntax**: ``//<direct-ancestors><target-node>[@<leaf-name>=<leaf-value>]``
- - ``direct-ancestors``: Optional path to direct ancestors of the target node. This can contain zero to many ancestor nodes separated by a /.
- - ``target-node``: The name of the (list) node which elements will queried.
- - ``leaf1-name .. leafN-name:``: One or more leaves whose value needs to be compared.
- - ``leaf1-value .. leafN-value:``: One or more required leaf values (multiple condition can be combined using the 'and' keyword).
+**Syntax**: ``<xpath> '[' @<leaf-name1> '=' <leaf-value1> ( ' and ' @<leaf-name> '=' <leaf-value> )* ']'``
+ - ``xpath``: Absolute or descendant or xpath to the (list) node which elements will be queried.
+ - ``leaf-name``: The name of the leaf which value needs to be compared.
+ - ``leaf-value``: The required value of the leaf.
**Examples**
+ - ``/shops/bookstore/categories[@numberOfBooks=1]``
+ - ``//categories[@name="Kids"]``
- ``//categories[@name='Kids']``
- - ``//categories[@name='Kids' and @numberOfBooks=1]``
+ - ``//categories[@code=1]/book[@title='Dune' and price=5]``
**Limitations**
- - Only string and integer values are supported (boolean and float values are not supported).
- - Multiple attributes can only be combined using 'and'. 'or' and bracketing is not supported.
+ - Only the last list or container can be queried leaf values. Any ancestor list will have to be referenced by its key name-value pair(s).
+ - Multiple attributes can only be combined using ``and``. ``or`` and bracketing is not supported.
+ - Only leaves can be used, leaf-list are not supported.
+ - Only string and integer values are supported, boolean and float values are not supported.
+
+**Notes**
+ - For performance reasons it does not make sense to query using key leaf as attribute. If the key value is known it is better to execute a get request with the complete xpath.
+
+text()-condition
+----------------
+
+The text()-condition can be added to any CPS path query.
-Query Extensions
-================
+**Syntax**: ``<cps-path> ( '/' <leaf-name> '[text()=' <string-value> ']' )?``
+ - ``cps-path``: Any CPS path query.
+ - ``leaf-name``: The name of the leaf or leaf-list which value needs to be compared.
+ - ``string-value``: The required value of the leaf or leaf-list element as a string wrapped in quotation marks (U+0022) or apostrophes (U+0027). This wil still match integer values.
+
+**Examples**
+ - ``//book/label[text()="classic"]``
+ - ``//book/edition[text()="1965"]``
+
+**Limitations**
+ - Only the last list or container can be queried for leaf values with a text() condition. Any ancestor list will have to be referenced by its key name-value pair(s).
+ - Only one leaf or leaf-list can be tested.
+ - Only string and integer values are supported, boolean and float values are not supported.
+ - Since CPS cannot return individual leaves it will always return the container with all its leaves. Ancestor-axis can be used to specify a parent higher up the tree.
+ - When querying a leaf value (instead of leaf-list) it is better, more performant to use a text value condition use @<leaf-name> as described above.
-Ancestor Axis
+ancestor-axis
-------------
-The ancestor axis can be added to any CPS path query.
+The ancestor axis can be added to any CPS path query but has to be the last part.
-**Syntax**: ``//<cps-path>/ancestor::<ancestor-path>``
+**Syntax**: ``<cps-path> ( '/ancestor::' <ancestor-path> )?``
- ``cps-path``: Any CPS path query.
- - ``ancestor-path``: Partial path to ancestors of the target node. This can contain one or more ancestor nodes separated by a /.
+ - ``ancestor-path``: Partial path to ancestors of the target node. This can contain one or more ancestor nodes separated by a ``/``.
**Examples**
- ``//book/ancestor::categories``
- ``//categories[@genre="SciFi"]/book/ancestor::bookstore``
- ``book/ancestor::categories[@code=1]/books``
+ - ``//book/label[text()="classic"]/ancestor::shop``
**Limitations**
- Ancestor list elements can only be addressed using the list key leaf.
- - List elements with compound keys are not supported. \ No newline at end of file
+ - List elements with compound keys are not supported.
diff --git a/docs/release-notes.rst b/docs/release-notes.rst
index 8d8bbe3335..f213c7e4de 100755
--- a/docs/release-notes.rst
+++ b/docs/release-notes.rst
@@ -16,9 +16,9 @@ CPS Release Notes
-.. =========================
-.. * * * HONOULULU * * *
-.. =========================
+.. ========================
+.. * * * HONOLULU * * *
+.. ========================
Version: 1.0.1
==============