Handle partial failure (improvements) 75/130875/2
authorToineSiebelink <toine.siebelink@est.tech>
Tue, 13 Sep 2022 11:51:21 +0000 (12:51 +0100)
committerToineSiebelink <toine.siebelink@est.tech>
Tue, 13 Sep 2022 14:26:30 +0000 (15:26 +0100)
- catching of failures on retry of individual nodes
- extract cm handle id from xpaths (can only report xpaths in cps core)
- add test for same

Issue-ID: CPS-1232
Issue-ID: CPS-1126
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
Change-Id: Ice2032c8b15fea97ae0aaa4d1ed642b3499228fa

cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponse.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/CmHandleRegistrationResponseSpec.groovy
cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
cps-service/src/main/java/org/onap/cps/spi/exceptions/AlreadyDefinedExceptionBatch.java

index f12a1c5..58a60d2 100755 (executable)
@@ -32,6 +32,8 @@ import org.onap.cps.ncmp.rest.controller.NetworkCmProxyInventoryController;
 import org.onap.cps.ncmp.rest.model.DmiErrorMessage;
 import org.onap.cps.ncmp.rest.model.DmiErrorMessageDmiresponse;
 import org.onap.cps.ncmp.rest.model.ErrorMessage;
+import org.onap.cps.spi.exceptions.AlreadyDefinedException;
+import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch;
 import org.onap.cps.spi.exceptions.CpsException;
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
 import org.onap.cps.spi.exceptions.DataValidationException;
@@ -80,6 +82,11 @@ public class NetworkCmProxyRestExceptionHandler {
         return buildErrorResponse(HttpStatus.BAD_REQUEST, exception);
     }
 
