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 java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.List;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37 import java.util.stream.Collectors;
38 import javax.transaction.Transactional;
39 import lombok.RequiredArgsConstructor;
40 import lombok.extern.slf4j.Slf4j;
41 import org.hibernate.StaleStateException;
42 import org.onap.cps.cpspath.parser.CpsPathQuery;
43 import org.onap.cps.cpspath.parser.CpsPathUtil;
44 import org.onap.cps.cpspath.parser.PathParsingException;
45 import org.onap.cps.spi.CpsDataPersistenceService;
46 import org.onap.cps.spi.FetchDescendantsOption;
47 import org.onap.cps.spi.entities.AnchorEntity;
48 import org.onap.cps.spi.entities.DataspaceEntity;
49 import org.onap.cps.spi.entities.FragmentEntity;
50 import org.onap.cps.spi.entities.SchemaSetEntity;
51 import org.onap.cps.spi.entities.YangResourceEntity;
52 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
53 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch;
54 import org.onap.cps.spi.exceptions.ConcurrencyException;
55 import org.onap.cps.spi.exceptions.CpsAdminException;
56 import org.onap.cps.spi.exceptions.CpsPathException;
57 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
58 import org.onap.cps.spi.model.DataNode;
59 import org.onap.cps.spi.model.DataNodeBuilder;
60 import org.onap.cps.spi.repository.AnchorRepository;
61 import org.onap.cps.spi.repository.DataspaceRepository;
62 import org.onap.cps.spi.repository.FragmentRepository;
63 import org.onap.cps.spi.utils.SessionManager;
64 import org.onap.cps.utils.JsonObjectMapper;
65 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
66 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
67 import org.springframework.dao.DataIntegrityViolationException;
68 import org.springframework.stereotype.Service;
72 @RequiredArgsConstructor
73 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
75 private final DataspaceRepository dataspaceRepository;
77 private final AnchorRepository anchorRepository;
79 private final FragmentRepository fragmentRepository;
81 private final JsonObjectMapper jsonObjectMapper;
83 private final SessionManager sessionManager;
85 private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
86 private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE =
87 Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$");
90 public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath,
91 final DataNode newChildDataNode) {
92 addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChildDataNode);
96 public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
97 final Collection<DataNode> newListElements) {
98 addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newListElements);
102 public void addMultipleLists(final String dataspaceName, final String anchorName, final String parentNodeXpath,
103 final Collection<Collection<DataNode>> newLists) {
104 final Collection<String> failedXpaths = new HashSet<>();
105 newLists.forEach(newList -> {
107 addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newList);
108 } catch (final AlreadyDefinedExceptionBatch e) {
109 failedXpaths.addAll(e.getAlreadyDefinedXpaths());
113 if (!failedXpaths.isEmpty()) {
114 throw new AlreadyDefinedExceptionBatch(failedXpaths);
119 private void addNewChildDataNode(final String dataspaceName, final String anchorName,
120 final String parentNodeXpath, final DataNode newChild) {
121 final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
122 final FragmentEntity newChildAsFragmentEntity =
123 convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(),
124 parentFragmentEntity.getAnchor(), newChild);
125 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
127 fragmentRepository.save(newChildAsFragmentEntity);
128 } catch (final DataIntegrityViolationException e) {
129 throw AlreadyDefinedException.forDataNode(newChild.getXpath(), anchorName, e);
134 private void addChildrenDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
135 final Collection<DataNode> newChildren) {
136 final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
137 final List<FragmentEntity> fragmentEntities = new ArrayList<>(newChildren.size());
139 newChildren.forEach(newChildAsDataNode -> {
140 final FragmentEntity newChildAsFragmentEntity =
141 convertToFragmentWithAllDescendants(parentFragmentEntity.getDataspace(),
142 parentFragmentEntity.getAnchor(), newChildAsDataNode);
143 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
144 fragmentEntities.add(newChildAsFragmentEntity);
146 fragmentRepository.saveAll(fragmentEntities);
147 } catch (final DataIntegrityViolationException e) {
148 log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
149 e, fragmentEntities.size());
150 retrySavingEachChildIndividually(dataspaceName, anchorName, parentNodeXpath, newChildren);
154 private void retrySavingEachChildIndividually(final String dataspaceName, final String anchorName,
155 final String parentNodeXpath,
156 final Collection<DataNode> newChildren) {
157 final Collection<String> failedXpaths = new HashSet<>();
158 for (final DataNode newChild : newChildren) {
160 addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChild);
161 } catch (final AlreadyDefinedException e) {
162 failedXpaths.add(newChild.getXpath());
165 if (!failedXpaths.isEmpty()) {
166 throw new AlreadyDefinedExceptionBatch(failedXpaths);
171 public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
172 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
173 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
174 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
177 fragmentRepository.save(fragmentEntity);
178 } catch (final DataIntegrityViolationException exception) {
179 throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
184 * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
185 * for all DataNode children recursively.
187 * @param dataspaceEntity dataspace
188 * @param anchorEntity anchorEntity
189 * @param dataNodeToBeConverted dataNode
190 * @return a Fragment built from current DataNode
192 private FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
193 final AnchorEntity anchorEntity,
194 final DataNode dataNodeToBeConverted) {
195 final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
196 final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
197 for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
198 final FragmentEntity childFragment =
199 convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(),
201 childFragmentsImmutableSetBuilder.add(childFragment);
203 parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
204 return parentFragment;
207 private FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity,
208 final AnchorEntity anchorEntity, final DataNode dataNode) {
209 return FragmentEntity.builder()
210 .dataspace(dataspaceEntity)
211 .anchor(anchorEntity)
212 .xpath(dataNode.getXpath())
213 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
218 public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
219 final FetchDescendantsOption fetchDescendantsOption) {
220 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
221 return toDataNode(fragmentEntity, fetchDescendantsOption);
224 private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName,
225 final String xpath) {
226 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
227 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
228 if (isRootXpath(xpath)) {
229 return fragmentRepository.findFirstRootByDataspaceAndAnchor(
230 dataspaceEntity, anchorEntity);
232 final String normalizedXpath;
234 normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath);
235 } catch (final PathParsingException e) {
236 throw new CpsPathException(e.getMessage());
239 return fragmentRepository.getByDataspaceAndAnchorAndXpath(
240 dataspaceEntity, anchorEntity, normalizedXpath);
245 public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
246 final FetchDescendantsOption fetchDescendantsOption) {
247 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
248 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
249 final CpsPathQuery cpsPathQuery;
251 cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
252 } catch (final PathParsingException e) {
253 throw new CpsPathException(e.getMessage());
255 List<FragmentEntity> fragmentEntities =
256 fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
257 if (cpsPathQuery.hasAncestorAxis()) {
258 final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
259 fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList()
260 : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
262 return fragmentEntities.stream()
263 .map(fragmentEntity -> toDataNode(fragmentEntity, fetchDescendantsOption))
264 .collect(Collectors.toUnmodifiableList());
268 public String startSession() {
269 return sessionManager.startSession();
273 public void closeSession(final String sessionId) {
274 sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT);
278 public void lockAnchor(final String sessionId, final String dataspaceName,
279 final String anchorName, final Long timeoutInMilliseconds) {
280 sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
283 private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
284 final CpsPathQuery cpsPathQuery) {
285 final Set<String> ancestorXpath = new HashSet<>();
286 final Pattern pattern =
287 Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
288 + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*");
289 for (final FragmentEntity fragmentEntity : fragmentEntities) {
290 final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
291 if (matcher.matches()) {
292 ancestorXpath.add(matcher.group(1));
295 return ancestorXpath;
298 private DataNode toDataNode(final FragmentEntity fragmentEntity,
299 final FetchDescendantsOption fetchDescendantsOption) {
300 final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
301 Map<String, Object> leaves = new HashMap<>();
302 if (fragmentEntity.getAttributes() != null) {
303 leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
305 return new DataNodeBuilder()
306 .withModuleNamePrefix(getFirstModuleName(fragmentEntity))
307 .withXpath(fragmentEntity.getXpath())
309 .withChildDataNodes(childDataNodes).build();
312 private String getFirstModuleName(final FragmentEntity fragmentEntity) {
313 final SchemaSetEntity schemaSetEntity = fragmentEntity.getAnchor().getSchemaSet();
314 final Map<String, String> yangResourceNameToContent =
315 schemaSetEntity.getYangResources().stream().collect(
316 Collectors.toMap(YangResourceEntity::getFileName, YangResourceEntity::getContent));
317 final SchemaContext schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent)
319 return schemaContext.getModules().iterator().next().getName();
322 private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
323 final FetchDescendantsOption fetchDescendantsOption) {
324 if (fetchDescendantsOption.hasNext()) {
325 return fragmentEntity.getChildFragments().stream()
326 .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption.next()))
327 .collect(Collectors.toList());
329 return Collections.emptyList();
333 public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
334 final Map<String, Object> leaves) {
335 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
336 fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
337 fragmentRepository.save(fragmentEntity);
341 public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
342 final DataNode dataNode) {
343 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
344 updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode);
346 fragmentRepository.save(fragmentEntity);
347 } catch (final StaleStateException staleStateException) {
348 throw new ConcurrencyException("Concurrent Transactions",
349 String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.",
350 dataspaceName, anchorName, dataNode.getXpath()));
355 public void updateDataNodesAndDescendants(final String dataspaceName,
356 final String anchorName,
357 final List<DataNode> dataNodes) {
359 final Map<DataNode, FragmentEntity> dataNodeFragmentEntityMap = dataNodes.stream()
360 .collect(Collectors.toMap(
361 dataNode -> dataNode,
362 dataNode -> getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath())));
363 dataNodeFragmentEntityMap.forEach(
364 (dataNode, fragmentEntity) -> updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode));
366 fragmentRepository.saveAll(dataNodeFragmentEntityMap.values());
367 } catch (final StaleStateException staleStateException) {
368 retryUpdateDataNodesIndividually(dataspaceName, anchorName, dataNodeFragmentEntityMap.values());
372 private void retryUpdateDataNodesIndividually(final String dataspaceName, final String anchorName,
373 final Collection<FragmentEntity> fragmentEntities) {
374 final Collection<String> failedXpaths = new HashSet<>();
376 fragmentEntities.forEach(dataNodeFragment -> {
378 fragmentRepository.save(dataNodeFragment);
379 } catch (final StaleStateException e) {
380 failedXpaths.add(dataNodeFragment.getXpath());
384 if (!failedXpaths.isEmpty()) {
385 final String failedXpathsConcatenated = String.join(",", failedXpaths);
386 throw new ConcurrencyException("Concurrent Transactions", String.format(
387 "DataNodes : %s in Dataspace :'%s' with Anchor : '%s' are updated by another transaction.",
388 failedXpathsConcatenated, dataspaceName, anchorName));
392 private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
393 final DataNode newDataNode) {
395 existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
397 final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
398 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
400 final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
402 for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
403 final FragmentEntity childFragment;
404 if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
405 childFragment = convertToFragmentWithAllDescendants(
406 existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
408 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
409 updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild);
411 updatedChildFragments.add(childFragment);
414 existingFragmentEntity.getChildFragments().clear();
415 existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
420 public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
421 final Collection<DataNode> newListElements) {
422 final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
423 final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
424 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
425 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
426 deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
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);
434 updatedChildFragmentEntities.add(entityToBeAdded);
436 parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
437 fragmentRepository.save(parentEntity);
442 public void deleteDataNodes(final String dataspaceName, final String anchorName) {
443 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
444 anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
446 anchorEntity -> fragmentRepository.deleteByAnchorIn(Set.of(anchorEntity)));
451 public void deleteListDataNode(final String dataspaceName, final String anchorName,
452 final String targetXpath) {
453 deleteDataNode(dataspaceName, anchorName, targetXpath, true);
458 public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
459 deleteDataNode(dataspaceName, anchorName, targetXpath, false);
462 private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
463 final boolean onlySupportListNodeDeletion) {
464 final String parentNodeXpath;
465 FragmentEntity parentFragmentEntity = null;
466 boolean targetDeleted = false;
467 if (isRootXpath(targetXpath)) {
468 deleteDataNodes(dataspaceName, anchorName);
469 targetDeleted = true;
471 if (isRootContainerNodeXpath(targetXpath)) {
472 parentNodeXpath = targetXpath;
474 parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
476 parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
477 final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/'));
478 final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE
479 .matcher(lastXpathElement).find();
481 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
483 targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath);
484 final boolean tryToDeleteDataNode = !targetDeleted && !onlySupportListNodeDeletion;
485 if (tryToDeleteDataNode) {
486 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
490 if (!targetDeleted) {
491 final String additionalInformation = onlySupportListNodeDeletion
492 ? "The target is probably not a List." : "";
493 throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
494 parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
498 private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
499 final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath);
500 if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) {
501 fragmentRepository.delete(parentFragmentEntity);
504 if (parentFragmentEntity.getChildFragments()
505 .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) {
506 fragmentRepository.save(parentFragmentEntity);
512 private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
513 final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath);
514 final String deleteTargetXpathPrefix = normalizedListXpath + "[";
515 if (parentFragmentEntity.getChildFragments()
516 .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
517 fragmentRepository.save(parentFragmentEntity);
523 private static void deleteListElements(
524 final Collection<FragmentEntity> fragmentEntities,
525 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
526 fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
529 private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
530 if (newListElements.isEmpty()) {
531 throw new CpsAdminException("Invalid list replacement",
532 "Cannot replace list elements with empty collection");
534 final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
535 return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
538 private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
539 final DataNode newListElement,
540 final FragmentEntity existingListElementEntity) {
541 if (existingListElementEntity == null) {
542 return convertToFragmentWithAllDescendants(
543 parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
545 if (newListElement.getChildDataNodes().isEmpty()) {
546 copyAttributesFromNewListElement(existingListElementEntity, newListElement);
547 existingListElementEntity.getChildFragments().clear();
549 updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
551 return existingListElementEntity;
554 private static boolean isNewDataNode(final DataNode replacementDataNode,
555 final Map<String, FragmentEntity> existingListElementsByXpath) {
556 return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
559 private static boolean isRootContainerNodeXpath(final String xpath) {
560 return 0 == xpath.lastIndexOf('/');
563 private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
564 final DataNode newListElement) {
565 final FragmentEntity replacementFragmentEntity =
566 FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
567 newListElement.getLeaves())).build();
568 existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
571 private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
572 final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
573 return childEntities.stream()
574 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
575 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
578 private static boolean isRootXpath(final String xpath) {
579 return "/".equals(xpath) || "".equals(xpath);