Fix bugs for Policy Executor requests: 95/142895/2
authorToineSiebelink <toine.siebelink@est.tech>
Thu, 8 Jan 2026 13:22:35 +0000 (13:22 +0000)
committerToineSiebelink <toine.siebelink@est.tech>
Fri, 9 Jan 2026 15:30:15 +0000 (15:30 +0000)
- applied naming convention, main ones:
  requestParameters.fdn() For fdn from URI and or extendedFdn in case of patch -operation
  (extended)path for anything that might have attributes
  only use terms "resourceIdentifier" and "targetIdentifier" JUST before calling Policy Execution method
- Ensure path for each patch operation is appended to target FDN
- DeleteOperationDetails is not needed, using blank default instead
- So only one type of OperationDetails required, so removed interface and renamed the only impl.
- Refactored code to correctly handle possible child paths in patchItems
- hardcoded attribute conversion to object for standard /attributes without #
- objectName in body for patch is optional, should be same as in URI, id should be same too. So we CAN ignore (no validation!)
- added convenience method for getting parent or targetFdn (when /attributes is used)
- added convenience method for removing trailing # (when #/attributes is used)
- renamed ParameterMapper to Helper as it is more generic now. Also made is static for easier access

TODO
- Check URL send to DMI (test!) when using (#)/attributes

Decisions Csaba K (to record in Wiki)
- (#)/attributes is NOT allowed in get or create or delete
- (#)/attributes is NOT compulsory for Patch.replace and Patch.uppate
- (#)/attributes should NOT be in URI to PolicyExecutor (targetIdentifier)

Issue-ID: CPS-2826
Change-Id: Icd1dbcd6033b79019d35d6cb68c1d6caee64fb01
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
18 files changed:
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnSControllerSpec.groovy
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/CreateOperationDetails.java [deleted file]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java [deleted file]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiRestClient.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterHelper.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java [deleted file]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestParameters.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/utils/DmiDataOperationsHelperSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterHelperSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterMapperSpec.groovy [deleted file]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/RequestParametersSpec.groovy [deleted file]
docker-compose/docker-compose.yml

index 128ad92..881dfff 100644 (file)
@@ -21,8 +21,9 @@
 package org.onap.cps.ncmp.rest.controller;
 
 import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE;
+import static org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetailsFactory.DELETE_OPERATION_DETAILS;
 import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA;
-import static org.onap.cps.ncmp.impl.provmns.ParameterMapper.NO_OP;
+import static org.onap.cps.ncmp.impl.provmns.ParameterHelper.NO_OP;
 import static org.springframework.http.HttpStatus.BAD_REQUEST;
 import static org.springframework.http.HttpStatus.CONFLICT;
 import static org.springframework.http.HttpStatus.GATEWAY_TIMEOUT;
@@ -43,15 +44,13 @@ import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException;
 import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
 import org.onap.cps.ncmp.api.inventory.models.CmHandleState;
 import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException;
-import org.onap.cps.ncmp.impl.data.policyexecutor.CreateOperationDetails;
-import org.onap.cps.ncmp.impl.data.policyexecutor.DeleteOperationDetails;
 import org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetails;
 import org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetailsFactory;
 import org.onap.cps.ncmp.impl.data.policyexecutor.PolicyExecutor;
 import org.onap.cps.ncmp.impl.dmi.DmiRestClient;
 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence;
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
-import org.onap.cps.ncmp.impl.provmns.ParameterMapper;
+import org.onap.cps.ncmp.impl.provmns.ParameterHelper;
 import org.onap.cps.ncmp.impl.provmns.ParametersBuilder;
 import org.onap.cps.ncmp.impl.provmns.RequestParameters;
 import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter;
@@ -82,7 +81,6 @@ public class ProvMnSController implements ProvMnS {
     private final DmiRestClient dmiRestClient;
     private final InventoryPersistence inventoryPersistence;
     private final ParametersBuilder parametersBuilder;
-    private final ParameterMapper parameterMapper;
     private final PolicyExecutor policyExecutor;
     private final JsonObjectMapper jsonObjectMapper;
     private final OperationDetailsFactory operationDetailsFactory;
@@ -97,12 +95,11 @@ public class ProvMnSController implements ProvMnS {
                                          final List<String> attributes,
                                          final List<String> fields,
                                          final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector) {
-        final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
+        final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest);
         try {
             final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters);
-            final String targetFdn = requestParameters.toTargetFdn();
             final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForRead(
-                yangModelCmHandle, targetFdn, scope, filter, attributes, fields, dataNodeSelector);
+                yangModelCmHandle, requestParameters.fdn(), scope, filter, attributes, fields, dataNodeSelector);
             return dmiRestClient.synchronousGetOperation(DATA, urlTemplateParameters);
         } catch (final Exception exception) {
             throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
@@ -117,13 +114,12 @@ public class ProvMnSController implements ProvMnS {
                 + maxNumberOfPatchOperations;
             throw new ProvMnSException(httpServletRequest.getMethod(), PAYLOAD_TOO_LARGE, title, NO_OP);
         }
-        final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
+        final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest);
         try {
             final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters);
-            checkPermissionForEachPatchItem(requestParameters, patchItems, yangModelCmHandle);
-            final String targetFdn = requestParameters.toTargetFdn();
+            checkPermissionForEachPatchItem(requestParameters.fdn(), patchItems, yangModelCmHandle);
             final UrlTemplateParameters urlTemplateParameters =
-                parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
+                parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn());
             return dmiRestClient.synchronousPatchOperation(DATA, patchItems, urlTemplateParameters,
                 httpServletRequest.getContentType());
         } catch (final Exception exception) {
@@ -133,15 +129,14 @@ public class ProvMnSController implements ProvMnS {
 
     @Override
     public ResponseEntity<Object> putMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
-        final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
+        final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest);
         try {
             final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters);
-            final CreateOperationDetails createOperationDetails =
-                operationDetailsFactory.buildCreateOperationDetails(CREATE, requestParameters, resource);
-            checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), createOperationDetails);
-            final String targetFdn = requestParameters.toTargetFdn();
+            final OperationDetails operationDetails =
+                operationDetailsFactory.buildOperationDetails(CREATE, requestParameters, resource);
+            checkPermission(yangModelCmHandle, requestParameters.fdn(), operationDetails);
             final UrlTemplateParameters urlTemplateParameters =
-                parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
+                parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn());
             return dmiRestClient.synchronousPutOperation(DATA, resource, urlTemplateParameters);
         } catch (final Exception exception) {
             throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
@@ -150,15 +145,12 @@ public class ProvMnSController implements ProvMnS {
 
     @Override
     public ResponseEntity<Object> deleteMoi(final HttpServletRequest httpServletRequest) {
-        final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
+        final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest);
         try {
             final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters);
-            final DeleteOperationDetails deleteOperationDetails =
-                operationDetailsFactory.buildDeleteOperationDetails(requestParameters.toTargetFdn());
-            checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), deleteOperationDetails);
-            final String targetFdn = requestParameters.toTargetFdn();
+            checkPermission(yangModelCmHandle, requestParameters.fdn(), DELETE_OPERATION_DETAILS);
             final UrlTemplateParameters urlTemplateParameters =
