From faa3f04aa90768151f211e52caf94e1b7a9cde32 Mon Sep 17 00:00:00 2001 From: leventecsanyi Date: Tue, 25 Nov 2025 09:24:12 +0100 Subject: [PATCH] Improve mapping for Resource objects in PolicyExecutor - 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 --- .../ncmp/rest/controller/ProvMnsController.java | 13 +- .../rest/controller/ProvMnsControllerSpec.groovy | 4 + .../policyexecutor/OperationDetailsFactory.java | 200 +++++++++++++++++++++ .../impl/data/policyexecutor/OperationEntry.java | 17 +- .../impl/data/policyexecutor/PolicyExecutor.java | 152 +--------------- .../data/policyexecutor/ResourceObjectDetails.java | 23 +++ .../OperationDetailsFactorySpec.groovy | 146 +++++++++++++++ .../data/policyexecutor/PolicyExecutorSpec.groovy | 110 +----------- 8 files changed, 384 insertions(+), 281 deletions(-) create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/ResourceObjectDetails.java create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java index 76a2b57f56..9ea71de586 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java @@ -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 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); diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy index b1435ac38c..b06e407eee 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy @@ -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 index 0000000000..301979ee31 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java @@ -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 patchItems) { + final List 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> 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> typeReference = new TypeReference<>() {}; + final Map 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 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> operationEntriesPerObjectClass = new HashMap<>(); + final String className = requestPathParameters.getClassName(); + + final Map 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 createNestedMap(final PatchItem patchItem) { + final Map attributeHierarchyMap = new HashMap<>(); + Map 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 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; + } +} + diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java index 542a6b7692..184234f5d8 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java @@ -20,19 +20,4 @@ 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 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 b477954e05..c04ae62d6a 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 @@ -21,17 +21,14 @@ 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 patchItems) { - final List 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> changeRequest = new HashMap<>(); - final OperationEntry operationEntry = new OperationEntry(); - - final String resourceAsJson = jsonObjectMapper.asJsonString(resourceAsObject); - String className = requestPathParameters.getClassName(); - try { - final TypeReference> typeReference = - new TypeReference>() {}; - final Map 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> changeRequest = new HashMap<>(); - final OperationEntry operationEntry = new OperationEntry(); - final String className = requestPathParameters.getClassName(); - - final Map 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 createNestedMap(final PatchItem patchItem) { - final Map attributeHierarchyMap = new HashMap<>(); - Map 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 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 getSingleOperationAsMap(final YangModelCmHandle yangModelCmHandle, final OperationType operationType, final String resourceIdentifier, @@ -277,7 +139,7 @@ public class PolicyExecutor { private Object createBodyAsObject(final Map operationAsMap) { final Collection> operations = Collections.singletonList(operationAsMap); final Map 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 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 index 0000000000..e261cbf87c --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/ResourceObjectDetails.java @@ -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 index 0000000000..0264282672 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy @@ -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') + } + +} 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 d05165740f..96330ab8ec 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 @@ -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) 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)) -- 2.16.6