Persistence layer - Query Datanodes using cpsPath that contains contains a leaf name... 59/117759/8
authorniamhcore <niamh.core@est.tech>
Thu, 11 Feb 2021 14:49:11 +0000 (14:49 +0000)
committerniamhcore <niamh.core@est.tech>
Fri, 19 Feb 2021 10:22:59 +0000 (10:22 +0000)
Issue-ID: CPS-231
Signed-off-by: niamhcore <niamh.core@est.tech>
Change-Id: I9bd483a4b76e233ab6c64b3ef8aacb593e4e9da0

cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/query/CpsPathQuery.java [new file with mode: 0644]
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/query/CpsPathQuerySpec.groovy [new file with mode: 0644]
cps-ri/src/test/resources/data/fragment.sql
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/main/java/org/onap/cps/spi/exceptions/CpsPathException.java [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/spi/exceptions/CpsExceptionsSpec.groovy

index fd4e768..2d9588e 100644 (file)
@@ -21,6 +21,7 @@
 package org.onap.cps.spi.impl;
 
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSet.Builder;
@@ -38,6 +39,7 @@ import org.onap.cps.spi.entities.DataspaceEntity;
 import org.onap.cps.spi.entities.FragmentEntity;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.DataNodeBuilder;
+import org.onap.cps.spi.query.CpsPathQuery;
 import org.onap.cps.spi.repository.AnchorRepository;
 import org.onap.cps.spi.repository.DataspaceRepository;
 import org.onap.cps.spi.repository.FragmentRepository;
@@ -124,6 +126,19 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, xpath);
     }
 
+    @Override
+    public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath) {
+        final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+        final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+        final CpsPathQuery cpsPathQuery = CpsPathQuery.createFrom(cpsPath);
+        final List<FragmentEntity> fragmentEntities = fragmentRepository
+            .getByAnchorAndXpathAndLeafAttributes(anchorEntity.getId(), cpsPathQuery
+                .getXpathPrefix(), cpsPathQuery.getLeafName(), cpsPathQuery.getLeafValue());
+        return fragmentEntities.stream()
+            .map(fragmentEntity -> toDataNode(fragmentEntity, OMIT_DESCENDANTS))
+            .collect(Collectors.toUnmodifiableList());
+    }
+
     private static DataNode toDataNode(final FragmentEntity fragmentEntity,
         final FetchDescendantsOption fetchDescendantsOption) {
         final Map<String, Object> leaves = GSON.fromJson(fragmentEntity.getAttributes(), Map.class);
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/query/CpsPathQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/query/CpsPathQuery.java
new file mode 100644 (file)
index 0000000..4fcf6e4
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2021 Nordix Foundation
+ *  Modifications Copyright (C) 2021 Bell Canada. All rights reserved.
+ *  ================================================================================
+ *  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.query;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import org.onap.cps.spi.exceptions.CpsPathException;
+
+@Getter
+@Setter(AccessLevel.PRIVATE)
+public class CpsPathQuery {
+
+    private String xpathPrefix;
+    private String leafName;
+    private Object leafValue;
+
+    public static final Pattern QUERY_CPS_PATH_WITH_SINGLE_LEAF_PATTERN =
+        Pattern.compile("(.*)\\[\\s*@(.*?)\\s*=\\s*(.*?)\\s*]");
+
+    public static final Pattern LEAF_STRING_VALUE_PATTERN = Pattern.compile("['\"](.*)['\"]");
+
+    public static final Pattern LEAF_INTEGER_VALUE_PATTERN = Pattern.compile("[-+]?\\d+");
+
+    /**
+     * Returns a xpath prefix, leaf name and leaf value for the given cps path.
+     *
+     * @param cpsPath cps path
+     * @return a CpsPath object containing the xpath prefix, leaf name and leaf value.
+     */
+    public static CpsPathQuery createFrom(final String cpsPath) {
+        final Matcher matcher = QUERY_CPS_PATH_WITH_SINGLE_LEAF_PATTERN.matcher(cpsPath);
+        if (matcher.matches()) {
+            final CpsPathQuery cpsPathQuery = new CpsPathQuery();
+            cpsPathQuery.setXpathPrefix(matcher.group(1));
+            cpsPathQuery.setLeafName(matcher.group(2));
+            cpsPathQuery.setLeafValue(convertLeafValueToCorrectType(matcher.group(3)));
+            return cpsPathQuery;
+        }
+        throw new CpsPathException("Invalid cps path.",
+            String.format("Cannot interpret or parse cps path %s.", cpsPath));
+    }
+
+    private static Object convertLeafValueToCorrectType(final String leafValueString) {
+        final Matcher stringValueWithQuotesMatcher = LEAF_STRING_VALUE_PATTERN.matcher(leafValueString);
+        if (stringValueWithQuotesMatcher.matches()) {
+            return stringValueWithQuotesMatcher.group(1);
+        }
+        final Matcher integerValueMatcher = LEAF_INTEGER_VALUE_PATTERN.matcher(leafValueString);
+        if (integerValueMatcher.matches()) {
+            return Integer.valueOf(leafValueString);
+        }
+        throw new CpsPathException("Unsupported leaf value.",
+            String.format("Unsupported leaf value %s in cps path.", leafValueString));
+    }
+}
index bf55172..a40168a 100755 (executable)
@@ -22,6 +22,7 @@
 package org.onap.cps.spi.repository;\r
 \r
 import java.util.Collection;\r
+import java.util.List;\r
 import java.util.Optional;\r
 import javax.validation.constraints.NotNull;\r
 import org.checkerframework.checker.nullness.qual.NonNull;\r
@@ -51,4 +52,12 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>
     @Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)")\r
     void deleteByAnchorIn(@NotNull @Param("anchors") Collection<AnchorEntity> anchorEntities);\r
 \r
