diff options
author | 2022-12-15 16:06:20 +0000 | |
---|---|---|
committer | 2023-01-11 13:45:36 +0000 | |
commit | 9c56b3032222b57549aa17dd281e4794fd9e25b6 (patch) | |
tree | 5bb6dacf0c5c84463c4a8810609776069ce16d09 /cps-ri/src | |
parent | b48469262c83dc1e88b12d162de88a05ce61159c (diff) |
Fetch CM handles by collection of xpaths
- 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
Diffstat (limited to 'cps-ri/src')
8 files changed, 197 insertions, 34 deletions
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 3bd299430..8293f642c 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 @@ -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( 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 c9461bf06..4b42b2da8 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 @@ -39,7 +39,8 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository;
@Repository
-public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery {
+public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery,
+ FragmentRepositoryMultiPathQuery {
Optional<FragmentEntity> findByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity,
@NonNull AnchorEntity anchorEntity,
@@ -80,9 +81,6 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, return fragmentExtracts;
}
- List<FragmentEntity> findAllByAnchorAndXpathIn(@NonNull AnchorEntity anchorEntity,
- @NonNull Collection<String> xpath);
-
@Modifying
@Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)")
void deleteByAnchorIn(@NotNull @Param("anchors") Collection<AnchorEntity> anchorEntities);
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 index 000000000..9c34a459e --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQuery.java @@ -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 index 000000000..b936e5c76 --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQueryImpl.java @@ -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; + } +} diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java index 8cad9f5e4..d713746e4 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java @@ -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("'", "''"); + } + } 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 56e388335..b6d2c5d65 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 @@ -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) } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy index 255e8e52f..30d438cb4 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy @@ -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 diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy index 910d8a460..45cdb4d07 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy @@ -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() |