Merge "Normalize JSON attributes during update"
[cps.git] / cps-ri / src / main / java / org / onap / cps / spi / impl / CpsDataPersistenceServiceImpl.java
index 19302d6..19547bb 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2023 Nordix Foundation
+ *  Copyright (C) 2021-2024 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
@@ -28,6 +28,7 @@ import static org.onap.cps.spi.PaginationOption.NO_PAGINATION;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSet.Builder;
 import io.micrometer.core.annotation.Timed;
+import jakarta.transaction.Transactional;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -37,10 +38,10 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import javax.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.hibernate.StaleStateException;
@@ -120,7 +121,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
         try {
             fragmentRepository.save(newChildAsFragmentEntity);
-        } catch (final DataIntegrityViolationException e) {
+        } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
             throw AlreadyDefinedException.forDataNodes(Collections.singletonList(newChild.getXpath()),
                     anchorEntity.getName());
         }
@@ -138,9 +139,9 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
                 fragmentEntities.add(newChildAsFragmentEntity);
             }
             fragmentRepository.saveAll(fragmentEntities);
-        } catch (final DataIntegrityViolationException e) {
+        } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
             log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
-                    e, fragmentEntities.size());
+                    dataIntegrityViolationException, fragmentEntities.size());
             retrySavingEachChildIndividually(anchorEntity, parentNodeXpath, newChildren);
         }
     }
@@ -151,7 +152,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         for (final DataNode newChild : newChildren) {
             try {
                 addNewChildDataNode(anchorEntity, parentNodeXpath, newChild);
-            } catch (final AlreadyDefinedException e) {
+            } catch (final AlreadyDefinedException alreadyDefinedException) {
                 failedXpaths.add(newChild.getXpath());
             }
         }
@@ -184,7 +185,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             try {
                 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
                 fragmentRepository.save(fragmentEntity);
-            } catch (final DataIntegrityViolationException e) {
+            } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
                 failedXpaths.add(dataNode.getXpath());
             }
         }
@@ -251,22 +252,28 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     private Collection<FragmentEntity> getFragmentEntities(final AnchorEntity anchorEntity,
                                                            final Collection<String> xpaths) {
-        final Collection<String> nonRootXpaths = new HashSet<>(xpaths);
-        final boolean haveRootXpath = nonRootXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
+        final Collection<String> normalizedXpaths = getNormalizedXpaths(xpaths);
 
-        final Collection<String> normalizedXpaths = new HashSet<>(nonRootXpaths.size());
-        for (final String xpath : nonRootXpaths) {
-            try {
-                normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath));
-            } catch (final PathParsingException e) {
-                log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage());
+        final boolean haveRootXpath = normalizedXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
+
+        final List<FragmentEntity> fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity,
+                normalizedXpaths);
+
+        for (final FragmentEntity fragmentEntity : fragmentEntities) {
+            normalizedXpaths.remove(fragmentEntity.getXpath());
+        }
+
+        for (final String xpath : normalizedXpaths) {
+            if (!CpsPathUtil.isPathToListElement(xpath)) {
+                fragmentEntities.addAll(fragmentRepository.findListByAnchorAndXpath(anchorEntity, xpath));
             }
         }
+
         if (haveRootXpath) {
-            normalizedXpaths.addAll(fragmentRepository.findAllXpathByAnchorAndParentIdIsNull(anchorEntity));
+            fragmentEntities.addAll(fragmentRepository.findRootsByAnchorId(anchorEntity.getId()));
         }
 