+    @Query(value =\r
+        "SELECT * FROM FRAGMENT WHERE (anchor_id = :anchor) AND (xpath = (:xpath) OR xpath LIKE "\r
+            + "CONCAT(:xpath,'\\[@%]')) AND attributes @> jsonb_build_object(:leafName , :leafValue)",\r
+                nativeQuery = true)\r
+    // Above query will match an xpath with or without the index for a list [@key=value]\r
+    // and match anchor id, leaf name and leaf value\r
+    List<FragmentEntity> getByAnchorAndXpathAndLeafAttributes(@Param("anchor") int anchorId, @Param("xpath")\r
+        String xpathPrefix, @Param("leafName") String leafName, @Param("leafValue") Object leafValue);\r
 }
\ No newline at end of file
index a6e4701..0158938 100644 (file)
@@ -19,9 +19,6 @@
  */
 package org.onap.cps.spi.impl
 
-import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
-
 import com.google.common.collect.ImmutableSet
 import com.google.gson.Gson
 import com.google.gson.GsonBuilder
@@ -37,6 +34,9 @@ import org.springframework.dao.DataIntegrityViolationException
 import org.springframework.test.context.jdbc.Sql
 import spock.lang.Unroll
 
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+
 class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
 
     @Autowired
@@ -87,6 +87,7 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
             grandchildFragment.xpath == grandChildXpath
     }
 
+    @Unroll
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Store datanode error scenario: #scenario.'() {
         when: 'attempt to store a data node with #scenario'
@@ -116,6 +117,7 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
             parentFragment.getChildFragments().find({ it.xpath == newChild.xpath })
     }
 
+    @Unroll
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Add child error scenario: #scenario.'() {
         when: 'attempt to add a child data node with #scenario'
@@ -283,7 +285,7 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Replace data node tree error scenario: #scenario.'() {
         given: 'data node object'
-            def submittedDataNode = buildDataNode(xpath, ['leaf-name':'leaf-value'], [])
+            def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
         when: 'attempt to update data node for #scenario'
             objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
         then: 'a #expectedException is thrown'
@@ -302,4 +304,35 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
     static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
         return GSON.fromJson(fragmentEntity.getAttributes(), Map<String, Object>.class)
     }
