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