From 57cd82357265cb71f810b5ac506027e8c299cd23 Mon Sep 17 00:00:00 2001 From: leventecsanyi Date: Thu, 13 Nov 2025 10:02:45 +0100 Subject: [PATCH] Update Policy Executor mapping for ProvMnS inputs - update mapping for Policy Executor for FDNs with a # - added extra testware Issue-ID: CPS-2704 Change-Id: I250265d31bcdf61b053bf77b83a4ba8dc3e0ac67 Signed-off-by: leventecsanyi --- .../impl/data/policyexecutor/PolicyExecutor.java | 67 ++++++++++++++++-- .../data/policyexecutor/PolicyExecutorSpec.groovy | 79 +++++++++++++++++----- 2 files changed, 124 insertions(+), 22 deletions(-) diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java index d4dbe3fc42..d0d397f81f 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java @@ -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> 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> typeReference = new TypeReference>() {}; - final Map fullValue = objectMapper.readValue(resourceJson, typeReference); + final Map 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> changeRequest = new HashMap<>(); + final OperationEntry operationEntry = new OperationEntry(); + final String className = requestPathParameters.getClassName(); + + final Map 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 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 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. * diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy index 4ad7cbafad..a71ddf4434 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy @@ -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) 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') -- 2.16.6