-                parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
+                parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn());
             return dmiRestClient.synchronousDeleteOperation(DATA, urlTemplateParameters);
         } catch (final Exception exception) {
             throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
@@ -167,46 +159,49 @@ public class ProvMnSController implements ProvMnS {
 
     private YangModelCmHandle getAndValidateYangModelCmHandle(final RequestParameters requestParameters)
                                                               throws ProvMnSException {
-        final String alternateId = requestParameters.toTargetFdn();
+        final String fdn = requestParameters.fdn();
         try {
-            final String cmHandleId = alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(alternateId, "/");
+            final String cmHandleId = alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(fdn, "/");
             final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId);
             if (!StringUtils.hasText(yangModelCmHandle.getDataProducerIdentifier())) {
-                throw new ProvMnSException(requestParameters.getHttpMethodName(), UNPROCESSABLE_ENTITY,
+                throw new ProvMnSException(requestParameters.httpMethodName(), UNPROCESSABLE_ENTITY,
                                            PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE, NO_OP);
             }
             if (yangModelCmHandle.getCompositeState().getCmHandleState() != CmHandleState.READY) {
                 final String title = yangModelCmHandle.getId() + " is not in READY state. Current state: "
                     + yangModelCmHandle.getCompositeState().getCmHandleState().name();
-                throw new ProvMnSException(requestParameters.getHttpMethodName(), NOT_ACCEPTABLE, title, NO_OP);
+                throw new ProvMnSException(requestParameters.httpMethodName(), NOT_ACCEPTABLE, title, NO_OP);
             }
             return yangModelCmHandle;
         } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
-            final String title = alternateId + " not found";
-            throw new ProvMnSException(requestParameters.getHttpMethodName(), NOT_FOUND, title, NO_OP);
+            throw new ProvMnSException(requestParameters.httpMethodName(), NOT_FOUND, fdn + " not found", NO_OP);
         }
     }
 
     private void checkPermission(final YangModelCmHandle yangModelCmHandle,
-                                 final String alternateId,
+                                 final String resourceIdentifier,
                                  final OperationDetails operationDetails) {
         final OperationType operationType = OperationType.fromOperationName(operationDetails.operation());
         final String operationDetailsAsJson = jsonObjectMapper.asJsonString(operationDetails);
-        policyExecutor.checkPermission(yangModelCmHandle, operationType, NO_AUTHORIZATION, alternateId,
+        policyExecutor.checkPermission(yangModelCmHandle, operationType, NO_AUTHORIZATION, resourceIdentifier,
             operationDetailsAsJson);
     }
 
-    private void checkPermissionForEachPatchItem(final RequestParameters requestParameters,
+    private void checkPermissionForEachPatchItem(final String baseFdn,
                                                  final List<PatchItem> patchItems,
                                                  final YangModelCmHandle yangModelCmHandle) {
+        int patchItemCounter = 0;
         for (final PatchItem patchItem : patchItems) {
+            final String extendedPath = baseFdn + patchItem.getPath();
+            final RequestParameters requestParameters = ParameterHelper.createRequestParametersForPatch(extendedPath);
             final OperationDetails operationDetails =
                 operationDetailsFactory.buildOperationDetails(requestParameters, patchItem);
             try {
-                checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), operationDetails);
+                checkPermission(yangModelCmHandle, requestParameters.fdn(), operationDetails);
+                patchItemCounter++;
             } catch (final Exception exception) {
                 final String httpMethodName = "PATCH";
-                throw toProvMnSException(httpMethodName, exception, patchItem.getOp().getValue());
+                throw toProvMnSException(httpMethodName, exception, "/" + patchItemCounter);
             }
         }
     }
index 0f85917..e6e9bcd 100644 (file)
@@ -19,7 +19,7 @@
 
 package org.onap.cps.ncmp.rest.controller;
 
-import static org.onap.cps.ncmp.impl.provmns.ParameterMapper.NO_OP;
+import static org.onap.cps.ncmp.impl.provmns.ParameterHelper.NO_OP;
 
 import java.util.Map;
 import java.util.Objects;
index 3c30932..f2edc9c 100644 (file)
@@ -22,6 +22,8 @@ package org.onap.cps.ncmp.rest.controller
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import io.netty.handler.timeout.TimeoutException
+import org.onap.cps.api.exceptions.DataValidationException
+import org.onap.cps.ncmp.api.data.models.OperationType
 import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException
 import org.onap.cps.ncmp.api.inventory.models.CompositeState
 import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException
@@ -30,7 +32,6 @@ import org.onap.cps.ncmp.impl.data.policyexecutor.PolicyExecutor
 import org.onap.cps.ncmp.impl.dmi.DmiRestClient
 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
-import org.onap.cps.ncmp.impl.provmns.ParameterMapper
 import org.onap.cps.ncmp.impl.provmns.ParametersBuilder
 import org.onap.cps.ncmp.impl.provmns.model.PatchItem
 import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
@@ -55,7 +56,6 @@ import static org.springframework.http.HttpStatus.NOT_ACCEPTABLE
 import static org.springframework.http.HttpStatus.NOT_FOUND
 import static org.springframework.http.HttpStatus.OK
 import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE
-import static org.springframework.http.HttpStatus.SEE_OTHER
 import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY
 import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
@@ -81,9 +81,6 @@ class ProvMnSControllerSpec extends Specification {
     @Autowired
     OperationDetailsFactory operationDetailsFactory
 
-    @SpringBean
-    ParameterMapper parameterMapper = new ParameterMapper()
-
     @SpringBean
     PolicyExecutor mockPolicyExecutor = Mock()
 
@@ -94,18 +91,20 @@ class ProvMnSControllerSpec extends Specification {
     ObjectMapper objectMapper = new ObjectMapper()
 
     @SpringBean
-    JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
+    JsonObjectMapper spiedJsonObjectMapper = Spy(new JsonObjectMapper(objectMapper))
 
-    static def resourceAsJson = '{"id":"test"}'
+    static def resourceAsJson = '{"id":"test", "objectClass": "Test", "attributes": { "attr1": "value1"} }'
     static def validCmHandle = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY))
     static def cmHandleWithoutDataProducer = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', compositeState: new CompositeState(cmHandleState: READY))
     static def cmHandleNotReady            = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: ADVISED))
 
     static def patchMediaType       = new MediaType('application', 'json-patch+json')
     static def patchMediaType3gpp   = new MediaType('application', '3gpp-json-patch+json')
-    static def patchJsonBody        = '[{"op":"replace","path":"className/attributes/attr1","value":{"id":"test"}}]'
-    static def patchJsonBody3gpp    = '[{"op":"replace","path":"className#/attributes/attr1","value":{"id":"test"}}]'
-    static def patchJsonBodyInvalid = '[{"op":"replace","path":"/test","value":"INVALID"}]'
+    static def patchJsonBody        = '[{"op":"replace","path":"/child=id2/attributes","value":{"attr1":"test"}}]'
+    static def patchJsonBody3gpp    = '[{"op":"replace","path":"/child=id2#/attributes/attr1","value":"test"}]'
+    static def patchJsonBodyInvalid = '[{"op":"replace","path":"/test","value":{}}]'
+
+    static def expectedDeleteOperationDetails = '{"operation":"delete","targetIdentifier":"","changeRequest":{}}'
 
     @Value('${rest.api.provmns-base-path}')
     def provMnSBasePath
