Update Policy Executor mapping for ProvMnS inputs 23/142423/6
authorleventecsanyi <levente.csanyi@est.tech>
Thu, 13 Nov 2025 09:02:45 +0000 (10:02 +0100)
committerleventecsanyi <levente.csanyi@est.tech>
Thu, 13 Nov 2025 17:54:19 +0000 (18:54 +0100)
  - update mapping for Policy Executor for FDNs with a #
  - added extra testware

Issue-ID: CPS-2704
Change-Id: I250265d31bcdf61b053bf77b83a4ba8dc3e0ac67
Signed-off-by: leventecsanyi <levente.csanyi@est.tech>
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy

index d4dbe3f..d0d397f 100644 (file)
@@ -28,6 +28,7 @@ import java.net.UnknownHostException;
 import java.time.Duration;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -61,6 +62,8 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
 @RequiredArgsConstructor
 public class PolicyExecutor {
 
+    public static final String ATTRIBUTES_WITH_HASHTAG = "#/attributes";
+
     @Value("${ncmp.policy-executor.enabled:false}")
     private boolean enabled;
 
@@ -140,8 +143,7 @@ public class PolicyExecutor {
                     buildCreateOperationDetails(OperationType.CREATE, requestPathParameters,
                     (Resource) patchItem.getValue()));
                 case REPLACE -> operations.add(
-                    buildCreateOperationDetails(OperationType.UPDATE, requestPathParameters,
-                    (Resource) patchItem.getValue()));
+                    buildCreateOperationDetailsForUpdate(OperationType.UPDATE, requestPathParameters, patchItem));
                 case REMOVE -> operations.add(
                     buildDeleteOperationDetails(requestPathParameters.toAlternateId()));
                 default -> log.warn("Unsupported Patch Operation Type:{}", patchItem.getOp().getValue());