+
+    @Unroll
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Cps Path query for single leaf value with type: #type.'() {
+        when: 'a query is executed to get a data node by the given cps path'
+            def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath)
+        then: 'the correct data is returned'
+            def leaves ='[common-leaf-name:common-leaf-value, common-leaf-name-int:5.0]'
+            result.size() == 1
+            def dataNode = result.stream().findFirst().get()
+            dataNode.getLeaves().toString() == leaves
+        where: 'the following data is used'
+            type      | cpsPath
+            'String'  | '/parent-200/child-202[@common-leaf-name=\'common-leaf-value\']'
+            'Integer' | '/parent-200/child-202[@common-leaf-name-int=5]'
+    }
+
+    @Unroll
+    @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'
+            def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath)
+        then: 'no data is returned'
+            result.isEmpty()
+        where: 'following cps queries are performed'
+            scenario                         | cpsPath
+            'cps path is incomplete'               | '/parent-200[@common-leaf-name-int=5]'
+            'missing / at beginning of path' | 'parent-200/child-202[@common-leaf-name-int=5]'
+            'leaf value does not exist'      | '/parent-200/child-202[@common-leaf-name=\'does not exist\']'
+            'incomplete end of xpath prefix' | '/parent-200/child-20[@common-leaf-name-int=5]'
+    }
 }
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/query/CpsPathQuerySpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/query/CpsPathQuerySpec.groovy
new file mode 100644 (file)
index 0000000..1e457fb
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * ============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.query
+
+import org.onap.cps.spi.exceptions.CpsPathException
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class CpsPathQuerySpec extends Specification {
+
+    def objectUnderTest = new CpsPathQuery()
+
+    @Unroll
+    def 'Parse cps path with valid cps path and a filter for a leaf of type : #type.'()
+    {   when: 'the given cps path is parsed'
+        def result = objectUnderTest.createFrom(cpsPath)
+        then: 'object has the expected attribute'
+            result.xpathPrefix == '/parent-200/child-202'
+            result.leafName == expectedLeafName
+            result.leafValue == expectedLeafValue
+        where: 'the following data is used'
+            type      | cpsPath                                                          || expectedLeafName       | expectedLeafValue
+            'String'  | '/parent-200/child-202[@common-leaf-name=\'common-leaf-value\']' || 'common-leaf-name'     | 'common-leaf-value'
+            'Integer' | '/parent-200/child-202[@common-leaf-name-int=5]'                 || 'common-leaf-name-int' | 5
+    }
+
+    @Unroll
+    def 'Parse cps path with : #scenario.'()
+    {   when: 'the given cps path is parsed'
+            objectUnderTest.createFrom(cpsPath)
+        then: 'a CpsPathException is thrown'
+            thrown(CpsPathException)
+        where: 'the following data is used'
+            scenario                    | cpsPath
+            'invalid cps path'          | 'invalid-cps-path'
+            'cps path with float value' | '/parent-200/child-202[@common-leaf-name-float=5.0]'
+    }
+}
index 9599146..4b50578 100644 (file)
@@ -25,4 +25,5 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
 INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
     (4201, 1001, 3003, null, '/parent-200', '{"leaf-value": "original"}'),
     (4202, 1001, 3003, 4201, '/parent-200/child-201', '{"leaf-value": "original"}'),
-    (4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}');
\ No newline at end of file
+    (4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'),
+    (4204, 1001, 3003, 4201, '/parent-200/child-202', '{"common-leaf-name": "common-leaf-value", "common-leaf-name-int" : 5}');
\ No newline at end of file
index 9a0c180..d2b6d45 100644 (file)
@@ -21,6 +21,7 @@
 
 package org.onap.cps.spi;
 
+import java.util.Collection;
 import java.util.Map;
 import org.checkerframework.checker.nullness.qual.NonNull;
 import org.onap.cps.spi.model.DataNode;
@@ -86,4 +87,15 @@ public interface CpsDataPersistenceService {
      * @param dataNode      data node
      */
     void replaceDataNodeTree(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull DataNode dataNode);
+
+    /**
+     * Get a datanode by cps path.
+     *
+     * @param dataspaceName          dataspace name
+     * @param anchorName             anchor name
+     * @param cpsPath                cps path
+     * @return the data nodes found i.e. 0 or more data nodes
+     */
+    Collection<DataNode> queryDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
+        @NonNull String cpsPath);
 }
diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/CpsPathException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/CpsPathException.java
new file mode 100644 (file)
index 0000000..fde5566
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * ============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.exceptions;
+
+public class CpsPathException extends CpsException {
+
+    private static final long serialVersionUID = 1006899957127327791L;
+
+    /**
+     * Constructor.
+     *
+     * @param message the error message
+     * @param details the error details
+     */
+    public CpsPathException(final String message, final String details) {
+        super(message, details);
+    }
+}
index 500b801..a4a13ff 100755 (executable)
@@ -133,4 +133,13 @@ class CpsExceptionsSpec extends Specification {
             (new DataNodeNotFoundException(dataspaceName, anchorName, xpath)).details
                     == "DataNode with xpath ${xpath} was not found for anchor ${anchorName} and dataspace ${dataspaceName}."
     }
+
+    def 'Creating a cps path exception.'() {
+        given: 'a cps path exception is created'
+            def exception = new CpsPathException(providedMessage, providedDetails)
+        expect: 'the exception has the provided message'
+            exception.message == providedMessage
+        and: 'the exception has the provided details'
+            exception.details == providedDetails
+    }
 }
\ No newline at end of file