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 static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
27 import com.google.common.collect.ImmutableSet;
28 import com.google.common.collect.ImmutableSet.Builder;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 import java.util.stream.Collectors;
40 import javax.transaction.Transactional;
41 import lombok.RequiredArgsConstructor;
42 import lombok.extern.slf4j.Slf4j;
43 import org.hibernate.StaleStateException;
44 import org.onap.cps.cpspath.parser.CpsPathQuery;
45 import org.onap.cps.cpspath.parser.CpsPathUtil;
46 import org.onap.cps.cpspath.parser.PathParsingException;
47 import org.onap.cps.spi.CpsDataPersistenceService;
48 import org.onap.cps.spi.FetchDescendantsOption;
49 import org.onap.cps.spi.entities.AnchorEntity;
50 import org.onap.cps.spi.entities.DataspaceEntity;
51 import org.onap.cps.spi.entities.FragmentEntity;
52 import org.onap.cps.spi.entities.SchemaSetEntity;
53 import org.onap.cps.spi.entities.YangResourceEntity;
54 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
55 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch;
56 import org.onap.cps.spi.exceptions.ConcurrencyException;
57 import org.onap.cps.spi.exceptions.CpsAdminException;
58 import org.onap.cps.spi.exceptions.CpsPathException;
59 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
60 import org.onap.cps.spi.model.DataNode;
61 import org.onap.cps.spi.model.DataNodeBuilder;
62 import org.onap.cps.spi.repository.AnchorRepository;
63 import org.onap.cps.spi.repository.DataspaceRepository;
64 import org.onap.cps.spi.repository.FragmentRepository;
65 import org.onap.cps.spi.utils.SessionManager;
66 import org.onap.cps.utils.JsonObjectMapper;
67 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
68 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
69 import org.springframework.dao.DataIntegrityViolationException;
70 import org.springframework.stereotype.Service;
74 @RequiredArgsConstructor
75 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
77 private final DataspaceRepository dataspaceRepository;
79 private final AnchorRepository anchorRepository;
81 private final FragmentRepository fragmentRepository;
83 private final JsonObjectMapper jsonObjectMapper;
85 private final SessionManager sessionManager;
87 private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
88 private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE =
89 Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$");
92 public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath,
93 final DataNode newChildDataNode) {
94 addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChildDataNode);
98 public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
99 final Collection<DataNode> newListElements) {
100 addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newListElements);
104 public void addMultipleLists(final String dataspaceName, final String anchorName, final String parentNodeXpath,
105 final Collection<Collection<DataNode>> newLists) {
106 final Collection<String> failedCmHandleIds = new HashSet<>();
107 newLists.forEach(newList -> {
109 addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newList);
110 } catch (final AlreadyDefinedException e) {
111 newList.forEach(listElement -> failedCmHandleIds.add((String) listElement.getLeaves().get("id")));
115 if (!failedCmHandleIds.isEmpty()) {
116 throw new AlreadyDefinedExceptionBatch(failedCmHandleIds);
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());
129 fragmentRepository.save(newChildAsFragmentEntity);
130 } catch (final DataIntegrityViolationException e) {
131 throw AlreadyDefinedException.forDataNode(newChild.getXpath(), anchorName, e);
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());
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);
148 fragmentRepository.saveAll(fragmentEntities);
149 } catch (final DataIntegrityViolationException e) {
150 log.warn("Exception occurred : {} , Batch with size : {} will be retried using individual save operations",
151 e, fragmentEntities.size());
152 retrySavingEachChildIndividually(dataspaceName, anchorName, parentNodeXpath, newChildren);
156 private void retrySavingEachChildIndividually(final String dataspaceName, final String anchorName,
157 final String parentNodeXpath, final Collection<DataNode> newChildren) {
158 newChildren.forEach(newChild -> addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChild));
162 public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
163 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
164 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
165 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
168 fragmentRepository.save(fragmentEntity);
169 } catch (final DataIntegrityViolationException exception) {
170 throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
175 * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
176 * for all DataNode children recursively.
178 * @param dataspaceEntity dataspace
179 * @param anchorEntity anchorEntity
180 * @param dataNodeToBeConverted dataNode
181 * @return a Fragment built from current DataNode
183 private FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
184 final AnchorEntity anchorEntity, final DataNode dataNodeToBeConverted) {
185 final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
186 final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
187 for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
188 final FragmentEntity childFragment =
189 convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(),
191 childFragmentsImmutableSetBuilder.add(childFragment);
193 parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
194 return parentFragment;
197 private FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity,
198 final AnchorEntity anchorEntity, final DataNode dataNode) {
199 return FragmentEntity.builder()
200 .dataspace(dataspaceEntity)
201 .anchor(anchorEntity)
202 .xpath(dataNode.getXpath())
203 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
208 public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
209 final FetchDescendantsOption fetchDescendantsOption) {
210 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
211 return toDataNode(fragmentEntity, fetchDescendantsOption);
214 private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName,
215 final String xpath) {
216 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
217 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
218 if (isRootXpath(xpath)) {
219 return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity);
221 final String normalizedXpath;
223 normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath);
224 } catch (final PathParsingException e) {
225 throw new CpsPathException(e.getMessage());
227 return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity,
233 public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
234 final FetchDescendantsOption fetchDescendantsOption) {
235 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
236 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
237 final CpsPathQuery cpsPathQuery;
239 cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
240 } catch (final PathParsingException e) {
241 throw new CpsPathException(e.getMessage());
243 List<FragmentEntity> fragmentEntities =
244 fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
245 if (cpsPathQuery.hasAncestorAxis()) {
246 final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
247 fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList()
248 : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
250 return fragmentEntities.stream()
251 .map(fragmentEntity -> toDataNode(fragmentEntity, fetchDescendantsOption))
252 .collect(Collectors.toUnmodifiableList());
256 public String startSession() {
257 return sessionManager.startSession();
261 public void closeSession(final String sessionId) {
262 sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT);
266 public void lockAnchor(final String sessionId, final String dataspaceName,
267 final String anchorName, final Long timeoutInMilliseconds) {
268 sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
271 private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
272 final CpsPathQuery cpsPathQuery) {
273 final Set<String> ancestorXpath = new HashSet<>();
274 final Pattern pattern =
275 Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
276 + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*");
277 for (final FragmentEntity fragmentEntity : fragmentEntities) {
278 final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
279 if (matcher.matches()) {
280 ancestorXpath.add(matcher.group(1));
283 return ancestorXpath;
286 private DataNode toDataNode(final FragmentEntity fragmentEntity,
287 final FetchDescendantsOption fetchDescendantsOption) {
288 final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
289 Map<String, Object> leaves = new HashMap<>();
290 if (fragmentEntity.getAttributes() != null) {
291 leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
293 return new DataNodeBuilder()
294 .withModuleNamePrefix(getFirstModuleName(fragmentEntity))
295 .withXpath(fragmentEntity.getXpath())
297 .withChildDataNodes(childDataNodes).build();
300 private String getFirstModuleName(final FragmentEntity fragmentEntity) {
301 final SchemaSetEntity schemaSetEntity = fragmentEntity.getAnchor().getSchemaSet();
302 final Map<String, String> yangResourceNameToContent =
303 schemaSetEntity.getYangResources().stream().collect(
304 Collectors.toMap(YangResourceEntity::getFileName, YangResourceEntity::getContent));
305 final SchemaContext schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent)
307 return schemaContext.getModules().iterator().next().getName();
310 private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
311 final FetchDescendantsOption fetchDescendantsOption) {
312 if (fetchDescendantsOption == INCLUDE_ALL_DESCENDANTS) {
313 return fragmentEntity.getChildFragments().stream()
314 .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption))
315 .collect(Collectors.toUnmodifiableList());
317 return Collections.emptyList();
321 public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
322 final Map<String, Object> leaves) {
323 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
324 fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
325 fragmentRepository.save(fragmentEntity);
329 public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
330 final DataNode dataNode) {
331 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
332 updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode);
334 fragmentRepository.save(fragmentEntity);
335 } catch (final StaleStateException staleStateException) {
336 throw new ConcurrencyException("Concurrent Transactions",
337 String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.",
338 dataspaceName, anchorName, dataNode.getXpath()),
339 staleStateException);
344 public void updateDataNodesAndDescendants(final String dataspaceName,
345 final String anchorName,
346 final List<DataNode> dataNodes) {
347 final Map<DataNode, FragmentEntity> dataNodeFragmentEntityMap = dataNodes.stream()
348 .collect(Collectors.toMap(
349 dataNode -> dataNode, dataNode -> getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath())));
350 dataNodeFragmentEntityMap.forEach(
351 (dataNode, fragmentEntity) -> updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode));
353 fragmentRepository.saveAll(dataNodeFragmentEntityMap.values());
354 } catch (final StaleStateException staleStateException) {
355 throw new ConcurrencyException("Concurrent Transactions",
356 String.format("A data node in dataspace :'%s' with Anchor : '%s' is updated by another transaction.",
357 dataspaceName, anchorName),
358 staleStateException);
362 private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
363 final DataNode newDataNode) {
365 existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
367 final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
368 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
370 final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
372 for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
373 final FragmentEntity childFragment;
374 if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
375 childFragment = convertToFragmentWithAllDescendants(
376 existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
378 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
379 updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild);
381 updatedChildFragments.add(childFragment);
384 existingFragmentEntity.getChildFragments().clear();
385 existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
390 public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
391 final Collection<DataNode> newListElements) {
392 final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
393 final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
394 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
395 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
396 deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
397 final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
398 for (final DataNode newListElement : newListElements) {
399 final FragmentEntity existingListElementEntity =
400 existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
401 final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
402 existingListElementEntity);
404 updatedChildFragmentEntities.add(entityToBeAdded);
406 parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
407 fragmentRepository.save(parentEntity);
412 public void deleteDataNodes(final String dataspaceName, final String anchorName) {
413 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
414 anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
416 anchorEntity -> fragmentRepository.deleteByAnchorIn(Set.of(anchorEntity)));
421 public void deleteListDataNode(final String dataspaceName, final String anchorName,
422 final String targetXpath) {
423 deleteDataNode(dataspaceName, anchorName, targetXpath, true);
428 public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
429 deleteDataNode(dataspaceName, anchorName, targetXpath, false);
432 private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
433 final boolean onlySupportListNodeDeletion) {
434 final String parentNodeXpath;
435 FragmentEntity parentFragmentEntity = null;
436 boolean targetDeleted = false;
437 if (isRootXpath(targetXpath)) {
438 deleteDataNodes(dataspaceName, anchorName);
439 targetDeleted = true;
441 if (isRootContainerNodeXpath(targetXpath)) {
442 parentNodeXpath = targetXpath;
444 parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
446 parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
447 final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/'));
448 final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE
449 .matcher(lastXpathElement).find();
451 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
453 targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath);
454 final boolean tryToDeleteDataNode = !targetDeleted && !onlySupportListNodeDeletion;
455 if (tryToDeleteDataNode) {
456 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
460 if (!targetDeleted) {
461 final String additionalInformation = onlySupportListNodeDeletion
462 ? "The target is probably not a List." : "";
463 throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
464 parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
468 private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
469 final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath);
470 if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) {
471 fragmentRepository.delete(parentFragmentEntity);
474 if (parentFragmentEntity.getChildFragments()
475 .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) {
476 fragmentRepository.save(parentFragmentEntity);
482 private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
483 final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath);
484 final String deleteTargetXpathPrefix = normalizedListXpath + "[";
485 if (parentFragmentEntity.getChildFragments()
486 .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
487 fragmentRepository.save(parentFragmentEntity);
493 private static void deleteListElements(
494 final Collection<FragmentEntity> fragmentEntities,
495 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
496 fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
499 private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
500 if (newListElements.isEmpty()) {
501 throw new CpsAdminException("Invalid list replacement",
502 "Cannot replace list elements with empty collection");
504 final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
505 return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
508 private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
509 final DataNode newListElement,
510 final FragmentEntity existingListElementEntity) {
511 if (existingListElementEntity == null) {
512 return convertToFragmentWithAllDescendants(
513 parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
515 if (newListElement.getChildDataNodes().isEmpty()) {
516 copyAttributesFromNewListElement(existingListElementEntity, newListElement);
517 existingListElementEntity.getChildFragments().clear();
519 updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
521 return existingListElementEntity;
524 private static boolean isNewDataNode(final DataNode replacementDataNode,
525 final Map<String, FragmentEntity> existingListElementsByXpath) {
526 return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
529 private static boolean isRootContainerNodeXpath(final String xpath) {
530 return 0 == xpath.lastIndexOf('/');
533 private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
534 final DataNode newListElement) {
535 final FragmentEntity replacementFragmentEntity =
536 FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
537 newListElement.getLeaves())).build();
538 existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
541 private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
542 final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
543 return childEntities.stream()
544 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
545 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
548 private static boolean isRootXpath(final String xpath) {
549 return "/".equals(xpath) || "".equals(xpath);