Fix Response code returned by Apply Delta Report API 42/142742/12
authorSourabh Verma <sv001010507@techmahindra.com>
Fri, 12 Dec 2025 11:46:30 +0000 (17:16 +0530)
committerSourabh Verma <sv001010507@techmahindra.com>
Fri, 9 Jan 2026 06:54:42 +0000 (12:24 +0530)
-Changed response message when syntactically wrong json used.
-Added Response Code and Exception messages for invalid xpath,invalid action,semantically wrong json and executing same request in delta report.

Issue-ID: CPS-3098
Change-Id: Ic76004fdcf7e9f63cdf415f8da7641016ba17258
Signed-off-by: Sourabh Verma <sv001010507@techmahindra.com>
cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java
cps-service/src/main/java/org/onap/cps/utils/JsonObjectMapper.java
cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportExecutor.java
cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/deltareport/DeltaReportExecutorSpec.groovy

index 51e48e2..e3be97a 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023-2025 Deutsche Telekom AG
+ *  Copyright (C) 2023-2026 Deutsche Telekom AG
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ import org.onap.cps.api.CpsAnchorService;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsDeltaService;
 import org.onap.cps.api.DataNodeFactory;
+import org.onap.cps.api.exceptions.DataInUseException;
 import org.onap.cps.api.exceptions.DataValidationException;
 import org.onap.cps.api.model.Anchor;
 import org.onap.cps.api.model.DataNode;
@@ -48,6 +49,7 @@ import org.onap.cps.utils.JsonObjectMapper;
 import org.onap.cps.utils.deltareport.DeltaReportExecutor;
 import org.onap.cps.utils.deltareport.DeltaReportGenerator;
 import org.onap.cps.utils.deltareport.GroupedDeltaReportGenerator;
+import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.stereotype.Service;
 
 @Slf4j
@@ -115,7 +117,12 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
     public void applyChangesInDeltaReport(final String dataspaceName, final String anchorName,
                                           final String deltaReportAsJsonString) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
-        deltaReportExecutor.applyChangesInDeltaReport(dataspaceName, anchorName, deltaReportAsJsonString);
+        try {
+            deltaReportExecutor.applyChangesInDeltaReport(dataspaceName, anchorName, deltaReportAsJsonString);
+        } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
+            throw new DataInUseException("Duplicate key error",
+                dataIntegrityViolationException.getRootCause().getMessage());
+        }
     }
 
     private List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
index d52b6bd..1262c0f 100644 (file)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Nordix Foundation
- *  Modifications Copyright (C) 2025 Deutsche Telekom AG
+ *  Modifications Copyright (C) 2025-2026 Deutsche Telekom AG
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -139,8 +139,8 @@ public class JsonObjectMapper {
             return objectMapper.readValue(jsonContent, collectionType);
         } catch (final JsonProcessingException e) {
             log.error("Parsing error occurred while converting JSON content to json array.");
-            throw new DataValidationException("Parsing error occurred while converting "
-                + "JSON content to specific class type.", e.getMessage());
+            throw new DataValidationException(String.format("JSON parsing error at line: %d, column: %d",
+                e.getLocation().getLineNr(), e.getLocation().getColumnNr()), e.getOriginalMessage());
         }
     }
 }
index 86af434..94c154d 100644 (file)
@@ -20,6 +20,9 @@
 
 package org.onap.cps.utils.deltareport;
 
+import static org.onap.cps.api.model.DeltaReport.CREATE_ACTION;
+import static org.onap.cps.api.model.DeltaReport.REMOVE_ACTION;
+import static org.onap.cps.api.model.DeltaReport.REPLACE_ACTION;
 import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH;
 import static org.onap.cps.utils.ContentType.JSON;
 
@@ -30,10 +33,12 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.CpsAnchorService;
 import org.onap.cps.api.DataNodeFactory;
+import org.onap.cps.api.exceptions.DataValidationException;
 import org.onap.cps.api.model.Anchor;
 import org.onap.cps.api.model.DataNode;
 import org.onap.cps.api.model.DeltaReport;
 import org.onap.cps.cpspath.parser.CpsPathUtil;
