X-Git-Url: https://gerrit.onap.org/r/gitweb?a=blobdiff_plain;f=cps-ri%2Fsrc%2Fmain%2Fjava%2Forg%2Fonap%2Fcps%2Fspi%2Fimpl%2FCpsDataPersistenceServiceImpl.java;h=f634008dc618c0dbdd893f8703008574660ae4d5;hb=ceda6a0b4aa498ae236092cf36d396c004c61cd7;hp=ed414fc30c002345a749d2aadedf04a77b8f0624;hpb=eaef84ed9d8dadadbe462d0d065c9bc802e84a74;p=cps.git 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 ed414fc30..f634008dc 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 @@ -1,8 +1,9 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2022 Nordix Foundation + * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2020-2022 Bell Canada. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +23,10 @@ package org.onap.cps.spi.impl; -import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; - import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; +import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -41,21 +42,29 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.StaleStateException; import org.onap.cps.cpspath.parser.CpsPathQuery; +import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.cpspath.parser.PathParsingException; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.entities.AnchorEntity; import org.onap.cps.spi.entities.DataspaceEntity; import org.onap.cps.spi.entities.FragmentEntity; +import org.onap.cps.spi.entities.FragmentEntityArranger; +import org.onap.cps.spi.entities.FragmentExtract; import org.onap.cps.spi.exceptions.AlreadyDefinedException; +import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch; import org.onap.cps.spi.exceptions.ConcurrencyException; import org.onap.cps.spi.exceptions.CpsAdminException; import org.onap.cps.spi.exceptions.CpsPathException; import org.onap.cps.spi.exceptions.DataNodeNotFoundException; +import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch; 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.FragmentQueryBuilder; import org.onap.cps.spi.repository.FragmentRepository; +import org.onap.cps.spi.utils.SessionManager; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; @@ -66,64 +75,143 @@ import org.springframework.stereotype.Service; public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService { private final DataspaceRepository dataspaceRepository; - private final AnchorRepository anchorRepository; - private final FragmentRepository fragmentRepository; - private final JsonObjectMapper jsonObjectMapper; - + private final SessionManager sessionManager; private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})"; - private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE = - Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$"); @Override - public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentXpath, - final DataNode dataNode) { - final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentXpath); - final FragmentEntity fragmentEntity = - toFragmentEntity(parentFragment.getDataspace(), parentFragment.getAnchor(), dataNode); - parentFragment.getChildFragments().add(fragmentEntity); - try { - fragmentRepository.save(parentFragment); - } catch (final DataIntegrityViolationException exception) { - throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception); - } + public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final DataNode newChildDataNode) { + addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChildDataNode); + } + + @Override + public void addChildDataNodes(final String dataspaceName, final String anchorName, + final String parentNodeXpath, final Collection dataNodes) { + addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); } @Override public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final Collection dataNodes) { - final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath); - final List newFragmentEntities = - dataNodes.stream().map( - dataNode -> toFragmentEntity(parentFragment.getDataspace(), parentFragment.getAnchor(), dataNode) - ).collect(Collectors.toUnmodifiableList()); - parentFragment.getChildFragments().addAll(newFragmentEntities); + final Collection newListElements) { + addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newListElements); + } + + @Override + public void addMultipleLists(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final Collection> newLists) { + final Collection failedXpaths = new HashSet<>(); + newLists.forEach(newList -> { + try { + addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newList); + } catch (final AlreadyDefinedExceptionBatch e) { + failedXpaths.addAll(e.getAlreadyDefinedXpaths()); + } + }); + + if (!failedXpaths.isEmpty()) { + throw new AlreadyDefinedExceptionBatch(failedXpaths); + } + + } + + private void addNewChildDataNode(final String dataspaceName, final String anchorName, + final String parentNodeXpath, final DataNode newChild) { + final FragmentEntity parentFragmentEntity = + getFragmentEntity(dataspaceName, anchorName, parentNodeXpath); + final FragmentEntity newChildAsFragmentEntity = + convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(), + parentFragmentEntity.getAnchor(), newChild); + newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId()); try { - fragmentRepository.save(parentFragment); - dataNodes.forEach( - dataNode -> getChildFragments(dataspaceName, anchorName, dataNode) - ); - } catch (final DataIntegrityViolationException exception) { - final List conflictXpaths = dataNodes.stream() - .map(DataNode::getXpath) - .collect(Collectors.toList()); - throw AlreadyDefinedException.forDataNodes(conflictXpaths, anchorName, exception); + fragmentRepository.save(newChildAsFragmentEntity); + } catch (final DataIntegrityViolationException e) { + throw AlreadyDefinedException.forDataNode(newChild.getXpath(), anchorName, e); + } + + } + + private void addChildrenDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final Collection newChildren) { + final FragmentEntity parentFragmentEntity = + getFragmentEntity(dataspaceName, anchorName, parentNodeXpath); + final List fragmentEntities = new ArrayList<>(newChildren.size()); + try { + newChildren.forEach(newChildAsDataNode -> { + final FragmentEntity newChildAsFragmentEntity = + convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(), + parentFragmentEntity.getAnchor(), newChildAsDataNode); + newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId()); + fragmentEntities.add(newChildAsFragmentEntity); + }); + fragmentRepository.saveAll(fragmentEntities); + } catch (final DataIntegrityViolationException e) { + log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations", + e, fragmentEntities.size()); + retrySavingEachChildIndividually(dataspaceName, anchorName, parentNodeXpath, newChildren); + } + } + + private void retrySavingEachChildIndividually(final String dataspaceName, final String anchorName, + final String parentNodeXpath, + final Collection newChildren) { + final Collection failedXpaths = new HashSet<>(); + for (final DataNode newChild : newChildren) { + try { + addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChild); + } catch (final AlreadyDefinedException e) { + failedXpaths.add(newChild.getXpath()); + } + } + if (!failedXpaths.isEmpty()) { + throw new AlreadyDefinedExceptionBatch(failedXpaths); } } @Override public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) { + storeDataNodes(dataspaceName, anchorName, Collections.singletonList(dataNode)); + } + + @Override + public void storeDataNodes(final String dataspaceName, final String anchorName, + final Collection dataNodes) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); - final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity, - dataNode); + final List fragmentEntities = new ArrayList<>(dataNodes.size()); try { - fragmentRepository.save(fragmentEntity); + for (final DataNode dataNode: dataNodes) { + final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity, + dataNode); + fragmentEntities.add(fragmentEntity); + } + fragmentRepository.saveAll(fragmentEntities); } catch (final DataIntegrityViolationException exception) { - throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception); + log.warn("Exception occurred : {} , While saving : {} data nodes, Retrying saving data nodes individually", + exception, dataNodes.size()); + storeDataNodesIndividually(dataspaceName, anchorName, dataNodes); + } + } + + private void storeDataNodesIndividually(final String dataspaceName, final String anchorName, + final Collection dataNodes) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + final Collection failedXpaths = new HashSet<>(); + for (final DataNode dataNode: dataNodes) { + try { + final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity, + dataNode); + fragmentRepository.save(fragmentEntity); + } catch (final DataIntegrityViolationException e) { + failedXpaths.add(dataNode.getXpath()); + } + } + if (!failedXpaths.isEmpty()) { + throw new AlreadyDefinedExceptionBatch(failedXpaths); } } @@ -137,88 +225,223 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService * @return a Fragment built from current DataNode */ private FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity, - final AnchorEntity anchorEntity, final DataNode dataNodeToBeConverted) { + final AnchorEntity anchorEntity, + final DataNode dataNodeToBeConverted) { final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted); final Builder childFragmentsImmutableSetBuilder = ImmutableSet.builder(); for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) { final FragmentEntity childFragment = - convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(), - childDataNode); + convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(), + childDataNode); childFragmentsImmutableSetBuilder.add(childFragment); } parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build()); return parentFragment; } - private void getChildFragments(final String dataspaceName, final String anchorName, final DataNode dataNode) { - for (final DataNode childDataNode: dataNode.getChildDataNodes()) { - final FragmentEntity getChildsParentFragmentByXPath = - getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath()); - final FragmentEntity childFragmentEntity = toFragmentEntity(getChildsParentFragmentByXPath.getDataspace(), - getChildsParentFragmentByXPath.getAnchor(), childDataNode); - getChildsParentFragmentByXPath.getChildFragments().add(childFragmentEntity); - fragmentRepository.save(getChildsParentFragmentByXPath); - } - } - private FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity, - final AnchorEntity anchorEntity, final DataNode dataNode) { + final AnchorEntity anchorEntity, final DataNode dataNode) { return FragmentEntity.builder() - .dataspace(dataspaceEntity) - .anchor(anchorEntity) - .xpath(dataNode.getXpath()) - .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves())) - .build(); + .dataspace(dataspaceEntity) + .anchor(anchorEntity) + .xpath(dataNode.getXpath()) + .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves())) + .build(); } @Override - public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath, - final FetchDescendantsOption fetchDescendantsOption) { - final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath); - return toDataNode(fragmentEntity, fetchDescendantsOption); + public Collection getDataNodes(final String dataspaceName, final String anchorName, + final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { + final String targetXpath = isRootXpath(xpath) ? xpath : CpsPathUtil.getNormalizedXpath(xpath); + final Collection dataNodes = getDataNodesForMultipleXpaths(dataspaceName, anchorName, + Collections.singletonList(targetXpath), fetchDescendantsOption); + if (dataNodes.isEmpty()) { + throw new DataNodeNotFoundException(dataspaceName, anchorName, xpath); + } + return dataNodes; } - private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName, - final String xpath) { + @Override + public Collection getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName, + final Collection xpaths, + final FetchDescendantsOption fetchDescendantsOption) { + final Collection fragmentEntities = getFragmentEntities(dataspaceName, anchorName, xpaths); + return toDataNodes(fragmentEntities, fetchDescendantsOption); + } + + private Collection getFragmentEntities(final String dataspaceName, final String anchorName, + final Collection xpaths) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + + final Collection nonRootXpaths = new HashSet<>(xpaths); + final boolean haveRootXpath = nonRootXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath); + + final Collection normalizedXpaths = new HashSet<>(nonRootXpaths.size()); + for (final String xpath : nonRootXpaths) { + try { + normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath)); + } catch (final PathParsingException e) { + log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage()); + } + } + final Collection fragmentEntities = + new HashSet<>(fragmentRepository.findByAnchorAndMultipleCpsPaths(anchorEntity.getId(), normalizedXpaths)); + + if (haveRootXpath) { + final List fragmentExtracts = fragmentRepository.getTopLevelFragments(dataspaceEntity, + anchorEntity); + fragmentEntities.addAll(FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts)); + } + + return fragmentEntities; + } + + private FragmentEntity getFragmentEntity(final String dataspaceName, final String anchorName, final String xpath) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + final FragmentEntity fragmentEntity; if (isRootXpath(xpath)) { - return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity); + final List fragmentExtracts = fragmentRepository.getTopLevelFragments(dataspaceEntity, + anchorEntity); + fragmentEntity = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts) + .stream().findFirst().orElse(null); } else { - return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, - xpath); + final String normalizedXpath = getNormalizedXpath(xpath); + fragmentEntity = + fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, normalizedXpath); } + if (fragmentEntity == null) { + throw new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath); + } + return fragmentEntity; + + } + + private Collection buildFragmentEntitiesFromFragmentExtracts(final AnchorEntity anchorEntity, + final String normalizedXpath) { + final List fragmentExtracts = + fragmentRepository.findByAnchorIdAndParentXpath(anchorEntity.getId(), normalizedXpath); + log.debug("Fetched {} fragment entities by anchor {} and cps path {}.", + fragmentExtracts.size(), anchorEntity.getName(), normalizedXpath); + return FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts); + } @Override public List queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath, - final FetchDescendantsOption fetchDescendantsOption) { + final FetchDescendantsOption fetchDescendantsOption) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); final CpsPathQuery cpsPathQuery; try { - cpsPathQuery = CpsPathQuery.createFrom(cpsPath); - } catch (final IllegalStateException e) { + cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath); + } catch (final PathParsingException e) { throw new CpsPathException(e.getMessage()); } - List fragmentEntities = - fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery); + + Collection fragmentEntities; + if (canUseRegexQuickFind(fetchDescendantsOption, cpsPathQuery)) { + return getDataNodesUsingRegexQuickFind(fetchDescendantsOption, anchorEntity, cpsPathQuery); + } + fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery); + if (cpsPathQuery.hasAncestorAxis()) { + fragmentEntities = getAncestorFragmentEntities(anchorEntity.getId(), cpsPathQuery, fragmentEntities); + } + return createDataNodesFromProxiedFragmentEntities(fetchDescendantsOption, anchorEntity, fragmentEntities); + } + + private static boolean canUseRegexQuickFind(final FetchDescendantsOption fetchDescendantsOption, + final CpsPathQuery cpsPathQuery) { + return fetchDescendantsOption.equals(FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + && !cpsPathQuery.hasLeafConditions() + && !cpsPathQuery.hasTextFunctionCondition(); + } + + private List getDataNodesUsingRegexQuickFind(final FetchDescendantsOption fetchDescendantsOption, + final AnchorEntity anchorEntity, + final CpsPathQuery cpsPathQuery) { + Collection fragmentEntities; + final String xpathRegex = FragmentQueryBuilder.getXpathSqlRegex(cpsPathQuery, true); + final List fragmentExtracts = + fragmentRepository.quickFindWithDescendants(anchorEntity.getId(), xpathRegex); + fragmentEntities = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts); if (cpsPathQuery.hasAncestorAxis()) { - final Set ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); - fragmentEntities = ancestorXpaths.isEmpty() - ? Collections.emptyList() : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths); + fragmentEntities = getAncestorFragmentEntities(anchorEntity.getId(), cpsPathQuery, fragmentEntities); } - return fragmentEntities.stream() - .map(fragmentEntity -> toDataNode(fragmentEntity, fetchDescendantsOption)) - .collect(Collectors.toUnmodifiableList()); + return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities); } - private static Set processAncestorXpath(final List fragmentEntities, - final CpsPathQuery cpsPathQuery) { + private Collection getAncestorFragmentEntities(final int anchorId, + final CpsPathQuery cpsPathQuery, + final Collection fragmentEntities) { + final Collection ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); + return ancestorXpaths.isEmpty() ? Collections.emptyList() + : fragmentRepository.findByAnchorAndMultipleCpsPaths(anchorId, ancestorXpaths); + } + + private List createDataNodesFromProxiedFragmentEntities( + final FetchDescendantsOption fetchDescendantsOption, + final AnchorEntity anchorEntity, + final Collection proxiedFragmentEntities) { + final List dataNodes = new ArrayList<>(proxiedFragmentEntities.size()); + for (final FragmentEntity proxiedFragmentEntity : proxiedFragmentEntities) { + if (FetchDescendantsOption.OMIT_DESCENDANTS.equals(fetchDescendantsOption)) { + dataNodes.add(toDataNode(proxiedFragmentEntity, fetchDescendantsOption)); + } else { + final String normalizedXpath = getNormalizedXpath(proxiedFragmentEntity.getXpath()); + final Collection unproxiedFragmentEntities = + buildFragmentEntitiesFromFragmentExtracts(anchorEntity, normalizedXpath); + for (final FragmentEntity unproxiedFragmentEntity : unproxiedFragmentEntities) { + dataNodes.add(toDataNode(unproxiedFragmentEntity, fetchDescendantsOption)); + } + } + } + return Collections.unmodifiableList(dataNodes); + } + + private List createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption, + final Collection fragmentEntities) { + final List dataNodes = new ArrayList<>(fragmentEntities.size()); + for (final FragmentEntity fragmentEntity : fragmentEntities) { + dataNodes.add(toDataNode(fragmentEntity, fetchDescendantsOption)); + } + return Collections.unmodifiableList(dataNodes); + } + + private static String getNormalizedXpath(final String xpathSource) { + final String normalizedXpath; + try { + normalizedXpath = CpsPathUtil.getNormalizedXpath(xpathSource); + } catch (final PathParsingException e) { + throw new CpsPathException(e.getMessage()); + } + return normalizedXpath; + } + + @Override + public String startSession() { + return sessionManager.startSession(); + } + + @Override + public void closeSession(final String sessionId) { + sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT); + } + + @Override + public void lockAnchor(final String sessionId, final String dataspaceName, + final String anchorName, final Long timeoutInMilliseconds) { + sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds); + } + + private static Set processAncestorXpath(final Collection fragmentEntities, + final CpsPathQuery cpsPathQuery) { final Set ancestorXpath = new HashSet<>(); final Pattern pattern = - Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier()) - + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*"); + Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier()) + + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*"); for (final FragmentEntity fragmentEntity : fragmentEntities) { final Matcher matcher = pattern.matcher(fragmentEntity.getXpath()); if (matcher.matches()) { @@ -229,57 +452,110 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } private DataNode toDataNode(final FragmentEntity fragmentEntity, - final FetchDescendantsOption fetchDescendantsOption) { + final FetchDescendantsOption fetchDescendantsOption) { final List childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption); - Map leaves = new HashMap<>(); + Map leaves = new HashMap<>(); if (fragmentEntity.getAttributes() != null) { leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class); } return new DataNodeBuilder() - .withXpath(fragmentEntity.getXpath()) - .withLeaves(leaves) - .withChildDataNodes(childDataNodes).build(); + .withXpath(fragmentEntity.getXpath()) + .withLeaves(leaves) + .withChildDataNodes(childDataNodes).build(); + } + + private Collection toDataNodes(final Collection fragmentEntities, + final FetchDescendantsOption fetchDescendantsOption) { + final Collection dataNodes = new ArrayList<>(fragmentEntities.size()); + for (final FragmentEntity fragmentEntity : fragmentEntities) { + dataNodes.add(toDataNode(fragmentEntity, fetchDescendantsOption)); + } + return dataNodes; } private List getChildDataNodes(final FragmentEntity fragmentEntity, - final FetchDescendantsOption fetchDescendantsOption) { - if (fetchDescendantsOption == INCLUDE_ALL_DESCENDANTS) { + final FetchDescendantsOption fetchDescendantsOption) { + if (fetchDescendantsOption.hasNext()) { return fragmentEntity.getChildFragments().stream() - .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption)) - .collect(Collectors.toUnmodifiableList()); + .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption.next())) + .collect(Collectors.toList()); } return Collections.emptyList(); } @Override public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath, - final Map leaves) { - final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath); - fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves)); + final Map updateLeaves) { + final FragmentEntity fragmentEntity = getFragmentEntity(dataspaceName, anchorName, xpath); + final String currentLeavesAsString = fragmentEntity.getAttributes(); + final String mergedLeaves = mergeLeaves(updateLeaves, currentLeavesAsString); + fragmentEntity.setAttributes(mergedLeaves); fragmentRepository.save(fragmentEntity); } @Override - public void replaceDataNodeTree(final String dataspaceName, final String anchorName, final DataNode dataNode) { - final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath()); - replaceDataNodeTree(fragmentEntity, dataNode); + public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName, + final DataNode dataNode) { + final FragmentEntity fragmentEntity = getFragmentEntity(dataspaceName, anchorName, dataNode.getXpath()); + updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode); try { fragmentRepository.save(fragmentEntity); } catch (final StaleStateException staleStateException) { throw new ConcurrencyException("Concurrent Transactions", - String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.", - dataspaceName, anchorName, dataNode.getXpath()), - staleStateException); + String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.", + dataspaceName, anchorName, dataNode.getXpath())); + } + } + + @Override + public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName, + final List updatedDataNodes) { + final Map xpathToUpdatedDataNode = updatedDataNodes.stream() + .collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode)); + + final Collection xpaths = xpathToUpdatedDataNode.keySet(); + final Collection existingFragmentEntities = + getFragmentEntities(dataspaceName, anchorName, xpaths); + + for (final FragmentEntity existingFragmentEntity : existingFragmentEntities) { + final DataNode updatedDataNode = xpathToUpdatedDataNode.get(existingFragmentEntity.getXpath()); + updateFragmentEntityAndDescendantsWithDataNode(existingFragmentEntity, updatedDataNode); + } + + try { + fragmentRepository.saveAll(existingFragmentEntities); + } catch (final StaleStateException staleStateException) { + retryUpdateDataNodesIndividually(dataspaceName, anchorName, existingFragmentEntities); } } - private void replaceDataNodeTree(final FragmentEntity existingFragmentEntity, - final DataNode newDataNode) { + private void retryUpdateDataNodesIndividually(final String dataspaceName, final String anchorName, + final Collection fragmentEntities) { + final Collection failedXpaths = new HashSet<>(); + + fragmentEntities.forEach(dataNodeFragment -> { + try { + fragmentRepository.save(dataNodeFragment); + } catch (final StaleStateException e) { + failedXpaths.add(dataNodeFragment.getXpath()); + } + }); + + if (!failedXpaths.isEmpty()) { + final String failedXpathsConcatenated = String.join(",", failedXpaths); + throw new ConcurrencyException("Concurrent Transactions", String.format( + "DataNodes : %s in Dataspace :'%s' with Anchor : '%s' are updated by another transaction.", + failedXpathsConcatenated, dataspaceName, anchorName)); + } + } + + private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity, + final DataNode newDataNode) { existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves())); - final Map existingChildrenByXpath = existingFragmentEntity.getChildFragments() - .stream().collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity)); + final Map existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream() + .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity)); final Collection updatedChildFragments = new HashSet<>(); @@ -287,13 +563,14 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService final FragmentEntity childFragment; if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) { childFragment = convertToFragmentWithAllDescendants( - existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild); + existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild); } else { childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath()); - replaceDataNodeTree(childFragment, newDataNodeChild); + updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild); } updatedChildFragments.add(childFragment); } + existingFragmentEntity.getChildFragments().clear(); existingFragmentEntity.getChildFragments().addAll(updatedChildFragments); } @@ -302,17 +579,17 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Transactional public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection newListElements) { - final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath); + final FragmentEntity parentEntity = getFragmentEntity(dataspaceName, anchorName, parentNodeXpath); final String listElementXpathPrefix = getListElementXpathPrefix(newListElements); final Map existingListElementFragmentEntitiesByXPath = - extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix); + extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix); deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath); final Set updatedChildFragmentEntities = new HashSet<>(); for (final DataNode newListElement : newListElements) { final FragmentEntity existingListElementEntity = - existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath()); + existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath()); final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement, - existingListElementEntity); + existingListElementEntity); updatedChildFragmentEntities.add(entityToBeAdded); } @@ -325,8 +602,50 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService public void deleteDataNodes(final String dataspaceName, final String anchorName) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName) - .ifPresent( - anchorEntity -> fragmentRepository.deleteByAnchorIn(Set.of(anchorEntity))); + .ifPresent( + anchorEntity -> fragmentRepository.deleteByAnchorIn(Set.of(anchorEntity))); + } + + @Override + @Transactional + public void deleteDataNodes(final String dataspaceName, final Collection anchorNames) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final Collection anchorEntities = + anchorRepository.findAllByDataspaceAndNameIn(dataspaceEntity, anchorNames); + fragmentRepository.deleteByAnchorIn(anchorEntities); + } + + @Override + @Transactional + public void deleteDataNodes(final String dataspaceName, final String anchorName, + final Collection xpathsToDelete) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + + final Collection deleteChecklist = new HashSet<>(xpathsToDelete.size()); + for (final String xpath : xpathsToDelete) { + try { + deleteChecklist.add(CpsPathUtil.getNormalizedXpath(xpath)); + } catch (final PathParsingException e) { + log.debug("Error parsing xpath \"{}\": {}", xpath, e.getMessage()); + } + } + + final Collection xpathsToExistingContainers = + fragmentRepository.findAllXpathByAnchorAndXpathIn(anchorEntity, deleteChecklist); + deleteChecklist.removeAll(xpathsToExistingContainers); + + final Collection xpathsToExistingLists = deleteChecklist.stream() + .filter(xpath -> fragmentRepository.existsByAnchorAndXpathStartsWith(anchorEntity, xpath + "[")) + .collect(Collectors.toList()); + deleteChecklist.removeAll(xpathsToExistingLists); + + if (!deleteChecklist.isEmpty()) { + throw new DataNodeNotFoundExceptionBatch(dataspaceName, anchorName, deleteChecklist); + } + + fragmentRepository.deleteByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingContainers); + fragmentRepository.deleteListsByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingLists); } @Override @@ -344,42 +663,56 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath, final boolean onlySupportListNodeDeletion) { - final String parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/')); - final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath); - final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/')); - final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE.matcher(lastXpathElement).find(); - boolean targetExist; - if (isListElement) { - targetExist = deleteDataNode(parentFragmentEntity, targetXpath); + final String parentNodeXpath; + FragmentEntity parentFragmentEntity = null; + boolean targetDeleted; + if (isRootXpath(targetXpath)) { + deleteDataNodes(dataspaceName, anchorName); + targetDeleted = true; } else { - targetExist = deleteAllListElements(parentFragmentEntity, targetXpath); - final boolean tryToDeleteDataNode = !targetExist && !onlySupportListNodeDeletion; - if (tryToDeleteDataNode) { - targetExist = deleteDataNode(parentFragmentEntity, targetXpath); + if (isRootContainerNodeXpath(targetXpath)) { + parentNodeXpath = targetXpath; + } else { + parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(targetXpath); + } + parentFragmentEntity = getFragmentEntity(dataspaceName, anchorName, parentNodeXpath); + if (CpsPathUtil.isPathToListElement(targetXpath)) { + targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath); + } else { + targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath); + final boolean tryToDeleteDataNode = !targetDeleted && !onlySupportListNodeDeletion; + if (tryToDeleteDataNode) { + targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath); + } } } - if (!targetExist) { + if (!targetDeleted) { final String additionalInformation = onlySupportListNodeDeletion - ? "The target is probably not a List." : ""; + ? "The target is probably not a List." : ""; throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(), - parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation); + parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation); } } private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) { + final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath); + if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) { + fragmentRepository.deleteFragmentEntity(parentFragmentEntity.getId()); + return true; + } if (parentFragmentEntity.getChildFragments() - .removeIf(fragment -> fragment.getXpath().equals(targetXpath))) { + .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) { fragmentRepository.save(parentFragmentEntity); return true; } return false; } - private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) { - final String deleteTargetXpathPrefix = listXpath + "["; + final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath); + final String deleteTargetXpathPrefix = normalizedListXpath + "["; if (parentFragmentEntity.getChildFragments() - .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) { + .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) { fragmentRepository.save(parentFragmentEntity); return true; } @@ -387,32 +720,32 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } private static void deleteListElements( - final Collection fragmentEntities, - final Map existingListElementFragmentEntitiesByXPath) { + final Collection fragmentEntities, + final Map existingListElementFragmentEntitiesByXPath) { fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values()); } private static String getListElementXpathPrefix(final Collection newListElements) { if (newListElements.isEmpty()) { throw new CpsAdminException("Invalid list replacement", - "Cannot replace list elements with empty collection"); + "Cannot replace list elements with empty collection"); } final String firstChildNodeXpath = newListElements.iterator().next().getXpath(); - return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf("[") + 1); + return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1); } private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity, - final DataNode newListElement, - final FragmentEntity existingListElementEntity) { + final DataNode newListElement, + final FragmentEntity existingListElementEntity) { if (existingListElementEntity == null) { return convertToFragmentWithAllDescendants( - parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement); + parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement); } if (newListElement.getChildDataNodes().isEmpty()) { copyAttributesFromNewListElement(existingListElementEntity, newListElement); existingListElementEntity.getChildFragments().clear(); } else { - replaceDataNodeTree(existingListElementEntity, newListElement); + updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement); } return existingListElementEntity; } @@ -422,8 +755,12 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath()); } + private static boolean isRootContainerNodeXpath(final String xpath) { + return 0 == xpath.lastIndexOf('/'); + } + private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity, - final DataNode newListElement) { + final DataNode newListElement) { final FragmentEntity replacementFragmentEntity = FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString( newListElement.getLeaves())).build(); @@ -431,13 +768,23 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } private static Map extractListElementFragmentEntitiesByXPath( - final Set childEntities, final String listElementXpathPrefix) { + final Set childEntities, final String listElementXpathPrefix) { return childEntities.stream() - .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix)) - .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity)); + .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix)) + .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity)); } private static boolean isRootXpath(final String xpath) { return "/".equals(xpath) || "".equals(xpath); } + + private String mergeLeaves(final Map updateLeaves, final String currentLeavesAsString) { + final Map currentLeavesAsMap = currentLeavesAsString.isEmpty() + ? new HashMap<>() : jsonObjectMapper.convertJsonString(currentLeavesAsString, Map.class); + currentLeavesAsMap.putAll(updateLeaves); + if (currentLeavesAsMap.isEmpty()) { + return ""; + } + return jsonObjectMapper.asJsonString(currentLeavesAsMap); + } }