2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2024 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2020-2022 Bell Canada.
6 * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
20 * SPDX-License-Identifier: Apache-2.0
21 * ============LICENSE_END=========================================================
24 package org.onap.cps.spi.impl;
26 import static org.onap.cps.spi.PaginationOption.NO_PAGINATION;
28 import com.google.common.collect.ImmutableSet;
29 import com.google.common.collect.ImmutableSet.Builder;
30 import io.micrometer.core.annotation.Timed;
31 import jakarta.transaction.Transactional;
32 import java.io.Serializable;
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
41 import java.util.TreeMap;
42 import java.util.regex.Matcher;
43 import java.util.regex.Pattern;
44 import java.util.stream.Collectors;
45 import lombok.RequiredArgsConstructor;
46 import lombok.extern.slf4j.Slf4j;
47 import org.hibernate.StaleStateException;
48 import org.onap.cps.cpspath.parser.CpsPathQuery;
49 import org.onap.cps.cpspath.parser.CpsPathUtil;
50 import org.onap.cps.cpspath.parser.PathParsingException;
51 import org.onap.cps.spi.CpsDataPersistenceService;
52 import org.onap.cps.spi.FetchDescendantsOption;
53 import org.onap.cps.spi.PaginationOption;
54 import org.onap.cps.spi.entities.AnchorEntity;
55 import org.onap.cps.spi.entities.DataspaceEntity;
56 import org.onap.cps.spi.entities.FragmentEntity;
57 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
58 import org.onap.cps.spi.exceptions.ConcurrencyException;
59 import org.onap.cps.spi.exceptions.CpsAdminException;
60 import org.onap.cps.spi.exceptions.CpsPathException;
61 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
62 import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch;
63 import org.onap.cps.spi.model.DataNode;
64 import org.onap.cps.spi.model.DataNodeBuilder;
65 import org.onap.cps.spi.repository.AnchorRepository;
66 import org.onap.cps.spi.repository.DataspaceRepository;
67 import org.onap.cps.spi.repository.FragmentRepository;
68 import org.onap.cps.spi.utils.SessionManager;
69 import org.onap.cps.utils.JsonObjectMapper;
70 import org.springframework.dao.DataIntegrityViolationException;
71 import org.springframework.stereotype.Service;
75 @RequiredArgsConstructor
76 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
78 private final DataspaceRepository dataspaceRepository;
79 private final AnchorRepository anchorRepository;
80 private final FragmentRepository fragmentRepository;
81 private final JsonObjectMapper jsonObjectMapper;
82 private final SessionManager sessionManager;
84 private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@.+?])?)";
87 public void addChildDataNodes(final String dataspaceName, final String anchorName,
88 final String parentNodeXpath, final Collection<DataNode> dataNodes) {
89 final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
90 addChildrenDataNodes(anchorEntity, parentNodeXpath, dataNodes);
94 public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
95 final Collection<DataNode> newListElements) {
96 final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
97 addChildrenDataNodes(anchorEntity, parentNodeXpath, newListElements);
100 private void addNewChildDataNode(final AnchorEntity anchorEntity, final String parentNodeXpath,
101 final DataNode newChild) {
102 final FragmentEntity parentFragmentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
103 final FragmentEntity newChildAsFragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, newChild);
104 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
106 fragmentRepository.save(newChildAsFragmentEntity);
107 } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
108 throw AlreadyDefinedException.forDataNodes(Collections.singletonList(newChild.getXpath()),
109 anchorEntity.getName());
113 private void addChildrenDataNodes(final AnchorEntity anchorEntity, final String parentNodeXpath,
114 final Collection<DataNode> newChildren) {
115 final FragmentEntity parentFragmentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
116 final List<FragmentEntity> fragmentEntities = new ArrayList<>(newChildren.size());
118 for (final DataNode newChildAsDataNode : newChildren) {
119 final FragmentEntity newChildAsFragmentEntity =
120 convertToFragmentWithAllDescendants(anchorEntity, newChildAsDataNode);
121 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
122 fragmentEntities.add(newChildAsFragmentEntity);
124 fragmentRepository.saveAll(fragmentEntities);
125 } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
126 log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
127 dataIntegrityViolationException, fragmentEntities.size());
128 retrySavingEachChildIndividually(anchorEntity, parentNodeXpath, newChildren);
132 private void retrySavingEachChildIndividually(final AnchorEntity anchorEntity, final String parentNodeXpath,
133 final Collection<DataNode> newChildren) {
134 final Collection<String> failedXpaths = new HashSet<>();
135 for (final DataNode newChild : newChildren) {
137 addNewChildDataNode(anchorEntity, parentNodeXpath, newChild);
138 } catch (final AlreadyDefinedException alreadyDefinedException) {
139 failedXpaths.add(newChild.getXpath());
142 if (!failedXpaths.isEmpty()) {
143 throw AlreadyDefinedException.forDataNodes(failedXpaths, anchorEntity.getName());
148 public void storeDataNodes(final String dataspaceName, final String anchorName,
149 final Collection<DataNode> dataNodes) {
150 final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
151 final List<FragmentEntity> fragmentEntities = new ArrayList<>(dataNodes.size());
153 for (final DataNode dataNode: dataNodes) {
154 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
155 fragmentEntities.add(fragmentEntity);
157 fragmentRepository.saveAll(fragmentEntities);
158 } catch (final DataIntegrityViolationException exception) {
159 log.warn("Exception occurred : {} , While saving : {} data nodes, Retrying saving data nodes individually",
160 exception, dataNodes.size());
161 storeDataNodesIndividually(anchorEntity, dataNodes);
165 private void storeDataNodesIndividually(final AnchorEntity anchorEntity, final Collection<DataNode> dataNodes) {
166 final Collection<String> failedXpaths = new HashSet<>();
167 for (final DataNode dataNode: dataNodes) {
169 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
170 fragmentRepository.save(fragmentEntity);
171 } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
172 failedXpaths.add(dataNode.getXpath());
175 if (!failedXpaths.isEmpty()) {
176 throw AlreadyDefinedException.forDataNodes(failedXpaths, anchorEntity.getName());
181 * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
182 * for all DataNode children recursively.
184 * @param anchorEntity anchorEntity
185 * @param dataNodeToBeConverted dataNode
186 * @return a Fragment built from current DataNode
188 private FragmentEntity convertToFragmentWithAllDescendants(final AnchorEntity anchorEntity,
189 final DataNode dataNodeToBeConverted) {
190 final FragmentEntity parentFragment = toFragmentEntity(anchorEntity, dataNodeToBeConverted);
191 final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
192 for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
193 final FragmentEntity childFragment = convertToFragmentWithAllDescendants(anchorEntity, childDataNode);
194 childFragmentsImmutableSetBuilder.add(childFragment);
196 parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
197 return parentFragment;
200 private FragmentEntity toFragmentEntity(final AnchorEntity anchorEntity, final DataNode dataNode) {
201 return FragmentEntity.builder()
202 .anchor(anchorEntity)
203 .xpath(dataNode.getXpath())
204 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
209 @Timed(value = "cps.data.persistence.service.datanode.get",
210 description = "Time taken to get a data node")
211 public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
213 final FetchDescendantsOption fetchDescendantsOption) {
214 final String targetXpath = getNormalizedXpath(xpath);
215 final Collection<DataNode> dataNodes = getDataNodesForMultipleXpaths(dataspaceName, anchorName,
216 Collections.singletonList(targetXpath), fetchDescendantsOption);
217 if (dataNodes.isEmpty()) {
218 throw new DataNodeNotFoundException(dataspaceName, anchorName, xpath);
224 @Timed(value = "cps.data.persistence.service.datanode.batch.get",
225 description = "Time taken to get data nodes")
226 public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName,
227 final Collection<String> xpaths,
228 final FetchDescendantsOption fetchDescendantsOption) {
229 final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
230 Collection<FragmentEntity> fragmentEntities = getFragmentEntities(anchorEntity, xpaths);
231 fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
233 return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
236 private Collection<FragmentEntity> getFragmentEntities(final AnchorEntity anchorEntity,
237 final Collection<String> xpaths) {
238 final Collection<String> normalizedXpaths = getNormalizedXpaths(xpaths);
240 final boolean haveRootXpath = normalizedXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
242 final List<FragmentEntity> fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity,
245 for (final FragmentEntity fragmentEntity : fragmentEntities) {
246 normalizedXpaths.remove(fragmentEntity.getXpath());
249 for (final String xpath : normalizedXpaths) {
250 if (!CpsPathUtil.isPathToListElement(xpath)) {
251 fragmentEntities.addAll(fragmentRepository.findListByAnchorAndXpath(anchorEntity, xpath));
256 fragmentEntities.addAll(fragmentRepository.findRootsByAnchorId(anchorEntity.getId()));
259 return fragmentEntities;
262 private FragmentEntity getFragmentEntity(final AnchorEntity anchorEntity, final String xpath) {
263 final FragmentEntity fragmentEntity;
264 if (isRootXpath(xpath)) {
265 fragmentEntity = fragmentRepository.findOneByAnchorId(anchorEntity.getId()).orElse(null);
267 fragmentEntity = fragmentRepository.getByAnchorAndXpath(anchorEntity, getNormalizedXpath(xpath));
269 if (fragmentEntity == null) {
270 throw new DataNodeNotFoundException(anchorEntity.getDataspace().getName(), anchorEntity.getName(), xpath);
272 return fragmentEntity;
276 @Timed(value = "cps.data.persistence.service.datanode.query",
277 description = "Time taken to query data nodes")
278 public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
279 final FetchDescendantsOption fetchDescendantsOption) {
280 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
281 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
282 final CpsPathQuery cpsPathQuery;
284 cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
285 } catch (final PathParsingException pathParsingException) {
286 throw new CpsPathException(pathParsingException.getMessage());
289 Collection<FragmentEntity> fragmentEntities;
290 fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
291 if (cpsPathQuery.hasAncestorAxis()) {
292 final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
293 fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
295 fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
297 return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
301 @Timed(value = "cps.data.persistence.service.datanode.query.anchors",
302 description = "Time taken to query data nodes across all anchors or list of anchors")
303 public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
304 final FetchDescendantsOption fetchDescendantsOption,
305 final PaginationOption paginationOption) {
306 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
307 final CpsPathQuery cpsPathQuery;
309 cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
310 } catch (final PathParsingException e) {
311 throw new CpsPathException(e.getMessage());
314 final List<Long> anchorIds;
315 if (paginationOption == NO_PAGINATION) {
316 anchorIds = Collections.emptyList();
318 anchorIds = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
319 if (anchorIds.isEmpty()) {
320 return Collections.emptyList();
323 Collection<FragmentEntity> fragmentEntities =
324 fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds);
326 if (cpsPathQuery.hasAncestorAxis()) {
327 final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
328 if (anchorIds.isEmpty()) {
329 fragmentEntities = fragmentRepository.findByDataspaceAndXpathIn(dataspaceEntity, ancestorXpaths);
331 fragmentEntities = fragmentRepository.findByAnchorIdsAndXpathIn(
332 anchorIds.toArray(new Long[0]), ancestorXpaths.toArray(new String[0]));
336 fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
338 return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
341 private List<Long> getAnchorIdsForPagination(final DataspaceEntity dataspaceEntity, final CpsPathQuery cpsPathQuery,
342 final PaginationOption paginationOption) {
343 return fragmentRepository.findAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
346 private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption,
347 final Collection<FragmentEntity> fragmentEntities) {
348 final List<DataNode> dataNodes = new ArrayList<>(fragmentEntities.size());
349 for (final FragmentEntity fragmentEntity : fragmentEntities) {
350 dataNodes.add(toDataNode(fragmentEntity, fetchDescendantsOption));
352 return Collections.unmodifiableList(dataNodes);
355 private static String getNormalizedXpath(final String xpathSource) {
356 if (isRootXpath(xpathSource)) {
360 return CpsPathUtil.getNormalizedXpath(xpathSource);
361 } catch (final PathParsingException pathParsingException) {
362 throw new CpsPathException(pathParsingException.getMessage());
366 private static Collection<String> getNormalizedXpaths(final Collection<String> xpaths) {
367 final Collection<String> normalizedXpaths = new HashSet<>(xpaths.size());
368 for (final String xpath : xpaths) {
370 normalizedXpaths.add(getNormalizedXpath(xpath));
371 } catch (final CpsPathException cpsPathException) {
372 log.warn("Error parsing xpath \"{}\": {}", xpath, cpsPathException.getMessage());
375 return normalizedXpaths;
379 public String startSession() {
380 return sessionManager.startSession();
384 public void closeSession(final String sessionId) {
385 sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT);
389 public void lockAnchor(final String sessionId, final String dataspaceName,
390 final String anchorName, final Long timeoutInMilliseconds) {
391 sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
395 public Integer countAnchorsForDataspaceAndCpsPath(final String dataspaceName, final String cpsPath) {
396 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
397 final CpsPathQuery cpsPathQuery;
399 cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
400 } catch (final PathParsingException e) {
401 throw new CpsPathException(e.getMessage());
403 final List<Long> anchorIdList = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, NO_PAGINATION);
404 return anchorIdList.size();
407 private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities,
408 final CpsPathQuery cpsPathQuery) {
409 final Set<String> ancestorXpath = new HashSet<>();
410 final Pattern pattern =
411 Pattern.compile("(.*/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
412 + REG_EX_FOR_OPTIONAL_LIST_INDEX + "/.*");
413 for (final FragmentEntity fragmentEntity : fragmentEntities) {
414 final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
415 if (matcher.matches()) {
416 ancestorXpath.add(matcher.group(1));
419 return ancestorXpath;
422 private DataNode toDataNode(final FragmentEntity fragmentEntity,
423 final FetchDescendantsOption fetchDescendantsOption) {
424 final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
425 Map<String, Serializable> leaves = new HashMap<>();
426 if (fragmentEntity.getAttributes() != null) {
427 leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
429 return new DataNodeBuilder()
430 .withXpath(fragmentEntity.getXpath())
432 .withDataspace(fragmentEntity.getAnchor().getDataspace().getName())
433 .withAnchor(fragmentEntity.getAnchor().getName())
434 .withChildDataNodes(childDataNodes).build();
437 private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
438 final FetchDescendantsOption fetchDescendantsOption) {
439 if (fetchDescendantsOption.hasNext()) {
440 return fragmentEntity.getChildFragments().stream()
441 .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption.next()))
442 .collect(Collectors.toList());
444 return Collections.emptyList();
448 public void batchUpdateDataLeaves(final String dataspaceName, final String anchorName,
449 final Map<String, Map<String, Serializable>> updatedLeavesPerXPath) {
450 final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
452 final Collection<String> xpathsOfUpdatedLeaves = updatedLeavesPerXPath.keySet();
453 final Collection<FragmentEntity> fragmentEntities = getFragmentEntities(anchorEntity, xpathsOfUpdatedLeaves);
455 for (final FragmentEntity fragmentEntity : fragmentEntities) {
456 final Map<String, Serializable> updatedLeaves = updatedLeavesPerXPath.get(fragmentEntity.getXpath());
457 final String mergedLeaves = mergeLeaves(updatedLeaves, fragmentEntity.getAttributes());
458 fragmentEntity.setAttributes(mergedLeaves);
462 fragmentRepository.saveAll(fragmentEntities);
463 } catch (final StaleStateException staleStateException) {
464 retryUpdateDataNodesIndividually(anchorEntity, fragmentEntities);
469 public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName,
470 final Collection<DataNode> updatedDataNodes) {
471 final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
473 final Map<String, DataNode> xpathToUpdatedDataNode = updatedDataNodes.stream()
474 .collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode));
476 final Collection<String> xpaths = xpathToUpdatedDataNode.keySet();
477 Collection<FragmentEntity> existingFragmentEntities = getFragmentEntities(anchorEntity, xpaths);
478 existingFragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(
479 FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS, existingFragmentEntities);
481 for (final FragmentEntity existingFragmentEntity : existingFragmentEntities) {
482 final DataNode updatedDataNode = xpathToUpdatedDataNode.get(existingFragmentEntity.getXpath());
483 updateFragmentEntityAndDescendantsWithDataNode(existingFragmentEntity, updatedDataNode);
487 fragmentRepository.saveAll(existingFragmentEntities);
488 } catch (final StaleStateException staleStateException) {
489 retryUpdateDataNodesIndividually(anchorEntity, existingFragmentEntities);
493 private void retryUpdateDataNodesIndividually(final AnchorEntity anchorEntity,
494 final Collection<FragmentEntity> fragmentEntities) {
495 final Collection<String> failedXpaths = new HashSet<>();
496 for (final FragmentEntity dataNodeFragment : fragmentEntities) {
498 fragmentRepository.save(dataNodeFragment);
499 } catch (final StaleStateException staleStateException) {
500 failedXpaths.add(dataNodeFragment.getXpath());
503 if (!failedXpaths.isEmpty()) {
504 final String failedXpathsConcatenated = String.join(",", failedXpaths);
505 throw new ConcurrencyException("Concurrent Transactions", String.format(
506 "DataNodes : %s in Dataspace :'%s' with Anchor : '%s' are updated by another transaction.",
507 failedXpathsConcatenated, anchorEntity.getDataspace().getName(), anchorEntity.getName()));
511 private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
512 final DataNode newDataNode) {
513 copyAttributesFromNewDataNode(existingFragmentEntity, newDataNode);
515 final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
516 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
518 final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
519 for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
520 final FragmentEntity childFragment;
521 if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
522 childFragment = convertToFragmentWithAllDescendants(existingFragmentEntity.getAnchor(),
525 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
526 updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild);
528 updatedChildFragments.add(childFragment);
531 existingFragmentEntity.getChildFragments().clear();
532 existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
537 public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
538 final Collection<DataNode> newListElements) {
539 final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
540 final FragmentEntity parentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
541 final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
542 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
543 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
544 parentEntity.getChildFragments().removeAll(existingListElementFragmentEntitiesByXPath.values());
545 final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
546 for (final DataNode newListElement : newListElements) {
547 final FragmentEntity existingListElementEntity =
548 existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
549 final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
550 existingListElementEntity);
551 updatedChildFragmentEntities.add(entityToBeAdded);
553 parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
554 fragmentRepository.save(parentEntity);
559 public void deleteDataNodes(final String dataspaceName, final String anchorName) {
560 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
561 anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
562 .ifPresent(anchorEntity -> fragmentRepository.deleteByAnchorIn(Collections.singletonList(anchorEntity)));
567 public void deleteDataNodes(final String dataspaceName, final Collection<String> anchorNames) {
568 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
569 final Collection<AnchorEntity> anchorEntities =
570 anchorRepository.findAllByDataspaceAndNameIn(dataspaceEntity, anchorNames);
571 fragmentRepository.deleteByAnchorIn(anchorEntities);
576 public void deleteDataNodes(final String dataspaceName, final String anchorName,
577 final Collection<String> xpathsToDelete) {
578 deleteDataNodes(dataspaceName, anchorName, xpathsToDelete, false);
581 private void deleteDataNodes(final String dataspaceName, final String anchorName,
582 final Collection<String> xpathsToDelete, final boolean onlySupportListDeletion) {
583 final boolean haveRootXpath = xpathsToDelete.stream().anyMatch(CpsDataPersistenceServiceImpl::isRootXpath);
585 deleteDataNodes(dataspaceName, anchorName);
589 final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
591 final Collection<String> deleteChecklist = getNormalizedXpaths(xpathsToDelete);
592 final Collection<String> xpathsToExistingContainers =
593 fragmentRepository.findAllXpathByAnchorAndXpathIn(anchorEntity, deleteChecklist);
594 if (onlySupportListDeletion) {
595 final Collection<String> xpathsToExistingListElements = xpathsToExistingContainers.stream()
596 .filter(CpsPathUtil::isPathToListElement).collect(Collectors.toList());
597 deleteChecklist.removeAll(xpathsToExistingListElements);
599 deleteChecklist.removeAll(xpathsToExistingContainers);
602 final Collection<String> xpathsToExistingLists = deleteChecklist.stream()
603 .filter(xpath -> fragmentRepository.existsByAnchorAndXpathStartsWith(anchorEntity, xpath + "["))
604 .collect(Collectors.toList());
605 deleteChecklist.removeAll(xpathsToExistingLists);
607 if (!deleteChecklist.isEmpty()) {
608 throw new DataNodeNotFoundExceptionBatch(dataspaceName, anchorName, deleteChecklist);
611 fragmentRepository.deleteByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingContainers);
612 fragmentRepository.deleteListsByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingLists);
617 public void deleteListDataNode(final String dataspaceName, final String anchorName,
618 final String targetXpath) {
619 deleteDataNode(dataspaceName, anchorName, targetXpath, true);
624 public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
625 deleteDataNode(dataspaceName, anchorName, targetXpath, false);
628 private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
629 final boolean onlySupportListNodeDeletion) {
630 final String normalizedXpath = getNormalizedXpath(targetXpath);
632 deleteDataNodes(dataspaceName, anchorName, Collections.singletonList(normalizedXpath),
633 onlySupportListNodeDeletion);
634 } catch (final DataNodeNotFoundExceptionBatch dataNodeNotFoundExceptionBatch) {
635 throw new DataNodeNotFoundException(dataspaceName, anchorName, targetXpath);
639 private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
640 if (newListElements.isEmpty()) {
641 throw new CpsAdminException("Invalid list replacement",
642 "Cannot replace list elements with empty collection");
644 final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
645 return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
648 private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
649 final DataNode newListElement,
650 final FragmentEntity existingListElementEntity) {
651 if (existingListElementEntity == null) {
652 return convertToFragmentWithAllDescendants(parentEntity.getAnchor(), newListElement);
654 if (newListElement.getChildDataNodes().isEmpty()) {
655 copyAttributesFromNewDataNode(existingListElementEntity, newListElement);
656 existingListElementEntity.getChildFragments().clear();
658 updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
660 return existingListElementEntity;
663 private static boolean isNewDataNode(final DataNode replacementDataNode,
664 final Map<String, FragmentEntity> existingListElementsByXpath) {
665 return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
668 private void copyAttributesFromNewDataNode(final FragmentEntity existingFragmentEntity,
669 final DataNode newDataNode) {
670 final String oldOrderedLeavesAsJson = getOrderedLeavesAsJson(existingFragmentEntity.getAttributes());
671 final String newOrderedLeavesAsJson = getOrderedLeavesAsJson(newDataNode.getLeaves());
672 if (!oldOrderedLeavesAsJson.equals(newOrderedLeavesAsJson)) {
673 existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
677 private String getOrderedLeavesAsJson(final Map<String, Serializable> currentLeaves) {
678 final Map<String, Serializable> sortedLeaves = new TreeMap<>(String::compareTo);
679 sortedLeaves.putAll(currentLeaves);
680 return jsonObjectMapper.asJsonString(sortedLeaves);
683 private String getOrderedLeavesAsJson(final String currentLeavesAsString) {
684 if (currentLeavesAsString == null) {
687 final Map<String, Serializable> sortedLeaves = jsonObjectMapper.convertJsonString(currentLeavesAsString,
689 return jsonObjectMapper.asJsonString(sortedLeaves);
692 private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
693 final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
694 return childEntities.stream()
695 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
696 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
699 private static boolean isRootXpath(final String xpath) {
700 return "/".equals(xpath) || "".equals(xpath);
703 private String mergeLeaves(final Map<String, Serializable> updateLeaves, final String currentLeavesAsString) {
704 Map<String, Serializable> currentLeavesAsMap = new HashMap<>();
705 if (currentLeavesAsString != null) {
706 currentLeavesAsMap = jsonObjectMapper.convertJsonString(currentLeavesAsString, Map.class);
707 currentLeavesAsMap.putAll(updateLeaves);
710 if (currentLeavesAsMap.isEmpty()) {
713 return jsonObjectMapper.asJsonString(currentLeavesAsMap);
716 private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) {
717 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
718 return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);