+import org.onap.cps.cpspath.parser.PathParsingException;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.stereotype.Service;
@@ -64,12 +69,12 @@ public class DeltaReportExecutor {
         final List<DeltaReport> deltaReports =
             jsonObjectMapper.convertToJsonArray(deltaReportAsJsonString, DeltaReport.class);
         for (final DeltaReport deltaReport: deltaReports) {
-            final String action = deltaReport.getAction();
-            final String xpath = deltaReport.getXpath();
-            if (action.equals(DeltaReport.REPLACE_ACTION)) {
+            final String xpath = validateXpath(deltaReport.getXpath());
+            final String action = validateAction(deltaReport.getAction(), xpath);
+            if (REPLACE_ACTION.equals(action)) {
                 final String dataForUpdate = jsonObjectMapper.asJsonString(deltaReport.getTargetData());
                 updateDataNodes(dataspaceName, anchorName, xpath, dataForUpdate);
-            } else if (action.equals(DeltaReport.REMOVE_ACTION)) {
+            } else if (REMOVE_ACTION.equals(action)) {
                 final String dataForDelete = jsonObjectMapper.asJsonString(deltaReport.getSourceData());
                 deleteDataNodesUsingDelta(dataspaceName, anchorName, xpath, dataForDelete);
             } else {
@@ -114,4 +119,24 @@ public class DeltaReportExecutor {
         final Anchor anchor = cpsAnchorService.getAnchor(datasapceName, anchorName);
         return dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor, xpath, nodeData, JSON);
     }
+
+    private String validateXpath(final String xpath) {
+        try {
+            return CpsPathUtil.getNormalizedXpath(xpath);
+        } catch (final PathParsingException pathParsingException) {
+            throw new DataValidationException(String.format("Error while parsing xpath expression '%s'.", xpath),
+                pathParsingException.getMessage());
+        }
+    }
+
+    private String validateAction(final String action, final String xpath) {
+        if (CREATE_ACTION.equals(action) || REMOVE_ACTION.equals(action) || REPLACE_ACTION.equals(action)) {
+            return action;
+        }
+        throw new DataValidationException("Invalid 'action' in delta report.",
+            String.format(
+                "Unsupported action '%s' at xpath: %s. Valid actions are: '%s', '%s' or '%s'.",
+                action, xpath, CREATE_ACTION, REMOVE_ACTION, REPLACE_ACTION
+            ));
+    }
 }
index 076c4de..8c24d7a 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023-2025 Deutsche Telekom AG
+ *  Copyright (C) 2023-2026 Deutsche Telekom AG
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
+import org.onap.cps.api.exceptions.DataInUseException
 import org.onap.cps.api.exceptions.DataValidationException
 import org.onap.cps.api.model.Anchor
 import org.onap.cps.api.model.DataNode
@@ -46,6 +47,7 @@ import org.onap.cps.yang.YangTextSchemaSourceSet
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import org.slf4j.LoggerFactory
 import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.dao.DataIntegrityViolationException
 import spock.lang.Shared
 import spock.lang.Specification
 
@@ -357,6 +359,23 @@ class CpsDeltaServiceImplSpec extends Specification {
             1 * mockDeltaReportExecutor.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
     }
 
+    def 'Apply changes from the same delta report to an anchor more than once'() {
+        given: 'delta report as JSON string and a DataIntegrityViolationException with a root cause'
+            def deltaReportJson = '[{"action":"create","xpath":"/bookstore"}]'
+            def rootCause = new RuntimeException('duplicate key')
+            def dataIntegrityViolationException = new DataIntegrityViolationException('error', rootCause)
+        and: 'mock to throw DataIntegrityViolationException on second call'
+            1 * mockDeltaReportExecutor.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+            1 * mockDeltaReportExecutor.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson) >> { throw dataIntegrityViolationException }
+        when: 'first attempt to apply the delta report to the anchor'
+            objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+        and: 'second attempt to apply the same delta report to the anchor'
+            objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+        then: 'expected exception is thrown with message'
+            def thrownException = thrown(DataInUseException)
+            thrownException.message == 'Duplicate key error'
+    }
+
     def setupSchemaSetMocks(yangResources) {
         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
index e10a1c8..1a750ef 100644 (file)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Nordix Foundation
- *  Modifications Copyright (C) 2025 Deutsche Telekom AG
+ *  Modifications Copyright (C) 2025-2026 Deutsche Telekom AG
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -154,6 +154,6 @@ class JsonObjectMapperSpec extends Specification {
             jsonObjectMapper.convertToJsonArray(jsonContent, Map)
         then: 'a DataValidationException is thrown'
             def thrown = thrown(DataValidationException)
-            thrown.message.contains('Parsing error occurred while converting JSON content to specific class type.')
+        thrown.message.contains('JSON parsing error at line: 1, column: 19')
     }
 }
index ca7bd68..eabccbf 100644 (file)
@@ -186,6 +186,28 @@ class DeltaReportExecutorSpec extends Specification {
         and: 'a DataValidationException is thrown'
             thrown(DataValidationException)
     }
+    def 'Apply delta report with an invalid xpath'() {
+        given: 'delta report as JSON string with an invalid xpath for #action action'
+            def deltaReportJson = '[{"action":"create","xpath":"/invalid[","targetData":{"data":[{"key":"value"}]}}]'
+        when: 'attempt to apply delta'
+            objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+        then: 'expected exception is thrown'
+            def thrownException = thrown(DataValidationException)
+            assert thrownException.message == 'Error while parsing xpath expression \'/invalid[\'.'
+            assert thrownException.details == 'failed to parse at line 1 due to no viable alternative at input \'[\''
+    }
+
+    def 'Apply delta report with an unsupported action'() {
+        given: 'delta report as JSON string'
+            def deltaReportJson = '[{"action":"invalidAction","xpath":"/bookstore","targetData":{"categories":[{"code":"1"}]}}]'
+        when: 'attempt to apply delta report'
+            objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+        then: 'expected exception is thrown with correct details'
+            def thrownException = thrown(DataValidationException)
+            assert thrownException.message == 'Invalid \'action\' in delta report.'
+            assert thrownException.details.contains('invalidAction')
+            assert thrownException.details.contains('/bookstore')
+    }
 
     def setupSchemaSetMocks(yangResources) {
         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)