Fetch CM handles by collection of xpaths 62/132762/4
authorseanbeirne <sean.beirne@est.tech>
Thu, 15 Dec 2022 16:06:20 +0000 (16:06 +0000)
committerseanbeirne <sean.beirne@est.tech>
Wed, 11 Jan 2023 13:45:36 +0000 (13:45 +0000)
- Added FragmentRepositoryMultiPathQuery
- Removed Hibernate method for same
- Added perf. test
- Handle escaping of single qoutes in sql-data
- Increased timing for path paser performance test

Issue-ID: CPS-1422
Signed-off-by: seanbeirne <sean.beirne@est.tech>
Change-Id: Ibea12a44bffd29ed43cc1560b507d1fa7e968b8b

12 files changed:
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/performance/CpsPathUtilPerfTest.groovy
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQuery.java [new file with mode: 0644]
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQueryImpl.java [new file with mode: 0644]
cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java

index 2ba20c1..e5e304b 100644 (file)
@@ -35,9 +35,9 @@ class CpsPathUtilPerfTest extends Specification {
                 CpsPathUtil.getNormalizedXpath('//child[@other-leaf=1]/leaf-name[text()="search"]/ancestor::parent')
             }
             stopWatch.stop()
-        then: 'it takes less then 1,000 milliseconds'
+        then: 'it takes less then 1,100 milliseconds'
             // In CI this actually takes about 0.3-0.5 sec  which  is approx. 50+ parser executions per millisecond!
-            assert stopWatch.getTotalTimeMillis() < 1000
+            assert stopWatch.getTotalTimeMillis() < 1100
     }
 
 }
index 3bd2994..8293f64 100644 (file)
@@ -256,6 +256,21 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         return toDataNode(fragmentEntity, fetchDescendantsOption);
     }
 
+    @Override
+    public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
+                                             final Collection<String> xpaths,
+                                             final FetchDescendantsOption fetchDescendantsOption) {
+        final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+        final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+        final List<FragmentEntity> fragmentEntities =
+                fragmentRepository.findByAnchorAndMultipleCpsPaths(anchorEntity.getId(), xpaths);
+        final Collection<DataNode> dataNodesCollection = new ArrayList<>(fragmentEntities.size());
+        for (final FragmentEntity fragmentEntity : fragmentEntities) {
+            dataNodesCollection.add(toDataNode(fragmentEntity, fetchDescendantsOption));
+        }
+        return dataNodesCollection;
+    }
+
     private FragmentEntity getFragmentWithoutDescendantsByXpath(final String dataspaceName,
                                                                 final String anchorName,
                                                                 final String xpath) {
@@ -317,7 +332,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         }
         fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
         if (cpsPathQuery.hasAncestorAxis()) {
-            fragmentEntities = getAncestorFragmentEntities(anchorEntity, cpsPathQuery, fragmentEntities);
+            fragmentEntities = getAncestorFragmentEntities(anchorEntity.getId(), cpsPathQuery, fragmentEntities);
         }
         return createDataNodesFromProxiedFragmentEntities(fetchDescendantsOption, anchorEntity, fragmentEntities);
     }
@@ -338,18 +353,17 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             fragmentRepository.quickFindWithDescendants(anchorEntity.getId(), xpathRegex);
         fragmentEntities = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts);
         if (cpsPathQuery.hasAncestorAxis()) {
-            fragmentEntities = getAncestorFragmentEntities(anchorEntity, cpsPathQuery, fragmentEntities);
+            fragmentEntities = getAncestorFragmentEntities(anchorEntity.getId(), cpsPathQuery, fragmentEntities);
         }
         return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
     }
 
