Improve mapping for Resource objects in PolicyExecutor 13/142513/4
authorleventecsanyi <levente.csanyi@est.tech>
Tue, 25 Nov 2025 08:24:12 +0000 (09:24 +0100)
committerleventecsanyi <levente.csanyi@est.tech>
Tue, 25 Nov 2025 17:59:25 +0000 (18:59 +0100)
  - added ResourceObjectDetails for improved mapping
  - added OperationDetailsFactory service and moved related code there from PolicyExecutor

Issue-ID: CPS-3059
Change-Id: Ib6c9329cf89b25f5507a07bb7a6e36c2f06cf259
Signed-off-by: leventecsanyi <levente.csanyi@est.tech>
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.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/OperationDetailsFactory.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/ResourceObjectDetails.java [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy

index 76a2b57..9ea71de 100644 (file)
@@ -27,6 +27,7 @@ import org.onap.cps.ncmp.api.data.models.OperationType;
 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.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;
@@ -65,6 +66,7 @@ public class ProvMnsController implements ProvMnS {
     private final ErrorResponseBuilder errorResponseBuilder;
     private final PolicyExecutor policyExecutor;
     private final JsonObjectMapper jsonObjectMapper;
+    private final OperationDetailsFactory operationDetailsFactory;
 
     @Override
     public ResponseEntity<Object> getMoi(final HttpServletRequest httpServletRequest,
@@ -107,8 +109,8 @@ public class ProvMnsController implements ProvMnS {
                 OperationType.CREATE,
                 NO_AUTHORIZATION,
                 requestPathParameters.toAlternateId(),
-                jsonObjectMapper.asJsonString(
-                    policyExecutor.buildPatchOperationDetails(requestPathParameters, patchItems))
+                jsonObjectMapper.asJsonString(operationDetailsFactory.buildPatchOperationDetails(requestPathParameters,
+                                                                                                 patchItems))
             );
             final UrlTemplateParameters urlTemplateParameters =
                 parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters);
@@ -139,8 +141,9 @@ public class ProvMnsController implements ProvMnS {
                 NO_AUTHORIZATION,
                 requestPathParameters.toAlternateId(),
                 jsonObjectMapper.asJsonString(
-                    policyExecutor.buildCreateOperationDetails(OperationType.CREATE, requestPathParameters, resource))
-            );
+                        operationDetailsFactory.buildCreateOperationDetails(OperationType.CREATE,
+                                                                            requestPathParameters,
+                                                                            resource)));
             final UrlTemplateParameters urlTemplateParameters =
                 parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters);
             return dmiRestClient.synchronousPutOperation(RequiredDmiService.DATA, resource, urlTemplateParameters);
@@ -169,7 +172,7 @@ public class ProvMnsController implements ProvMnS {
                 NO_AUTHORIZATION,
                 requestPathParameters.toAlternateId(),
                 jsonObjectMapper.asJsonString(
-                    policyExecutor.buildDeleteOperationDetails(requestPathParameters.toAlternateId()))
+                        operationDetailsFactory.buildDeleteOperationDetails(requestPathParameters.toAlternateId()))
             );
             final UrlTemplateParameters urlTemplateParameters =
                 parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters);
index b1435ac..b06e407 100644 (file)
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import jakarta.servlet.ServletException
 import org.onap.cps.ncmp.api.inventory.models.CompositeState
 import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException
