Reduce dataspace/anchor lookups in CpsDataPersistenceService
[cps.git] / cps-ri / src / main / java / org / onap / cps / spi / impl / CpsDataPersistenceServiceImpl.java
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2023 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 com.google.common.collect.ImmutableSet;
27 import com.google.common.collect.ImmutableSet.Builder;
28 import java.io.Serializable;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 import java.util.stream.Collectors;
40 import javax.transaction.Transactional;
41 import lombok.RequiredArgsConstructor;
42 import lombok.extern.slf4j.Slf4j;
43 import org.hibernate.StaleStateException;
44 import org.onap.cps.cpspath.parser.CpsPathQuery;
45 import org.onap.cps.cpspath.parser.CpsPathUtil;
46 import org.onap.cps.cpspath.parser.PathParsingException;
47 import org.onap.cps.spi.CpsDataPersistenceService;
48 import org.onap.cps.spi.FetchDescendantsOption;
49 import org.onap.cps.spi.entities.AnchorEntity;
50 import org.onap.cps.spi.entities.DataspaceEntity;
51 import org.onap.cps.spi.entities.FragmentEntity;
52 import org.onap.cps.spi.entities.FragmentEntityArranger;
53 import org.onap.cps.spi.entities.FragmentExtract;
54 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
55 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch;
56 import org.onap.cps.spi.exceptions.ConcurrencyException;
57 import org.onap.cps.spi.exceptions.CpsAdminException;
58 import org.onap.cps.spi.exceptions.CpsPathException;
59 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
60 import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch;
61 import org.onap.cps.spi.model.DataNode;
62 import org.onap.cps.spi.model.DataNodeBuilder;
63 import org.onap.cps.spi.repository.AnchorRepository;
64 import org.onap.cps.spi.repository.DataspaceRepository;
65 import org.onap.cps.spi.repository.FragmentQueryBuilder;
66 import org.onap.cps.spi.repository.FragmentRepository;
67 import org.onap.cps.spi.utils.SessionManager;
68 import org.onap.cps.utils.JsonObjectMapper;
69 import org.springframework.dao.DataIntegrityViolationException;
70 import org.springframework.stereotype.Service;
71
72 @Service
73 @Slf4j
74 @RequiredArgsConstructor
75 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
76
77     private final DataspaceRepository dataspaceRepository;
78     private final AnchorRepository anchorRepository;
79     private final FragmentRepository fragmentRepository;
80     private final JsonObjectMapper jsonObjectMapper;
81     private final SessionManager sessionManager;
82
83     private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?])?)";
84
85     @Override
86     public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath,
87                                  final DataNode newChildDataNode) {
88         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
89         addNewChildDataNode(anchorEntity, parentNodeXpath, newChildDataNode);
90     }
91
92     @Override
93     public void addChildDataNodes(final String dataspaceName, final String anchorName,
94                                   final String parentNodeXpath, final Collection<DataNode> dataNodes) {
95         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
96         addChildrenDataNodes(anchorEntity, parentNodeXpath, dataNodes);
97     }
98
99     @Override
100     public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
101                                 final Collection<DataNode> newListElements) {
102         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
103         addChildrenDataNodes(anchorEntity, parentNodeXpath, newListElements);
104     }
105
106     @Override
107     public void addMultipleLists(final String dataspaceName, final String anchorName, final String parentNodeXpath,
108                                  final Collection<Collection<DataNode>> newLists) {
109         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
110         final Collection<String> failedXpaths = new HashSet<>();
111         for (final Collection<DataNode> newList : newLists) {
112             try {
113                 addChildrenDataNodes(anchorEntity, parentNodeXpath, newList);
114             } catch (final AlreadyDefinedExceptionBatch e) {
115                 failedXpaths.addAll(e.getAlreadyDefinedXpaths());
116             }
117         }
118         if (!failedXpaths.isEmpty()) {
119             throw new AlreadyDefinedExceptionBatch(failedXpaths);
120         }
121     }
122
123     private void addNewChildDataNode(final AnchorEntity anchorEntity, final String parentNodeXpath,
124                                      final DataNode newChild) {
125         final FragmentEntity parentFragmentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
126         final FragmentEntity newChildAsFragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, newChild);
127         newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
128         try {
129             fragmentRepository.save(newChildAsFragmentEntity);
130         } catch (final DataIntegrityViolationException e) {
131             throw AlreadyDefinedException.forDataNode(newChild.getXpath(), anchorEntity.getName(), e);
132         }
133     }
134
135     private void addChildrenDataNodes(final AnchorEntity anchorEntity, final String parentNodeXpath,
136                                       final Collection<DataNode> newChildren) {
137         final FragmentEntity parentFragmentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
138         final List<FragmentEntity> fragmentEntities = new ArrayList<>(newChildren.size());
139         try {
140             for (final DataNode newChildAsDataNode : newChildren) {
141                 final FragmentEntity newChildAsFragmentEntity =
142                     convertToFragmentWithAllDescendants(anchorEntity, newChildAsDataNode);
143                 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
144                 fragmentEntities.add(newChildAsFragmentEntity);
145             }
146             fragmentRepository.saveAll(fragmentEntities);
147         } catch (final DataIntegrityViolationException e) {
148             log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
149                     e, fragmentEntities.size());
150             retrySavingEachChildIndividually(anchorEntity, parentNodeXpath, newChildren);
151         }
152     }
153
154     private void retrySavingEachChildIndividually(final AnchorEntity anchorEntity, final String parentNodeXpath,
155                                                   final Collection<DataNode> newChildren) {
156         final Collection<String> failedXpaths = new HashSet<>();
157         for (final DataNode newChild : newChildren) {
158             try {
159                 addNewChildDataNode(anchorEntity, parentNodeXpath, newChild);
160             } catch (final AlreadyDefinedException e) {
161                 failedXpaths.add(newChild.getXpath());
162             }
163         }
164         if (!failedXpaths.isEmpty()) {
165             throw new AlreadyDefinedExceptionBatch(failedXpaths);
166         }
167     }
168
169     @Override
170     public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
171         storeDataNodes(dataspaceName, anchorName, Collections.singletonList(dataNode));
172     }
173
174     @Override
175     public void storeDataNodes(final String dataspaceName, final String anchorName,
176                                final Collection<DataNode> dataNodes) {
177         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
178         final List<FragmentEntity> fragmentEntities = new ArrayList<>(dataNodes.size());
179         try {
180             for (final DataNode dataNode: dataNodes) {
181                 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
182                 fragmentEntities.add(fragmentEntity);
183             }
184             fragmentRepository.saveAll(fragmentEntities);
185         } catch (final DataIntegrityViolationException exception) {
186             log.warn("Exception occurred : {} , While saving : {} data nodes, Retrying saving data nodes individually",
187                     exception, dataNodes.size());
188             storeDataNodesIndividually(anchorEntity, dataNodes);
189         }
190     }
191
192     private void storeDataNodesIndividually(final AnchorEntity anchorEntity, final Collection<DataNode> dataNodes) {
193         final Collection<String> failedXpaths = new HashSet<>();
194         for (final DataNode dataNode: dataNodes) {
195             try {
196                 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
197                 fragmentRepository.save(fragmentEntity);
198             } catch (final DataIntegrityViolationException e) {
199                 failedXpaths.add(dataNode.getXpath());
200             }
201         }
202         if (!failedXpaths.isEmpty()) {
203             throw new AlreadyDefinedExceptionBatch(failedXpaths);
204         }
205     }
206
207     /**
208      * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
209      * for all DataNode children recursively.
210      *
211      * @param anchorEntity          anchorEntity
212      * @param dataNodeToBeConverted dataNode
213      * @return a Fragment built from current DataNode
214      */
215     private FragmentEntity convertToFragmentWithAllDescendants(final AnchorEntity anchorEntity,
216                                                                final DataNode dataNodeToBeConverted) {
217         final FragmentEntity parentFragment = toFragmentEntity(anchorEntity, dataNodeToBeConverted);
218         final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
219         for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
220             final FragmentEntity childFragment = convertToFragmentWithAllDescendants(anchorEntity, childDataNode);
221             childFragmentsImmutableSetBuilder.add(childFragment);
222         }
223         parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
224         return parentFragment;
225     }
226
227     private FragmentEntity toFragmentEntity(final AnchorEntity anchorEntity, final DataNode dataNode) {
228         return FragmentEntity.builder()
229                 .dataspace(anchorEntity.getDataspace())
230                 .anchor(anchorEntity)
231                 .xpath(dataNode.getXpath())
232                 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
233                 .build();
234     }
235
236     @Override
237     public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
238                                              final String xpath,
239                                              final FetchDescendantsOption fetchDescendantsOption) {
240         final String targetXpath = getNormalizedXpath(xpath);
241         final Collection<DataNode> dataNodes = getDataNodesForMultipleXpaths(dataspaceName, anchorName,
242                 Collections.singletonList(targetXpath), fetchDescendantsOption);
243         if (dataNodes.isEmpty()) {
244             throw new DataNodeNotFoundException(dataspaceName, anchorName, xpath);
245         }
246         return dataNodes;
247     }
248
249     @Override
250     public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName,
251                                                               final Collection<String> xpaths,
252                                                               final FetchDescendantsOption fetchDescendantsOption) {
253         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
254         final Collection<FragmentEntity> fragmentEntities = getFragmentEntities(anchorEntity, xpaths);
255         return toDataNodes(fragmentEntities, fetchDescendantsOption);
256     }
257
258     private Collection<FragmentEntity> getFragmentEntities(final AnchorEntity anchorEntity,
259                                                            final Collection<String> xpaths) {
260         final Collection<String> nonRootXpaths = new HashSet<>(xpaths);
261         final boolean haveRootXpath = nonRootXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
262
263         final Collection<String> normalizedXpaths = new HashSet<>(nonRootXpaths.size());
264         for (final String xpath : nonRootXpaths) {
265             try {
266                 normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath));
267             } catch (final PathParsingException e) {
268                 log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage());
269             }
270         }
271         final Collection<FragmentEntity> fragmentEntities =
272             new HashSet<>(fragmentRepository.findByAnchorAndMultipleCpsPaths(anchorEntity.getId(), normalizedXpaths));
273
274         if (haveRootXpath) {
275             final List<FragmentExtract> fragmentExtracts = fragmentRepository.findAllExtractsByAnchor(anchorEntity);
276             fragmentEntities.addAll(FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts));
277         }
278
279         return fragmentEntities;
280     }
281
282     private FragmentEntity getFragmentEntity(final AnchorEntity anchorEntity, final String xpath) {
283         final FragmentEntity fragmentEntity;
284         if (isRootXpath(xpath)) {
285             final List<FragmentExtract> fragmentExtracts = fragmentRepository.findAllExtractsByAnchor(anchorEntity);
286             fragmentEntity = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts)
287                 .stream().findFirst().orElse(null);
288         } else {
289             fragmentEntity = fragmentRepository.getByAnchorAndXpath(anchorEntity, getNormalizedXpath(xpath));
290         }
291         if (fragmentEntity == null) {
292             throw new DataNodeNotFoundException(anchorEntity.getDataspace().getName(), anchorEntity.getName(), xpath);
293         }
294         return fragmentEntity;
295     }
296
297     private Collection<FragmentEntity> buildFragmentEntitiesFromFragmentExtracts(final AnchorEntity anchorEntity,
298                                                                                  final String normalizedXpath) {
299         final List<FragmentExtract> fragmentExtracts =
300             fragmentRepository.findByAnchorAndParentXpath(anchorEntity, normalizedXpath);
301         return FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts);
302     }
303
304     @Override
305     public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
306                                          final FetchDescendantsOption fetchDescendantsOption) {
307         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
308         final CpsPathQuery cpsPathQuery;
309         try {
310             cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
311         } catch (final PathParsingException e) {
312             throw new CpsPathException(e.getMessage());
313         }
314
315         Collection<FragmentEntity> fragmentEntities;
316         if (canUseRegexQuickFind(fetchDescendantsOption, cpsPathQuery)) {
317             return getDataNodesUsingRegexQuickFind(fetchDescendantsOption, anchorEntity, cpsPathQuery);
318         }
319         fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
320         if (cpsPathQuery.hasAncestorAxis()) {
321             fragmentEntities = getAncestorFragmentEntities(anchorEntity.getId(), cpsPathQuery, fragmentEntities);
322         }
323         return createDataNodesFromProxiedFragmentEntities(fetchDescendantsOption, anchorEntity, fragmentEntities);
324     }
325
326     private static boolean canUseRegexQuickFind(final FetchDescendantsOption fetchDescendantsOption,
327                                                 final CpsPathQuery cpsPathQuery) {
328         return fetchDescendantsOption.equals(FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
329             && !cpsPathQuery.hasLeafConditions()
330             && !cpsPathQuery.hasTextFunctionCondition();
331     }
332
333     private List<DataNode> getDataNodesUsingRegexQuickFind(final FetchDescendantsOption fetchDescendantsOption,
334                                                            final AnchorEntity anchorEntity,
335                                                            final CpsPathQuery cpsPathQuery) {
336         Collection<FragmentEntity> fragmentEntities;
337         final String xpathRegex = FragmentQueryBuilder.getXpathSqlRegex(cpsPathQuery, true);
338         final List<FragmentExtract> fragmentExtracts =
339             fragmentRepository.quickFindWithDescendants(anchorEntity.getId(), xpathRegex);
340         fragmentEntities = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts);
341         if (cpsPathQuery.hasAncestorAxis()) {
342             fragmentEntities = getAncestorFragmentEntities(anchorEntity.getId(), cpsPathQuery, fragmentEntities);
343         }
344         return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
345     }
346
347     private Collection<FragmentEntity> getAncestorFragmentEntities(final int anchorId,
348                                                                    final CpsPathQuery cpsPathQuery,
349                                                                    final Collection<FragmentEntity> fragmentEntities) {
350         final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
351         return ancestorXpaths.isEmpty() ? Collections.emptyList()
352             : fragmentRepository.findByAnchorAndMultipleCpsPaths(anchorId, ancestorXpaths);
353     }
354
355     private List<DataNode> createDataNodesFromProxiedFragmentEntities(
356                                             final FetchDescendantsOption fetchDescendantsOption,
357                                             final AnchorEntity anchorEntity,
358                                             final Collection<FragmentEntity> proxiedFragmentEntities) {
359         final List<DataNode> dataNodes = new ArrayList<>(proxiedFragmentEntities.size());
360         for (final FragmentEntity proxiedFragmentEntity : proxiedFragmentEntities) {
361             if (FetchDescendantsOption.OMIT_DESCENDANTS.equals(fetchDescendantsOption)) {
362                 dataNodes.add(toDataNode(proxiedFragmentEntity, fetchDescendantsOption));
363             } else {
364                 final String normalizedXpath = getNormalizedXpath(proxiedFragmentEntity.getXpath());
365                 final Collection<FragmentEntity> unproxiedFragmentEntities =
366                     buildFragmentEntitiesFromFragmentExtracts(anchorEntity, normalizedXpath);
367                 for (final FragmentEntity unproxiedFragmentEntity : unproxiedFragmentEntities) {
368                     dataNodes.add(toDataNode(unproxiedFragmentEntity, fetchDescendantsOption));
369                 }
370             }
371         }
372         return Collections.unmodifiableList(dataNodes);
373     }
374
375     private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption,
376                                                                final Collection<FragmentEntity> fragmentEntities) {
377         final List<DataNode> dataNodes = new ArrayList<>(fragmentEntities.size());
378         for (final FragmentEntity fragmentEntity : fragmentEntities) {
379             dataNodes.add(toDataNode(fragmentEntity, fetchDescendantsOption));
380         }
381         return Collections.unmodifiableList(dataNodes);
382     }
383
384     private static String getNormalizedXpath(final String xpathSource) {
385         if (isRootXpath(xpathSource)) {
386             return xpathSource;
387         }
388         try {
389             return CpsPathUtil.getNormalizedXpath(xpathSource);
390         } catch (final PathParsingException e) {
391             throw new CpsPathException(e.getMessage());
392         }
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     private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities,
412                                                     final CpsPathQuery cpsPathQuery) {
413         final Set<String> ancestorXpath = new HashSet<>();
414         final Pattern pattern =
415                 Pattern.compile("([\\s\\S]*/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
416                         + REG_EX_FOR_OPTIONAL_LIST_INDEX + "/[\\s\\S]*");
417         for (final FragmentEntity fragmentEntity : fragmentEntities) {
418             final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
419             if (matcher.matches()) {
420                 ancestorXpath.add(matcher.group(1));
421             }
422         }
423         return ancestorXpath;
424     }
425
426     private DataNode toDataNode(final FragmentEntity fragmentEntity,
427                                 final FetchDescendantsOption fetchDescendantsOption) {
428         final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
429         Map<String, Serializable> leaves = new HashMap<>();
430         if (fragmentEntity.getAttributes() != null) {
431             leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
432         }
433         return new DataNodeBuilder()
434                 .withXpath(fragmentEntity.getXpath())
435                 .withLeaves(leaves)
436                 .withChildDataNodes(childDataNodes).build();
437     }
438
439     private Collection<DataNode> toDataNodes(final Collection<FragmentEntity> fragmentEntities,
440                                              final FetchDescendantsOption fetchDescendantsOption) {
441         final Collection<DataNode> dataNodes = new ArrayList<>(fragmentEntities.size());
442         for (final FragmentEntity fragmentEntity : fragmentEntities) {
443             dataNodes.add(toDataNode(fragmentEntity, fetchDescendantsOption));
444         }
445         return dataNodes;
446     }
447
448     private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
449                                              final FetchDescendantsOption fetchDescendantsOption) {
450         if (fetchDescendantsOption.hasNext()) {
451             return fragmentEntity.getChildFragments().stream()
452                     .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption.next()))
453                     .collect(Collectors.toList());
454         }
455         return Collections.emptyList();
456     }
457
458     @Override
459     public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
460                                  final Map<String, Serializable> updateLeaves) {
461         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
462         final FragmentEntity fragmentEntity = getFragmentEntity(anchorEntity, xpath);
463         final String currentLeavesAsString = fragmentEntity.getAttributes();
464         final String mergedLeaves = mergeLeaves(updateLeaves, currentLeavesAsString);
465         fragmentEntity.setAttributes(mergedLeaves);
466         fragmentRepository.save(fragmentEntity);
467     }
468
469     @Override
470     public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
471                                              final DataNode dataNode) {
472         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
473         final FragmentEntity fragmentEntity = getFragmentEntity(anchorEntity, dataNode.getXpath());
474         updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode);
475         try {
476             fragmentRepository.save(fragmentEntity);
477         } catch (final StaleStateException staleStateException) {
478             throw new ConcurrencyException("Concurrent Transactions",
479                     String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.",
480                             dataspaceName, anchorName, dataNode.getXpath()));
481         }
482     }
483
484     @Override
485     public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName,
486                                               final List<DataNode> updatedDataNodes) {
487         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
488
489         final Map<String, DataNode> xpathToUpdatedDataNode = updatedDataNodes.stream()
490             .collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode));
491
492         final Collection<String> xpaths = xpathToUpdatedDataNode.keySet();
493         final Collection<FragmentEntity> existingFragmentEntities = getFragmentEntities(anchorEntity, xpaths);
494
495         for (final FragmentEntity existingFragmentEntity : existingFragmentEntities) {
496             final DataNode updatedDataNode = xpathToUpdatedDataNode.get(existingFragmentEntity.getXpath());
497             updateFragmentEntityAndDescendantsWithDataNode(existingFragmentEntity, updatedDataNode);
498         }
499
500         try {
501             fragmentRepository.saveAll(existingFragmentEntities);
502         } catch (final StaleStateException staleStateException) {
503             retryUpdateDataNodesIndividually(anchorEntity, existingFragmentEntities);
504         }
505     }
506
507     private void retryUpdateDataNodesIndividually(final AnchorEntity anchorEntity,
508                                                   final Collection<FragmentEntity> fragmentEntities) {
509         final Collection<String> failedXpaths = new HashSet<>();
510         for (final FragmentEntity dataNodeFragment : fragmentEntities) {
511             try {
512                 fragmentRepository.save(dataNodeFragment);
513             } catch (final StaleStateException e) {
514                 failedXpaths.add(dataNodeFragment.getXpath());
515             }
516         }
517         if (!failedXpaths.isEmpty()) {
518             final String failedXpathsConcatenated = String.join(",", failedXpaths);
519             throw new ConcurrencyException("Concurrent Transactions", String.format(
520                     "DataNodes : %s in Dataspace :'%s' with Anchor : '%s'  are updated by another transaction.",
521                     failedXpathsConcatenated, anchorEntity.getDataspace().getName(), anchorEntity.getName()));
522         }
523     }
524
525     private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
526                                                                 final DataNode newDataNode) {
527         existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
528
529         final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
530                 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
531
532         final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
533         for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
534             final FragmentEntity childFragment;
535             if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
536                 childFragment = convertToFragmentWithAllDescendants(existingFragmentEntity.getAnchor(),
537                     newDataNodeChild);
538             } else {
539                 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
540                 updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild);
541             }
542             updatedChildFragments.add(childFragment);
543         }
544
545         existingFragmentEntity.getChildFragments().clear();
546         existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
547     }
548
549     @Override
550     @Transactional
551     public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
552                                    final Collection<DataNode> newListElements) {
553         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
554         final FragmentEntity parentEntity = getFragmentEntity(anchorEntity, parentNodeXpath);
555         final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
556         final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
557                 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
558         parentEntity.getChildFragments().removeAll(existingListElementFragmentEntitiesByXPath.values());
559         final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
560         for (final DataNode newListElement : newListElements) {
561             final FragmentEntity existingListElementEntity =
562                     existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
563             final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
564                     existingListElementEntity);
565             updatedChildFragmentEntities.add(entityToBeAdded);
566         }
567         parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
568         fragmentRepository.save(parentEntity);
569     }
570
571     @Override
572     @Transactional
573     public void deleteDataNodes(final String dataspaceName, final String anchorName) {
574         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
575         anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
576             .ifPresent(anchorEntity -> fragmentRepository.deleteByAnchorIn(Collections.singletonList(anchorEntity)));
577     }
578
579     @Override
580     @Transactional
581     public void deleteDataNodes(final String dataspaceName, final Collection<String> anchorNames) {
582         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
583         final Collection<AnchorEntity> anchorEntities =
584             anchorRepository.findAllByDataspaceAndNameIn(dataspaceEntity, anchorNames);
585         fragmentRepository.deleteByAnchorIn(anchorEntities);
586     }
587
588     @Override
589     @Transactional
590     public void deleteDataNodes(final String dataspaceName, final String anchorName,
591                                 final Collection<String> xpathsToDelete) {
592         deleteDataNodes(dataspaceName, anchorName, xpathsToDelete, false);
593     }
594
595     private void deleteDataNodes(final String dataspaceName, final String anchorName,
596                                  final Collection<String> xpathsToDelete, final boolean onlySupportListDeletion) {
597         final boolean haveRootXpath = xpathsToDelete.stream().anyMatch(CpsDataPersistenceServiceImpl::isRootXpath);
598         if (haveRootXpath) {
599             deleteDataNodes(dataspaceName, anchorName);
600             return;
601         }
602
603         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
604
605         final Collection<String> deleteChecklist = new HashSet<>(xpathsToDelete.size());
606         for (final String xpath : xpathsToDelete) {
607             try {
608                 deleteChecklist.add(CpsPathUtil.getNormalizedXpath(xpath));
609             } catch (final PathParsingException e) {
610                 log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage());
611             }
612         }
613
614         final Collection<String> xpathsToExistingContainers =
615             fragmentRepository.findAllXpathByAnchorAndXpathIn(anchorEntity, deleteChecklist);
616         if (onlySupportListDeletion) {
617             final Collection<String> xpathsToExistingListElements = xpathsToExistingContainers.stream()
618                 .filter(CpsPathUtil::isPathToListElement).collect(Collectors.toList());
619             deleteChecklist.removeAll(xpathsToExistingListElements);
620         } else {
621             deleteChecklist.removeAll(xpathsToExistingContainers);
622         }
623
624         final Collection<String> xpathsToExistingLists = deleteChecklist.stream()
625             .filter(xpath -> fragmentRepository.existsByAnchorAndXpathStartsWith(anchorEntity, xpath + "["))
626             .collect(Collectors.toList());
627         deleteChecklist.removeAll(xpathsToExistingLists);
628
629         if (!deleteChecklist.isEmpty()) {
630             throw new DataNodeNotFoundExceptionBatch(dataspaceName, anchorName, deleteChecklist);
631         }
632
633         fragmentRepository.deleteByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingContainers);
634         fragmentRepository.deleteListsByAnchorIdAndXpaths(anchorEntity.getId(), xpathsToExistingLists);
635     }
636
637     @Override
638     @Transactional
639     public void deleteListDataNode(final String dataspaceName, final String anchorName,
640                                    final String targetXpath) {
641         deleteDataNode(dataspaceName, anchorName, targetXpath, true);
642     }
643
644     @Override
645     @Transactional
646     public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
647         deleteDataNode(dataspaceName, anchorName, targetXpath, false);
648     }
649
650     private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
651                                 final boolean onlySupportListNodeDeletion) {
652         final String normalizedXpath = getNormalizedXpath(targetXpath);
653         try {
654             deleteDataNodes(dataspaceName, anchorName, Collections.singletonList(normalizedXpath),
655                 onlySupportListNodeDeletion);
656         } catch (final DataNodeNotFoundExceptionBatch dataNodeNotFoundExceptionBatch) {
657             throw new DataNodeNotFoundException(dataspaceName, anchorName, targetXpath);
658         }
659     }
660
661     private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
662         if (newListElements.isEmpty()) {
663             throw new CpsAdminException("Invalid list replacement",
664                     "Cannot replace list elements with empty collection");
665         }
666         final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
667         return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
668     }
669
670     private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
671                                                      final DataNode newListElement,
672                                                      final FragmentEntity existingListElementEntity) {
673         if (existingListElementEntity == null) {
674             return convertToFragmentWithAllDescendants(parentEntity.getAnchor(), newListElement);
675         }
676         if (newListElement.getChildDataNodes().isEmpty()) {
677             copyAttributesFromNewListElement(existingListElementEntity, newListElement);
678             existingListElementEntity.getChildFragments().clear();
679         } else {
680             updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
681         }
682         return existingListElementEntity;
683     }
684
685     private static boolean isNewDataNode(final DataNode replacementDataNode,
686                                          final Map<String, FragmentEntity> existingListElementsByXpath) {
687         return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
688     }
689
690     private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
691                                                   final DataNode newListElement) {
692         final FragmentEntity replacementFragmentEntity =
693                 FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
694                         newListElement.getLeaves())).build();
695         existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
696     }
697
698     private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
699             final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
700         return childEntities.stream()
701                 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
702                 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
703     }
704
705     private static boolean isRootXpath(final String xpath) {
706         return "/".equals(xpath) || "".equals(xpath);
707     }
708
709     private String mergeLeaves(final Map<String, Serializable> updateLeaves, final String currentLeavesAsString) {
710         final Map<String, Serializable> currentLeavesAsMap = currentLeavesAsString.isEmpty()
711             ? new HashMap<>() : jsonObjectMapper.convertJsonString(currentLeavesAsString, Map.class);
712         currentLeavesAsMap.putAll(updateLeaves);
713         if (currentLeavesAsMap.isEmpty()) {
714             return "";
715         }
716         return jsonObjectMapper.asJsonString(currentLeavesAsMap);
717     }
718
719     private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) {
720         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
721         return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
722     }
723 }