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
11 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 * SPDX-License-Identifier: Apache-2.0
20 * ============LICENSE_END=========================================================
23 package org.onap.cps.spi.impl;
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;
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.FragmentEntityArranger;
55 import org.onap.cps.spi.entities.FragmentExtract;
56 import org.onap.cps.spi.entities.SchemaSetEntity;
57 import org.onap.cps.spi.entities.YangResourceEntity;
58 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
59 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch;
60 import org.onap.cps.spi.exceptions.ConcurrencyException;
61 import org.onap.cps.spi.exceptions.CpsAdminException;
62 import org.onap.cps.spi.exceptions.CpsPathException;
63 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
64 import org.onap.cps.spi.model.DataNode;
65 import org.onap.cps.spi.model.DataNodeBuilder;
66 import org.onap.cps.spi.repository.AnchorRepository;
67 import org.onap.cps.spi.repository.DataspaceRepository;
68 import org.onap.cps.spi.repository.FragmentRepository;
69 import org.onap.cps.spi.utils.SessionManager;
70 import org.onap.cps.utils.JsonObjectMapper;
71 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
72 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
73 import org.springframework.dao.DataIntegrityViolationException;
74 import org.springframework.stereotype.Service;
78 @RequiredArgsConstructor
79 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
81 private final DataspaceRepository dataspaceRepository;
82 private final AnchorRepository anchorRepository;
83 private final FragmentRepository fragmentRepository;
84 private final JsonObjectMapper jsonObjectMapper;
85 private final SessionManager sessionManager;
86 private final IMap<String, AnchorDataCacheEntry> anchorDataCache;
88 private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
89 private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE =
90 Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$");
91 private static final String TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME = "topLevelModulePrefix";
94 public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath,
95 final DataNode newChildDataNode) {
96 addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChildDataNode);
100 public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
101 final Collection<DataNode> newListElements) {
102 addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newListElements);
106 public void addMultipleLists(final String dataspaceName, final String anchorName, final String parentNodeXpath,
107 final Collection<Collection<DataNode>> newLists) {
108 final Collection<String> failedXpaths = new HashSet<>();
109 newLists.forEach(newList -> {
111 addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newList);
112 } catch (final AlreadyDefinedExceptionBatch e) {
113 failedXpaths.addAll(e.getAlreadyDefinedXpaths());
117 if (!failedXpaths.isEmpty()) {
118 throw new AlreadyDefinedExceptionBatch(failedXpaths);
123 private void addNewChildDataNode(final String dataspaceName, final String anchorName,
124 final String parentNodeXpath, final DataNode newChild) {
125 final FragmentEntity parentFragmentEntity =
126 getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath);
127 final FragmentEntity newChildAsFragmentEntity =
128 convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(),
129 parentFragmentEntity.getAnchor(), newChild);
130 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
132 fragmentRepository.save(newChildAsFragmentEntity);
133 } catch (final DataIntegrityViolationException e) {
134 throw AlreadyDefinedException.forDataNode(newChild.getXpath(), anchorName, e);
139 private void addChildrenDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
140 final Collection<DataNode> newChildren) {
141 final FragmentEntity parentFragmentEntity =
142 getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath);
143 final List<FragmentEntity> fragmentEntities = new ArrayList<>(newChildren.size());
145 newChildren.forEach(newChildAsDataNode -> {
146 final FragmentEntity newChildAsFragmentEntity =
147 convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(),
148 parentFragmentEntity.getAnchor(), newChildAsDataNode);
149 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
150 fragmentEntities.add(newChildAsFragmentEntity);
152 fragmentRepository.saveAll(fragmentEntities);
153 } catch (final DataIntegrityViolationException e) {
154 log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
155 e, fragmentEntities.size());
156 retrySavingEachChildIndividually(dataspaceName, anchorName, parentNodeXpath, newChildren);
160 private void retrySavingEachChildIndividually(final String dataspaceName, final String anchorName,
161 final String parentNodeXpath,
162 final Collection<DataNode> newChildren) {
163 final Collection<String> failedXpaths = new HashSet<>();
164 for (final DataNode newChild : newChildren) {
166 addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChild);
167 } catch (final AlreadyDefinedException e) {
168 failedXpaths.add(newChild.getXpath());
171 if (!failedXpaths.isEmpty()) {
172 throw new AlreadyDefinedExceptionBatch(failedXpaths);
177 public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
178 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
179 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
180 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
183 fragmentRepository.save(fragmentEntity);
184 } catch (final DataIntegrityViolationException exception) {
185 throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
190 * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
191 * for all DataNode children recursively.
193 * @param dataspaceEntity dataspace
194 * @param anchorEntity anchorEntity
195 * @param dataNodeToBeConverted dataNode
196 * @return a Fragment built from current DataNode
198 private FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
199 final AnchorEntity anchorEntity,
200 final DataNode dataNodeToBeConverted) {
201 final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
202 final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
203 for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
204 final FragmentEntity childFragment =
205 convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(),
207 childFragmentsImmutableSetBuilder.add(childFragment);
209 parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
210 return parentFragment;
213 private FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity,
214 final AnchorEntity anchorEntity, final DataNode dataNode) {
215 return FragmentEntity.builder()
216 .dataspace(dataspaceEntity)
217 .anchor(anchorEntity)
218 .xpath(dataNode.getXpath())
219 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
224 public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
225 final FetchDescendantsOption fetchDescendantsOption) {
226 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath,
227 fetchDescendantsOption);
228 final DataNode dataNode = toDataNode(fragmentEntity, fetchDescendantsOption);
229 dataNode.setModuleNamePrefix(getRootModuleNamePrefix(fragmentEntity.getAnchor()));
233 private FragmentEntity getFragmentWithoutDescendantsByXpath(final String dataspaceName,
234 final String anchorName,
235 final String xpath) {
236 return getFragmentByXpath(dataspaceName, anchorName, xpath, FetchDescendantsOption.OMIT_DESCENDANTS);
239 private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName,
240 final String xpath, final FetchDescendantsOption fetchDescendantsOption) {
241 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
242 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
243 if (isRootXpath(xpath)) {
244 return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity);
246 final String normalizedXpath;
248 normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath);
249 } catch (final PathParsingException e) {
250 throw new CpsPathException(e.getMessage());
252 final FragmentEntity fragmentEntity;
253 if (FetchDescendantsOption.OMIT_DESCENDANTS.equals(fetchDescendantsOption)) {
255 fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, normalizedXpath);
257 final List<FragmentExtract> fragmentExtracts =
258 fragmentRepository.findByAnchorIdAndParentXpath(anchorEntity.getId(), normalizedXpath);
259 log.debug("Fetched {} fragment entities by anchor {} and cps path {}.",
260 fragmentExtracts.size(), anchorName, xpath);
261 fragmentEntity = FragmentEntityArranger.toFragmentEntityTree(anchorEntity, fragmentExtracts);
263 if (fragmentEntity == null) {
264 throw new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath);
266 return fragmentEntity;
271 public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
272 final FetchDescendantsOption fetchDescendantsOption) {
273 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
274 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
275 final CpsPathQuery cpsPathQuery;
277 cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
278 } catch (final PathParsingException e) {
279 throw new CpsPathException(e.getMessage());
281 List<FragmentEntity> fragmentEntities =
282 fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
283 if (cpsPathQuery.hasAncestorAxis()) {
284 final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
285 fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList()
286 : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
288 return fragmentEntities.stream()
289 .map(fragmentEntity -> {
290 final DataNode dataNode = toDataNode(fragmentEntity, fetchDescendantsOption);
291 dataNode.setModuleNamePrefix(getRootModuleNamePrefix(anchorEntity));
293 }).collect(Collectors.toUnmodifiableList());
297 public String startSession() {
298 return sessionManager.startSession();
302 public void closeSession(final String sessionId) {
303 sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT);
307 public void lockAnchor(final String sessionId, final String dataspaceName,
308 final String anchorName, final Long timeoutInMilliseconds) {
309 sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
312 private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
313 final CpsPathQuery cpsPathQuery) {
314 final Set<String> ancestorXpath = new HashSet<>();
315 final Pattern pattern =
316 Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
317 + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*");
318 for (final FragmentEntity fragmentEntity : fragmentEntities) {
319 final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
320 if (matcher.matches()) {
321 ancestorXpath.add(matcher.group(1));
324 return ancestorXpath;
327 private DataNode toDataNode(final FragmentEntity fragmentEntity,
328 final FetchDescendantsOption fetchDescendantsOption) {
329 final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
330 Map<String, Object> leaves = new HashMap<>();
331 if (fragmentEntity.getAttributes() != null) {
332 leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
334 return new DataNodeBuilder()
335 .withXpath(fragmentEntity.getXpath())
337 .withChildDataNodes(childDataNodes).build();
340 private String getRootModuleNamePrefix(final AnchorEntity anchorEntity) {
341 final String cachedModuleNamePrefix = getModuleNamePrefixFromCache(anchorEntity.getName());
342 if (cachedModuleNamePrefix != null) {
343 return cachedModuleNamePrefix;
345 final String moduleNamePrefix = buildSchemaContextAndRetrieveModulePrefix(anchorEntity);
346 cacheModuleNamePrefix(anchorEntity.getName(), moduleNamePrefix);
347 return moduleNamePrefix;
350 private String buildSchemaContextAndRetrieveModulePrefix(final AnchorEntity anchorEntity) {
351 final SchemaSetEntity schemaSetEntity = anchorEntity.getSchemaSet();
352 final Map<String, String> yangResourceNameToContent =
353 schemaSetEntity.getYangResources().stream().collect(
354 Collectors.toMap(YangResourceEntity::getFileName, YangResourceEntity::getContent));
355 final SchemaContext schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent)
357 return schemaContext.getModules().iterator().next().getName();
360 private void cacheModuleNamePrefix(final String anchorName, final String moduleNamePrefix) {
361 final AnchorDataCacheEntry anchorDataCacheEntry = new AnchorDataCacheEntry();
362 anchorDataCacheEntry.setProperty(TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME, moduleNamePrefix);
363 if (anchorDataCache.putIfAbsent(anchorName, anchorDataCacheEntry,
364 AnchorDataCacheConfig.ANCHOR_DATA_CACHE_TTL_SECS, TimeUnit.SECONDS) == null) {
365 log.debug("Module name prefix for an anchor {} is cached", anchorName);
369 private String getModuleNamePrefixFromCache(final String anchorName) {
370 if (anchorDataCache.containsKey(anchorName)) {
371 final AnchorDataCacheEntry anchorDataCacheEntry = anchorDataCache.get(anchorName);
372 return anchorDataCacheEntry.hasProperty(TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME)
373 ? anchorDataCacheEntry.getProperty(TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME).toString() : null;
378 private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
379 final FetchDescendantsOption fetchDescendantsOption) {
380 if (fetchDescendantsOption.hasNext()) {
381 return fragmentEntity.getChildFragments().stream()
382 .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption.next()))
383 .collect(Collectors.toList());
385 return Collections.emptyList();
389 public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
390 final Map<String, Object> leaves) {
391 final FragmentEntity fragmentEntity = getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, xpath);
392 fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
393 fragmentRepository.save(fragmentEntity);
397 public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
398 final DataNode dataNode) {
399 final FragmentEntity fragmentEntity =
400 getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, dataNode.getXpath());
401 updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode);
403 fragmentRepository.save(fragmentEntity);
404 } catch (final StaleStateException staleStateException) {
405 throw new ConcurrencyException("Concurrent Transactions",
406 String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.",
407 dataspaceName, anchorName, dataNode.getXpath()));
412 public void updateDataNodesAndDescendants(final String dataspaceName,
413 final String anchorName,
414 final List<DataNode> dataNodes) {
416 final Map<DataNode, FragmentEntity> dataNodeFragmentEntityMap = dataNodes.stream()
417 .collect(Collectors.toMap(
418 dataNode -> dataNode,
420 getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, dataNode.getXpath())));
421 dataNodeFragmentEntityMap.forEach(
422 (dataNode, fragmentEntity) -> updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode));
424 fragmentRepository.saveAll(dataNodeFragmentEntityMap.values());
425 } catch (final StaleStateException staleStateException) {
426 retryUpdateDataNodesIndividually(dataspaceName, anchorName, dataNodeFragmentEntityMap.values());
430 private void retryUpdateDataNodesIndividually(final String dataspaceName, final String anchorName,
431 final Collection<FragmentEntity> fragmentEntities) {
432 final Collection<String> failedXpaths = new HashSet<>();
434 fragmentEntities.forEach(dataNodeFragment -> {
436 fragmentRepository.save(dataNodeFragment);
437 } catch (final StaleStateException e) {
438 failedXpaths.add(dataNodeFragment.getXpath());
442 if (!failedXpaths.isEmpty()) {
443 final String failedXpathsConcatenated = String.join(",", failedXpaths);
444 throw new ConcurrencyException("Concurrent Transactions", String.format(
445 "DataNodes : %s in Dataspace :'%s' with Anchor : '%s' are updated by another transaction.",
446 failedXpathsConcatenated, dataspaceName, anchorName));
450 private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
451 final DataNode newDataNode) {
453 existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
455 final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
456 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
458 final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
460 for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
461 final FragmentEntity childFragment;
462 if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
463 childFragment = convertToFragmentWithAllDescendants(
464 existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
466 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
467 updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild);
469 updatedChildFragments.add(childFragment);
472 existingFragmentEntity.getChildFragments().clear();
473 existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
478 public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
479 final Collection<DataNode> newListElements) {
480 final FragmentEntity parentEntity =
481 getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath);
482 final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
483 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
484 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
485 deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
486 final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
487 for (final DataNode newListElement : newListElements) {
488 final FragmentEntity existingListElementEntity =
489 existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
490 final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
491 existingListElementEntity);
493 updatedChildFragmentEntities.add(entityToBeAdded);
495 parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
496 fragmentRepository.save(parentEntity);
501 public void deleteDataNodes(final String dataspaceName, final String anchorName) {
502 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
503 anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
505 anchorEntity -> fragmentRepository.deleteByAnchorIn(Set.of(anchorEntity)));
510 public void deleteListDataNode(final String dataspaceName, final String anchorName,
511 final String targetXpath) {
512 deleteDataNode(dataspaceName, anchorName, targetXpath, true);
517 public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
518 deleteDataNode(dataspaceName, anchorName, targetXpath, false);
521 private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
522 final boolean onlySupportListNodeDeletion) {
523 final String parentNodeXpath;
524 FragmentEntity parentFragmentEntity = null;
525 boolean targetDeleted = false;
526 if (isRootXpath(targetXpath)) {
527 deleteDataNodes(dataspaceName, anchorName);
528 targetDeleted = true;
530 if (isRootContainerNodeXpath(targetXpath)) {
531 parentNodeXpath = targetXpath;
533 parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
535 parentFragmentEntity = getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath);
536 final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/'));
537 final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE
538 .matcher(lastXpathElement).find();
540 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
542 targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath);
543 final boolean tryToDeleteDataNode = !targetDeleted && !onlySupportListNodeDeletion;
544 if (tryToDeleteDataNode) {
545 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
549 if (!targetDeleted) {
550 final String additionalInformation = onlySupportListNodeDeletion
551 ? "The target is probably not a List." : "";
552 throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
553 parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
557 private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
558 final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath);
559 if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) {
560 fragmentRepository.delete(parentFragmentEntity);
563 if (parentFragmentEntity.getChildFragments()
564 .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) {
565 fragmentRepository.save(parentFragmentEntity);
571 private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
572 final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath);
573 final String deleteTargetXpathPrefix = normalizedListXpath + "[";
574 if (parentFragmentEntity.getChildFragments()
575 .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
576 fragmentRepository.save(parentFragmentEntity);
582 private static void deleteListElements(
583 final Collection<FragmentEntity> fragmentEntities,
584 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
585 fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
588 private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
589 if (newListElements.isEmpty()) {
590 throw new CpsAdminException("Invalid list replacement",
591 "Cannot replace list elements with empty collection");
593 final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
594 return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
597 private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
598 final DataNode newListElement,
599 final FragmentEntity existingListElementEntity) {
600 if (existingListElementEntity == null) {
601 return convertToFragmentWithAllDescendants(
602 parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
604 if (newListElement.getChildDataNodes().isEmpty()) {
605 copyAttributesFromNewListElement(existingListElementEntity, newListElement);
606 existingListElementEntity.getChildFragments().clear();
608 updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
610 return existingListElementEntity;
613 private static boolean isNewDataNode(final DataNode replacementDataNode,
614 final Map<String, FragmentEntity> existingListElementsByXpath) {
615 return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
618 private static boolean isRootContainerNodeXpath(final String xpath) {
619 return 0 == xpath.lastIndexOf('/');
622 private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
623 final DataNode newListElement) {
624 final FragmentEntity replacementFragmentEntity =
625 FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
626 newListElement.getLeaves())).build();
627 existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
630 private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
631 final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
632 return childEntities.stream()
633 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
634 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
637 private static boolean isRootXpath(final String xpath) {
638 return "/".equals(xpath) || "".equals(xpath);