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
}
}
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) {
}
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);
}
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(
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
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
--- /dev/null
+/*-
+ * ============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);
+
+}
--- /dev/null
+/*-
+ * ============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;
+ }
+}
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;
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);
sqlStringBuilder.append(";");
}
+ private static String escapeSingleQuotesByDoublingThem(final String value) {
+ return value.replace("'", "''");
+ }
+
}
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
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)
}
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}}")
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()
}
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
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
@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
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).'() {
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()
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)
}
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()
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.
*
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) {
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.
*