+
+ @Override
+ public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
+ final Map<String, Object> leaves) {
+ final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
+ fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
+ 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);
+ 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);
+ }
+ }
+
+ private void replaceDataNodeTree(final FragmentEntity existingFragmentEntity,
+ final DataNode newDataNode) {
+
+ existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
+
+ final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments()
+ .stream().collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
+
+ final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
+
+ for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
+ final FragmentEntity childFragment;
+ if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
+ childFragment = convertToFragmentWithAllDescendants(
+ existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
+ } else {
+ childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
+ replaceDataNodeTree(childFragment, newDataNodeChild);
+ }
+ updatedChildFragments.add(childFragment);
+ }
+ existingFragmentEntity.getChildFragments().clear();
+ existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
+ }
+
+ @Override
+ @Transactional
+ public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
+ final Collection<DataNode> newListElements) {
+ final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
+ final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
+ final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
+ extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
+ deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
+ final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
+ for (final DataNode newListElement : newListElements) {
+ final FragmentEntity existingListElementEntity =
+ existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
+ final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
+ existingListElementEntity);
+
+ updatedChildFragmentEntities.add(entityToBeAdded);
+ }
+ parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
+ fragmentRepository.save(parentEntity);
+ }
+
+ @Override
+ @Transactional
+ 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)));
+ }
+
+ @Override
+ @Transactional
+ public void deleteListDataNode(final String dataspaceName, final String anchorName,
+ final String targetXpath) {
+ deleteDataNode(dataspaceName, anchorName, targetXpath, true);
+ }
+
+ @Override
+ @Transactional
+ public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
+ deleteDataNode(dataspaceName, anchorName, targetXpath, false);
+ }
+
+ private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
+ final boolean onlySupportListNodeDeletion) {
+ final String parentNodeXpath;
+ FragmentEntity parentFragmentEntity = null;
+ boolean targetDeleted = false;
+ if (isRootXpath(targetXpath)) {
+ deleteDataNodes(dataspaceName, anchorName);
+ targetDeleted = true;
+ } else {
+ if (isRootContainerNodeXpath(targetXpath)) {
+ parentNodeXpath = targetXpath;
+ } else {
+ parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
+ }
+ 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();
+ if (isListElement) {
+ targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
+ } else {
+ targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath);
+ final boolean tryToDeleteDataNode = !targetDeleted && !onlySupportListNodeDeletion;
+ if (tryToDeleteDataNode) {
+ targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
+ }
+ }
+ }
+ if (!targetDeleted) {
+ final String additionalInformation = onlySupportListNodeDeletion
+ ? "The target is probably not a List." : "";
+ throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
+ parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
+ }
+ }
+
+ private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
+ if (parentFragmentEntity.getXpath().equals(targetXpath)) {
+ fragmentRepository.delete(parentFragmentEntity);
+ return true;
+ }
+ if (parentFragmentEntity.getChildFragments()
+ .removeIf(fragment -> fragment.getXpath().equals(targetXpath))) {
+ fragmentRepository.save(parentFragmentEntity);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
+ final String deleteTargetXpathPrefix = listXpath + "[";
+ if (parentFragmentEntity.getChildFragments()
+ .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
+ fragmentRepository.save(parentFragmentEntity);
+ return true;
+ }
+ return false;
+ }
+
+ private static void deleteListElements(
+ final Collection<FragmentEntity> fragmentEntities,
+ final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
+ fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
+ }
+
+ private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
+ if (newListElements.isEmpty()) {
+ throw new CpsAdminException("Invalid list replacement",
+ "Cannot replace list elements with empty collection");
+ }
+ final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
+ return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
+ }
+
+ private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
+ final DataNode newListElement,
+ final FragmentEntity existingListElementEntity) {
+ if (existingListElementEntity == null) {
+ return convertToFragmentWithAllDescendants(
+ parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
+ }
+ if (newListElement.getChildDataNodes().isEmpty()) {
+ copyAttributesFromNewListElement(existingListElementEntity, newListElement);
+ existingListElementEntity.getChildFragments().clear();
+ } else {
+ replaceDataNodeTree(existingListElementEntity, newListElement);
+ }
+ return existingListElementEntity;
+ }
+
+ private static boolean isNewDataNode(final DataNode replacementDataNode,
+ final Map<String, FragmentEntity> existingListElementsByXpath) {
+ 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 FragmentEntity replacementFragmentEntity =
+ FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
+ newListElement.getLeaves())).build();
+ existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
+ }
+
+ private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
+ final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
+ return childEntities.stream()
+ .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);
+ }