+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
@@ -67,6 +68,9 @@ class ProvMnsControllerSpec extends Specification {
     @SpringBean
     DmiRestClient dmiRestClient = Mock()
 
+    @SpringBean
+    OperationDetailsFactory operationDetailsFactory = Mock()
+
     @SpringBean
     ErrorResponseBuilder errorResponseBuilder = new ErrorResponseBuilder()
 
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java
new file mode 100644 (file)
index 0000000..301979e
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ *  ============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.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Strings;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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.RequestPathParameters;
+import org.onap.cps.ncmp.impl.provmns.model.PatchItem;
+import org.onap.cps.utils.JsonObjectMapper;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OperationDetailsFactory {
+
+    private static final String ATTRIBUTE_NAME_SEPARATOR = "/";
+    private static final String REGEX_FOR_LEADING_AND_TRAILING_SEPARATORS = "(^/)|(/$)";
+
+    private final JsonObjectMapper jsonObjectMapper;
+    private final ObjectMapper objectMapper;
+
+    /**
+     * Build a PatchOperationDetails object from ProvMnS request details.
+     *
+     * @param requestPathParameters    request parameters including uri-ldn-first-part, className and id
+     * @param patchItems               provided request list of patch Items
+     * @return CreateOperationDetails object
+     */
+    public PatchOperationsDetails buildPatchOperationDetails(final RequestPathParameters requestPathParameters,
+                                                             final List<PatchItem> patchItems) {
+        final List<Object> operations = new ArrayList<>(patchItems.size());
+        for (final PatchItem patchItem : patchItems) {
+            switch (patchItem.getOp()) {
+                case ADD -> operations.add(buildCreateOperationDetails(OperationType.CREATE, requestPathParameters,
+                                        patchItem.getValue()));
+                case REPLACE -> operations.add(buildCreateOperationDetailsForUpdate(OperationType.UPDATE,
+                                        requestPathParameters,
+                                        patchItem));
+                case REMOVE -> operations.add(buildDeleteOperationDetails(requestPathParameters.toAlternateId()));
+                default -> log.warn("Unsupported Patch Operation Type:{}", patchItem.getOp().getValue());
+            }
+        }
+        return new PatchOperationsDetails("Some Permission Id", "cm-legacy", operations);
+    }
+
+    /**
+     * 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 resourceAsObject         provided request payload
+     * @return CreateOperationDetails object
+     */
+    public CreateOperationDetails buildCreateOperationDetails(final OperationType operationType,
+                                                              final RequestPathParameters requestPathParameters,
+                                                              final Object resourceAsObject) {
+
+        final ResourceObjectDetails resourceObjectDetails = createResourceObjectDetails(resourceAsObject,
+                                                                                        requestPathParameters);
+
+        final OperationEntry operationEntry = new OperationEntry(resourceObjectDetails.id(),
+                resourceObjectDetails.attributes());
+
+        final Map<String, List<OperationEntry>> operationEntriesPerObjectClass =
+                Map.of(resourceObjectDetails.objectClass(), List.of(operationEntry));
+
+        return new CreateOperationDetails(
+                operationType.name(),
+                requestPathParameters.getUriLdnFirstPart(),
+                operationEntriesPerObjectClass
+        );
+    }
+
+    /**
+     * 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")) {
+            return buildCreateOperationDetailsForUpdateWithHash(operationType, requestPathParameters, patchItem);
+        } else {
+            return buildCreateOperationDetails(operationType, requestPathParameters, patchItem.getValue());
+        }
+    }
+
+    /**
+     * Builds a DeleteOperationDetails object from provided alternate id.
+     *
+     * @param alternateId        alternate id for request
+     * @return DeleteOperationDetails object
+     */
+    public DeleteOperationDetails buildDeleteOperationDetails(final String alternateId) {
+        return new DeleteOperationDetails(OperationType.DELETE.name(), alternateId);
+    }
+
+    private ResourceObjectDetails createResourceObjectDetails(final Object resourceAsObject,
+                                                              final RequestPathParameters requestPathParameters) {
+        try {
+            final String resourceAsJson = jsonObjectMapper.asJsonString(resourceAsObject);
+            final TypeReference<Map<String, Object>> typeReference = new TypeReference<>() {};
+            final Map<String, Object> resourceAsMap = objectMapper.readValue(resourceAsJson, typeReference);
+
+            return new ResourceObjectDetails(requestPathParameters.getId(),
+                                             extractObjectClass(resourceAsMap, requestPathParameters),
+                                             resourceAsMap.get("attributes"));
+        } catch (final JsonProcessingException e) {
+            log.debug("JSON processing error: {}", e.getMessage());
+            throw new ProvMnSException("Cannot convert Resource Object", e.getMessage());
+        }
+    }
+
+    private static String extractObjectClass(final Map<String, Object> resourceAsMap,
+                                             final RequestPathParameters requestPathParameters) {
+        final String objectClass = (String) resourceAsMap.get("objectClass");
+        if (Strings.isNullOrEmpty(objectClass)) {
+            return requestPathParameters.getClassName();
+        }
+        return objectClass;
+    }
+
+
+    private CreateOperationDetails buildCreateOperationDetailsForUpdateWithHash(final OperationType operationType,
+                                                                     final RequestPathParameters requestPathParameters,
+                                                                     final PatchItem patchItem) {
+        final Map<String, List<OperationEntry>> operationEntriesPerObjectClass = new HashMap<>();
+        final String className = requestPathParameters.getClassName();
+
+        final Map<String, Object> attributeHierarchyAsMap = createNestedMap(patchItem);
+
+        final OperationEntry operationEntry = new OperationEntry(requestPathParameters.getId(),
+                                                                 attributeHierarchyAsMap);
+        operationEntriesPerObjectClass.put(className, List.of(operationEntry));
+
+        return new CreateOperationDetails(operationType.getOperationName(),
+                requestPathParameters.getUriLdnFirstPart(),
+                operationEntriesPerObjectClass);
+    }
+
+    private Map<String, Object> createNestedMap(final PatchItem patchItem) {
+        final Map<String, Object> attributeHierarchyMap = new HashMap<>();
+        Map<String, Object> currentLevel = attributeHierarchyMap;
+
+        final String[] attributeHierarchyNames = patchItem.getPath().split("#/attributes")[1]
+                .replaceAll(REGEX_FOR_LEADING_AND_TRAILING_SEPARATORS, "")
+                .split(ATTRIBUTE_NAME_SEPARATOR);
+
+        for (int level = 0; level < attributeHierarchyNames.length; level++) {
+            final String attributeName = attributeHierarchyNames[level];
+
+            if (isLastLevel(attributeHierarchyNames, level)) {
+                currentLevel.put(attributeName, patchItem.getValue());
+            } else {
+                final Map<String, Object> nextLevel = new HashMap<>();
+                currentLevel.put(attributeName, nextLevel);
+                currentLevel = nextLevel;
+            }
+        }
+        return attributeHierarchyMap;
+    }
+
+    private boolean isLastLevel(final String[] attributeNamesArray, final int level) {
+        return level == attributeNamesArray.length - 1;
+    }
+}
+
index 542a6b7..184234f 100644 (file)
 
 package org.onap.cps.ncmp.impl.data.policyexecutor;
 
-import com.fasterxml.jackson.annotation.JsonInclude;
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * Represents a single managed object included in a change request,
- * containing its identifier and arbitrary attributes.
- */
-@Setter
-@Getter
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class OperationEntry {
-    private String id;
-    private Object attributes;
-
-}
\ No newline at end of file
+public record OperationEntry(String id, Object attributes) {}
\ No newline at end of file
index b477954..c04ae62 100644 (file)
 package org.onap.cps.ncmp.impl.data.policyexecutor;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.net.UnknownHostException;
 import java.time.Duration;
 import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeoutException;
 import lombok.RequiredArgsConstructor;
@@ -40,11 +37,8 @@ import org.onap.cps.ncmp.api.data.models.OperationType;
 import org.onap.cps.ncmp.api.exceptions.NcmpException;
 import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException;
 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;
 import org.onap.cps.ncmp.impl.utils.http.RestServiceUrlTemplateBuilder;
 import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters;
-import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.HttpHeaders;
@@ -60,8 +54,6 @@ 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;
 
@@ -78,7 +70,6 @@ public class PolicyExecutor {
     @Value("${ncmp.policy-executor.httpclient.all-services.readTimeoutInSeconds:30}")
     private long readTimeoutInSeconds;
 
-    private static final String CHANGE_REQUEST_FORMAT = "cm-legacy";
     private static final String PERMISSION_BASE_PATH = "operation-permission";
     private static final String REQUEST_PATH = "permissions";
 
@@ -86,11 +77,8 @@ public class PolicyExecutor {
     private final WebClient policyExecutorWebClient;
 
     private final ObjectMapper objectMapper;
-    private final JsonObjectMapper jsonObjectMapper;
 
     private static final Throwable NO_ERROR = null;
-    private static final String ATTRIBUTE_NAME_SEPARATOR = "/";
-    private static final String REGEX_FOR_LEADING_AND_TRAILING_SEPARATORS = "(^/)|(/$)";
 
     /**
      * Use the Policy Executor to check permission for a cm write operation.
@@ -127,132 +115,6 @@ public class PolicyExecutor {
         }
     }
 
-    /**
-     * Build a PatchOperationDetails object from ProvMnS request details.
-     *
-     * @param requestPathParameters    request parameters including uri-ldn-first-part, className and id
-     * @param patchItems               provided request list of patch Items
-     * @return CreateOperationDetails object
-     */
-    public PatchOperationsDetails buildPatchOperationDetails(final RequestPathParameters requestPathParameters,
-                                                             final List<PatchItem> patchItems) {
-        final List<Object> operations = new ArrayList<>(patchItems.size());
-        for (final PatchItem patchItem : patchItems) {
-            switch (patchItem.getOp()) {
-                case ADD -> operations.add(
-                    buildCreateOperationDetails(OperationType.CREATE, requestPathParameters,
-                    patchItem.getValue()));
-                case REPLACE -> operations.add(
-                    buildCreateOperationDetailsForUpdate(OperationType.UPDATE, requestPathParameters, patchItem));
-                case REMOVE -> operations.add(
-                    buildDeleteOperationDetails(requestPathParameters.toAlternateId()));
-                default -> log.warn("Unsupported Patch Operation Type:{}", patchItem.getOp().getValue());
-            }
-        }
-        return new PatchOperationsDetails("Some Permission Id", CHANGE_REQUEST_FORMAT, operations);
-    }
-
-    /**
-     * 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 resourceAsObject         provided request payload
-     * @return CreateOperationDetails object
-     */
-    public CreateOperationDetails buildCreateOperationDetails(final OperationType operationType,
-                                                              final RequestPathParameters requestPathParameters,
-                                                              final Object resourceAsObject) {
-        final Map<String, List<OperationEntry>> changeRequest = new HashMap<>();
-        final OperationEntry operationEntry = new OperationEntry();
-
-        final String resourceAsJson = jsonObjectMapper.asJsonString(resourceAsObject);
-        String className = requestPathParameters.getClassName();
-        try {
-            final TypeReference<HashMap<String, Object>> typeReference =
-                new TypeReference<HashMap<String, Object>>() {};
-            final Map<String, Object> valueMap = objectMapper.readValue(resourceAsJson, typeReference);
-
-            operationEntry.setId(requestPathParameters.getId());
-            operationEntry.setAttributes(valueMap.get("attributes"));
-            className = isNullEmptyOrBlank(valueMap)
-                ? requestPathParameters.getClassName() : valueMap.get("objectClass").toString();
-        } catch (final JsonProcessingException exception) {
-            log.debug("JSON processing error: {}", exception);
-        }
-        changeRequest.put(className, List.of(operationEntry));
-        return new CreateOperationDetails(operationType.name(),
-            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, 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> attributeHierarchyAsMap = createNestedMap(patchItem);
-
-        operationEntry.setId(requestPathParameters.getId());
-        operationEntry.setAttributes(attributeHierarchyAsMap);
-        changeRequest.put(className, List.of(operationEntry));
-
-        return new CreateOperationDetails(operationType.getOperationName(),
-                                          requestPathParameters.getUriLdnFirstPart(),
-                                          changeRequest);
-    }
-
-    private Map<String, Object> createNestedMap(final PatchItem patchItem) {
-        final Map<String, Object> attributeHierarchyMap = new HashMap<>();
-        Map<String, Object> currentLevel = attributeHierarchyMap;
-
-        final String[] attributeHierarchyNames = patchItem.getPath().split(ATTRIBUTES_WITH_HASHTAG)[1]
-                                                   .replaceAll(REGEX_FOR_LEADING_AND_TRAILING_SEPARATORS, "")
-                                                       .split(ATTRIBUTE_NAME_SEPARATOR);
-
-        for (int level = 0; level < attributeHierarchyNames.length; level++) {
-            final String attributeName = attributeHierarchyNames[level];
-
-            if (isLastLevel(attributeHierarchyNames, level)) {
-                currentLevel.put(attributeName, patchItem.getValue());
-            } else {
-                final Map<String, Object> nextLevel = new HashMap<>();
-                currentLevel.put(attributeName, nextLevel);
-                currentLevel = nextLevel;
-            }
-        }
-        return attributeHierarchyMap;
-    }
-
-    /**
-     * Builds a DeleteOperationDetails object from provided alternate id.
-     *
-     * @param alternateId        alternate id for request
-     * @return DeleteOperationDetails object
-     */
-    public DeleteOperationDetails buildDeleteOperationDetails(final String alternateId) {
-        return new DeleteOperationDetails(OperationType.DELETE.name(), alternateId);
-    }
-
     private Map<String, Object> getSingleOperationAsMap(final YangModelCmHandle yangModelCmHandle,
                                                         final OperationType operationType,
                                                         final String resourceIdentifier,
@@ -277,7 +139,7 @@ public class PolicyExecutor {
     private Object createBodyAsObject(final Map<String, Object> operationAsMap) {
         final Collection<Map<String, Object>> operations = Collections.singletonList(operationAsMap);
         final Map<String, Object> permissionRequestAsMap = new HashMap<>(2);
-        permissionRequestAsMap.put("changeRequestFormat", CHANGE_REQUEST_FORMAT);
+        permissionRequestAsMap.put("changeRequestFormat", "cm-legacy");
         permissionRequestAsMap.put("operations", operations);
         return permissionRequestAsMap;
     }
@@ -378,16 +240,4 @@ public class PolicyExecutor {
         log.warn(warning);
         processDecision(decisionId, decision, warning, cause);
     }
-
-    private boolean isNullEmptyOrBlank(final Map<String, Object> jsonObject) {
-        try {
-            return jsonObject.get("objectClass").toString().isBlank();
-        } catch (final NullPointerException exception) {
-            return true;
-        }
-    }
-
-    private boolean isLastLevel(final String[] attributeNamesArray, final int level) {
-        return level == attributeNamesArray.length - 1;
-    }
 }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/ResourceObjectDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/ResourceObjectDetails.java
new file mode 100644 (file)
index 0000000..e261cbf
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ *  ============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;
+
+public record ResourceObjectDetails(String id, String objectClass, Object attributes) {}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy
new file mode 100644 (file)
index 0000000..0264282
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024-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.data.policyexecutor
+
+import com.fasterxml.jackson.core.JsonProcessingException
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.ncmp.api.exceptions.NcmpException;
+import org.onap.cps.ncmp.impl.provmns.RequestPathParameters;
+import org.onap.cps.ncmp.impl.provmns.model.PatchItem;
+import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf
+import org.onap.cps.utils.JsonObjectMapper;
+import spock.lang.Specification;
+
+import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE;
+
+class OperationDetailsFactorySpec extends Specification {
+
+    def spiedObjectMapper = Spy(ObjectMapper)
+    def jsonObjectMapper = new JsonObjectMapper(spiedObjectMapper)
+
+    OperationDetailsFactory objectUnderTest = new OperationDetailsFactory(jsonObjectMapper, spiedObjectMapper)
+
+    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 policy executor patch operation details from ProvMnS request parameters where #scenario.'() {
+        given: 'a provMnsRequestParameter and a patchItem list'
+            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 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.createNestedMap(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 simple attribute with a trailing /' | '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: '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)'
+            def expectedJsonString = '{"permissionId":"Some Permission Id","changeRequestFormat":"cm-legacy","operations":[]}'
+            assert expectedJsonString == jsonObjectMapper.asJsonString(result)
+    }
+
+    def 'Build policy executor create operation details from ProvMnS request parameters where #scenario.'() {
+        given: 'a provMnsRequestParameter and a resource'
+            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":"myUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}'
+            assert jsonObjectMapper.asJsonString(result) == expectedJsonString
+        where:
+            scenario                   | objectClass        || changeRequestClassReference
+            'objectClass is populated' | 'someObjectClass'  || 'someObjectClass'
+            'objectClass is empty'     | ''                 || 'someClassName'
+            'objectClass is null'      | null               || 'someClassName'
+    }
+
+    def 'Build Policy Executor Operation Details with a exception during conversion'() {
+        given: 'a provMnsRequestParameter and a resource'
+            def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'someClassName', id: 'someId')
+            def resource = new ResourceOneOf(id: 'myResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'])
+        and: 'json object mapper throws an exception'
+            def originalException = new JsonProcessingException('some-exception')
+            spiedObjectMapper.readValue(*_) >> {throw originalException}
+        when: 'a configurationManagementOperation is created and converted to JSON'
+            objectUnderTest.buildCreateOperationDetails(CREATE, path, resource)
+        then: 'the expected exception is throw and matches the original'
+            def thrown = thrown(NcmpException)
+            assert thrown.message.contains('Cannot convert Resource Object')
+            assert thrown.details.contains('some-exception')
+    }
+
+}
index d051657..96330ab 100644 (file)
@@ -20,7 +20,6 @@
 
 package org.onap.cps.ncmp.impl.data.policyexecutor
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import ch.qos.logback.classic.Level
 import ch.qos.logback.classic.Logger
 import ch.qos.logback.classic.spi.ILoggingEvent
@@ -30,10 +29,6 @@ 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.impl.inventory.models.YangModelCmHandle
-import org.onap.cps.ncmp.impl.provmns.RequestPathParameters
-import org.onap.cps.ncmp.impl.provmns.model.PatchItem
-import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf
-import org.onap.cps.utils.JsonObjectMapper
 import org.slf4j.LoggerFactory
 import org.springframework.http.HttpStatus
 import org.springframework.http.ResponseEntity
@@ -55,16 +50,12 @@ class PolicyExecutorSpec extends Specification {
     def mockRequestBodyUriSpec = Mock(WebClient.RequestBodyUriSpec)
     def mockResponseSpec = Mock(WebClient.ResponseSpec)
     def spiedObjectMapper = Spy(ObjectMapper)
-    def jsonObjectMapper = new JsonObjectMapper(spiedObjectMapper)
 
-    PolicyExecutor objectUnderTest = new PolicyExecutor(mockWebClient, spiedObjectMapper,jsonObjectMapper)
+    PolicyExecutor objectUnderTest = new PolicyExecutor(mockWebClient, spiedObjectMapper)
 
     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,105 +223,6 @@ class PolicyExecutorSpec extends Specification {
             thrownException.cause == webClientRequestException
     }
 
-    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: '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 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.createNestedMap(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 simple attribute with a trailing /' | '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: '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)'
-            def expectedJsonString = '{"permissionId":"Some Permission Id","changeRequestFormat":"cm-legacy","operations":[]}'
-            assert expectedJsonString == jsonObjectMapper.asJsonString(result)
-    }
-
-    def 'Build policy executor create operation details from ProvMnS request parameters where #scenario.'() {
-        given: 'a provMnsRequestParameter and a resource'
-            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":"myUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}'
-            assert jsonObjectMapper.asJsonString(result) == expectedJsonString
-        where:
-            scenario                   | objectClass        || changeRequestClassReference
-            'objectClass is populated' | 'someObjectClass'  || 'someObjectClass'
-            'objectClass is empty'     | ''                 || 'someClassName'
-            'objectClass is null'      | null               || 'someClassName'
-    }
-
-    def 'Build Policy Executor Operation Details with a exception during conversion'() {
-        given: 'a provMnsRequestParameter and a resource'
-            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')
-            spiedObjectMapper.readValue(*_) >> {throw originalException}
-        when: 'a configurationManagementOperation is created and converted to JSON'
-            objectUnderTest.buildCreateOperationDetails(CREATE, path, resource)
-        then: 'the expected exception is throw and matches the original'
-            noExceptionThrown()
-    }
-
     def mockResponse(mockResponseAsMap, httpStatus) {
         JsonNode jsonNode = spiedObjectMapper.readTree(spiedObjectMapper.writeValueAsString(mockResponseAsMap))
         def mono = Mono.just(new ResponseEntity<>(jsonNode, httpStatus))