Merge "[NCMP] Add Basic Auth to OpenAPI Definitions"
[cps.git] / cps-ri / src / main / java / org / onap / cps / spi / impl / CpsDataPersistenceServiceImpl.java
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2022 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  Modifications Copyright (C) 2020-2022 Bell Canada.
6  *  ================================================================================
7  *  Licensed under the Apache License, Version 2.0 (the "License");
8  *  you may not use this file except in compliance with the License.
9  *  You may obtain a copy of the License at
10  *
11  *        http://www.apache.org/licenses/LICENSE-2.0
12  *
13  *  Unless required by applicable law or agreed to in writing, software
14  *  distributed under the License is distributed on an "AS IS" BASIS,
15  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  *  See the License for the specific language governing permissions and
17  *  limitations under the License.
18  *
19  *  SPDX-License-Identifier: Apache-2.0
20  *  ============LICENSE_END=========================================================
21  */
22
23 package org.onap.cps.spi.impl;
24
25 import com.google.common.collect.ImmutableSet;
26 import com.google.common.collect.ImmutableSet.Builder;
27 import com.hazelcast.map.IMap;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.concurrent.TimeUnit;
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.cache.AnchorDataCacheConfig;
50 import org.onap.cps.spi.cache.AnchorDataCacheEntry;
51 import org.onap.cps.spi.entities.AnchorEntity;
52 import org.onap.cps.spi.entities.DataspaceEntity;
53 import org.onap.cps.spi.entities.FragmentEntity;
54 import org.onap.cps.spi.entities.SchemaSetEntity;
55 import org.onap.cps.spi.entities.YangResourceEntity;
56 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
57 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch;
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.model.DataNode;
63 import org.onap.cps.spi.model.DataNodeBuilder;
64 import org.onap.cps.spi.repository.AnchorRepository;
65 import org.onap.cps.spi.repository.DataspaceRepository;
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.onap.cps.yang.YangTextSchemaSourceSetBuilder;
70 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
71 import org.springframework.dao.DataIntegrityViolationException;
72 import org.springframework.stereotype.Service;
73
74 @Service
75 @Slf4j
76 @RequiredArgsConstructor
77 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
78
79     private final DataspaceRepository dataspaceRepository;
80     private final AnchorRepository anchorRepository;
81     private final FragmentRepository fragmentRepository;
82     private final JsonObjectMapper jsonObjectMapper;
83     private final SessionManager sessionManager;
84     private final IMap<String, AnchorDataCacheEntry> anchorDataCache;
85
86     private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
87     private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE =
88             Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$");
89     private static final String TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME = "topLevelModulePrefix";
90
91     @Override
92     public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath,
93                                  final DataNode newChildDataNode) {
94         addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChildDataNode);
95     }
96
97     @Override
98     public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
99                                 final Collection<DataNode> newListElements) {
100         addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newListElements);
101     }
102
103     @Override
104     public void addMultipleLists(final String dataspaceName, final String anchorName, final String parentNodeXpath,
105                                  final Collection<Collection<DataNode>> newLists) {
106         final Collection<String> failedXpaths = new HashSet<>();
107         newLists.forEach(newList -> {
108             try {
109                 addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newList);
110             } catch (final AlreadyDefinedExceptionBatch e) {
111                 failedXpaths.addAll(e.getAlreadyDefinedXpaths());
112             }
113         });
114
115         if (!failedXpaths.isEmpty()) {
116             throw new AlreadyDefinedExceptionBatch(failedXpaths);
117         }
118
119     }
120
121     private void addNewChildDataNode(final String dataspaceName, final String anchorName,
122                                      final String parentNodeXpath, final DataNode newChild) {
123         final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
124         final FragmentEntity newChildAsFragmentEntity =
125                 convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(),
126                         parentFragmentEntity.getAnchor(), newChild);
127         newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
128         try {
129             fragmentRepository.save(newChildAsFragmentEntity);
130         } catch (final DataIntegrityViolationException e) {
131             throw AlreadyDefinedException.forDataNode(newChild.getXpath(), anchorName, e);
132         }
133
134     }
135
136     private void addChildrenDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
137                                       final Collection<DataNode> newChildren) {
138         final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
139         final List<FragmentEntity> fragmentEntities = new ArrayList<>(newChildren.size());
140         try {
141             newChildren.forEach(newChildAsDataNode -> {
142                 final FragmentEntity newChildAsFragmentEntity =
143                         convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(),
144                                 parentFragmentEntity.getAnchor(), newChildAsDataNode);
145                 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
146                 fragmentEntities.add(newChildAsFragmentEntity);
147             });
148             fragmentRepository.saveAll(fragmentEntities);
149         } catch (final DataIntegrityViolationException e) {
150             log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
151                     e, fragmentEntities.size());
152             retrySavingEachChildIndividually(dataspaceName, anchorName, parentNodeXpath, newChildren);
153         }
154     }
155
156     private void retrySavingEachChildIndividually(final String dataspaceName, final String anchorName,
157                                                   final String parentNodeXpath,
158                                                   final Collection<DataNode> newChildren) {
159         final Collection<String> failedXpaths = new HashSet<>();
160         for (final DataNode newChild : newChildren) {
161             try {
162                 addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChild);
163             } catch (final AlreadyDefinedException e) {
164                 failedXpaths.add(newChild.getXpath());
165             }
166         }
167         if (!failedXpaths.isEmpty()) {
168             throw new AlreadyDefinedExceptionBatch(failedXpaths);
169         }
170     }
171
172     @Override
173     public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
174         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
175         final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
176         final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
177                 dataNode);
178         try {
179             fragmentRepository.save(fragmentEntity);
180         } catch (final DataIntegrityViolationException exception) {
181             throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
182         }
183     }
184
185     /**
186      * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
187      * for all DataNode children recursively.
188      *
189      * @param dataspaceEntity       dataspace
190      * @param anchorEntity          anchorEntity
191      * @param dataNodeToBeConverted dataNode
192      * @return a Fragment built from current DataNode
193      */
194     private FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
195                                                                final AnchorEntity anchorEntity,
196                                                                final DataNode dataNodeToBeConverted) {
197         final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
198         final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
199         for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
200             final FragmentEntity childFragment =
201                     convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(),
202                             childDataNode);
203             childFragmentsImmutableSetBuilder.add(childFragment);
204         }
205         parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
206         return parentFragment;
207     }
208
209     private FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity,
210                                             final AnchorEntity anchorEntity, final DataNode dataNode) {
211         return FragmentEntity.builder()
212                 .dataspace(dataspaceEntity)
213                 .anchor(anchorEntity)
214                 .xpath(dataNode.getXpath())
215                 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
216                 .build();
217     }
218
219     @Override
220     public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
221                                 final FetchDescendantsOption fetchDescendantsOption) {
222         final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
223         final DataNode dataNode = toDataNode(fragmentEntity, fetchDescendantsOption);
224         dataNode.setModuleNamePrefix(getRootModuleNamePrefix(fragmentEntity.getAnchor()));
225         return dataNode;
226     }
227
228     private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName,
229                                               final String xpath) {
230         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
231         final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
232         if (isRootXpath(xpath)) {
233             return fragmentRepository.findFirstRootByDataspaceAndAnchor(
234                     dataspaceEntity, anchorEntity);
235         } else {
236             final String normalizedXpath;
237             try {
238                 normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath);
239             } catch (final PathParsingException e) {
240                 throw new CpsPathException(e.getMessage());
241             }
242
243             return fragmentRepository.getByDataspaceAndAnchorAndXpath(
244                     dataspaceEntity, anchorEntity, normalizedXpath);
245         }
246     }
247
248     @Override
249     public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
250                                          final FetchDescendantsOption fetchDescendantsOption) {
251         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
252         final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
253         final CpsPathQuery cpsPathQuery;
254         try {
255             cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
256         } catch (final PathParsingException e) {
257             throw new CpsPathException(e.getMessage());
258         }
259         List<FragmentEntity> fragmentEntities =
260                 fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
261         if (cpsPathQuery.hasAncestorAxis()) {
262             final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
263             fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList()
264                     : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
265         }
266         return fragmentEntities.stream()
267                 .map(fragmentEntity -> {
268                     final DataNode dataNode = toDataNode(fragmentEntity, fetchDescendantsOption);
269                     dataNode.setModuleNamePrefix(getRootModuleNamePrefix(anchorEntity));
270                     return dataNode;
271                 }).collect(Collectors.toUnmodifiableList());
272     }
273
274     @Override
275     public String startSession() {
276         return sessionManager.startSession();
277     }
278
279     @Override
280     public void closeSession(final String sessionId) {
281         sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT);
282     }
283
284     @Override
285     public void lockAnchor(final String sessionId, final String dataspaceName,
286                            final String anchorName, final Long timeoutInMilliseconds) {
287         sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
288     }
289
290     private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
291                                                     final CpsPathQuery cpsPathQuery) {
292         final Set<String> ancestorXpath = new HashSet<>();
293         final Pattern pattern =
294                 Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
295                         + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*");
296         for (final FragmentEntity fragmentEntity : fragmentEntities) {
297             final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
298             if (matcher.matches()) {
299                 ancestorXpath.add(matcher.group(1));
300             }
301         }
302         return ancestorXpath;
303     }
304
305     private DataNode toDataNode(final FragmentEntity fragmentEntity,
306                                 final FetchDescendantsOption fetchDescendantsOption) {
307         final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
308         Map<String, Object> leaves = new HashMap<>();
309         if (fragmentEntity.getAttributes() != null) {
310             leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
311         }
312         return new DataNodeBuilder()
313                 .withXpath(fragmentEntity.getXpath())
314                 .withLeaves(leaves)
315                 .withChildDataNodes(childDataNodes).build();
316     }
317
318     private String getRootModuleNamePrefix(final AnchorEntity anchorEntity) {
319         final String cachedModuleNamePrefix = getModuleNamePrefixFromCache(anchorEntity);
320         if (cachedModuleNamePrefix != null) {
321             return cachedModuleNamePrefix;
322         }
323         final String moduleNamePrefix = buildSchemaContextAndRetrieveModulePrefix(anchorEntity);
324         cacheModuleNamePrefix(anchorEntity.getName(), moduleNamePrefix);
325         return moduleNamePrefix;
326     }
327
328     private String buildSchemaContextAndRetrieveModulePrefix(final AnchorEntity anchorEntity) {
329         final SchemaSetEntity schemaSetEntity = anchorEntity.getSchemaSet();
330         final Map<String, String> yangResourceNameToContent =
331                 schemaSetEntity.getYangResources().stream().collect(
332                         Collectors.toMap(YangResourceEntity::getFileName, YangResourceEntity::getContent));
333         final SchemaContext schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent)
334                 .getSchemaContext();
335         return schemaContext.getModules().iterator().next().getName();
336     }
337
338     private void cacheModuleNamePrefix(final String anchorName, final String moduleNamePrefix) {
339         final AnchorDataCacheEntry anchorDataCacheEntry = new AnchorDataCacheEntry();
340         anchorDataCacheEntry.setProperty(TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME, moduleNamePrefix);
341         if (anchorDataCache.putIfAbsent(anchorName, anchorDataCacheEntry,
342             AnchorDataCacheConfig.ANCHOR_DATA_CACHE_TTL_SECS, TimeUnit.SECONDS) == null) {
343             log.debug("Module name prefix for an anchor  {} is cached", anchorName);
344         }
345     }
346
347     private String getModuleNamePrefixFromCache(final AnchorEntity anchorEntity) {
348         if (anchorDataCache.containsKey(anchorEntity.getName())) {
349             final AnchorDataCacheEntry anchorDataCacheEntry = anchorDataCache.get(anchorEntity.getName());
350             return anchorDataCacheEntry.hasProperty(TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME)
351                     ? anchorDataCacheEntry.getProperty(TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME).toString() : null;
352         }
353         return null;
354     }
355
356     private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
357                                              final FetchDescendantsOption fetchDescendantsOption) {
358         if (fetchDescendantsOption.hasNext()) {
359             return fragmentEntity.getChildFragments().stream()
360                     .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption.next()))
361                     .collect(Collectors.toList());
362         }
363         return Collections.emptyList();
364     }
365
366     @Override
367     public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
368                                  final Map<String, Object> leaves) {
369         final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
370         fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
371         fragmentRepository.save(fragmentEntity);
372     }
373
374     @Override
375     public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
376                                              final DataNode dataNode) {
377         final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
378         updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode);
379         try {
380             fragmentRepository.save(fragmentEntity);
381         } catch (final StaleStateException staleStateException) {
382             throw new ConcurrencyException("Concurrent Transactions",
383                     String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.",
384                             dataspaceName, anchorName, dataNode.getXpath()));
385         }
386     }
387
388     @Override
389     public void updateDataNodesAndDescendants(final String dataspaceName,
390                                               final String anchorName,
391                                               final List<DataNode> dataNodes) {
392
393         final Map<DataNode, FragmentEntity> dataNodeFragmentEntityMap = dataNodes.stream()
394                 .collect(Collectors.toMap(
395                         dataNode -> dataNode,
396                         dataNode -> getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath())));
397         dataNodeFragmentEntityMap.forEach(
398                 (dataNode, fragmentEntity) -> updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode));
399         try {
400             fragmentRepository.saveAll(dataNodeFragmentEntityMap.values());
401         } catch (final StaleStateException staleStateException) {
402             retryUpdateDataNodesIndividually(dataspaceName, anchorName, dataNodeFragmentEntityMap.values());
403         }
404     }
405
406     private void retryUpdateDataNodesIndividually(final String dataspaceName, final String anchorName,
407                                                   final Collection<FragmentEntity> fragmentEntities) {
408         final Collection<String> failedXpaths = new HashSet<>();
409
410         fragmentEntities.forEach(dataNodeFragment -> {
411             try {
412                 fragmentRepository.save(dataNodeFragment);
413             } catch (final StaleStateException e) {
414                 failedXpaths.add(dataNodeFragment.getXpath());
415             }
416         });
417
418         if (!failedXpaths.isEmpty()) {
419             final String failedXpathsConcatenated = String.join(",", failedXpaths);
420             throw new ConcurrencyException("Concurrent Transactions", String.format(
421                     "DataNodes : %s in Dataspace :'%s' with Anchor : '%s'  are updated by another transaction.",
422                     failedXpathsConcatenated, dataspaceName, anchorName));
423         }
424     }
425
426     private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
427                                                                 final DataNode newDataNode) {
428
429         existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
430
431         final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
432                 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
433
434         final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
435
436         for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
437             final FragmentEntity childFragment;
438             if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
439                 childFragment = convertToFragmentWithAllDescendants(
440                         existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
441             } else {
442                 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
443                 updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild);
444             }
445             updatedChildFragments.add(childFragment);
446         }
447
448         existingFragmentEntity.getChildFragments().clear();
449         existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
450     }
451
452     @Override
453     @Transactional
454     public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
455                                    final Collection<DataNode> newListElements) {
456         final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
457         final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
458         final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
459                 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
460         deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
461         final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
462         for (final DataNode newListElement : newListElements) {
463             final FragmentEntity existingListElementEntity =
464                     existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
465             final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
466                     existingListElementEntity);
467
468             updatedChildFragmentEntities.add(entityToBeAdded);
469         }
470         parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
471         fragmentRepository.save(parentEntity);
472     }
473
474     @Override
475     @Transactional
476     public void deleteDataNodes(final String dataspaceName, final String anchorName) {
477         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
478         anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
479                 .ifPresent(
480                         anchorEntity -> fragmentRepository.deleteByAnchorIn(Set.of(anchorEntity)));
481     }
482
483     @Override
484     @Transactional
485     public void deleteListDataNode(final String dataspaceName, final String anchorName,
486                                    final String targetXpath) {
487         deleteDataNode(dataspaceName, anchorName, targetXpath, true);
488     }
489
490     @Override
491     @Transactional
492     public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
493         deleteDataNode(dataspaceName, anchorName, targetXpath, false);
494     }
495
496     private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
497                                 final boolean onlySupportListNodeDeletion) {
498         final String parentNodeXpath;
499         FragmentEntity parentFragmentEntity = null;
500         boolean targetDeleted = false;
501         if (isRootXpath(targetXpath)) {
502             deleteDataNodes(dataspaceName, anchorName);
503             targetDeleted = true;
504         } else {
505             if (isRootContainerNodeXpath(targetXpath)) {
506                 parentNodeXpath = targetXpath;
507             } else {
508                 parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
509             }
510             parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
511             final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/'));
512             final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE
513                     .matcher(lastXpathElement).find();
514             if (isListElement) {
515                 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
516             } else {
517                 targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath);
518                 final boolean tryToDeleteDataNode = !targetDeleted && !onlySupportListNodeDeletion;
519                 if (tryToDeleteDataNode) {
520                     targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
521                 }
522             }
523         }
524         if (!targetDeleted) {
525             final String additionalInformation = onlySupportListNodeDeletion
526                     ? "The target is probably not a List." : "";
527             throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
528                     parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
529         }
530     }
531
532     private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
533         final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath);
534         if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) {
535             fragmentRepository.delete(parentFragmentEntity);
536             return true;
537         }
538         if (parentFragmentEntity.getChildFragments()
539                 .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) {
540             fragmentRepository.save(parentFragmentEntity);
541             return true;
542         }
543         return false;
544     }
545
546     private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
547         final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath);
548         final String deleteTargetXpathPrefix = normalizedListXpath + "[";
549         if (parentFragmentEntity.getChildFragments()
550                 .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
551             fragmentRepository.save(parentFragmentEntity);
552             return true;
553         }
554         return false;
555     }
556
557     private static void deleteListElements(
558             final Collection<FragmentEntity> fragmentEntities,
559             final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
560         fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
561     }
562
563     private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
564         if (newListElements.isEmpty()) {
565             throw new CpsAdminException("Invalid list replacement",
566                     "Cannot replace list elements with empty collection");
567         }
568         final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
569         return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
570     }
571
572     private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
573                                                      final DataNode newListElement,
574                                                      final FragmentEntity existingListElementEntity) {
575         if (existingListElementEntity == null) {
576             return convertToFragmentWithAllDescendants(
577                     parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
578         }
579         if (newListElement.getChildDataNodes().isEmpty()) {
580             copyAttributesFromNewListElement(existingListElementEntity, newListElement);
581             existingListElementEntity.getChildFragments().clear();
582         } else {
583             updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
584         }
585         return existingListElementEntity;
586     }
587
588     private static boolean isNewDataNode(final DataNode replacementDataNode,
589                                          final Map<String, FragmentEntity> existingListElementsByXpath) {
590         return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
591     }
592
593     private static boolean isRootContainerNodeXpath(final String xpath) {
594         return 0 == xpath.lastIndexOf('/');
595     }
596
597     private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
598                                                   final DataNode newListElement) {
599         final FragmentEntity replacementFragmentEntity =
600                 FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
601                         newListElement.getLeaves())).build();
602         existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
603     }
604
605     private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
606             final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
607         return childEntities.stream()
608                 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
609                 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
610     }
611
612     private static boolean isRootXpath(final String xpath) {
613         return "/".equals(xpath) || "".equals(xpath);
614     }
615 }