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.ConcurrencyException;
56 import org.onap.cps.spi.exceptions.CpsAdminException;
57 import org.onap.cps.spi.exceptions.CpsPathException;
58 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
59 import org.onap.cps.spi.model.DataNode;
60 import org.onap.cps.spi.model.DataNodeBuilder;
61 import org.onap.cps.spi.repository.AnchorRepository;
62 import org.onap.cps.spi.repository.DataspaceRepository;
63 import org.onap.cps.spi.repository.FragmentRepository;
64 import org.onap.cps.spi.utils.SessionManager;
65 import org.onap.cps.utils.JsonObjectMapper;
66 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
67 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
68 import org.springframework.dao.DataIntegrityViolationException;
69 import org.springframework.stereotype.Service;
73 @RequiredArgsConstructor
74 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
76 private final DataspaceRepository dataspaceRepository;
78 private final AnchorRepository anchorRepository;
80 private final FragmentRepository fragmentRepository;
82 private final JsonObjectMapper jsonObjectMapper;
84 private final SessionManager sessionManager;
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}))\\]$");
92 public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath,
93 final DataNode newChildDataNode) {
94 addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, Collections.singleton(newChildDataNode));
99 public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
100 final Collection<DataNode> newListElements) {
101 addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, newListElements);
106 public void addListElementsBatch(final String dataspaceName, final String anchorName, final String parentNodeXpath,
107 final Collection<Collection<DataNode>> newListsElements) {
109 newListsElements.forEach(
110 newListElement -> addListElements(dataspaceName, anchorName, parentNodeXpath, newListElement));
114 private void addChildDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
115 final Collection<DataNode> newChildren) {
116 final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
118 final List<FragmentEntity> fragmentEntities = new ArrayList<>();
119 newChildren.forEach(newChildAsDataNode -> {
120 final FragmentEntity newChildAsFragmentEntity = convertToFragmentWithAllDescendants(
121 parentFragmentEntity.getDataspace(),
122 parentFragmentEntity.getAnchor(),
124 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
125 fragmentEntities.add(newChildAsFragmentEntity);
127 fragmentRepository.saveAll(fragmentEntities);
128 } catch (final DataIntegrityViolationException exception) {
129 final List<String> conflictXpaths = newChildren.stream()
130 .map(DataNode::getXpath)
131 .collect(Collectors.toList());
132 throw AlreadyDefinedException.forDataNodes(conflictXpaths, anchorName, exception);
137 public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
138 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
139 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
140 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
143 fragmentRepository.save(fragmentEntity);
144 } catch (final DataIntegrityViolationException exception) {
145 throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
150 * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
151 * for all DataNode children recursively.
153 * @param dataspaceEntity dataspace
154 * @param anchorEntity anchorEntity
155 * @param dataNodeToBeConverted dataNode
156 * @return a Fragment built from current DataNode
158 private FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
159 final AnchorEntity anchorEntity, final DataNode dataNodeToBeConverted) {
160 final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
161 final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
162 for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
163 final FragmentEntity childFragment =
164 convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(),
166 childFragmentsImmutableSetBuilder.add(childFragment);
168 parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
169 return parentFragment;
172 private FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity,
173 final AnchorEntity anchorEntity, final DataNode dataNode) {
174 return FragmentEntity.builder()
175 .dataspace(dataspaceEntity)
176 .anchor(anchorEntity)
177 .xpath(dataNode.getXpath())
178 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
183 public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
184 final FetchDescendantsOption fetchDescendantsOption) {
185 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
186 return toDataNode(fragmentEntity, fetchDescendantsOption);
189 private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName,
190 final String xpath) {
191 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
192 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
193 if (isRootXpath(xpath)) {
194 return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity);
196 final String normalizedXpath;
198 normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath);
199 } catch (final PathParsingException e) {
200 throw new CpsPathException(e.getMessage());
202 return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity,
208 public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
209 final FetchDescendantsOption fetchDescendantsOption) {
210 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
211 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
212 final CpsPathQuery cpsPathQuery;
214 cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
215 } catch (final PathParsingException e) {
216 throw new CpsPathException(e.getMessage());
218 List<FragmentEntity> fragmentEntities =
219 fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
220 if (cpsPathQuery.hasAncestorAxis()) {
221 final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
222 fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList()
223 : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
225 return fragmentEntities.stream()
226 .map(fragmentEntity -> toDataNode(fragmentEntity, fetchDescendantsOption))
227 .collect(Collectors.toUnmodifiableList());
231 public String startSession() {
232 return sessionManager.startSession();
236 public void closeSession(final String sessionId) {
237 sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT);
241 public void lockAnchor(final String sessionId, final String dataspaceName,
242 final String anchorName, final Long timeoutInMilliseconds) {
243 sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
246 private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
247 final CpsPathQuery cpsPathQuery) {
248 final Set<String> ancestorXpath = new HashSet<>();
249 final Pattern pattern =
250 Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
251 + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*");
252 for (final FragmentEntity fragmentEntity : fragmentEntities) {
253 final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
254 if (matcher.matches()) {
255 ancestorXpath.add(matcher.group(1));
258 return ancestorXpath;
261 private DataNode toDataNode(final FragmentEntity fragmentEntity,
262 final FetchDescendantsOption fetchDescendantsOption) {
263 final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
264 Map<String, Object> leaves = new HashMap<>();
265 if (fragmentEntity.getAttributes() != null) {
266 leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
268 return new DataNodeBuilder()
269 .withModuleNamePrefix(getFirstModuleName(fragmentEntity))
270 .withXpath(fragmentEntity.getXpath())
272 .withChildDataNodes(childDataNodes).build();
275 private String getFirstModuleName(final FragmentEntity fragmentEntity) {
276 final SchemaSetEntity schemaSetEntity = fragmentEntity.getAnchor().getSchemaSet();
277 final Map<String, String> yangResourceNameToContent =
278 schemaSetEntity.getYangResources().stream().collect(
279 Collectors.toMap(YangResourceEntity::getFileName, YangResourceEntity::getContent));
280 final SchemaContext schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent)
282 return schemaContext.getModules().iterator().next().getName();
285 private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
286 final FetchDescendantsOption fetchDescendantsOption) {
287 if (fetchDescendantsOption == INCLUDE_ALL_DESCENDANTS) {
288 return fragmentEntity.getChildFragments().stream()
289 .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption))
290 .collect(Collectors.toUnmodifiableList());
292 return Collections.emptyList();
296 public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
297 final Map<String, Object> leaves) {
298 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
299 fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
300 fragmentRepository.save(fragmentEntity);
304 public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
305 final DataNode dataNode) {
306 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
307 updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode);
309 fragmentRepository.save(fragmentEntity);
310 } catch (final StaleStateException staleStateException) {
311 throw new ConcurrencyException("Concurrent Transactions",
312 String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.",
313 dataspaceName, anchorName, dataNode.getXpath()),
314 staleStateException);
319 public void updateDataNodesAndDescendants(final String dataspaceName,
320 final String anchorName,
321 final List<DataNode> dataNodes) {
322 final Map<DataNode, FragmentEntity> dataNodeFragmentEntityMap = dataNodes.stream()
323 .collect(Collectors.toMap(
324 dataNode -> dataNode, dataNode -> getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath())));
325 dataNodeFragmentEntityMap.forEach(
326 (dataNode, fragmentEntity) -> updateFragmentEntityAndDescendantsWithDataNode(fragmentEntity, dataNode));
328 fragmentRepository.saveAll(dataNodeFragmentEntityMap.values());
329 } catch (final StaleStateException staleStateException) {
330 throw new ConcurrencyException("Concurrent Transactions",
331 String.format("A data node in dataspace :'%s' with Anchor : '%s' is updated by another transaction.",
332 dataspaceName, anchorName),
333 staleStateException);
337 private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
338 final DataNode newDataNode) {
340 existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
342 final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
343 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
345 final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
347 for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
348 final FragmentEntity childFragment;
349 if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
350 childFragment = convertToFragmentWithAllDescendants(
351 existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
353 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
354 updateFragmentEntityAndDescendantsWithDataNode(childFragment, newDataNodeChild);
356 updatedChildFragments.add(childFragment);
359 existingFragmentEntity.getChildFragments().clear();
360 existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
365 public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
366 final Collection<DataNode> newListElements) {
367 final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
368 final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
369 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
370 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
371 deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
372 final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
373 for (final DataNode newListElement : newListElements) {
374 final FragmentEntity existingListElementEntity =
375 existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
376 final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
377 existingListElementEntity);
379 updatedChildFragmentEntities.add(entityToBeAdded);
381 parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
382 fragmentRepository.save(parentEntity);
387 public void deleteDataNodes(final String dataspaceName, final String anchorName) {
388 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
389 anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
391 anchorEntity -> fragmentRepository.deleteByAnchorIn(Set.of(anchorEntity)));
396 public void deleteListDataNode(final String dataspaceName, final String anchorName,
397 final String targetXpath) {
398 deleteDataNode(dataspaceName, anchorName, targetXpath, true);
403 public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
404 deleteDataNode(dataspaceName, anchorName, targetXpath, false);
407 private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
408 final boolean onlySupportListNodeDeletion) {
409 final String parentNodeXpath;
410 FragmentEntity parentFragmentEntity = null;
411 boolean targetDeleted = false;
412 if (isRootXpath(targetXpath)) {
413 deleteDataNodes(dataspaceName, anchorName);
414 targetDeleted = true;
416 if (isRootContainerNodeXpath(targetXpath)) {
417 parentNodeXpath = targetXpath;
419 parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
421 parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
422 final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/'));
423 final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE
424 .matcher(lastXpathElement).find();
426 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
428 targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath);
429 final boolean tryToDeleteDataNode = !targetDeleted && !onlySupportListNodeDeletion;
430 if (tryToDeleteDataNode) {
431 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
435 if (!targetDeleted) {
436 final String additionalInformation = onlySupportListNodeDeletion
437 ? "The target is probably not a List." : "";
438 throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
439 parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
443 private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
444 final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath);
445 if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) {
446 fragmentRepository.delete(parentFragmentEntity);
449 if (parentFragmentEntity.getChildFragments()
450 .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) {
451 fragmentRepository.save(parentFragmentEntity);
457 private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
458 final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath);
459 final String deleteTargetXpathPrefix = normalizedListXpath + "[";
460 if (parentFragmentEntity.getChildFragments()
461 .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
462 fragmentRepository.save(parentFragmentEntity);
468 private static void deleteListElements(
469 final Collection<FragmentEntity> fragmentEntities,
470 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
471 fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
474 private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
475 if (newListElements.isEmpty()) {
476 throw new CpsAdminException("Invalid list replacement",
477 "Cannot replace list elements with empty collection");
479 final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
480 return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
483 private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
484 final DataNode newListElement,
485 final FragmentEntity existingListElementEntity) {
486 if (existingListElementEntity == null) {
487 return convertToFragmentWithAllDescendants(
488 parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
490 if (newListElement.getChildDataNodes().isEmpty()) {
491 copyAttributesFromNewListElement(existingListElementEntity, newListElement);
492 existingListElementEntity.getChildFragments().clear();
494 updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
496 return existingListElementEntity;
499 private static boolean isNewDataNode(final DataNode replacementDataNode,
500 final Map<String, FragmentEntity> existingListElementsByXpath) {
501 return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
504 private static boolean isRootContainerNodeXpath(final String xpath) {
505 return 0 == xpath.lastIndexOf('/');
508 private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
509 final DataNode newListElement) {
510 final FragmentEntity replacementFragmentEntity =
511 FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
512 newListElement.getLeaves())).build();
513 existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
516 private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
517 final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
518 return childEntities.stream()
519 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
520 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
523 private static boolean isRootXpath(final String xpath) {
524 return "/".equals(xpath) || "".equals(xpath);