+    @ExceptionHandler({AlreadyDefinedException.class, AlreadyDefinedExceptionBatch.class })
+    public static ResponseEntity<Object> handleAlreadyDefinedExceptions(final Exception exception) {
+        return buildErrorResponse(HttpStatus.CONFLICT, exception);
+    }
+
     @ExceptionHandler({DataNodeNotFoundException.class})
     public static ResponseEntity<Object> handleNotFoundExceptions(final CpsException exception) {
         return buildErrorResponse(HttpStatus.NOT_FOUND, exception);
index b8b7fe3..9d1077f 100644 (file)
@@ -33,6 +33,8 @@ import org.onap.cps.ncmp.rest.controller.handlers.NcmpDatastoreResourceRequestHa
 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
 import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper
 import org.onap.cps.ncmp.rest.util.DeprecationHelper
+import org.onap.cps.spi.exceptions.AlreadyDefinedException
+import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch
 import org.onap.cps.spi.exceptions.CpsException
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
 import org.onap.cps.spi.exceptions.DataValidationException
@@ -106,13 +108,15 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification {
         then: 'an HTTP response is returned with correct message and details'
             assertTestResponse(response, expectedErrorCode, expectedErrorMessage, expectedErrorDetails)
         where:
-            scenario              | exception                                                        || expectedErrorDetails | expectedErrorMessage | expectedErrorCode
-            'CPS'                 | new CpsException(sampleErrorMessage, sampleErrorDetails)         || sampleErrorDetails   | sampleErrorMessage   | INTERNAL_SERVER_ERROR
-            'NCMP-server'         | new ServerNcmpException(sampleErrorMessage, sampleErrorDetails)  || null                 | sampleErrorMessage   | INTERNAL_SERVER_ERROR
-            'NCMP-client'         | new DmiRequestException(sampleErrorMessage, sampleErrorDetails)  || null                 | sampleErrorMessage   | BAD_REQUEST
-            'DataNode Validation' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || null                 | 'DataNode not found' | NOT_FOUND
-            'other'               | new IllegalStateException(sampleErrorMessage)                    || null                 | sampleErrorMessage   | INTERNAL_SERVER_ERROR
-            'Data Node Not Found' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || 'DataNode not found' | 'DataNode not found' | NOT_FOUND
+            scenario              | exception                                                        || expectedErrorDetails     | expectedErrorMessage        | expectedErrorCode
+            'CPS'                 | new CpsException(sampleErrorMessage, sampleErrorDetails)         || sampleErrorDetails       | sampleErrorMessage          | INTERNAL_SERVER_ERROR
+            'NCMP-server'         | new ServerNcmpException(sampleErrorMessage, sampleErrorDetails)  || null                     | sampleErrorMessage          | INTERNAL_SERVER_ERROR
+            'NCMP-client'         | new DmiRequestException(sampleErrorMessage, sampleErrorDetails)  || null                     | sampleErrorMessage          | BAD_REQUEST
+            'DataNode Validation' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || null                     | 'DataNode not found'        | NOT_FOUND
+            'other'               | new IllegalStateException(sampleErrorMessage)                    || null                     | sampleErrorMessage          | INTERNAL_SERVER_ERROR
+            'Data Node Not Found' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || 'DataNode not found'     | 'DataNode not found'        | NOT_FOUND
+            'Existing entry'      | new AlreadyDefinedException('name',null)                         || 'name already exists'    | 'Already defined exception' | CONFLICT
+            'Existing entries'    | new AlreadyDefinedExceptionBatch(["x[@id='abc']"])               || 'Check logs for details' | null                        | CONFLICT
     }
 
     def 'Post request with exception returns correct HTTP Status.'() {
@@ -156,7 +160,7 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification {
         assert response.status == expectedStatus.value()
         def content = new JsonSlurper().parseText(response.contentAsString)
         assert content['status'].toString().contains(expectedStatus.toString())
-        assert content['message'].toString().contains(expectedErrorMessage)
+        assert expectedErrorMessage == null || content['message'].toString().contains(expectedErrorMessage)
         assert expectedErrorDetails == null || content['details'].toString().contains(expectedErrorDetails)
     }
 
index 0eb275c..3f440d6 100755 (executable)
@@ -367,7 +367,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
             return CmHandleRegistrationResponse.createSuccessResponses(cmHandleIds);
         } catch (final AlreadyDefinedExceptionBatch alreadyDefinedExceptionBatch) {
             return CmHandleRegistrationResponse.createFailureResponses(
-                    alreadyDefinedExceptionBatch.getAlreadyDefinedCmHandleIds(),
+                    alreadyDefinedExceptionBatch.getAlreadyDefinedXpaths(),
                     RegistrationError.CM_HANDLE_ALREADY_EXIST);
         } catch (final Exception exception) {
             return CmHandleRegistrationResponse.createFailureResponses(cmHandleIds, exception);
index 9f80218..d5b27b6 100644 (file)
 
 package org.onap.cps.ncmp.api.models;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import lombok.Builder;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 
 @Data
 @Builder
+@Slf4j
 public class CmHandleRegistrationResponse {
 
     private final String cmHandle;
@@ -37,16 +42,19 @@ public class CmHandleRegistrationResponse {
     private RegistrationError registrationError;
     private String errorText;
 
+    private static final Pattern cmHandleIdInXpathPattern = Pattern.compile("\\[@id='(.*?)']");
+
     /**
      * Creates a failure response based on exception.
      *
-     * @param cmHandle  cmHandle
+     * @param cmHandleId  cmHandleId
      * @param exception exception
      * @return CmHandleRegistrationResponse
      */
-    public static CmHandleRegistrationResponse createFailureResponse(final String cmHandle, final Exception exception) {
+    public static CmHandleRegistrationResponse createFailureResponse(final String cmHandleId,
+                                                                     final Exception exception) {
         return CmHandleRegistrationResponse.builder()
-            .cmHandle(cmHandle)
+            .cmHandle(cmHandleId)
             .status(Status.FAILURE)
             .registrationError(RegistrationError.UNKNOWN_ERROR)
             .errorText(exception.getMessage()).build();
@@ -55,13 +63,13 @@ public class CmHandleRegistrationResponse {
     /**
      * Creates a failure response based on registration error.
      *
-     * @param cmHandle          cmHandle
+     * @param cmHandleId          cmHandleId
      * @param registrationError registrationError
      * @return CmHandleRegistrationResponse
      */
-    public static CmHandleRegistrationResponse createFailureResponse(final String cmHandle,
+    public static CmHandleRegistrationResponse createFailureResponse(final String cmHandleId,
         final RegistrationError registrationError) {
-        return CmHandleRegistrationResponse.builder().cmHandle(cmHandle)
+        return CmHandleRegistrationResponse.builder().cmHandle(cmHandleId)
             .status(Status.FAILURE)
             .registrationError(registrationError)
             .errorText(registrationError.errorText)
@@ -71,15 +79,23 @@ public class CmHandleRegistrationResponse {
     /**
      * Creates a failure response based on registration error.
      *
-     * @param cmHandleIds       list of failed cmHandleIds
+     * @param failedXpaths       list of failed Xpaths
      * @param registrationError enum describing the type of registration error
      * @return CmHandleRegistrationResponse
      */
-    public static List<CmHandleRegistrationResponse> createFailureResponses(final Collection<String> cmHandleIds,
+    public static List<CmHandleRegistrationResponse> createFailureResponses(final Collection<String> failedXpaths,
             final RegistrationError registrationError) {
-        return cmHandleIds.stream()
-                .map(cmHandleId -> CmHandleRegistrationResponse.createFailureResponse(cmHandleId, registrationError))
-                .collect(Collectors.toList());
+        final List<CmHandleRegistrationResponse> cmHandleRegistrationResponses = new ArrayList<>(failedXpaths.size());
+        for (final String xpath : failedXpaths) {
+            final Matcher matcher = cmHandleIdInXpathPattern.matcher(xpath);
+            if (matcher.find()) {
+                cmHandleRegistrationResponses.add(
+                    CmHandleRegistrationResponse.createFailureResponse(matcher.group(1), registrationError));
+            } else {
+                log.warn("Unexpected xpath {}", xpath);
+            }
+        }
+        return cmHandleRegistrationResponses;
     }
 
     /**
@@ -121,4 +137,4 @@ public class CmHandleRegistrationResponse {
         public final String errorText;
 
     }
-}
\ No newline at end of file
+}
index 2fe521c..0b58d44 100644 (file)
@@ -184,7 +184,8 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
                                        new NcmpServiceCmHandle(cmHandleId: 'cmhandle2'),
                                        new NcmpServiceCmHandle(cmHandleId: 'cmhandle3')])
         and: 'cm-handle creation is successful for 1st and 3rd; failed for 2nd'
-            mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(*_) >> { throw new AlreadyDefinedExceptionBatch(['cmhandle2']) }
+            def xpath = "somePathWithId[@id='cmhandle2']"
+            mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(*_) >> { throw new AlreadyDefinedExceptionBatch([xpath]) }
         when: 'registration is updated to create cm-handles'
             def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
         then: 'a response is received for all cm-handles'
@@ -215,10 +216,10 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
                 assert it.errorText == expectedErrorText
             }
         where:
-            scenario                                        | cmHandleId             | exception                                      || expectedError           | expectedErrorText
-            'cm-handle already exist'                       | 'cmhandle'             | new AlreadyDefinedExceptionBatch([cmHandleId]) || CM_HANDLE_ALREADY_EXIST | 'cm-handle already exists'
-            'cm-handle has invalid name'                    | 'cm handle with space' | new DataValidationException("", "")            || CM_HANDLE_INVALID_ID    | 'cm-handle has an invalid character(s) in id'
-            'unknown exception while registering cm-handle' | 'cmhandle'             | new RuntimeException('Failed')                 || UNKNOWN_ERROR           | 'Failed'
+            scenario                                        | cmHandleId             | exception                                                                  || expectedError           | expectedErrorText
+            'cm-handle already exist'                       | 'cmhandle'             | new AlreadyDefinedExceptionBatch(["path[@id='${cmHandleId}']".toString()]) || CM_HANDLE_ALREADY_EXIST | 'cm-handle already exists'
+            'cm-handle has invalid name'                    | 'cm handle with space' | new DataValidationException("", "")                                        || CM_HANDLE_INVALID_ID    | 'cm-handle has an invalid character(s) in id'
+            'unknown exception while registering cm-handle' | 'cmhandle'             | new RuntimeException('Failed')                                             || UNKNOWN_ERROR           | 'Failed'
     }
 
     def 'Update CM-Handle: Update Operation Response is added to the response'() {
index 4476998..dba2934 100644 (file)
@@ -24,6 +24,8 @@ import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationErr
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
 import spock.lang.Specification
 
+import java.util.stream.Collectors
+
 class CmHandleRegistrationResponseSpec extends Specification {
 
     def 'Successful cm-handle Registration Response'() {
@@ -68,4 +70,25 @@ class CmHandleRegistrationResponseSpec extends Specification {
             'cm-handle id is invalid'  | 'cm handle' | RegistrationError.CM_HANDLE_INVALID_ID
     }
 
+    def 'Failed cm-handle Registration with multiple responses.'() {
+        when: 'cm-handle failure response is created for 2 xpaths'
+            def cmHandleRegistrationResponses =
+                CmHandleRegistrationResponse.createFailureResponses(["somePathWithId[@id='123']","somePathWithId[@id='456']"], RegistrationError.CM_HANDLE_ALREADY_EXIST)
+        then: 'the response has the correct cm handle ids'
+            assert cmHandleRegistrationResponses.size() == 2
+            assert cmHandleRegistrationResponses.stream().map(it -> it.cmHandle).collect(Collectors.toList())
+                .containsAll(['123','456'])
+    }
+
+    def 'Failed cm-handle Registration with multiple responses with an unexpected xpath.'() {
+        when: 'cm-handle failure response is created for one valid and one unexpected xpath'
+            def cmHandleRegistrationResponses =
+                CmHandleRegistrationResponse.createFailureResponses(["somePathWithId[@id='123']","valid/xpath/without-id[@key='123']"], RegistrationError.CM_HANDLE_ALREADY_EXIST)
+        then: 'the response has only one entry'
+            assert cmHandleRegistrationResponses.size() == 1
+    }
+
+
+
+
 }
index 9495b3d..93233d9 100755 (executable)
@@ -31,6 +31,7 @@ import org.onap.cps.rest.controller.DataRestController;
 import org.onap.cps.rest.controller.QueryRestController;
 import org.onap.cps.rest.model.ErrorMessage;
 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
+import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch;
 import org.onap.cps.spi.exceptions.CpsAdminException;
 import org.onap.cps.spi.exceptions.CpsException;
 import org.onap.cps.spi.exceptions.CpsPathException;
@@ -75,7 +76,7 @@ public class CpsRestExceptionHandler {
             ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST, exception);
     }
 
-    @ExceptionHandler({DataInUseException.class, AlreadyDefinedException.class})
+    @ExceptionHandler({DataInUseException.class, AlreadyDefinedException.class, AlreadyDefinedExceptionBatch.class})
     public static ResponseEntity<Object> handleDataInUseException(final Exception exception) {
         return buildErrorResponse(HttpStatus.CONFLICT, exception);
     }
index e02fb73..d62421c 100644 (file)
@@ -103,17 +103,17 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     @Override
     public void addMultipleLists(final String dataspaceName, final String anchorName, final String parentNodeXpath,
             final Collection<Collection<DataNode>> newLists) {
-        final Collection<String> failedCmHandleIds = new HashSet<>();
+        final Collection<String> failedXpaths = new HashSet<>();
         newLists.forEach(newList -> {
             try {
                 addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newList);
-            } catch (final AlreadyDefinedException e) {
-                newList.forEach(listElement -> failedCmHandleIds.add((String) listElement.getLeaves().get("id")));
+            } catch (final AlreadyDefinedExceptionBatch e) {
+                failedXpaths.addAll(e.getAlreadyDefinedXpaths());
             }
         });
 
-        if (!failedCmHandleIds.isEmpty()) {
-            throw new AlreadyDefinedExceptionBatch(failedCmHandleIds);
+        if (!failedXpaths.isEmpty()) {
+            throw new AlreadyDefinedExceptionBatch(failedXpaths);
         }
 
     }
@@ -147,7 +147,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             });
             fragmentRepository.saveAll(fragmentEntities);
         } catch (final DataIntegrityViolationException e) {
-            log.warn("Exception occurred : {} , Batch with size : {} will be retried using individual save operations",
+            log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
                     e, fragmentEntities.size());
             retrySavingEachChildIndividually(dataspaceName, anchorName, parentNodeXpath, newChildren);
         }
@@ -155,7 +155,17 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     private void retrySavingEachChildIndividually(final String dataspaceName, final String anchorName,
             final String parentNodeXpath, final Collection<DataNode> newChildren) {
-        newChildren.forEach(newChild -> addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChild));
+        final Collection<String> failedXpaths = new HashSet<>();
+        for (final DataNode newChild : newChildren) {
+            try {
+                addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChild);
+            } catch (final AlreadyDefinedException e) {
+                failedXpaths.add(newChild.getXpath());
+            }
+        }
+        if (!failedXpaths.isEmpty()) {
+            throw new AlreadyDefinedExceptionBatch(failedXpaths);
+        }
     }
 
     @Override
index ba9bd6f..5e15ca7 100755 (executable)
@@ -49,6 +49,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     CpsDataPersistenceService objectUnderTest
 
     static final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+    static final DataNodeBuilder dataNodeBuilder = new DataNodeBuilder()
 
     static final String SET_DATA = '/data/fragment.sql'
     static final int DATASPACE_1001_ID = 1001L
@@ -180,33 +181,41 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Add already existing list elements'() {
+    def 'Add multiple list with a mix of existing and new elements'() {
         given: 'two new child list elements for an existing parent'
-            def listElementXpaths1 = ['/parent-100', '/parent-100/child-001']
-            def listElementXpaths2 = ['/parent-200', '/parent-200/child-201']
-            def listElements1 = toDataNodesWithId(listElementXpaths1, 'cmhandle1')
-            def listElements2 = toDataNodesWithId(listElementXpaths2, 'cmhandle2')
+            def existingDataNode = dataNodeBuilder.withXpath('/parent-100/child-001').withLeaves(['id': '001']).build()
+            def newDataNode1 = dataNodeBuilder.withXpath('/parent-100/child-new1').withLeaves(['id': 'new1']).build()
+            def newDataNode2 = dataNodeBuilder.withXpath('/parent-200/child-new2').withLeaves(['id': 'new2']).build()
+            def dataNodeList1 = [existingDataNode, newDataNode1]
+            def dataNodeList2 = [newDataNode2]
         when: 'duplicate data node is requested to be added'
-            objectUnderTest.addMultipleLists(DATASPACE_NAME, ANCHOR_NAME3, '/', [listElements1,listElements2])
+            objectUnderTest.addMultipleLists(DATASPACE_NAME, ANCHOR_NAME3, '/', [dataNodeList1,dataNodeList2])
         then: 'already defined batch exception is thrown'
-            def e = thrown(AlreadyDefinedExceptionBatch)
-        and: 'it contains both cmhandle ids'
-            assert e.alreadyDefinedCmHandleIds.size() == 2
-            assert e.alreadyDefinedCmHandleIds.containsAll(['cmhandle1', 'cmhandle2'])
+            def thrown = thrown(AlreadyDefinedExceptionBatch)
+        and: 'it only contains the xpath(s) of the duplicated elements'
+            assert thrown.alreadyDefinedXpaths.size() == 1
+            assert thrown.alreadyDefinedXpaths.contains('/parent-100/child-001')
+        and: 'it does NOT contains the xpaths of the new element that were not combined with existing elements'
+            assert !thrown.alreadyDefinedXpaths.contains('/parent-100/child-new1')
+            assert !thrown.alreadyDefinedXpaths.contains('/parent-100/child-new1')
+        and: 'the new entity is inserted correctly'
+            def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
+            def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
+            fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, '/parent-200/child-new2').isPresent()
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Add list element error scenario: #scenario.'() {
         given: 'list element as a collection of data nodes'
-            def listElementCollection = toDataNodes(listElementXpaths)
+            def listElements = toDataNodes(listElementXpaths)
         when: 'attempt to add list elements to parent node'
-            objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElementCollection)
+            objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElements)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'following parameters were used'
             scenario                        | parentNodeXpath | listElementXpaths                   || expectedException
             'parent node does not exist'    | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
-            'data fragment already exists'  | '/parent-201'   | ["/parent-201/child-204[@key='A']"] || AlreadyDefinedException
+            'data fragment already exists'  | '/parent-201'   | ["/parent-201/child-204[@key='A']"] || AlreadyDefinedExceptionBatch
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -576,12 +585,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
     }
 
-    static Collection<DataNode> toDataNodesWithId(xpaths, id) {
-        return xpaths.collect { new DataNodeBuilder().withXpath(it).withLeaves(['id': id]).build() }
-    }
 
     static DataNode buildDataNode(xpath, leaves, childDataNodes) {
-        return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
+        return dataNodeBuilder.withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
     }
 
     static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
index a5ce6fd..0ba656a 100644 (file)
@@ -26,9 +26,9 @@ import lombok.Getter;
 public class AlreadyDefinedExceptionBatch extends RuntimeException {
 
     @Getter
-    private final Collection<String> alreadyDefinedCmHandleIds;
+    private final Collection<String> alreadyDefinedXpaths;
 
-    public AlreadyDefinedExceptionBatch(final Collection<String> alreadyDefinedCmHandleIds) {
-        this.alreadyDefinedCmHandleIds = alreadyDefinedCmHandleIds;
+    public AlreadyDefinedExceptionBatch(final Collection<String> alreadyDefinedXPaths) {
+        this.alreadyDefinedXpaths = alreadyDefinedXPaths;
     }
 }