Normalize JSON attributes during update
[cps.git] / cps-ri / src / main / java / org / onap / cps / spi / impl / CpsDataPersistenceServiceImpl.java
1 /*
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
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
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.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.spi.impl;
25
26 import static org.onap.cps.spi.PaginationOption.NO_PAGINATION;
27
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;
39 import java.util.Map;
40 import java.util.Set;
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;
72
73 @Service
74 @Slf4j
75 @RequiredArgsConstructor
76 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
77
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;
83
84     private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@.+?])?)";
85
86     @Override
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);
91     }
92
93     @Override
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);
98     }
99
100     @Override
101     public void addMultipleLists(final String dataspaceName, final String anchorName, final String parentNodeXpath,
102                                  final Collection<Collection<DataNode>> newLists) {
103         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
104         final Collection<String> failedXpaths = new HashSet<>();
105         for (final Collection<DataNode> newList : newLists) {
106             try {
107                 addChildrenDataNodes(anchorEntity, parentNodeXpath, newList);
108             } catch (final AlreadyDefinedException alreadyDefinedException) {
109                 failedXpaths.addAll(alreadyDefinedException.getAlreadyDefinedObjectNames());
110             }
111         }
112         if (!failedXpaths.isEmpty()) {
113             throw AlreadyDefinedException.forDataNodes(failedXpaths, anchorEntity.getName());
114         }
115     }
116
117     private void addNewChildDataNode(final AnchorEntity anchorEntity, final String parentNodeXpath,
118                                      final DataNode newChild) {
119         final FragmentEntity parentFragmentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
120         final FragmentEntity newChildAsFragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, newChild);
121         newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
122         try {
123             fragmentRepository.save(newChildAsFragmentEntity);
124         } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
125             throw AlreadyDefinedException.forDataNodes(Collections.singletonList(newChild.getXpath()),
126                     anchorEntity.getName());
127         }
128     }
129
130     private void addChildrenDataNodes(final AnchorEntity anchorEntity, final String parentNodeXpath,
131                                       final Collection<DataNode> newChildren) {
132         final FragmentEntity parentFragmentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
133         final List<FragmentEntity> fragmentEntities = new ArrayList<>(newChildren.size());
134         try {
135             for (final DataNode newChildAsDataNode : newChildren) {
136                 final FragmentEntity newChildAsFragmentEntity =
137                     convertToFragmentWithAllDescendants(anchorEntity, newChildAsDataNode);
138                 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
139                 fragmentEntities.add(newChildAsFragmentEntity);
140             }
141             fragmentRepository.saveAll(fragmentEntities);
142         } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
143             log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
144                     dataIntegrityViolationException, fragmentEntities.size());
145             retrySavingEachChildIndividually(anchorEntity, parentNodeXpath, newChildren);
146         }
147     }
148
149     private void retrySavingEachChildIndividually(final AnchorEntity anchorEntity, final String parentNodeXpath,
150                                                   final Collection<DataNode> newChildren) {
151         final Collection<String> failedXpaths = new HashSet<>();
152         for (final DataNode newChild : newChildren) {
153             try {
154                 addNewChildDataNode(anchorEntity, parentNodeXpath, newChild);
155             } catch (final AlreadyDefinedException alreadyDefinedException) {
156                 failedXpaths.add(newChild.getXpath());
157             }
158         }
159         if (!failedXpaths.isEmpty()) {
160             throw AlreadyDefinedException.forDataNodes(failedXpaths, anchorEntity.getName());
161         }
162     }
163
164     @Override
165     public void storeDataNodes(final String dataspaceName, final String anchorName,
166                                final Collection<DataNode> dataNodes) {
167         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
168         final List<FragmentEntity> fragmentEntities = new ArrayList<>(dataNodes.size());
169         try {
170             for (final DataNode dataNode: dataNodes) {
171                 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
172                 fragmentEntities.add(fragmentEntity);
173             }
174             fragmentRepository.saveAll(fragmentEntities);
175         } catch (final DataIntegrityViolationException exception) {
176             log.warn("Exception occurred : {} , While saving : {} data nodes, Retrying saving data nodes individually",
177                     exception, dataNodes.size());
178             storeDataNodesIndividually(anchorEntity, dataNodes);
179         }
180     }
181
182     private void storeDataNodesIndividually(final AnchorEntity anchorEntity, final Collection<DataNode> dataNodes) {
183         final Collection<String> failedXpaths = new HashSet<>();
184         for (final DataNode dataNode: dataNodes) {
185             try {
186                 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
187                 fragmentRepository.save(fragmentEntity);
188             } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
189                 failedXpaths.add(dataNode.getXpath());
190             }
191         }
192         if (!failedXpaths.isEmpty()) {
193             throw AlreadyDefinedException.forDataNodes(failedXpaths, anchorEntity.getName());
194         }
195     }
196
197     /**
198      * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
199      * for all DataNode children recursively.
200      *
201      * @param anchorEntity          anchorEntity
202      * @param dataNodeToBeConverted dataNode
203      * @return a Fragment built from current DataNode
204      */
205     private FragmentEntity convertToFragmentWithAllDescendants(final AnchorEntity anchorEntity,
206                                                                final DataNode dataNodeToBeConverted) {
207         final FragmentEntity parentFragment = toFragmentEntity(anchorEntity, dataNodeToBeConverted);
208         final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
209         for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
210             final FragmentEntity childFragment = convertToFragmentWithAllDescendants(anchorEntity, childDataNode);
211             childFragmentsImmutableSetBuilder.add(childFragment);
212         }
213         parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
214         return parentFragment;
215     }
216
217     private FragmentEntity toFragmentEntity(final AnchorEntity anchorEntity, final DataNode dataNode) {
218         return FragmentEntity.builder()
219                 .anchor(anchorEntity)
220                 .xpath(dataNode.getXpath())
221                 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
222                 .build();
223     }
224
225     @Override
226     @Timed(value = "cps.data.persistence.service.datanode.get",
227             description = "Time taken to get a data node")
228     public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
229                                              final String xpath,
230                                              final FetchDescendantsOption fetchDescendantsOption) {
231         final String targetXpath = getNormalizedXpath(xpath);
232         final Collection<DataNode> dataNodes = getDataNodesForMultipleXpaths(dataspaceName, anchorName,
233                 Collections.singletonList(targetXpath), fetchDescendantsOption);
234         if (dataNodes.isEmpty()) {
235             throw new DataNodeNotFoundException(dataspaceName, anchorName, xpath);
236         }
237         return dataNodes;
238     }
239
240     @Override
241     @Timed(value = "cps.data.persistence.service.datanode.batch.get",
242             description = "Time taken to get data nodes")
243     public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName,
244                                                               final Collection<String> xpaths,
245                                                               final FetchDescendantsOption fetchDescendantsOption) {
246         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
247         Collection<FragmentEntity> fragmentEntities = getFragmentEntities(anchorEntity, xpaths);
248         fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
249                 fragmentEntities);
250         return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
251     }
252
253     private Collection<FragmentEntity> getFragmentEntities(final AnchorEntity anchorEntity,
254                                                            final Collection<String> xpaths) {
255         final Collection<String> normalizedXpaths = getNormalizedXpaths(xpaths);
256
257         final boolean haveRootXpath = normalizedXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
258
259         final List<FragmentEntity> fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity,
260                 normalizedXpaths);
261
262         for (final FragmentEntity fragmentEntity : fragmentEntities) {
263             normalizedXpaths.remove(fragmentEntity.getXpath());
264         }
265
266         for (final String xpath : normalizedXpaths) {
267             if (!CpsPathUtil.isPathToListElement(xpath)) {
268                 fragmentEntities.addAll(fragmentRepository.findListByAnchorAndXpath(anchorEntity, xpath));
269             }
270         }
271
272         if (haveRootXpath) {
273             fragmentEntities.addAll(fragmentRepository.findRootsByAnchorId(anchorEntity.getId()));
274         }
275
276         return fragmentEntities;
277     }
278
279     private FragmentEntity getFragmentEntity(final AnchorEntity anchorEntity, final String xpath) {
280         final FragmentEntity fragmentEntity;
281         if (isRootXpath(xpath)) {
282             fragmentEntity = fragmentRepository.findOneByAnchorId(anchorEntity.getId()).orElse(null);
283         } else {
284             fragmentEntity = fragmentRepository.getByAnchorAndXpath(anchorEntity, getNormalizedXpath(xpath));
285         }
286         if (fragmentEntity == null) {
287             throw new DataNodeNotFoundException(anchorEntity.getDataspace().getName(), anchorEntity.getName(), xpath);
288         }
289         return fragmentEntity;
290     }
291
292     @Override
293     @Timed(value = "cps.data.persistence.service.datanode.query",
294             description = "Time taken to query data nodes")
295     public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
296                                          final FetchDescendantsOption fetchDescendantsOption) {
297         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
298         final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
299         final CpsPathQuery cpsPathQuery;
300         try {
301             cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
302         } catch (final PathParsingException pathParsingException) {
303             throw new CpsPathException(pathParsingException.getMessage());
304         }
305
306         Collection<FragmentEntity> fragmentEntities;
307         fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
308         if (cpsPathQuery.hasAncestorAxis()) {
309             final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
310             fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
311         }
312         fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
313                 fragmentEntities);
314         return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
315     }
316
317     @Override
318     @Timed(value = "cps.data.persistence.service.datanode.query.anchors",
319             description = "Time taken to query data nodes across all anchors or list of anchors")
320     public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
321                                                       final FetchDescendantsOption fetchDescendantsOption,
322                                                       final PaginationOption paginationOption) {
323         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
324         final CpsPathQuery cpsPathQuery;
325         try {
326             cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
327         } catch (final PathParsingException e) {
328             throw new CpsPathException(e.getMessage());
329         }
330
331         final List<Long> anchorIds;
332         if (paginationOption == NO_PAGINATION) {
333             anchorIds = Collections.emptyList();
334         } else {
335             anchorIds = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
336             if (anchorIds.isEmpty()) {
337                 return Collections.emptyList();
338             }
339         }
340         Collection<FragmentEntity> fragmentEntities =
341             fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds);
342
343         if (cpsPathQuery.hasAncestorAxis()) {
344             final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
345             if (anchorIds.isEmpty()) {
346                 fragmentEntities = fragmentRepository.findByDataspaceAndXpathIn(dataspaceEntity, ancestorXpaths);
347             } else {
348                 fragmentEntities = fragmentRepository.findByAnchorIdsAndXpathIn(
349                         anchorIds.toArray(new Long[0]), ancestorXpaths.toArray(new String[0]));
350             }
351
352         }
353         fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
354                 fragmentEntities);
355         return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
356     }
357
358     private List<Long> getAnchorIdsForPagination(final DataspaceEntity dataspaceEntity, final CpsPathQuery cpsPathQuery,
359                                                  final PaginationOption paginationOption) {
360         return fragmentRepository.findAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
361     }
362
363     private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption,
364                                                                final Collection<FragmentEntity> fragmentEntities) {
365         final List<DataNode> dataNodes = new ArrayList<>(fragmentEntities.size());
366         for (final FragmentEntity fragmentEntity : fragmentEntities) {
367             dataNodes.add(toDataNode(fragmentEntity, fetchDescendantsOption));
368         }
369         return Collections.unmodifiableList(dataNodes);
370     }
371
372     private static String getNormalizedXpath(final String xpathSource) {
373         if (isRootXpath(xpathSource)) {
374             return xpathSource;
375         }
376         try {
377             return CpsPathUtil.getNormalizedXpath(xpathSource);
378         } catch (final PathParsingException pathParsingException) {
379             throw new CpsPathException(pathParsingException.getMessage());
380         }
381     }
382
383     private static Collection<String> getNormalizedXpaths(final Collection<String> xpaths) {
384         final Collection<String> normalizedXpaths = new HashSet<>(xpaths.size());
385         for (final String xpath : xpaths) {
386             try {
387                 normalizedXpaths.add(getNormalizedXpath(xpath));
388             } catch (final CpsPathException cpsPathException) {
389                 log.warn("Error parsing xpath \"{}\": {}", xpath, cpsPathException.getMessage());
390             }
391         }
392         return normalizedXpaths;
393     }
394
395     @Override
396     public String startSession() {
397         return sessionManager.startSession();
398     }
399
400     @Override
401     public void closeSession(final String sessionId) {
402         sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT);
403     }
404
405     @Override
406     public void lockAnchor(final String sessionId, final String dataspaceName,
407                            final String anchorName, final Long timeoutInMilliseconds) {
408         sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
409     }
410
411     @Override
412     public Integer countAnchorsForDataspaceAndCpsPath(final String dataspaceName, final String cpsPath) {
413         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
414         final CpsPathQuery cpsPathQuery;
415         try {
416             cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
417         } catch (final PathParsingException e) {
418             throw new CpsPathException(e.getMessage());
419         }
420         final List<Long> anchorIdList = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, NO_PAGINATION);
421         return anchorIdList.size();
422     }
423
424     private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities,
425                                                     final CpsPathQuery cpsPathQuery) {
426         final Set<String> ancestorXpath = new HashSet<>();
427         final Pattern pattern =
428                 Pattern.compile("(.*/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
429                         + REG_EX_FOR_OPTIONAL_LIST_INDEX + "/.*");
430         for (final FragmentEntity fragmentEntity : fragmentEntities) {
431             final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
432             if (matcher.matches()) {
433                 ancestorXpath.add(matcher.group(1));
434             }
435         }
436         return ancestorXpath;
437     }
438
439     private DataNode toDataNode(final FragmentEntity fragmentEntity,
440                                 final FetchDescendantsOption fetchDescendantsOption) {
441         final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
442         Map<String, Serializable> leaves = new HashMap<>();
443         if (fragmentEntity.getAttributes() != null) {
444             leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
445         }
446         return new DataNodeBuilder()
447                 .withXpath(fragmentEntity.getXpath())
448                 .withLeaves(leaves)
449                 .withDataspace(fragmentEntity.getAnchor().getDataspace().getName())
450                 .withAnchor(fragmentEntity.getAnchor().getName())
451                 .withChildDataNodes(childDataNodes).build();
452     }
453
454     private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
455                                              final FetchDescendantsOption fetchDescendantsOption) {
456         if (fetchDescendantsOption.hasNext()) {
457             return fragmentEntity.getChildFragments().stream()
458                     .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption.next()))
459                     .collect(Collectors.toList());
460         }
461         return Collections.emptyList();
462     }
463
464     @Override
465     public void batchUpdateDataLeaves(final String dataspaceName, final String anchorName,
466                                         final Map<String, Map<String, Serializable>> updatedLeavesPerXPath) {
467         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
468
469         final Collection<String> xpathsOfUpdatedLeaves = updatedLeavesPerXPath.keySet();
470         final Collection<FragmentEntity> fragmentEntities = getFragmentEntities(anchorEntity, xpathsOfUpdatedLeaves);
471
472         for (final FragmentEntity fragmentEntity : fragmentEntities) {
473             final Map<String, Serializable> updatedLeaves = updatedLeavesPerXPath.get(fragmentEntity.getXpath());
474             final String mergedLeaves = mergeLeaves(updatedLeaves, fragmentEntity.getAttributes());
475             fragmentEntity.setAttributes(mergedLeaves);
476         }
477
478         try {
479             fragmentRepository.saveAll(fragmentEntities);
480         } catch (final StaleStateException staleStateException) {
481             retryUpdateDataNodesIndividually(anchorEntity, fragmentEntities);
482         }
483     }
484
485     @Override
486     public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName,
487                                               final Collection<DataNode> updatedDataNodes) {
488         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
489
490         final Map<String, DataNode> xpathToUpdatedDataNode = updatedDataNodes.stream()
491             .collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode));
492
493         final Collection<String> xpaths = xpathToUpdatedDataNode.keySet();
494         Collection<FragmentEntity> existingFragmentEntities = getFragmentEntities(anchorEntity, xpaths);
495         existingFragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(
496                 FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS, existingFragmentEntities);
497
498         for (final FragmentEntity existingFragmentEntity : existingFragmentEntities) {
499             final DataNode updatedDataNode = xpathToUpdatedDataNode.get(existingFragmentEntity.getXpath());
500             updateFragmentEntityAndDescendantsWithDataNode(existingFragmentEntity, updatedDataNode);
501         }
502
503         try {
504             fragmentRepository.saveAll(existingFragmentEntities);
505         } catch (final StaleStateException staleStateException) {
506             retryUpdateDataNodesIndividually(anchorEntity, existingFragmentEntities);
507         }
508     }
509
510     private void retryUpdateDataNodesIndividually(final AnchorEntity anchorEntity,
511                                                   final Collection<FragmentEntity> fragmentEntities) {
512         final Collection<String> failedXpaths = new HashSet<>();
513         for (final FragmentEntity dataNodeFragment : fragmentEntities) {
514             try {
515                 fragmentRepository.save(dataNodeFragment);
516             } catch (final StaleStateException staleStateException) {
517                 failedXpaths.add(dataNodeFragment.getXpath());
518             }
519         }
520         if (!failedXpaths.isEmpty()) {
521             final String failedXpathsConcatenated = String.join(",", failedXpaths);
522             throw new ConcurrencyException("Concurrent Transactions", String.format(
523                     "DataNodes : %s in Dataspace :'%s' with Anchor : '%s'  are updated by another transaction.",
524                     failedXpathsConcatenated, anchorEntity.getDataspace().getName(), anchorEntity.getName()));
525         }
526     }
527
528     private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
529                                                                 final DataNode newDataNode) {
530         copyAttributesFromNewDataNode(existingFragmentEntity, newDataNode);
531
532         final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
533                 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
534
535         final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
536         for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
537             final FragmentEntity childFragment;
538             if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
539                 childFragment = convertToFragmentWithAllDescendants(existingFragmentEntity.getAnchor(),
540                     newDataNodeChild);
541             } else {
542                 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
543                 updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild);
544             }
545             updatedChildFragments.add(childFragment);
546         }
547
548         existingFragmentEntity.getChildFragments().clear();
549         existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
550     }
551
552     @Override
553     @Transactional
554     public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
555                                    final Collection<DataNode> newListElements) {
556         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
557         final FragmentEntity parentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
558         final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
559         final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
560                 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
561         parentEntity.getChildFragments().removeAll(existingListElementFragmentEntitiesByXPath.values());
562         final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
563         for (final DataNode newListElement : newListElements) {
564             final FragmentEntity existingListElementEntity =
565                     existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
566             final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
567                     existingListElementEntity);
568             updatedChildFragmentEntities.add(entityToBeAdded);
569         }
570         parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
571         fragmentRepository.save(parentEntity);
572     }
573
574     @Override
575     @Transactional
576     public void deleteDataNodes(final String dataspaceName, final String anchorName) {
577         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
578         anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
579             .ifPresent(anchorEntity -> fragmentRepository.deleteByAnchorIn(Collections.singletonList(anchorEntity)));
580     }
581
582     @Override
583     @Transactional
584     public void deleteDataNodes(final String dataspaceName, final Collection<String> anchorNames) {
585         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
586         final Collection<AnchorEntity> anchorEntities =
587             anchorRepository.findAllByDataspaceAndNameIn(dataspaceEntity, anchorNames);
588         fragmentRepository.deleteByAnchorIn(anchorEntities);
589     }
590
591     @Override
592     @Transactional
593     public void deleteDataNodes(final String dataspaceName, final String anchorName,
594                                 final Collection<String> xpathsToDelete) {
595         deleteDataNodes(dataspaceName, anchorName, xpathsToDelete, false);
596     }
597
598     private void deleteDataNodes(final String dataspaceName, final String anchorName,
599                                  final Collection<String> xpathsToDelete, final boolean onlySupportListDeletion) {
600         final boolean haveRootXpath = xpathsToDelete.stream().anyMatch(CpsDataPersistenceServiceImpl::isRootXpath);
601         if (haveRootXpath) {
602             deleteDataNodes(dataspaceName, anchorName);
603             return;
604         }
605
606         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
607
608         final Collection<String> deleteChecklist = getNormalizedXpaths(xpathsToDelete);
609         final Collection<String> xpathsToExistingContainers =
610             fragmentRepository.findAllXpathByAnchorAndXpathIn(anchorEntity, deleteChecklist);
611         if (onlySupportListDeletion) {
612             final Collection<String> xpathsToExistingListElements = xpathsToExistingContainers.stream()
613                 .filter(CpsPathUtil::isPathToListElement).collect(Collectors.toList());
614             deleteChecklist.removeAll(xpathsToExistingListElements);
615         } else {
616             deleteChecklist.removeAll(xpathsToExistingContainers);
617         }
618
619         final Collection<String> xpathsToExistingLists = deleteChecklist.stream()
620             .filter(xpath -> fragmentRepository.existsByAnchorAndXpathStartsWith(anchorEntity, xpath + "["))
621             .collect(Collectors.toList());
622         deleteChecklist.removeAll(xpathsToExistingLists);
623
624         if (!deleteChecklist.isEmpty()) {
625             throw new DataNodeNotFoundExceptionBatch(dataspaceName, anchorName, deleteChecklist);
626         }
627
628         fragmentRepository.deleteByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingContainers);
629         fragmentRepository.deleteListsByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingLists);
630     }
631
632     @Override
633     @Transactional
634     public void deleteListDataNode(final String dataspaceName, final String anchorName,
635                                    final String targetXpath) {
636         deleteDataNode(dataspaceName, anchorName, targetXpath, true);
637     }
638
639     @Override
640     @Transactional
641     public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
642         deleteDataNode(dataspaceName, anchorName, targetXpath, false);
643     }
644
645     private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
646                                 final boolean onlySupportListNodeDeletion) {
647         final String normalizedXpath = getNormalizedXpath(targetXpath);
648         try {
649             deleteDataNodes(dataspaceName, anchorName, Collections.singletonList(normalizedXpath),
650                 onlySupportListNodeDeletion);
651         } catch (final DataNodeNotFoundExceptionBatch dataNodeNotFoundExceptionBatch) {
652             throw new DataNodeNotFoundException(dataspaceName, anchorName, targetXpath);
653         }
654     }
655
656     private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
657         if (newListElements.isEmpty()) {
658             throw new CpsAdminException("Invalid list replacement",
659                     "Cannot replace list elements with empty collection");
660         }
661         final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
662         return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
663     }
664
665     private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
666                                                      final DataNode newListElement,
667                                                      final FragmentEntity existingListElementEntity) {
668         if (existingListElementEntity == null) {
669             return convertToFragmentWithAllDescendants(parentEntity.getAnchor(), newListElement);
670         }
671         if (newListElement.getChildDataNodes().isEmpty()) {
672             copyAttributesFromNewDataNode(existingListElementEntity, newListElement);
673             existingListElementEntity.getChildFragments().clear();
674         } else {
675             updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
676         }
677         return existingListElementEntity;
678     }
679
680     private static boolean isNewDataNode(final DataNode replacementDataNode,
681                                          final Map<String, FragmentEntity> existingListElementsByXpath) {
682         return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
683     }
684
685     private void copyAttributesFromNewDataNode(final FragmentEntity existingFragmentEntity,
686                                                final DataNode newDataNode) {
687         final String oldOrderedLeavesAsJson = getOrderedLeavesAsJson(existingFragmentEntity.getAttributes());
688         final String newOrderedLeavesAsJson = getOrderedLeavesAsJson(newDataNode.getLeaves());
689         if (!oldOrderedLeavesAsJson.equals(newOrderedLeavesAsJson)) {
690             existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
691         }
692     }
693
694     private String getOrderedLeavesAsJson(final Map<String, Serializable> currentLeaves) {
695         final Map<String, Serializable> sortedLeaves = new TreeMap<>(String::compareTo);
696         sortedLeaves.putAll(currentLeaves);
697         return jsonObjectMapper.asJsonString(sortedLeaves);
698     }
699
700     private String getOrderedLeavesAsJson(final String currentLeavesAsString) {
701         if (currentLeavesAsString == null) {
702             return "{}";
703         }
704         final Map<String, Serializable> sortedLeaves = jsonObjectMapper.convertJsonString(currentLeavesAsString,
705                 TreeMap.class);
706         return jsonObjectMapper.asJsonString(sortedLeaves);
707     }
708
709     private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
710             final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
711         return childEntities.stream()
712                 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
713                 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
714     }
715
716     private static boolean isRootXpath(final String xpath) {
717         return "/".equals(xpath) || "".equals(xpath);
718     }
719
720     private String mergeLeaves(final Map<String, Serializable> updateLeaves, final String currentLeavesAsString) {
721         Map<String, Serializable> currentLeavesAsMap = new HashMap<>();
722         if (currentLeavesAsString != null) {
723             currentLeavesAsMap = jsonObjectMapper.convertJsonString(currentLeavesAsString, Map.class);
724             currentLeavesAsMap.putAll(updateLeaves);
725         }
726
727         if (currentLeavesAsMap.isEmpty()) {
728             return "";
729         }
730         return jsonObjectMapper.asJsonString(currentLeavesAsMap);
731     }
732
733     private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) {
734         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
735         return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
736     }
737 }