-    private Collection<FragmentEntity> getAncestorFragmentEntities(final AnchorEntity anchorEntity,
+    private Collection<FragmentEntity> getAncestorFragmentEntities(final int anchorId,
                                                                    final CpsPathQuery cpsPathQuery,
-                                                                   Collection<FragmentEntity> fragmentEntities) {
-        final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
-        fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList()
-            : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
-        return fragmentEntities;
+                                                                   final Collection<FragmentEntity> fragmentEntities) {
+        final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
+        return ancestorXpaths.isEmpty() ? Collections.emptyList()
+            : fragmentRepository.findByAnchorAndMultipleCpsPaths(anchorId, ancestorXpaths);
     }
 
     private List<DataNode> createDataNodesFromProxiedFragmentEntities(
index c9461bf..4b42b2d 100755 (executable)
@@ -39,7 +39,8 @@ import org.springframework.data.repository.query.Param;
 import org.springframework.stereotype.Repository;\r
 \r
 @Repository\r
-public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery {\r
+public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery,\r
+        FragmentRepositoryMultiPathQuery {\r
 \r
     Optional<FragmentEntity> findByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity,\r
                                                               @NonNull AnchorEntity anchorEntity,\r
@@ -80,9 +81,6 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
         return fragmentExtracts;\r
     }\r
 \r
-    List<FragmentEntity> findAllByAnchorAndXpathIn(@NonNull AnchorEntity anchorEntity,\r
-                                                   @NonNull Collection<String> xpath);\r
-\r
     @Modifying\r
     @Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)")\r
     void deleteByAnchorIn(@NotNull @Param("anchors") Collection<AnchorEntity> anchorEntities);\r
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQuery.java
new file mode 100644 (file)
index 0000000..9c34a45
--- /dev/null
@@ -0,0 +1,31 @@
+/*-
+ * ============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.spi.repository;
+
+import java.util.Collection;
+import java.util.List;
+import org.onap.cps.spi.entities.FragmentEntity;
+
+public interface FragmentRepositoryMultiPathQuery {
+
+    List<FragmentEntity> findByAnchorAndMultipleCpsPaths(Integer anchorId, Collection<String> cpsPathQuery);
+
+}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQueryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQueryImpl.java
new file mode 100644 (file)
index 0000000..b936e5c
--- /dev/null
@@ -0,0 +1,70 @@
+/*-
+ * ============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.spi.repository;
+
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.transaction.Transactional;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.spi.entities.FragmentEntity;
+
+
+@Slf4j
+@AllArgsConstructor
+public class FragmentRepositoryMultiPathQueryImpl implements FragmentRepositoryMultiPathQuery {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    private TempTableCreator tempTableCreator;
+
+    @Override
+    @Transactional
+    public List<FragmentEntity> findByAnchorAndMultipleCpsPaths(final Integer anchorId,
+                                                                final Collection<String> cpsPathQueryList) {
+        final Collection<List<String>> sqlData = new HashSet<>(cpsPathQueryList.size());
+        for (final String query : cpsPathQueryList) {
+            final List<String> row = new ArrayList<>(1);
+            row.add(query);
+            sqlData.add(row);
+        }
+
+        final String tempTableName = tempTableCreator.createTemporaryTable(
+                "xpathTemporaryTable", sqlData, "xpath");
+        return selectMatchingFragments(anchorId, tempTableName);
+    }
+
+    private List<FragmentEntity> selectMatchingFragments(final Integer anchorId, final String tempTableName) {
+        final String sql = String.format(
+            "SELECT * FROM FRAGMENT WHERE anchor_id = %d AND xpath IN (select xpath FROM %s);",
+            anchorId, tempTableName);
+        final List<FragmentEntity> fragmentEntities = entityManager.createNativeQuery(sql, FragmentEntity.class)
+                .getResultList();
+        log.debug("Fetched {} fragment entities by anchor and cps path.", fragmentEntities.size());
+        return fragmentEntities;
+    }
+}
index 8cad9f5..d713746 100644 (file)
@@ -26,6 +26,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
+import java.util.stream.Collectors;
 import javax.persistence.EntityManager;
 import javax.persistence.PersistenceContext;
 import lombok.AllArgsConstructor;
@@ -82,8 +83,10 @@ public class TempTableCreator {
                                    final String[] columnNames,
                                    final Collection<List<String>> sqlData) {
         final Collection<String> sqlInserts = new HashSet<>(sqlData.size());
-        for (final Collection<String> row : sqlData) {
-            sqlInserts.add("('" + String.join("','", row) + "')");
+        for (final Collection<String> rowValues : sqlData) {
+            final Collection<String> escapedValues =
+                rowValues.stream().map(it -> escapeSingleQuotesByDoublingThem(it)).collect(Collectors.toList());
+            sqlInserts.add("('" + String.join("','", escapedValues) + "')");
         }
         sqlStringBuilder.append("INSERT INTO ");
         sqlStringBuilder.append(tempTableName);
@@ -94,4 +97,8 @@ public class TempTableCreator {
         sqlStringBuilder.append(";");
     }
 
+    private static String escapeSingleQuotesByDoublingThem(final String value) {
+        return value.replace("'", "''");
+    }
+
 }
index 56e3883..b6d2c5d 100644 (file)
@@ -26,6 +26,8 @@ import org.onap.cps.spi.exceptions.CpsPathException
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.test.context.jdbc.Sql
 
+import java.util.stream.Collectors
+
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
 
@@ -147,27 +149,30 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase {
             def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_SHOP_EXAMPLE, cpsPath, INCLUDE_ALL_DESCENDANTS)
         then: 'the xpaths of the retrieved data nodes are as expected'
             result.size() == expectedXPaths.size()
-            for (int i = 0; i < result.size(); i++) {
-                assert result[i].getXpath() == expectedXPaths[i]
-                assert result[i].childDataNodes.size() == expectedNumberOfChildren[i]
+            if (result.size() > 0) {
+                def resultXpaths = result.stream().map(it -> it.xpath).collect(Collectors.toSet())
+                resultXpaths.containsAll(expectedXPaths)
+                result.each {
+                    assert it.childDataNodes.size() == expectedNumberOfChildren
+                }
             }
         where: 'the following data is used'
             scenario                                    | cpsPath                                              || expectedXPaths                                                                               || expectedNumberOfChildren
-            'multiple list-ancestors'                   | '//book/ancestor::categories'                        || ["/shops/shop[@id='1']/categories[@code='1']", "/shops/shop[@id='1']/categories[@code='2']"] || [1, 1]
-            'one ancestor with list value'              | '//book/ancestor::categories[@code=1]'               || ["/shops/shop[@id='1']/categories[@code='1']"]                                               || [1]
-            'top ancestor'                              | '//shop[@id=1]/ancestor::shops'                      || ['/shops']                                                                                   || [5]
-            'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]'   || ["/shops/shop[@id='1']"]                                                                     || [3]
-            'ancestor with parent list'                 | '//book/ancestor::shop[@id=1]/categories[@code=2]'   || ["/shops/shop[@id='1']/categories[@code='2']"]                                               || [1]
-            'ancestor with parent'                      | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ["/shops/shop[@id='3']/info/contact"]                                                        || [3]
-            'ancestor combined with text condition'     | '//book/title[text()="Dune"]/ancestor::shop'         || ["/shops/shop[@id='1']"]                                                                     || [3]
-            'ancestor with parent that does not exist'  | '//book/ancestor::parentDoesNoExist/categories'      || []                                                                                           || []
-            'ancestor does not exist'                   | '//book/ancestor::ancestorDoesNotExist'              || []                                                                                           || []
+            'multiple list-ancestors'                   | '//book/ancestor::categories'                        || ["/shops/shop[@id='1']/categories[@code='2']", "/shops/shop[@id='1']/categories[@code='1']"] || 1
+            'one ancestor with list value'              | '//book/ancestor::categories[@code=1]'               || ["/shops/shop[@id='1']/categories[@code='1']"]                                               || 1
+            'top ancestor'                              | '//shop[@id=1]/ancestor::shops'                      || ['/shops']                                                                                   || 5
+            'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]'   || ["/shops/shop[@id='1']"]                                                                     || 3
+            'ancestor with parent list'                 | '//book/ancestor::shop[@id=1]/categories[@code=2]'   || ["/shops/shop[@id='1']/categories[@code='2']"]                                               || 1
+            'ancestor with parent'                      | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ["/shops/shop[@id='3']/info/contact"]                                                        || 3
+            'ancestor combined with text condition'     | '//book/title[text()="Dune"]/ancestor::shop'         || ["/shops/shop[@id='1']"]                                                                     || 3
+            'ancestor with parent that does not exist'  | '//book/ancestor::parentDoesNoExist/categories'      || []                                                                                           || null
+            'ancestor does not exist'                   | '//book/ancestor::ancestorDoesNotExist'              || []                                                                                           || null
     }
 
     def 'Cps Path query with syntax error throws a CPS Path Exception.'() {
         when: 'trying to execute a query with a syntax (parsing) error'
             objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_SHOP_EXAMPLE, 'cpsPath that cannot be parsed' , OMIT_DESCENDANTS)
-        then: 'exception is thrown'
+        then: 'a cps path exception is thrown'
             thrown(CpsPathException)
     }
 
index 255e8e5..30d438c 100644 (file)
@@ -107,7 +107,6 @@ class CpsDataPersistenceServiceSpec extends Specification {
             assert thrown.details.contains('/node3')
     }
 
-
     def 'Retrieving a data node with a property JSON value of #scenario'() {
         given: 'the db has a fragment with an attribute property JSON value of #scenario'
             mockFragmentWithJson("{\"some attribute\": ${dataString}}")
@@ -142,6 +141,20 @@ class CpsDataPersistenceServiceSpec extends Specification {
             thrown(DataValidationException)
     }
 
+    def 'Retrieving multiple data nodes.'() {
+        given: 'db contains an anchor'
+           def anchorEntity = new AnchorEntity(id:123)
+           mockAnchorRepository.getByDataspaceAndName(*_) >> anchorEntity
+        and: 'fragment repository returns a collection of fragments'
+            def fragmentEntity1 = new FragmentEntity(xpath: 'xpath1', childFragments: [])
+            def fragmentEntity2 = new FragmentEntity(xpath: 'xpath2', childFragments: [])
+           mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['xpath1','xpath2']) >> [ fragmentEntity1, fragmentEntity2 ]
+        when: 'getting data nodes for 2 xpaths'
+            def result = objectUnderTest.getDataNodes('some-dataspace', 'some-anchor', ['xpath1','xpath2'],FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+        then: '2 data nodes are returned'
+            assert result.size() == 2
+    }
+
     def 'start session'() {
         when: 'start session'
             objectUnderTest.startSession()
@@ -208,11 +221,8 @@ class CpsDataPersistenceServiceSpec extends Specification {
     }
 
     def mockFragmentWithJson(json) {
-        def anchorName = 'some anchor'
-        def mockAnchor = Mock(AnchorEntity)
-        mockAnchor.getId() >> 123
-        mockAnchor.getName() >> anchorName
-        mockAnchorRepository.getByDataspaceAndName(*_) >> mockAnchor
+        def anchorEntity = new AnchorEntity(id:123)
+        mockAnchorRepository.getByDataspaceAndName(*_) >> anchorEntity
         def mockFragmentExtract = Mock(FragmentExtract)
         mockFragmentExtract.getId() >> 456
         mockFragmentExtract.getAttributes() >> json
index 910d8a4..45cdb4d 100644 (file)
@@ -25,6 +25,9 @@ import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.impl.CpsPersistenceSpecBase
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
+import org.onap.cps.spi.repository.AnchorRepository
+import org.onap.cps.spi.repository.DataspaceRepository
+import org.onap.cps.spi.repository.FragmentRepository
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.test.context.jdbc.Sql
 
@@ -40,6 +43,15 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistenceSpecBase {
     @Autowired
     CpsDataPersistenceService objectUnderTest
 
+    @Autowired
+    DataspaceRepository dataspaceRepository
+
+    @Autowired
+    AnchorRepository anchorRepository
+
+    @Autowired
+    FragmentRepository fragmentRepository
+
     static def PERF_TEST_PARENT = '/perf-parent-1'
     static def NUMBER_OF_CHILDREN = 200
     static def NUMBER_OF_GRAND_CHILDREN = 50
@@ -48,6 +60,8 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistenceSpecBase {
     static def ALLOWED_READ_TIME_AL_NODES_MS = 500
 
     def stopWatch = new StopWatch()
+    def readStopWatch = new StopWatch()
+    static def xpathsToAllGrandChildren = []
 
     @Sql([CLEAR_DATA, PERF_TEST_DATA])
     def 'Create a node with many descendants (please note, subsequent tests depend on this running first).'() {
@@ -88,6 +102,19 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistenceSpecBase {
             assert countDataNodes(result) == TOTAL_NUMBER_OF_NODES
     }
 
+    def 'Performance of finding multiple xpaths'() {
+        when: 'we query for all grandchildren (except 1 for fun) with the new native method'
+            xpathsToAllGrandChildren.remove(0)
+            readStopWatch.start()
+            def result = objectUnderTest.getDataNodes('PERF-DATASPACE', 'PERF-ANCHOR', xpathsToAllGrandChildren, INCLUDE_ALL_DESCENDANTS)
+            readStopWatch.stop()
+            def readDurationInMillis = readStopWatch.getTotalTimeMillis()
+        then: 'the returned number of entities equal to the number of children * number of grandchildren'
+            assert result.size() == xpathsToAllGrandChildren.size()
+        and: 'it took less then 4000ms'
+            assert readDurationInMillis < 4000
+    }
+
     def 'Query many descendants by cps-path with #scenario'() {
         when: 'query is executed with all descendants'
             stopWatch.start()
@@ -107,7 +134,7 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistenceSpecBase {
     def 'Delete 50 grandchildren (that have no descendants)'() {
         when: 'target nodes are deleted'
             stopWatch.start()
-            (1..50).each {
+            (1..NUMBER_OF_GRAND_CHILDREN).each {
                 def grandchildPath = "${PERF_TEST_PARENT}/perf-test-child-1/perf-test-grand-child-${it}".toString();
                 objectUnderTest.deleteDataNode('PERF-DATASPACE', 'PERF-ANCHOR', grandchildPath)
             }
@@ -152,6 +179,7 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistenceSpecBase {
         def grandChildren = []
         (1..NUMBER_OF_GRAND_CHILDREN).each {
             def grandChild = new DataNodeBuilder().withXpath("${parentXpath}/${childName}/perf-test-grand-child-${it}").build()
+            xpathsToAllGrandChildren.add(grandChild.xpath)
             grandChildren.add(grandChild)
         }
         return new DataNodeBuilder().withXpath("${parentXpath}/${childName}").withChildDataNodes(grandChildren).build()
index 012d7f8..6332f09 100644 (file)
@@ -122,6 +122,19 @@ public interface CpsDataService {
     DataNode getDataNode(String dataspaceName, String anchorName, String xpath,
         FetchDescendantsOption fetchDescendantsOption);
 
+    /**
+     * Retrieves datanodes by XPath for given dataspace and anchor.
+     *
+     * @param dataspaceName          dataspace name
+     * @param anchorName             anchor name
+     * @param xpaths                 collection of xpath
+     * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes
+     *                               (recursively) as well
+     * @return data node object
+     */
+    Collection<DataNode> getDataNodes(String dataspaceName, String anchorName, Collection<String> xpaths,
+                         FetchDescendantsOption fetchDescendantsOption);
+
     /**
      * Updates data node for given dataspace and anchor using xpath to parent node.
      *
index 65dfa7f..38fa92a 100755 (executable)
@@ -129,6 +129,14 @@ public class CpsDataServiceImpl implements CpsDataService {
         return cpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption);
     }
 
+    @Override
+    public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
+                                             final Collection<String> xpaths,
+                                final FetchDescendantsOption fetchDescendantsOption) {
+        cpsValidator.validateNameCharacters(dataspaceName, anchorName);
+        return cpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpaths, fetchDescendantsOption);
+    }
+
     @Override
     public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
index b9da4af..0989cca 100644 (file)
@@ -111,6 +111,19 @@ public interface CpsDataPersistenceService {
     DataNode getDataNode(String dataspaceName, String anchorName, String xpath,
         FetchDescendantsOption fetchDescendantsOption);
 
+    /**
+     * Retrieves datanode by XPath for given dataspace and anchor.
+     *
+     * @param dataspaceName          dataspace name
+     * @param anchorName             anchor name
+     * @param xpaths                 collection of xpaths
+     * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes
+     *                               (recursively) as well
+     * @return data node object
+     */
+    Collection<DataNode> getDataNodes(String dataspaceName, String anchorName, Collection<String> xpaths,
+                         FetchDescendantsOption fetchDescendantsOption);
+
     /**
      * Updates leaves for existing data node.
      *