@@ -164,12 +166,12 @@ public class PolicyExecutor {
         final Map<String, List<OperationEntry>> changeRequest = new HashMap<>();
         final OperationEntry operationEntry = new OperationEntry();
 
-        final String resourceJson = jsonObjectMapper.asJsonString(resource);
+        final String resourceAsJson = jsonObjectMapper.asJsonString(resource);
         String className = requestPathParameters.getClassName();
         try {
             final TypeReference<HashMap<String, Object>> typeReference =
                 new TypeReference<HashMap<String, Object>>() {};
-            final Map<String, Object> fullValue = objectMapper.readValue(resourceJson, typeReference);
+            final Map<String, Object> fullValue = objectMapper.readValue(resourceAsJson, typeReference);
 
             operationEntry.setId(requestPathParameters.getId());
             operationEntry.setAttributes(fullValue.get("attributes"));
@@ -183,6 +185,63 @@ public class PolicyExecutor {
             requestPathParameters.getUriLdnFirstPart(), changeRequest);
     }
 
+    /**
+     * Build a CreateOperationDetails object from ProvMnS request details.
+     *
+     * @param operationType            Type of operation create, update.
+     * @param requestPathParameters    request parameters including uri-ldn-first-part, className and id
+     * @param patchItem                 provided request
+     * @return CreateOperationDetails object
+     */
+    public CreateOperationDetails buildCreateOperationDetailsForUpdate(final OperationType operationType,
+                                                                     final RequestPathParameters requestPathParameters,
+                                                                     final PatchItem patchItem) {
+        if (patchItem.getPath().contains(ATTRIBUTES_WITH_HASHTAG)) {
+            return buildCreateOperationDetailsForUpdateWithHash(operationType, requestPathParameters, patchItem);
+        } else {
+            return buildCreateOperationDetails(operationType, requestPathParameters, (Resource) patchItem.getValue());
+        }
+    }
+
+    private CreateOperationDetails buildCreateOperationDetailsForUpdateWithHash(final OperationType operationType,
+                                                                      final RequestPathParameters requestPathParameters,
+                                                                      final PatchItem patchItem) {
+        final Map<String, List<OperationEntry>> changeRequest = new HashMap<>();
+        final OperationEntry operationEntry = new OperationEntry();
+        final String className = requestPathParameters.getClassName();
+
+        final Map<String, Object> attributeHiearchyAsMap = getAttributeHierarchyMap(patchItem);
+
+        operationEntry.setId(requestPathParameters.getId());
+        operationEntry.setAttributes(attributeHiearchyAsMap);
+        changeRequest.put(className, List.of(operationEntry));
+
+        return new CreateOperationDetails(operationType.getOperationName(),
+                                          requestPathParameters.getUriLdnFirstPart(),
+                                          changeRequest);
+    }
+
+    private Map<String, Object> getAttributeHierarchyMap(final PatchItem patchItem) {
+        final String[] parts = patchItem.getPath().split(ATTRIBUTES_WITH_HASHTAG);
+
+        final String attributeHierarchy = parts[1];
+        final String[] attributeHierarchyAsArray = Arrays.stream(attributeHierarchy.split("/"))
+                .filter(attributeName -> !attributeName.isEmpty())
+                .toArray(String[]::new);
+
+        return buildAttributeHiearchyAsMap(attributeHierarchyAsArray, 0, patchItem.getValue());
+    }
+
+    private Map<String, Object> buildAttributeHiearchyAsMap(final String[] parts,
+                                                            final int index,
+                                                            final Object value) {
+        if (index == parts.length - 1) {
+            return Map.of(parts[index], value);
+        }
+
+        return Map.of(parts[index], buildAttributeHiearchyAsMap(parts, index + 1, value));
+    }
+
     /**
      * Builds a DeleteOperationDetails object from provided alternate id.
      *
index 4ad7cba..a71ddf4 100644 (file)
@@ -29,7 +29,6 @@ import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.ncmp.api.exceptions.NcmpException
 import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException
-import org.onap.cps.ncmp.api.exceptions.ProvMnSException
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
 import org.onap.cps.ncmp.impl.provmns.RequestPathParameters
 import org.onap.cps.ncmp.impl.provmns.model.PatchItem
@@ -63,6 +62,9 @@ class PolicyExecutorSpec extends Specification {
     def logAppender = Spy(ListAppender<ILoggingEvent>)
 
     def someValidJson = '{"Hello":"World"}'
+    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 setup() {
         setupLogger()
@@ -232,25 +234,66 @@ class PolicyExecutorSpec extends Specification {
 
     def 'Build policy executor patch operation details from ProvMnS request parameters where #scenario.'() {
         given: 'a provMnsRequestParameter and a patchItem list'
-            def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
-            def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: objectClass)
-            def patchItemsList = [new PatchItem(op: 'ADD', 'path':'someUriLdnFirstPart', value: resource), new PatchItem(op: 'REPLACE', 'path':'someUriLdnFirstPart', value: resource)]
-        when: 'a configurationManagementOperation is created and converted to JSON'
+            def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'classNameInUri', id: 'myId')
+            def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: classNameInBody)
+            def patchItemsList = [new PatchItem(op: 'ADD', 'path':'myUriLdnFirstPart', value: resource), new PatchItem(op: 'REPLACE', 'path':'myUriLdnFirstPart', value: resource), new PatchItem(op: 'REMOVE', 'path':'myUriLdnFirstPart'),]
+        when: 'patch operation details are created'
             def result = objectUnderTest.buildPatchOperationDetails(path, patchItemsList)
-        then: 'the result is as expected (using json to compare)'
-            def expectedJsonString = '{"permissionId":"Some Permission Id","changeRequestFormat":"cm-legacy","operations":[{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}},{"operation":"UPDATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}]}'
-            assert expectedJsonString == jsonObjectMapper.asJsonString(result)
-        where:
-            scenario                   | objectClass        || changeRequestClassReference
-            'objectClass is populated' | 'someObjectClass'  || 'someObjectClass'
-            'objectClass is empty'     | ''                 || 'someClassName'
-            'objectClass is null'      | null               || 'someClassName'
+        then: 'the result contain 3 operations of the correct types in the correct order'
+            result.operations.size() == 3
+        and: 'note that Add and Replace both are defined using Create Operation Details'
+            assert result.operations[0] instanceof CreateOperationDetails
+            assert result.operations[1] instanceof CreateOperationDetails
+            assert result.operations[2] instanceof DeleteOperationDetails
+        and: 'the add operation target identifier is just the uri first part'
+            assert result.operations[0]['targetIdentifier'] == 'myUriLdnFirstPart'
+        and: 'the replace operation target identifier is just the uri first part'
+            assert result.operations[1]['targetIdentifier'] == 'myUriLdnFirstPart'
+        and: 'the replace change request has the correct class name'
+            assert result.operations[1].changeRequest.keySet()[0] == expectedChangeRequestKey
+        and: 'the delete operation target identifier includes the target class and id'
+            assert result.operations[2]['targetIdentifier'] == 'myUriLdnFirstPart/classNameInUri=myId'
+        where: 'the following class names are used in the body'
+            scenario                          | classNameInBody || expectedChangeRequestKey
+            'class name in body is populated' | 'myClass'       || 'myClass'
+            'class name in body  is empty'    | ''              || 'classNameInUri'
+            'class name in body  is null'     | null            || 'classNameInUri'
+    }
+
+    def 'Build policy executor patch operation details with single replace operation and #scenario.'() {
+        given: 'a requestParameter and a patchItem list'
+            def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'myClassName', id: 'myId')
+            def pathItems = [new PatchItem(op: 'REPLACE', 'path':"myUriLdnFirstPart${suffix}", value: value)]
+        when: 'patch operation details are created'
+            def result = objectUnderTest.buildPatchOperationDetails(path, pathItems)
+        then: 'the result has the correct type'
+            assert result instanceof PatchOperationsDetails
+        and: 'the change request contains the correct attributes value'
+            assert result.operations[0]['changeRequest']['myClassName'][0]['attributes'].toString() == attributesValueInOperation
+        where: 'attributes are set using # or resource'
+            scenario                           | suffix                         | value                  || attributesValueInOperation
+            '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]'
+    }
+
+    def 'Build an attribute map with different depths of hierarchy with #scenario.'() {
+        given: 'a patch item with a path'
+            def patchItem = new PatchItem(op: 'REPLACE', 'path':path, value: 123)
+        when: 'transforming the attributes'
+            def hierarchyMap = objectUnderTest.getAttributeHierarchyMap(patchItem)
+        then: 'the map depth is equal to the expected number of attributes'
+            assert hierarchyMap.get(expectedAttributeName).toString() == expectedAttributeValue
+        where: 'simple and complex attributes are tested'
+            scenario                  | path                                                             || expectedAttributeName || expectedAttributeValue
+            'set a simple attribute'  | 'myUriLdnFirstPart#/attributes/simpleAttribute'                  || 'simpleAttribute'     || '123'
+            'set a complex attribute' | 'myUriLdnFirstPart#/attributes/complexAttribute/simpleAttribute' || 'complexAttribute'    || '[simpleAttribute:123]'
     }
 
     def 'Build policy executor patch operation details from ProvMnS request parameters with invalid op.'() {
         given: 'a provMnsRequestParameter and a patchItem list'
-            def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
-            def patchItemsList = [new PatchItem(op: 'TEST', 'path':'someUriLdnFirstPart')]
+            def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'someClassName', id: 'someId')
+            def patchItemsList = [new PatchItem(op: 'TEST', 'path':'myUriLdnFirstPart')]
         when: 'a configurationManagementOperation is created and converted to JSON'
             def result = objectUnderTest.buildPatchOperationDetails(path, patchItemsList)
         then: 'the result is as expected (using json to compare)'
@@ -260,12 +303,12 @@ class PolicyExecutorSpec extends Specification {
 
     def 'Build policy executor create operation details from ProvMnS request parameters where #scenario.'() {
         given: 'a provMnsRequestParameter and a resource'
-            def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
+            def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'someClassName', id: 'someId')
             def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: objectClass)
         when: 'a configurationManagementOperation is created and converted to JSON'
             def result = objectUnderTest.buildCreateOperationDetails(CREATE, path, resource)
         then: 'the result is as expected (using json to compare)'
-            String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}'
+            String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"myUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}'
             assert jsonObjectMapper.asJsonString(result) == expectedJsonString
         where:
             scenario                   | objectClass        || changeRequestClassReference
@@ -276,7 +319,7 @@ class PolicyExecutorSpec extends Specification {
 
     def 'Build Policy Executor Operation Details with a exception during conversion'() {
         given: 'a provMnsRequestParameter and a resource'
-            def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
+            def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'someClassName', id: 'someId')
             def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'])
         and: 'json object mapper throws an exception'
             def originalException = new JsonProcessingException('some-exception')