@@ -114,7 +113,6 @@ class ProvMnSControllerSpec extends Specification {
     int maxNumberOfPatchOperations
 
     static def NO_CONTENT = ''
-    static def STATUS_NOT_RELEVANT = SEE_OTHER
 
     def 'Get resource data #scenario.'() {
         given: 'resource data url'
@@ -180,7 +178,6 @@ class ProvMnSControllerSpec extends Specification {
             new Exception("my message")                         || INTERNAL_SERVER_ERROR | 'APPLICATION_LAYER_ERROR' | 'my message'
     }
 
-
     def 'Get resource data request with invalid URL: #scenario.'() {
         given: 'resource data url'
             def getUrl = "$provMnSBasePath/$version/$fdn$queryParameter"
@@ -228,6 +225,10 @@ class ProvMnSControllerSpec extends Specification {
             def provmnsUrl = "$provMnSBasePath/v1/myClass=id1"
         and: 'alternate Id can be matched'
             mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+        and: 'resource id for policy executor points to child node'
+            def expectedResourceIdForPolicyExecutor = '/myClass=id1/child=id2'
+        and: 'operation details has correct class and attributes, target identifier points to parent'
+            def expectedOperationDetails = '{"operation":"update","targetIdentifier":"/myClass=id1","changeRequest":{"child":[{"id":"id2","attributes":{"attr1":"test"}}]}}'
         and: 'persistence service returns yangModelCmHandle'
             mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
         and: 'dmi provides a response'
@@ -238,16 +239,74 @@ class ProvMnSControllerSpec extends Specification {
                     .content(jsonBody))
                     .andReturn().response
         then: 'response status is the same as what DMI gave'
-            assert response.status == expectedRespinsStatusFromProvMnS.value()
+            assert response.status == expectedResponseStatusFromProvMnS.value()
         and: 'the response contains the expected content'
-            assert response.contentAsString.contains(expectedResponseContent)
+            assert response.contentAsString.contains('content from DMI')
+        and: 'policy executor was invoked with the expected parameters'
+            1 * mockPolicyExecutor.checkPermission(_, OperationType.UPDATE, _, expectedResourceIdForPolicyExecutor, expectedOperationDetails)
         where: 'following scenarios are applied'
-            scenario          | contentMediaType   | jsonBody             | responseStatusFromDmi || expectedResponseContent     | expectedRespinsStatusFromProvMnS
-            'happy flow 3gpp' | patchMediaType3gpp | patchJsonBody3gpp    | OK                    || 'content from DMI'          | OK
-            'happy flow'      | patchMediaType     | patchJsonBody        | OK                    || 'content from DMI'          | OK
-            'error from DMI'  | patchMediaType     | patchJsonBody        | I_AM_A_TEAPOT         || 'content from DMI'          | I_AM_A_TEAPOT
-            'invalid Json'    | patchMediaType     | patchJsonBodyInvalid | STATUS_NOT_RELEVANT   || '"type":"VALIDATION_ERROR"' | BAD_REQUEST
-            'malformed Json'  | patchMediaType     | '{malformed]'        | STATUS_NOT_RELEVANT   || NO_CONTENT                  | BAD_REQUEST
+            scenario          | contentMediaType   | jsonBody             | responseStatusFromDmi || expectedResponseStatusFromProvMnS
+            'happy flow 3gpp' | patchMediaType3gpp | patchJsonBody3gpp    | OK                    || OK
+            'happy flow'      | patchMediaType     | patchJsonBody        | OK                    || OK
+            'error from DMI'  | patchMediaType     | patchJsonBody        | I_AM_A_TEAPOT         || I_AM_A_TEAPOT
+    }
+
+    def 'Attempt Patch request with malformed json.'() {
+        given: 'provmns url'
+            def provmnsUrl = "$provMnSBasePath/v1/myClass=id1"
+        and: 'alternate Id can be matched'
+            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+        and: 'persistence service returns yangModelCmHandle'
+            mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
+        when: 'patch request is performed'
+            def response = mvc.perform(patch(provmnsUrl)
+                .contentType(patchMediaType)
+                .content('{malformed}'))
+                .andReturn().response
+        then: 'response status is Bad Request'
+            assert response.status == BAD_REQUEST.value()
+        and: 'the response content is empty'
+            assert response.contentAsString.isEmpty()
+    }
+
+    def 'Attempt Patch request with json exception during processing.'() {
+        given: 'provmns url'
+            def provmnsUrl = "$provMnSBasePath/v1/myClass=id1"
+        and: 'alternate Id can be matched'
+            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+        and: 'persistence service returns yangModelCmHandle'
+            mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
+        and:
+            spiedJsonObjectMapper.asJsonString(_) >> { throw new DataValidationException('my message','some details') }
+        when: 'patch request is performed'
+            def response = mvc.perform(patch(provmnsUrl)
+                .contentType(patchMediaType)
+                .content(patchJsonBody))
+                .andReturn().response
+        then: 'response status is Bad Request'
+            assert response.status == BAD_REQUEST.value()
+        and: 'the response contains the correct type and original exception message'
+            assert response.contentAsString.contains('"type":"VALIDATION_ERROR"')
+            assert response.contentAsString.contains('my message')
+    }
+
+    def 'Patch remove request.'() {
+        given: 'resource data url'
+            def url = "$provMnSBasePath/v1/myClass=id1"
+        and: 'alternate Id can be matched'
+            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+        and: 'persistence service returns valid yangModelCmHandle'
+            mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
+            def expectedResourceIdentifier = '/myClass=id1/childClass=1/grandchildClass=1'
+        when: 'patch data resource request is performed'
+            def response = mvc.perform(patch(url)
+                .contentType(patchMediaType)
+                .content('[{"op":"remove","path":"/childClass=1/grandchildClass=1"}]'))
+                .andReturn().response
+        then: 'response status is OK'
+            assert response.status == OK.value()
+        and: 'Policy Executor was invoked with correct details'
+            1 * mockPolicyExecutor.checkPermission(_, OperationType.DELETE, _, expectedResourceIdentifier, expectedDeleteOperationDetails)
     }
 
     def 'Patch request with no permission from Coordination Management (aka Policy Executor).'() {
@@ -268,8 +327,8 @@ class ProvMnSControllerSpec extends Specification {
             assert response.status == CONFLICT.value()
         and: 'response contains the correct type'
             assert response.contentAsString.contains('"type":"APPLICATION_LAYER_ERROR"')
-        and: 'response contains the bad operation'
-            assert response.contentAsString.contains('"badOp":"replace"')
+        and: 'response contains the bad operation index'
+            assert response.contentAsString.contains('"badOp":"/0"')
         and: 'response contains the message from Policy Executor (as title)'
             assert response.contentAsString.contains('"title":"denied for test"')
     }
@@ -299,7 +358,7 @@ class ProvMnSControllerSpec extends Specification {
             for (def i = 0; i <= maxNumberOfPatchOperations; i++) {
                 patchItems.add(new PatchItem(op: 'REMOVE', path: 'somePath'))
             }
-           def patchItemsJsonRequestBody = jsonObjectMapper.asJsonString(patchItems)
+           def patchItemsJsonRequestBody = spiedJsonObjectMapper.asJsonString(patchItems)
         when: 'patch data resource request is performed'
             def response = mvc.perform(patch(url)
                     .contentType(patchMediaType)
@@ -315,13 +374,17 @@ class ProvMnSControllerSpec extends Specification {
 
     def 'Put resource data request with #scenario.'() {
         given: 'resource data url'
-            def putUrl = "$provMnSBasePath/v1/myClass=id1"
+            def putUrl = "$provMnSBasePath/v1/myClass=id1/childClass=1/grandChildClass=2"
         and: 'alternate Id can be matched'
-            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1/childClass=1/grandChildClass=2', "/") >> 'ch-1'
         and: 'persistence service returns yangModelCmHandle'
             mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
         and: 'dmi provides a response'
             mockDmiRestClient.synchronousPutOperation(*_) >> new ResponseEntity<>(responseContentFromDmi, responseStatusFromDmi)
+        and: 'The expected resource identifier for policy executor is the FDN to grandchild'
+            def expectedResourceIdentifier = '/myClass=id1/childClass=1/grandChildClass=2'
+        and: 'The change request target identifier is the FDN to parent and last class as object name in change request'
+            def expectedChangeRequest = '{"operation":"create","targetIdentifier":"/myClass=id1/childClass=1","changeRequest":{"grandChildClass":[{"id":"2","attributes":{"attr1":"value1"}}]}}'
         when: 'put data resource request is performed'
             def response = mvc.perform(put(putUrl)
                     .contentType(MediaType.APPLICATION_JSON)
@@ -331,6 +394,8 @@ class ProvMnSControllerSpec extends Specification {
             assert response.status == responseStatusFromDmi.value()
         and: 'the content is whatever the DMI returned'
             assert response.contentAsString == responseContentFromDmi
+        and: 'The policy executor was invoked with the expected parameters'
+            1 * mockPolicyExecutor.checkPermission(_, OperationType.CREATE, _, expectedResourceIdentifier, expectedChangeRequest)
         where: 'following responses returned by DMI'
             scenario         | responseStatusFromDmi | responseContentFromDmi
             'happy flow'     | OK                    | 'content from DMI'
@@ -355,9 +420,9 @@ class ProvMnSControllerSpec extends Specification {
 
     def 'Delete resource data request with #scenario.'() {
         given: 'resource data url'
-            def deleteUrl = "$provMnSBasePath/v1/myClass=id1"
+            def deleteUrl = "$provMnSBasePath/v1/myClass=id1/childClass=1/grandChildClass=2"
         and: 'alternate Id can be matched'
-            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1/childClass=1/grandChildClass=2', "/") >> 'ch-1'
         and: 'persistence service returns yangModelCmHandle'
             mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
         and: 'dmi provides a response'
@@ -368,6 +433,8 @@ class ProvMnSControllerSpec extends Specification {
             assert response.status == responseStatusFromDmi.value()
         and: 'the content is whatever the DMI returned'
             assert response.contentAsString == responseContentFromDmi
+        and: 'Policy Executor was invoked with correct resource identifier and almost empty operation details (not used for delete!)'
+            1 * mockPolicyExecutor.checkPermission(_, OperationType.DELETE, _, '/myClass=id1/childClass=1/grandChildClass=2', expectedDeleteOperationDetails)
         where: 'following responses returned by DMI'
             scenario         | responseStatusFromDmi | responseContentFromDmi
             'happy flow'     | OK                    | 'content from DMI'
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/CreateOperationDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/CreateOperationDetails.java
deleted file mode 100644 (file)
index 8f85eae..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.ncmp.impl.data.policyexecutor;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import java.util.List;
-import java.util.Map;
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public record CreateOperationDetails(String operation,
-                                     String targetIdentifier,
-                                     Map<String, List<OperationEntry>> changeRequest) implements OperationDetails {}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java
deleted file mode 100644 (file)
index 62547b5..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.ncmp.impl.data.policyexecutor;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public record DeleteOperationDetails(String operation,
-                                     String targetIdentifier) implements OperationDetails {}
index bb0a3fa..7c64283 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
+ *  Copyright (C) 2025-2026 OpenInfra Foundation Europe
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
 
 package org.onap.cps.ncmp.impl.data.policyexecutor;
 
-public interface OperationDetails {
-    String operation();
-}
+import com.fasterxml.jackson.annotation.JsonInclude;
+import java.util.List;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record OperationDetails(String operation,
+                               String targetIdentifier,
+                               Map<String, List<OperationEntry>> changeRequest) {}
index 614d95d..2ad53cb 100644 (file)
 
 package org.onap.cps.ncmp.impl.data.policyexecutor;
 
+import static java.util.Collections.emptyMap;
 import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE;
-import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE;
 import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE;
 
-import com.google.common.base.Strings;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -32,6 +31,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.api.data.models.OperationType;
 import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
+import org.onap.cps.ncmp.impl.provmns.ParameterHelper;
 import org.onap.cps.ncmp.impl.provmns.RequestParameters;
 import org.onap.cps.ncmp.impl.provmns.model.PatchItem;
 import org.onap.cps.utils.JsonObjectMapper;
@@ -43,6 +43,8 @@ import org.springframework.stereotype.Service;
 @RequiredArgsConstructor
 public class OperationDetailsFactory {
 
+    public static final OperationDetails DELETE_OPERATION_DETAILS  = new OperationDetails("delete", "", emptyMap());
+
     private static final String ATTRIBUTE_NAME_SEPARATOR = "/";
     private static final String REGEX_FOR_LEADING_AND_TRAILING_SEPARATORS = "(^/)|(/$)";
 
@@ -51,8 +53,8 @@ public class OperationDetailsFactory {
     /**
      * Create OperationDetails object from ProvMnS request details.
      *
-     * @param requestParameters    request parameters including uri-ldn-first-part, className and id
-     * @param patchItem                provided request payload
+     * @param requestParameters request parameters including uri-ldn-first-part, className and id
+     * @param patchItem provided request payload
      * @return OperationDetails object
      */
     public OperationDetails buildOperationDetails(final RequestParameters requestParameters,
@@ -60,17 +62,17 @@ public class OperationDetailsFactory {
         final OperationDetails operationDetails;
         switch (patchItem.getOp()) {
             case ADD:
-                operationDetails = buildCreateOperationDetails(CREATE, requestParameters, patchItem.getValue());
+                operationDetails = buildOperationDetailsForPatchItem(CREATE, requestParameters, patchItem);
                 break;
             case REPLACE:
                 if (patchItem.getPath().contains("#/attributes")) {
-                    operationDetails = buildCreateOperationDetailsForUpdateWithHash(requestParameters, patchItem);
+                    operationDetails = buildOperationDetailsForPatchItemWithHash(requestParameters, patchItem);
                 } else {
-                    operationDetails = buildCreateOperationDetails(UPDATE, requestParameters, patchItem.getValue());
+                    operationDetails = buildOperationDetailsForPatchItem(UPDATE, requestParameters, patchItem);
                 }
                 break;
             case REMOVE:
-                operationDetails = buildDeleteOperationDetails(requestParameters.toTargetFdn());
+                operationDetails = DELETE_OPERATION_DETAILS;
                 break;
             default:
                 throw new ProvMnSException("PATCH", HttpStatus.UNPROCESSABLE_ENTITY,
@@ -80,33 +82,51 @@ public class OperationDetailsFactory {
     }
 
     /**
-     * Build a CreateOperationDetails object from ProvMnS request details.
+     * Build a OperationDetails object from ProvMnS request details.
      *
-     * @param operationType            Type of operation create, update.
-     * @param requestParameters    request parameters including uri-ldn-first-part, className and id
-     * @param resourceAsObject         provided request payload
-     * @return CreateOperationDetails object
+     * @param operationType     Type of operation create, update.
+     * @param requestParameters request parameters including uri-ldn-first-part, className and id
+     * @param resourceAsObject  provided request payload
+     * @return OperationDetails object
      */
-    public CreateOperationDetails buildCreateOperationDetails(final OperationType operationType,
-                                                              final RequestParameters requestParameters,
-                                                              final Object resourceAsObject) {
+    public OperationDetails buildOperationDetails(final OperationType operationType,
+                                                  final RequestParameters requestParameters,
+                                                  final Object resourceAsObject) {
         final ResourceObjectDetails resourceObjectDetails = createResourceObjectDetails(resourceAsObject,
             requestParameters);
         final OperationEntry operationEntry = new OperationEntry(resourceObjectDetails.id(),
             resourceObjectDetails.attributes());
-        return new CreateOperationDetails(operationType.name(),
-            requestParameters.getUriLdnFirstPart(),
-            Map.of(resourceObjectDetails.objectClass(), List.of(operationEntry)));
+        final Map<String, List<OperationEntry>> changeRequestAsMap =
+            Map.of(resourceObjectDetails.objectClass(), List.of(operationEntry));
+        final String targetIdentifier = ParameterHelper.extractParentFdn(requestParameters.fdn());
+        return new OperationDetails(operationType.getOperationName(), targetIdentifier, changeRequestAsMap);
     }
 
     /**
-     * Builds a DeleteOperationDetails object from provided alternate id.
+     * Build OperationDetails for a specific patch item.
      *
-     * @param alternateId        alternate id for request
-     * @return DeleteOperationDetails object
+     * @param operationType     the type of operation (CREATE, UPDATE)
+     * @param requestParameters request parameters including uri-ldn-first-part, className and id
+     * @param patchItem         the patch item containing operation details
+     * @return OperationDetails object for the patch item
      */
-    public DeleteOperationDetails buildDeleteOperationDetails(final String alternateId) {
-        return new DeleteOperationDetails(DELETE.name(), alternateId);
+    public OperationDetails buildOperationDetailsForPatchItem(final OperationType operationType,
+                                                              final RequestParameters requestParameters,
+                                                              final PatchItem patchItem) {
+        final Map<String, Object> resourceAsObject = new HashMap<>(2);
+        resourceAsObject.put("id", requestParameters.id());
+        resourceAsObject.put("attributes", patchItem.getValue());
+        return buildOperationDetails(operationType, requestParameters, resourceAsObject);
+    }
+
+    private OperationDetails buildOperationDetailsForPatchItemWithHash(final RequestParameters requestParameters,
+                                                                       final PatchItem patchItem) {
+        final Map<String, Object> attributeHierarchyAsMap = createNestedMap(patchItem);
+        final OperationEntry operationEntry = new OperationEntry(requestParameters.id(), attributeHierarchyAsMap);
+        final String targetIdentifier = ParameterHelper.extractParentFdn(requestParameters.fdn());
+        final Map<String, List<OperationEntry>> operationEntriesPerObjectClass = new HashMap<>();
+        operationEntriesPerObjectClass.put(requestParameters.className(), List.of(operationEntry));
+        return new OperationDetails(UPDATE.getOperationName(), targetIdentifier, operationEntriesPerObjectClass);
     }
 
     @SuppressWarnings("unchecked")
@@ -114,31 +134,9 @@ public class OperationDetailsFactory {
                                                               final RequestParameters requestParameters) {
         final String resourceAsJson = jsonObjectMapper.asJsonString(resourceAsObject);
         final Map<String, Object> resourceAsMap = jsonObjectMapper.convertJsonString(resourceAsJson, Map.class);
-        return new ResourceObjectDetails(requestParameters.getId(),
-                                         extractObjectClass(resourceAsMap, requestParameters),
+        return new ResourceObjectDetails(requestParameters.id(),
+                                         requestParameters.className(),
                                          resourceAsMap.get("attributes"));
-
-    }
-
-    private static String extractObjectClass(final Map<String, Object> resourceAsMap,
-                                             final RequestParameters requestParameters) {
-        final String objectClass = (String) resourceAsMap.get("objectClass");
-        if (Strings.isNullOrEmpty(objectClass)) {
-            return requestParameters.getClassName();
-        }
-        return objectClass;
-    }
-
-    private CreateOperationDetails buildCreateOperationDetailsForUpdateWithHash(
-                                                                     final RequestParameters requestParameters,
-                                                                     final PatchItem patchItem) {
-        final Map<String, List<OperationEntry>> operationEntriesPerObjectClass = new HashMap<>();
-        final String className = requestParameters.getClassName();
-        final Map<String, Object> attributeHierarchyAsMap = createNestedMap(patchItem);
-        final OperationEntry operationEntry = new OperationEntry(requestParameters.getId(), attributeHierarchyAsMap);
-        operationEntriesPerObjectClass.put(className, List.of(operationEntry));
-        return new CreateOperationDetails(UPDATE.getOperationName(), requestParameters.getUriLdnFirstPart(),
-                                          operationEntriesPerObjectClass);
     }
 
     private Map<String, Object> createNestedMap(final PatchItem patchItem) {
index 96199f9..d94a69f 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2021-2026 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2022 Bell Canada
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -166,9 +166,9 @@ public class DmiRestClient {
      * @return                      ResponseEntity containing the response from the DMI.
      */
     public ResponseEntity<Object> synchronousPatchOperation(final RequiredDmiService requiredDmiService,
-                                                          final Object body,
-                                                          final UrlTemplateParameters urlTemplateParameters,
-                                                          final String contentType) {
+                                                            final Object body,
+                                                            final UrlTemplateParameters urlTemplateParameters,
+                                                            final String contentType) {
         return getWebClient(requiredDmiService)
             .patch()
             .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables())
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterHelper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterHelper.java
new file mode 100644 (file)
index 0000000..d290385
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025-2026 OpenInfra Foundation Europe
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.impl.provmns;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.NoArgsConstructor;
+import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
+import org.springframework.http.HttpStatus;
+
+@NoArgsConstructor
+public class ParameterHelper {
+
+    public static final String NO_OP = null;
+    private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/";
+    private static final String INVALID_PATH_DETAILS_TEMPLATE = "%s not a valid path";
+    private static final int PATH_VARIABLES_EXPECTED_LENGTH = 2;
+    private static final int REQUEST_FDN_INDEX = 1;
+
+    /**
+     * Converts HttpServletRequest to RequestParameters.
+     *
+     * @param httpServletRequest HttpServletRequest object containing the path
+     * @return RequestParameters object containing http method and FDN parameters
+     */
+    public static RequestParameters extractRequestParameters(final HttpServletRequest httpServletRequest) {
+        final String uriPath = (String) httpServletRequest.getAttribute(
+            "org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping");
+        final String[] pathVariables = uriPath.split(PROVMNS_BASE_PATH);
+        if (pathVariables.length != PATH_VARIABLES_EXPECTED_LENGTH) {
+            throw createProvMnSException(httpServletRequest.getMethod(), uriPath);
+        }
+        final String fdn = "/" + pathVariables[REQUEST_FDN_INDEX];
+        return createRequestParameters(httpServletRequest.getMethod(), fdn);
+    }
+
+    /**
+     * Create RequestParameters object for PATCH operations.
+     *
+     * @param pathWithAttributes the path a fdn possibly with containing attributes
+     * @return RequestParameters object for PATCH operation
+     */
+    public static RequestParameters createRequestParametersForPatch(final String pathWithAttributes) {
+        final String fdn = removeTrailingHash(extractFdn(pathWithAttributes));
+        return createRequestParameters("PATCH", fdn);
+    }
+
+    /**
+     * Extract parent FDN from the given path allowing only className=id pairs.
+     *
+     * @param path the path to convert
+     * @return parent FDN
+     */
+    public static String extractParentFdn(final String path) {
+        return extractFdn(path, 2);
+    }
+
+    /**
+     * Extract FDN from the given path allowing only className=id pairs.
+     *
+     * @param path the path to convert
+     * @return FDN
+     */
+    public static String extractFdn(final String path) {
+        return extractFdn(path, 1);
+    }
+
+    private static String extractFdn(final String path, final int indexFromEnd) {
+        final String[] segments = path.split("/");
+        int count = 0;
+        for (int i = segments.length - 1; i >= 0; i--) {
+            if (segments[i].contains("=") && ++count == indexFromEnd) {
+                return String.join("/", java.util.Arrays.copyOfRange(segments, 0, i + 1));
+            }
+        }
+        return "";
+    }
+
+    private static String removeTrailingHash(final String string) {
+        return string.endsWith("#") ? string.substring(0, string.length() - 1) : string;
+    }
+
+    private static RequestParameters createRequestParameters(final String httpMethodName,
+                                                             final String fdn) {
+        final int lastSlashIndex = fdn.lastIndexOf('/');
+        final String classNameAndId;
+        final String uriLdnFirstPart;
+        uriLdnFirstPart = fdn.substring(0, lastSlashIndex);
+        classNameAndId = fdn.substring(lastSlashIndex + 1);
+        final String[] splitClassNameId = classNameAndId.split("=", 2);
+        if (splitClassNameId.length != 2) {
+            throw createProvMnSException(httpMethodName, fdn);
+        }
+        final String className = splitClassNameId[0];
+        final String id = removeTrailingHash(splitClassNameId[1]);
+        return new RequestParameters(httpMethodName, fdn, uriLdnFirstPart, className, id);
+    }
+
+    private static ProvMnSException createProvMnSException(final String httpMethodName, final String uriPath) {
+        final String title = String.format(INVALID_PATH_DETAILS_TEMPLATE, uriPath);
+        return new ProvMnSException(httpMethodName, HttpStatus.UNPROCESSABLE_ENTITY, title, NO_OP);
+    }
+
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java
deleted file mode 100644 (file)
index 3b6f085..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.ncmp.impl.provmns;
-
-import jakarta.servlet.http.HttpServletRequest;
-import lombok.RequiredArgsConstructor;
-import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
-import org.springframework.http.HttpStatus;
-import org.springframework.stereotype.Service;
-
-@Service
-@RequiredArgsConstructor
-public class ParameterMapper {
-
-    public static final String NO_OP = null;
-    private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/";
-    private static final String INVALID_PATH_DETAILS_TEMPLATE = "%s not a valid path";
-    private static final int PATH_VARIABLES_EXPECTED_LENGTH = 2;
-    private static final int OBJECT_INSTANCE_INDEX = 1;
-
-    /**
-     * Converts HttpServletRequest to RequestParameters.
-     *
-     * @param httpServletRequest HttpServletRequest object containing the path
-     * @return RequestParameters object containing http method and parsed parameters
-     */
-    public RequestParameters extractRequestParameters(final HttpServletRequest httpServletRequest) {
-        final String uriPath = (String) httpServletRequest.getAttribute(
-            "org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping");
-        final String[] pathVariables = uriPath.split(PROVMNS_BASE_PATH);
-        if (pathVariables.length != PATH_VARIABLES_EXPECTED_LENGTH) {
-            throw createProvMnSException(httpServletRequest.getMethod(), uriPath);
-        }
-        final int lastSlashIndex = pathVariables[1].lastIndexOf('/');
-        final RequestParameters requestParameters = new RequestParameters();
-        requestParameters.setHttpMethodName(httpServletRequest.getMethod());
-        final String classNameAndId;
-        if (lastSlashIndex < 0) {
-            requestParameters.setUriLdnFirstPart("");
-            classNameAndId = pathVariables[OBJECT_INSTANCE_INDEX];
-        } else {
-            final String uriLdnFirstPart = "/" + pathVariables[OBJECT_INSTANCE_INDEX].substring(0, lastSlashIndex);
-            requestParameters.setUriLdnFirstPart(uriLdnFirstPart);
-            classNameAndId = pathVariables[OBJECT_INSTANCE_INDEX].substring(lastSlashIndex + 1);
-        }
-        final String[] splitClassNameId = classNameAndId.split("=", 2);
-        if (splitClassNameId.length != 2) {
-            throw createProvMnSException(httpServletRequest.getMethod(), uriPath);
-        }
-        requestParameters.setClassName(splitClassNameId[0]);
-        requestParameters.setId(splitClassNameId[1]);
-        return requestParameters;
-    }
-
-    private ProvMnSException createProvMnSException(final String httpMethodName, final String uriPath) {
-        final String title = String.format(INVALID_PATH_DETAILS_TEMPLATE, uriPath);
-        return new ProvMnSException(httpMethodName, HttpStatus.UNPROCESSABLE_ENTITY, title, NO_OP);
-    }
-
-}
index dc6a259..18e7965 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
+ *  Copyright (C) 2025-2026 OpenInfra Foundation Europe
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -38,17 +38,17 @@ public class ParametersBuilder {
     /**
      * Creates a UrlTemplateParameters object containing the relevant fields for read requests.
      *
-     * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID
-     * @param targetFdn         Target FDN for the resource
-     * @param scope             Provided className parameter
-     * @param filter            Filter string
-     * @param attributes        Attributes List
-     * @param fields            Fields list
-     * @param dataNodeSelector  dataNodeSelector parameter
+     * @param yangModelCmHandle  yangModelCmHandle object for resolved alternate ID
+     * @param resourceIdentifier Target FDN for the resource
+     * @param scope              Provided className parameter
+     * @param filter             Filter string
+     * @param attributes         Attributes List
+     * @param fields             Fields list
+     * @param dataNodeSelector   dataNodeSelector parameter
      * @return UrlTemplateParameters object.
      */
     public UrlTemplateParameters createUrlTemplateParametersForRead(final YangModelCmHandle yangModelCmHandle,
-                                                                    final String targetFdn,
+                                                                    final String resourceIdentifier,
                                                                     final Scope scope,
                                                                     final String filter,
                                                                     final List<String> attributes,
@@ -56,9 +56,9 @@ public class ParametersBuilder {
                                                                     final ClassNameIdGetDataNodeSelectorParameter
                                                                         dataNodeSelector) {
         final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(DATA);
-        final String targetFdnWithoutPrecedingSlash = targetFdn.substring(1);
+        final String resourceIdentifierWithoutPrecedingSlash = resourceIdentifier.substring(1);
         return RestServiceUrlTemplateBuilder.newInstance()
-            .fixedPathSegment(targetFdnWithoutPrecedingSlash)
+            .fixedPathSegment(resourceIdentifierWithoutPrecedingSlash)
             .queryParameter("scopeType", scope.getScopeType() != null ? scope.getScopeType().getValue() : null)
             .queryParameter("scopeLevel", scope.getScopeLevel() != null ? scope.getScopeLevel().toString() : null)
             .queryParameter("filter", filter)
index a85bb24..fb823d4 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe
+ *  Copyright (C) 2025-2026 OpenInfra Foundation Europe
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
 
 package org.onap.cps.ncmp.impl.provmns;
 
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class RequestParameters {
-
-    private String httpMethodName;
-    private String uriLdnFirstPart;
-    private String className;
-    private String id;
-
-    /**
-     * Gets target FDN by combining URI-LDN-First-Part, className and id.
-     *
-     * @return String of FDN
-     */
-    public String toTargetFdn() {
-        return uriLdnFirstPart + "/" + className + "=" + id;
-    }
-}
+public record RequestParameters(
+    String httpMethodName,
+    String fdn,
+    String uriLdnFirstPart,
+    String className,
+    String id) {}
index ab4abea..1c41b42 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2024-2026 OpenInfra Foundation Europe. All rights reserved.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@
 
 package org.onap.cps.ncmp.impl.data.policyexecutor
 
-
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.ncmp.api.exceptions.ProvMnSException
 import org.onap.cps.ncmp.impl.provmns.RequestParameters
@@ -36,58 +35,44 @@ import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE
 class OperationDetailsFactorySpec extends Specification {
 
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+    def requestPathParameters = new RequestParameters('some method', '/parent=1/child=2','some uri', 'class in uri', 'id in uri')
 
     OperationDetailsFactory objectUnderTest = new OperationDetailsFactory(jsonObjectMapper)
 
-    static def complexValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['myAttribute1:myValue1', 'myAttribute2:myValue2'], objectClass: 'myClassName')
-    static def simpleValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['simpleAttribute:1'], objectClass: 'myClassName')
-
     def 'Build create operation details with all properties.'() {
-        given: 'request parameters and resource'
-            def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'class in uri', id: 'my id')
-            def resource = new ResourceOneOf(id: 'some resource id', objectClass: 'class in resource')
+        given: 'a resource'
+            def resource = new ResourceOneOf(id: 'id in resource', objectClass: 'class in resource')
         when: 'create operation details are built'
-            def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
+            def result = objectUnderTest.buildOperationDetails(CREATE, requestPathParameters, resource)
         then: 'all details are correct'
-            assert result.targetIdentifier == 'my uri'
-            assert result.changeRequest.keySet()[0] == 'class in resource'
-            assert result.changeRequest['class in resource'][0].id == 'my id'
+            assert result.targetIdentifier == '/parent=1'
+            assert result.changeRequest.keySet()[0] == 'class in uri'
+            assert result.changeRequest['class in uri'][0].id == 'id in uri'
     }
 
-    def 'Build replace operation details with all properties where class name in body is #scenario.'() {
-        given: 'request parameters and resource'
-            def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'class in uri', id: 'some id')
+    def 'Build replace (~create) operation details with all properties where class name in body is #scenario.'() {
+        given: 'a resource'
             def resource = new ResourceOneOf(id: 'some resource id', objectClass: classNameInBody)
         when: 'replace operation details are built'
-            def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
+            def result = objectUnderTest.buildOperationDetails(CREATE, requestPathParameters, resource)
         then: 'all details are correct'
-            assert result.targetIdentifier == 'my uri'
-            assert result.changeRequest.keySet()[0] == expectedChangeRequestKey
+            assert result.targetIdentifier == '/parent=1'
+            assert result.changeRequest.keySet()[0] == 'class in uri'
         where:
-            scenario    | classNameInBody || expectedChangeRequestKey
-            'populated' | 'class in body' || 'class in body'
-            'empty'     | ''              || 'class in uri'
-            'null'      | null            || 'class in uri'
-    }
-
-    def 'Build delete operation details with all properties'() {
-        given: 'request parameters'
-            def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'classNameInUri', id: 'myId')
-        when: 'delete operation details are built'
-            def result = objectUnderTest.buildDeleteOperationDetails(requestPathParameters.toTargetFdn())
-        then: 'all details are correct'
-            assert result.targetIdentifier == 'my uri/classNameInUri=myId'
+            scenario    | classNameInBody
+            'populated' | 'class in body'
+            'empty'     | ''
+            'null'      | null
     }
 
     def 'Single patch operation with #patchOperationType checks correct operation type.'() {
-        given: 'request parameters and single patch item'
-            def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class')
+        given: 'a resource and single patch item'
             def resource = new ResourceOneOf(id: 'some resource id')
             def patchItem = new PatchItem(op: patchOperationType, 'path':'some uri', value: resource)
         when: 'operation details is created'
             def result = objectUnderTest.buildOperationDetails(requestPathParameters, patchItem)
         then: 'it has the correct operation type (for Policy Executor check)'
-            assert result.operation() == expectedPolicyExecutorOperationType.name()
+            assert result.operation() == expectedPolicyExecutorOperationType.operationName
         where: 'following operations are used'
             patchOperationType | expectedPolicyExecutorOperationType
             'ADD'              | CREATE
@@ -96,18 +81,16 @@ class OperationDetailsFactorySpec extends Specification {
     }
 
     def 'Build policy executor patch operation details with single replace operation and #scenario.'() {
-        given: 'a requestParameter and a patchItem list'
-            def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class')
-            def pathItem = new PatchItem(op: 'REPLACE', 'path':"some uri${suffix}", value: value)
+        given: 'a patchItem'
+            def patchItem = new PatchItem(op: 'REPLACE', 'path':"some uri${suffix}", value: value)
         when: 'patch operation details are checked'
-            def result = objectUnderTest.buildOperationDetails(requestPathParameters, pathItem)
+            def result = objectUnderTest.buildOperationDetails(requestPathParameters, patchItem)
         then: 'Attribute Value in operation is correct'
             result.changeRequest.values()[0].attributes[0] == expectedAttributesValueInOperation
         where: 'attributes are set using # or resource'
-            scenario                           | suffix                         | value                  || expectedAttributesValueInOperation
-            'set simple value using #'         | '#/attributes/simpleAttribute' | 1                      || [simpleAttribute:1]
-            'set simple value using resource'  | ''                             | simpleValueAsResource  || ['simpleAttribute:1']
-            'set complex value using resource' | ''                             | complexValueAsResource || ["myAttribute1:myValue1","myAttribute2:myValue2"]
+            scenario                            | suffix               | value                          || expectedAttributesValueInOperation
+            'set simple value using #'          | '#/attributes/attr1' | 1                              || [attr1:1]
+            'set complex value using resource'  | ''                   | '{"attr1":"abc","attr2":123}'  || '{"attr1":"abc","attr2":123}'
     }
 
     def 'Build an attribute map with different depths of hierarchy with #scenario.'() {
@@ -125,30 +108,13 @@ class OperationDetailsFactorySpec extends Specification {
     }
 
     def 'Attempt to Build Operation details with unsupported op (MOVE).'() {
-        given: 'a provMnsRequestParameter and a patchItem'
-            def path = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class')
+        given: 'a patchItem'
             def patchItem = new PatchItem(op: 'MOVE', 'path':'some uri')
         when: 'a build is attempted with an unsupported op'
-            objectUnderTest.buildOperationDetails(path, patchItem)
+            objectUnderTest.buildOperationDetails(requestPathParameters, patchItem)
         then: 'the result is as expected (exception thrown)'
             def exceptionThrown = thrown(ProvMnSException)
             assert exceptionThrown.title == 'Unsupported Patch Operation Type: move'
     }
 
-    def 'Build policy executor create operation details from ProvMnS request parameters where objectClass in resource #scenario.'() {
-        given: 'a provMnsRequestParameter and a resource'
-            def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'class in uri', id:'my id')
-            def resource = new ResourceOneOf(id: 'some resource id', objectClass: objectInResouce)
-        when: 'a configurationManagementOperation is created and converted to JSON'
-            def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
-        then: 'the result is as expected (using json to compare)'
-            String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"some uri","changeRequest":{"' + changeRequestClassReference + '":[{"id":"my id","attributes":null}]}}'
-            assert jsonObjectMapper.asJsonString(result) == expectedJsonString
-        where:
-            scenario    | objectInResouce     || changeRequestClassReference
-            'populated' | 'class in resource' || 'class in resource'
-            'empty'     | ''                  || 'class in uri'
-            'null'      | null                || 'class in uri'
-    }
-
 }
index 943377a..e9ebcfa 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved.
+ *  Copyright (C) 2023-2026 OpenInfra Foundation Europe. All rights reserved.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -55,7 +55,7 @@ class DmiDataOperationsHelperSpec extends MessagingBaseSpec {
     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
 
     @SpringBean
-    EventProducer eventsProducer = new EventProducer(legacyEventKafkaTemplate, cloudEventKafkaTemplate)
+    EventProducer eventProducer = new EventProducer(legacyEventKafkaTemplate, cloudEventKafkaTemplate)
 
     def 'Process per data operation request with #serviceName.'() {
         given: 'data operation request with 3 operations'
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterHelperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterHelperSpec.groovy
new file mode 100644 (file)
index 0000000..238bfdb
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025-2026 OpenInfra Foundation Europe. All rights reserved.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+package org.onap.cps.ncmp.impl.provmns
+
+import jakarta.servlet.http.HttpServletRequest
+import org.onap.cps.ncmp.api.exceptions.ProvMnSException
+import spock.lang.Specification
+
+class ParameterHelperSpec extends Specification {
+
+    def objectUnderTest = new ParameterHelper()
+
+    def mockHttpServletRequest = Mock(HttpServletRequest)
+
+    def uriPathAttributeName = 'org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping'
+
+    def 'Extract request parameters with url first part is a FDN with #scenario.'() {
+        given: 'a http request with all the required parts'
+            mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path
+        when: 'the request parameters are extracted'
+            def result = objectUnderTest.extractRequestParameters(mockHttpServletRequest)
+        then: 'the Uri LDN first part is as expected'
+            assert result.uriLdnFirstPart == expectedUriLdnFirstPart
+        and: 'the class name and id are mapped correctly'
+            assert result.className == 'myClass'
+            assert result.id == 'id'
+        where: 'The following URIs are used'
+            scenario            | path                                                        || expectedUriLdnFirstPart
+            '1 segment'         | 'ProvMnS/v1/segment1/myClass=id'                            || '/segment1'
+            '2 segments'        | 'ProvMnS/v1/segment1/segment2/myClass=id'                   || '/segment1/segment2'
+            'multiple segments' | 'ProvMnS/v1/segment1/segment2/segment3/segment4/myClass=id' || '/segment1/segment2/segment3/segment4'
+            'no slash'          | 'ProvMnS/v1/myClass=id'                                     || ''
+    }
+
+    def 'Extract request parameters for Patch Path with attributes.'() {
+        when: 'the request parameters are extracted from the path'
+            def result = objectUnderTest.createRequestParametersForPatch(path)
+        then: 'the FDN is as expected'
+            assert result.fdn == expectedFdn
+        and: 'the class name and id are mapped correctly'
+            assert result.className == 'myClass'
+            assert result.id == 'id'
+        where: 'the following paths are used'
+            scenario                 | path                              ||  expectedFdn
+            'attributes in path'     | '/myClass=id/attributes'          || '/myClass=id'
+            'attributes with parent' | '/parent=p/myClass=id/attributes' || '/parent=p/myClass=id'
+            '#/attributes in path'   | '/myClass=id#/attributes'         || '/myClass=id'
+    }
+
+    def 'Attempt to extract request parameters with #scenario.'() {
+        given: 'a http request with invalid path'
+            mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path
+            mockHttpServletRequest.getMethod() >> 'GET'
+        when: 'attempt to extract the request parameters'
+            objectUnderTest.extractRequestParameters(mockHttpServletRequest)
+        then: 'a ProvMnS exception is thrown'
+            def thrown = thrown(ProvMnSException)
+            assert thrown.message == 'GET failed'
+        and: 'the title contains the expected error message'
+            assert thrown.title == expectedPathInError + ' not a valid path'
+        where: 'the following invalid paths are used'
+            scenario                       | path                                || expectedPathInError
+            'no = After (last) class name' | 'ProvMnS/v1/myClass1=id/Class2'     || '/myClass1=id/Class2'
+            'attributes in path'           | 'ProvMnS/v1/myClass=id/attributes'  || '/myClass=id/attributes'
+            '#/attributes in path'         | 'ProvMnS/v1/myClass=id#/attributes' || '/myClass=id#/attributes'
+            'missing ProvMnS prefix'       | 'v1/segment1/myClass=id'            || 'v1/segment1/myClass=id'
+            'wrong version'                | 'ProvMnS/wrongVersion/myClass=id'   || 'ProvMnS/wrongVersion/myClass=id'
+            'empty path'                   | ''                                  ||  ''
+    }
+
+    def 'Extract Fdn.'() {
+        expect: 'Only valid name-id pairs are retuned up to the required index'
+            assert objectUnderTest.extractFdn('/a=1/b=2/c=3/d/e/f', indexFromEnd) == expectedResult
+        where: 'following fdns are used'
+            indexFromEnd || expectedResult
+            0            || ''
+            1            || '/a=1/b=2/c=3'
+            2            || '/a=1/b=2'
+            3            || '/a=1'
+            4            || ''
+    }
+
+    def 'Extract Parent Fdn.'() {
+        expect: 'Teh cortect Parent FDN (up to 2nd last name-id pair)) is returned'
+            assert objectUnderTest.extractParentFdn('/a=1/b=2/c=3/d/e/f') == '/a=1/b=2'
+    }
+
+}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterMapperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterMapperSpec.groovy
deleted file mode 100644 (file)
index 07ac96f..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-package org.onap.cps.ncmp.impl.provmns
-
-import jakarta.servlet.http.HttpServletRequest
-import org.onap.cps.ncmp.api.exceptions.ProvMnSException
-import spock.lang.Specification
-
-class ParameterMapperSpec extends Specification {
-
-    def objectUnderTest = new ParameterMapper()
-
-    def mockHttpServletRequest = Mock(HttpServletRequest)
-
-    def uriPathAttributeName = 'org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping'
-
-    def 'Extract request parameters with url first part is a FDN with #scenario.'() {
-        given: 'a http request with all the required parts'
-            mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path
-        when: 'the request parameters are extracted'
-            def result = objectUnderTest.extractRequestParameters(mockHttpServletRequest)
-        then: 'the Uri LDN first part is as expected'
-            assert result.uriLdnFirstPart == expectedUriLdnFirstPart
-        and: 'the class name and id are mapped correctly'
-            assert result.className == 'myClass'
-            assert result.id == 'myId'
-        where: 'The following FDN prefixes are used'
-            scenario            | path                                                          || expectedUriLdnFirstPart
-            '1 segment'         | 'ProvMnS/v1/segment1/myClass=myId'                            || '/segment1'
-            '2 segments'        | 'ProvMnS/v1/segment1/segment2/myClass=myId'                   || '/segment1/segment2'
-            'multiple segments' | 'ProvMnS/v1/segment1/segment2/segment3/segment4/myClass=myId' || '/segment1/segment2/segment3/segment4'
-            'no slash'          | 'ProvMnS/v1/myClass=myId'                                     || ''
-    }
-
-    def 'Attempt to extract request parameters with #scenario.'() {
-        given: 'a http request with invalid path'
-            mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path
-            mockHttpServletRequest.getMethod() >> 'GET'
-        when: 'attempt to extract the request parameters'
-            objectUnderTest.extractRequestParameters(mockHttpServletRequest)
-        then: 'a ProvMnS exception is thrown'
-            def thrown = thrown(ProvMnSException)
-            assert thrown.message == 'GET failed'
-        and: 'the title contains the expected error message'
-            assert thrown.title == path + ' not a valid path'
-        where: 'the following invalid paths are used'
-            scenario                      | path
-            'no = After (last) class name'| 'ProvMnS/v1/someOtherClass=someId/myClass'
-            'missing ProvMnS prefix'      | 'v1/segment1/myClass=myId'
-            'wrong version'               | 'ProvMnS/wrongVersion/myClass=myId'
-            'empty path'                  | ''
-            'multiple ProvMnS segments'   | 'ProvMnS/v1/myClass=myId/ProvMnS/v2/otherSegment'
-    }
-}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/RequestParametersSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/RequestParametersSpec.groovy
deleted file mode 100644 (file)
index fe8f186..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-package org.onap.cps.ncmp.impl.provmns
-
-import spock.lang.Specification
-
-class RequestParametersSpec extends Specification {
-
-    def objectUnderTest = new RequestParameters()
-
-    def 'Generate target FDN #scenario.'() {
-        given: 'request parameters with URI LDN first part, class name and id'
-            objectUnderTest.uriLdnFirstPart = uriLdnFirstPart
-            objectUnderTest.className = 'myClass'
-            objectUnderTest.id = 'myId'
-        when: 'target FDN is generated'
-            def result = objectUnderTest.toTargetFdn()
-        then: 'the target FDN is as expected'
-            result == expectedTargetFdn
-        where: 'the following uri first part is used'
-            scenario           | uriLdnFirstPart || expectedTargetFdn
-            'with segments'    | '/segment1'     || '/segment1/myClass=myId'
-            'empty first part' | ''              || '/myClass=myId'
-    }
-}
index deafbf5..4326335 100644 (file)
@@ -198,9 +198,9 @@ services:
     image: ${DOCKER_REPO:-nexus3.onap.org:10003}/onap/policy-executor-stub:latest
     ports:
       - ${POLICY_EXECUTOR_STUB_PORT:-8785}:8093
-    ### DEBUG: Uncomment next lines to enable java debugging in Policy Executor Stub
-    ###      - ${POLICY_EXECUTOR_STUB_DEBUG_PORT:-5005}:5005
-    ### environment:
+      ### DEBUG: Uncomment next lines to enable java debugging in Policy Executor Stub
+      ###      - ${POLICY_EXECUTOR_STUB_DEBUG_PORT:-5005}:5005
+      ### environment:
       #### JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
     restart: unless-stopped
     # Note policy-executor-stub does not have a healthcheck as it does not expose /actuator/health endpoint