2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2022 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2020-2021 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.spi.CpsDataPersistenceService;
45 import org.onap.cps.spi.FetchDescendantsOption;
46 import org.onap.cps.spi.entities.AnchorEntity;
47 import org.onap.cps.spi.entities.DataspaceEntity;
48 import org.onap.cps.spi.entities.FragmentEntity;
49 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
50 import org.onap.cps.spi.exceptions.ConcurrencyException;
51 import org.onap.cps.spi.exceptions.CpsAdminException;
52 import org.onap.cps.spi.exceptions.CpsPathException;
53 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
54 import org.onap.cps.spi.model.DataNode;
55 import org.onap.cps.spi.model.DataNodeBuilder;
56 import org.onap.cps.spi.repository.AnchorRepository;
57 import org.onap.cps.spi.repository.DataspaceRepository;
58 import org.onap.cps.spi.repository.FragmentRepository;
59 import org.onap.cps.utils.JsonObjectMapper;
60 import org.springframework.dao.DataIntegrityViolationException;
61 import org.springframework.stereotype.Service;
65 @RequiredArgsConstructor
66 public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService {
68 private final DataspaceRepository dataspaceRepository;
70 private final AnchorRepository anchorRepository;
72 private final FragmentRepository fragmentRepository;
74 private final JsonObjectMapper jsonObjectMapper;
77 private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
78 private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE =
79 Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$");
82 public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentXpath,
83 final DataNode dataNode) {
84 final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentXpath);
85 final FragmentEntity fragmentEntity =
86 toFragmentEntity(parentFragment.getDataspace(), parentFragment.getAnchor(), dataNode);
87 parentFragment.getChildFragments().add(fragmentEntity);
89 fragmentRepository.save(parentFragment);
90 } catch (final DataIntegrityViolationException exception) {
91 throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
96 public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
97 final Collection<DataNode> dataNodes) {
98 final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
99 final List<FragmentEntity> newFragmentEntities =
100 dataNodes.stream().map(
101 dataNode -> toFragmentEntity(parentFragment.getDataspace(), parentFragment.getAnchor(), dataNode)
102 ).collect(Collectors.toUnmodifiableList());
103 parentFragment.getChildFragments().addAll(newFragmentEntities);
105 fragmentRepository.save(parentFragment);
107 dataNode -> getChildFragments(dataspaceName, anchorName, dataNode)
109 } catch (final DataIntegrityViolationException exception) {
110 final List<String> conflictXpaths = dataNodes.stream()
111 .map(DataNode::getXpath)
112 .collect(Collectors.toList());
113 throw AlreadyDefinedException.forDataNodes(conflictXpaths, anchorName, exception);
118 public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
119 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
120 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
121 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
124 fragmentRepository.save(fragmentEntity);
125 } catch (final DataIntegrityViolationException exception) {
126 throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
131 * Convert DataNode object into Fragment and places the result in the fragments placeholder. Performs same action
132 * for all DataNode children recursively.
134 * @param dataspaceEntity dataspace
135 * @param anchorEntity anchorEntity
136 * @param dataNodeToBeConverted dataNode
137 * @return a Fragment built from current DataNode
139 private FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
140 final AnchorEntity anchorEntity, final DataNode dataNodeToBeConverted) {
141 final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
142 final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
143 for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
144 final FragmentEntity childFragment =
145 convertToFragmentWithAllDescendants(parentFragment.getDataspace(), parentFragment.getAnchor(),
147 childFragmentsImmutableSetBuilder.add(childFragment);
149 parentFragment.setChildFragments(childFragmentsImmutableSetBuilder.build());
150 return parentFragment;
153 private void getChildFragments(final String dataspaceName, final String anchorName, final DataNode dataNode) {
154 for (final DataNode childDataNode: dataNode.getChildDataNodes()) {
155 final FragmentEntity getChildsParentFragmentByXPath =
156 getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
157 final FragmentEntity childFragmentEntity = toFragmentEntity(getChildsParentFragmentByXPath.getDataspace(),
158 getChildsParentFragmentByXPath.getAnchor(), childDataNode);
159 getChildsParentFragmentByXPath.getChildFragments().add(childFragmentEntity);
160 fragmentRepository.save(getChildsParentFragmentByXPath);
164 private FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity,
165 final AnchorEntity anchorEntity, final DataNode dataNode) {
166 return FragmentEntity.builder()
167 .dataspace(dataspaceEntity)
168 .anchor(anchorEntity)
169 .xpath(dataNode.getXpath())
170 .attributes(jsonObjectMapper.asJsonString(dataNode.getLeaves()))
175 public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
176 final FetchDescendantsOption fetchDescendantsOption) {
177 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
178 return toDataNode(fragmentEntity, fetchDescendantsOption);
181 private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName,
182 final String xpath) {
183 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
184 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
185 if (isRootXpath(xpath)) {
186 return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity);
188 return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity,
194 public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
195 final FetchDescendantsOption fetchDescendantsOption) {
196 final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
197 final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
198 final CpsPathQuery cpsPathQuery;
200 cpsPathQuery = CpsPathQuery.createFrom(cpsPath);
201 } catch (final IllegalStateException e) {
202 throw new CpsPathException(e.getMessage());
204 List<FragmentEntity> fragmentEntities =
205 fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
206 if (cpsPathQuery.hasAncestorAxis()) {
207 final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
208 fragmentEntities = ancestorXpaths.isEmpty()
209 ? Collections.emptyList() : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
211 return fragmentEntities.stream()
212 .map(fragmentEntity -> toDataNode(fragmentEntity, fetchDescendantsOption))
213 .collect(Collectors.toUnmodifiableList());
216 private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
217 final CpsPathQuery cpsPathQuery) {
218 final Set<String> ancestorXpath = new HashSet<>();
219 final Pattern pattern =
220 Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
221 + REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*");
222 for (final FragmentEntity fragmentEntity : fragmentEntities) {
223 final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
224 if (matcher.matches()) {
225 ancestorXpath.add(matcher.group(1));
228 return ancestorXpath;
231 private DataNode toDataNode(final FragmentEntity fragmentEntity,
232 final FetchDescendantsOption fetchDescendantsOption) {
233 final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
234 Map<String, Object> leaves = new HashMap<>();
235 if (fragmentEntity.getAttributes() != null) {
236 leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
238 return new DataNodeBuilder()
239 .withXpath(fragmentEntity.getXpath())
241 .withChildDataNodes(childDataNodes).build();
244 private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
245 final FetchDescendantsOption fetchDescendantsOption) {
246 if (fetchDescendantsOption == INCLUDE_ALL_DESCENDANTS) {
247 return fragmentEntity.getChildFragments().stream()
248 .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption))
249 .collect(Collectors.toUnmodifiableList());
251 return Collections.emptyList();
255 public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
256 final Map<String, Object> leaves) {
257 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
258 fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
259 fragmentRepository.save(fragmentEntity);
263 public void replaceDataNodeTree(final String dataspaceName, final String anchorName, final DataNode dataNode) {
264 final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
265 replaceDataNodeTree(fragmentEntity, dataNode);
267 fragmentRepository.save(fragmentEntity);
268 } catch (final StaleStateException staleStateException) {
269 throw new ConcurrencyException("Concurrent Transactions",
270 String.format("dataspace :'%s', Anchor : '%s' and xpath: '%s' is updated by another transaction.",
271 dataspaceName, anchorName, dataNode.getXpath()),
272 staleStateException);
276 private void replaceDataNodeTree(final FragmentEntity existingFragmentEntity,
277 final DataNode newDataNode) {
279 existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
281 final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments()
282 .stream().collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
284 final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
286 for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
287 final FragmentEntity childFragment;
288 if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
289 childFragment = convertToFragmentWithAllDescendants(
290 existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
292 childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
293 replaceDataNodeTree(childFragment, newDataNodeChild);
295 updatedChildFragments.add(childFragment);
297 existingFragmentEntity.getChildFragments().clear();
298 existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
303 public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
304 final Collection<DataNode> newListElements) {
305 final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
306 final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
307 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
308 extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
309 deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
310 final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
311 for (final DataNode newListElement : newListElements) {
312 final FragmentEntity existingListElementEntity =
313 existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
314 final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
315 existingListElementEntity);
317 updatedChildFragmentEntities.add(entityToBeAdded);
319 parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
320 fragmentRepository.save(parentEntity);
326 public void deleteListDataNode(final String dataspaceName, final String anchorName,
327 final String targetXpath) {
328 deleteDataNode(dataspaceName, anchorName, targetXpath, true);
333 public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
334 deleteDataNode(dataspaceName, anchorName, targetXpath, false);
337 private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
338 final boolean onlySupportListNodeDeletion) {
339 final String parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
340 final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
341 final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/'));
342 final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE.matcher(lastXpathElement).find();
345 targetExist = deleteDataNode(parentFragmentEntity, targetXpath);
347 targetExist = deleteAllListElements(parentFragmentEntity, targetXpath);
348 final boolean tryToDeleteDataNode = !targetExist && !onlySupportListNodeDeletion;
349 if (tryToDeleteDataNode) {
350 targetExist = deleteDataNode(parentFragmentEntity, targetXpath);
354 final String additionalInformation = onlySupportListNodeDeletion
355 ? "The target is probably not a List." : "";
356 throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
357 parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
361 private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
362 if (parentFragmentEntity.getChildFragments()
363 .removeIf(fragment -> fragment.getXpath().equals(targetXpath))) {
364 fragmentRepository.save(parentFragmentEntity);
371 private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
372 final String deleteTargetXpathPrefix = listXpath + "[";
373 if (parentFragmentEntity.getChildFragments()
374 .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
375 fragmentRepository.save(parentFragmentEntity);
381 private static void deleteListElements(
382 final Collection<FragmentEntity> fragmentEntities,
383 final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
384 fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
387 private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
388 if (newListElements.isEmpty()) {
389 throw new CpsAdminException("Invalid list replacement",
390 "Cannot replace list elements with empty collection");
392 final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
393 return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf("[") + 1);
396 private FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
397 final DataNode newListElement,
398 final FragmentEntity existingListElementEntity) {
399 if (existingListElementEntity == null) {
400 return convertToFragmentWithAllDescendants(
401 parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
403 if (newListElement.getChildDataNodes().isEmpty()) {
404 copyAttributesFromNewListElement(existingListElementEntity, newListElement);
405 existingListElementEntity.getChildFragments().clear();
407 replaceDataNodeTree(existingListElementEntity, newListElement);
409 return existingListElementEntity;
412 private static boolean isNewDataNode(final DataNode replacementDataNode,
413 final Map<String, FragmentEntity> existingListElementsByXpath) {
414 return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
417 private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
418 final DataNode newListElement) {
419 final FragmentEntity replacementFragmentEntity =
420 FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
421 newListElement.getLeaves())).build();
422 existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
425 private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
426 final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
427 return childEntities.stream()
428 .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
429 .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
432 private static boolean isRootXpath(final String xpath) {
433 return "/".equals(xpath) || "".equals(xpath);