-        return fragmentRepository.findByAnchorAndXpathIn(anchorEntity, normalizedXpaths);
+        return fragmentEntities;
     }
 
     private FragmentEntity getFragmentEntity(final AnchorEntity anchorEntity, final String xpath) {
@@ -292,8 +299,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         final CpsPathQuery cpsPathQuery;
         try {
             cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
-        } catch (final PathParsingException e) {
-            throw new CpsPathException(e.getMessage());
+        } catch (final PathParsingException pathParsingException) {
+            throw new CpsPathException(pathParsingException.getMessage());
         }
 
         Collection<FragmentEntity> fragmentEntities;
@@ -323,7 +330,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
         final List<Long> anchorIds;
         if (paginationOption == NO_PAGINATION) {
-            anchorIds = Collections.EMPTY_LIST;
+            anchorIds = Collections.emptyList();
         } else {
             anchorIds = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
             if (anchorIds.isEmpty()) {
@@ -368,11 +375,23 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         }
         try {
             return CpsPathUtil.getNormalizedXpath(xpathSource);
-        } catch (final PathParsingException e) {
-            throw new CpsPathException(e.getMessage());
+        } catch (final PathParsingException pathParsingException) {
+            throw new CpsPathException(pathParsingException.getMessage());
         }
     }
 
+    private static Collection<String> getNormalizedXpaths(final Collection<String> xpaths) {
+        final Collection<String> normalizedXpaths = new HashSet<>(xpaths.size());
+        for (final String xpath : xpaths) {
+            try {
+                normalizedXpaths.add(getNormalizedXpath(xpath));
+            } catch (final CpsPathException cpsPathException) {
+                log.warn("Error parsing xpath \"{}\": {}", xpath, cpsPathException.getMessage());
+            }
+        }
+        return normalizedXpaths;
+    }
+
     @Override
     public String startSession() {
         return sessionManager.startSession();
@@ -494,7 +513,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         for (final FragmentEntity dataNodeFragment : fragmentEntities) {
             try {
                 fragmentRepository.save(dataNodeFragment);
-            } catch (final StaleStateException e) {
+            } catch (final StaleStateException staleStateException) {
                 failedXpaths.add(dataNodeFragment.getXpath());
             }
         }
@@ -508,7 +527,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     private void updateFragmentEntityAndDescendantsWithDataNode(final FragmentEntity existingFragmentEntity,
                                                                 final DataNode newDataNode) {
-        existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
+        copyAttributesFromNewDataNode(existingFragmentEntity, newDataNode);
 
         final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments().stream()
                 .collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
@@ -586,15 +605,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
 
-        final Collection<String> deleteChecklist = new HashSet<>(xpathsToDelete.size());
-        for (final String xpath : xpathsToDelete) {
-            try {
-                deleteChecklist.add(CpsPathUtil.getNormalizedXpath(xpath));
-            } catch (final PathParsingException e) {
-                log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage());
-            }
-        }
-
+        final Collection<String> deleteChecklist = getNormalizedXpaths(xpathsToDelete);
         final Collection<String> xpathsToExistingContainers =
             fragmentRepository.findAllXpathByAnchorAndXpathIn(anchorEntity, deleteChecklist);
         if (onlySupportListDeletion) {
@@ -658,7 +669,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             return convertToFragmentWithAllDescendants(parentEntity.getAnchor(), newListElement);
         }
         if (newListElement.getChildDataNodes().isEmpty()) {
-            copyAttributesFromNewListElement(existingListElementEntity, newListElement);
+            copyAttributesFromNewDataNode(existingListElementEntity, newListElement);
             existingListElementEntity.getChildFragments().clear();
         } else {
             updateFragmentEntityAndDescendantsWithDataNode(existingListElementEntity, newListElement);
@@ -671,12 +682,28 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
     }
 
-    private void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
-                                                  final DataNode newListElement) {
-        final FragmentEntity replacementFragmentEntity =
-                FragmentEntity.builder().attributes(jsonObjectMapper.asJsonString(
-                        newListElement.getLeaves())).build();
-        existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
+    private void copyAttributesFromNewDataNode(final FragmentEntity existingFragmentEntity,
+                                               final DataNode newDataNode) {
+        final String oldOrderedLeavesAsJson = getOrderedLeavesAsJson(existingFragmentEntity.getAttributes());
+        final String newOrderedLeavesAsJson = getOrderedLeavesAsJson(newDataNode.getLeaves());
+        if (!oldOrderedLeavesAsJson.equals(newOrderedLeavesAsJson)) {
+            existingFragmentEntity.setAttributes(jsonObjectMapper.asJsonString(newDataNode.getLeaves()));
+        }
+    }
+
+    private String getOrderedLeavesAsJson(final Map<String, Serializable> currentLeaves) {
+        final Map<String, Serializable> sortedLeaves = new TreeMap<>(String::compareTo);
+        sortedLeaves.putAll(currentLeaves);
+        return jsonObjectMapper.asJsonString(sortedLeaves);
+    }
+
+    private String getOrderedLeavesAsJson(final String currentLeavesAsString) {
+        if (currentLeavesAsString == null) {
+            return "{}";
+        }
+        final Map<String, Serializable> sortedLeaves = jsonObjectMapper.convertJsonString(currentLeavesAsString,
+                TreeMap.class);
+        return jsonObjectMapper.asJsonString(sortedLeaves);
     }
 
     private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(