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.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.regex.Matcher;
37 import java.util.regex.Pattern;
38 import java.util.stream.Collectors;
39 import javax.transaction.Transactional;
40 import lombok.RequiredArgsConstructor;
41 import lombok.extern.slf4j.Slf4j;
42 import org.hibernate.StaleStateException;
43 import org.onap.cps.cpspath.parser.CpsPathQuery;
44 import org.onap.cps.cpspath.parser.CpsPathUtil;
45 import org.onap.cps.cpspath.parser.PathParsingException;
46 import org.onap.cps.spi.CpsDataPersistenceService;
47 import org.onap.cps.spi.FetchDescendantsOption;
48 import org.onap.cps.spi.entities.AnchorEntity;
49 import org.onap.cps.spi.entities.DataspaceEntity;
50 import org.onap.cps.spi.entities.FragmentEntity;
51 import org.onap.cps.spi.entities.SchemaSetEntity;
52 import org.onap.cps.spi.entities.YangResourceEntity;
53 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
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}))\\]$");
91 public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath,
92 final DataNode newChildDataNode) {
93 addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, Collections.singleton(newChildDataNode));
98 public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
99 final Collection<DataNode> newListElements) {
100 addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, newListElements);
103 private void addChildDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
104 final Collection<DataNode> newChildren) {
105 final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
107 for (final DataNode newChildAsDataNode : newChildren) {
108 final FragmentEntity newChildAsFragmentEntity = convertToFragmentWithAllDescendants(
109 parentFragmentEntity.getDataspace(),
110 parentFragmentEntity.getAnchor(),
112 newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
113 fragmentRepository.save(newChildAsFragmentEntity);
115 } catch (final DataIntegrityViolationException exception) {
116 final List<String> conflictXpaths = newChildren.stream()
117 .map(DataNode::getXpath)
118 .collect(Collectors.toList());
119 throw AlreadyDefinedException.forDataNodes(conflictXpaths, anchorName, exception);
124 public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
125 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
126 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
127 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
130 fragmentRepository.save(fragmentEntity);
131 } catch (final DataIntegrityViolationException exception) {
132 throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
137 * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
138 * for all DataNode children recursively.
140 * @param dataspaceEntity dataspace
141 * @param anchorEntity anchorEntity
142 * @param dataNodeToBeConverted dataNode
143 * @return a Fragment built from current DataNode
145 private FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
146 final AnchorEntity anchorEntity, final DataNode dataNodeToBeConverted) {
147 final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
148 final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
149 for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
150 final FragmentEntity childFragment =
151 convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(),
153 childFragmentsImmutableSetBuilder.add(childFragment);
155 parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
156 return parentFragment;
159 private FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity,
160 final AnchorEntity anchorEntity, final DataNode dataNode) {
161 return FragmentEntity.builder()
162 .dataspace(dataspaceEntity)
163 .anchor(anchorEntity)
164 .xpath(dataNode.getXpath())
165 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
170 public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
171 final FetchDescendantsOption fetchDescendantsOption) {
172 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
173 return toDataNode(fragmentEntity, fetchDescendantsOption);
176 private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName,
177 final String xpath) {
178 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
179 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
180 if (isRootXpath(xpath)) {
181 return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity);
183 final String normalizedXpath;
185 normalizedXpath = CpsPathUtil.getNormalizedXpath(xpath);
186 } catch (final PathParsingException e) {
187 throw new CpsPathException(e.getMessage());
189 return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity,
195 public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
196 final FetchDescendantsOption fetchDescendantsOption) {
197 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
198 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
199 final CpsPathQuery cpsPathQuery;
201 cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
202 } catch (final PathParsingException e) {
203 throw new CpsPathException(e.getMessage());
205 List<FragmentEntity> fragmentEntities =
206 fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
207 if (cpsPathQuery.hasAncestorAxis()) {
208 final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
209 fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList()
210 : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
212 return fragmentEntities.stream()
213 .map(fragmentEntity -> toDataNode(fragmentEntity, fetchDescendantsOption))
214 .collect(Collectors.toUnmodifiableList());
218 public String startSession() {
219 return sessionManager.startSession();
223 public void closeSession(final String sessionId) {
224 sessionManager.closeSession(sessionId, SessionManager.WITH_COMMIT);
228 public void lockAnchor(final String sessionId, final String dataspaceName,
229 final String anchorName, final Long timeoutInMilliseconds) {
230 sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
233 private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
234 final CpsPathQuery cpsPathQuery) {
235 final Set<String> ancestorXpath = new HashSet<>();
236 final Pattern pattern =
237 Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
238 + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*");
239 for (final FragmentEntity fragmentEntity : fragmentEntities) {
240 final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
241 if (matcher.matches()) {
242 ancestorXpath.add(matcher.group(1));
245 return ancestorXpath;
248 private DataNode toDataNode(final FragmentEntity fragmentEntity,
249 final FetchDescendantsOption fetchDescendantsOption) {
250 final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
251 Map<String, Object> leaves = new HashMap<>();
252 if (fragmentEntity.getAttributes() != null) {
253 leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
255 return new DataNodeBuilder()
256 .withModuleNamePrefix(getFirstModuleName(fragmentEntity))
257 .withXpath(fragmentEntity.getXpath())
259 .withChildDataNodes(childDataNodes).build();
262 private String getFirstModuleName(final FragmentEntity fragmentEntity) {
263 final SchemaSetEntity schemaSetEntity = fragmentEntity.getAnchor().getSchemaSet();
264 final Map<String, String> yangResourceNameToContent =
265 schemaSetEntity.getYangResources().stream().collect(
266 Collectors.toMap(YangResourceEntity::getFileName, YangResourceEntity::getContent));
267 final SchemaContext schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent)
269 return schemaContext.getModules().iterator().next().getName();
272 private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
273 final FetchDescendantsOption fetchDescendantsOption) {
274 if (fetchDescendantsOption == INCLUDE_ALL_DESCENDANTS) {
275 return fragmentEntity.getChildFragments().stream()
276 .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption))
277 .collect(Collectors.toUnmodifiableList());
279 return Collections.emptyList();
283 public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
284 final Map<String, Object> leaves) {
285 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
286 fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
287 fragmentRepository.save(fragmentEntity);
291 public void replaceDataNodeTree(final String dataspaceName, final String anchorName, final DataNode dataNode) {
292 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
293 replaceDataNodeTree(fragmentEntity, dataNode);
295 fragmentRepository.save(fragmentEntity);
296 } catch (final StaleStateException staleStateException) {
297 throw new ConcurrencyException("Concurrent Transactions",
298 String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.",
299 dataspaceName, anchorName, dataNode.getXpath()),
300 staleStateException);
304 private void replaceDataNodeTree(final FragmentEntity existingFragmentEntity,
305 final DataNode newDataNode) {
307 existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
309 final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
310 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
312 final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
314 for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
315 final FragmentEntity childFragment;
316 if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
317 childFragment = convertToFragmentWithAllDescendants(
318 existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
320 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
321 replaceDataNodeTree(childFragment, newDataNodeChild);
323 updatedChildFragments.add(childFragment);
325 existingFragmentEntity.getChildFragments().clear();
326 existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
331 public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
332 final Collection<DataNode> newListElements) {
333 final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
334 final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
335 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
336 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
337 deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
338 final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
339 for (final DataNode newListElement : newListElements) {
340 final FragmentEntity existingListElementEntity =
341 existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
342 final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
343 existingListElementEntity);
345 updatedChildFragmentEntities.add(entityToBeAdded);
347 parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
348 fragmentRepository.save(parentEntity);
353 public void deleteDataNodes(final String dataspaceName, final String anchorName) {
354 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
355 anchorRepository.findByDataspaceAndName(dataspaceEntity, anchorName)
357 anchorEntity -> fragmentRepository.deleteByAnchorIn(Set.of(anchorEntity)));
362 public void deleteListDataNode(final String dataspaceName, final String anchorName,
363 final String targetXpath) {
364 deleteDataNode(dataspaceName, anchorName, targetXpath, true);
369 public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
370 deleteDataNode(dataspaceName, anchorName, targetXpath, false);
373 private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
374 final boolean onlySupportListNodeDeletion) {
375 final String parentNodeXpath;
376 FragmentEntity parentFragmentEntity = null;
377 boolean targetDeleted = false;
378 if (isRootXpath(targetXpath)) {
379 deleteDataNodes(dataspaceName, anchorName);
380 targetDeleted = true;
382 if (isRootContainerNodeXpath(targetXpath)) {
383 parentNodeXpath = targetXpath;
385 parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
387 parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
388 final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/'));
389 final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE
390 .matcher(lastXpathElement).find();
392 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
394 targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath);
395 final boolean tryToDeleteDataNode = !targetDeleted && !onlySupportListNodeDeletion;
396 if (tryToDeleteDataNode) {
397 targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath);
401 if (!targetDeleted) {
402 final String additionalInformation = onlySupportListNodeDeletion
403 ? "The target is probably not a List." : "";
404 throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
405 parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
409 private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
410 final String normalizedTargetXpath = CpsPathUtil.getNormalizedXpath(targetXpath);
411 if (parentFragmentEntity.getXpath().equals(normalizedTargetXpath)) {
412 fragmentRepository.delete(parentFragmentEntity);
415 if (parentFragmentEntity.getChildFragments()
416 .removeIf(fragment -> fragment.getXpath().equals(normalizedTargetXpath))) {
417 fragmentRepository.save(parentFragmentEntity);
423 private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
424 final String normalizedListXpath = CpsPathUtil.getNormalizedXpath(listXpath);
425 final String deleteTargetXpathPrefix = normalizedListXpath + "[";
426 if (parentFragmentEntity.getChildFragments()
427 .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
428 fragmentRepository.save(parentFragmentEntity);
434 private static void deleteListElements(
435 final Collection<FragmentEntity> fragmentEntities,
436 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
437 fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
440 private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
441 if (newListElements.isEmpty()) {
442 throw new CpsAdminException("Invalid list replacement",
443 "Cannot replace list elements with empty collection");
445 final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
446 return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf('[') + 1);
449 private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
450 final DataNode newListElement,
451 final FragmentEntity existingListElementEntity) {
452 if (existingListElementEntity == null) {
453 return convertToFragmentWithAllDescendants(
454 parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
456 if (newListElement.getChildDataNodes().isEmpty()) {
457 copyAttributesFromNewListElement(existingListElementEntity, newListElement);
458 existingListElementEntity.getChildFragments().clear();
460 replaceDataNodeTree(existingListElementEntity, newListElement);
462 return existingListElementEntity;
465 private static boolean isNewDataNode(final DataNode replacementDataNode,
466 final Map<String, FragmentEntity> existingListElementsByXpath) {
467 return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
470 private static boolean isRootContainerNodeXpath(final String xpath) {
471 return 0 == xpath.lastIndexOf('/');
474 private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
475 final DataNode newListElement) {
476 final FragmentEntity replacementFragmentEntity =
477 FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
478 newListElement.getLeaves())).build();
479 existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
482 private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
483 final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
484 return childEntities.stream()
485 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
486 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
489 private static boolean isRootXpath(final String xpath) {
490 return "/".equals(xpath) || "".equals(xpath);