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;
private final ErrorResponseBuilder errorResponseBuilder;
private final PolicyExecutor policyExecutor;
private final JsonObjectMapper jsonObjectMapper;
+ private final OperationDetailsFactory operationDetailsFactory;
@Override
public ResponseEntity<Object> getMoi(final HttpServletRequest httpServletRequest,
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);
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);
NO_AUTHORIZATION,
requestPathParameters.toAlternateId(),
jsonObjectMapper.asJsonString(
- policyExecutor.buildDeleteOperationDetails(requestPathParameters.toAlternateId()))
+ operationDetailsFactory.buildDeleteOperationDetails(requestPathParameters.toAlternateId()))
);
final UrlTemplateParameters urlTemplateParameters =
parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters);
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
@SpringBean
DmiRestClient dmiRestClient = Mock()
+ @SpringBean
+ OperationDetailsFactory operationDetailsFactory = Mock()
+
@SpringBean
ErrorResponseBuilder errorResponseBuilder = new ErrorResponseBuilder()
--- /dev/null
+/*
+ * ============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;
+ }
+}
+
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
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;
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;
@RequiredArgsConstructor
public class PolicyExecutor {
- public static final String ATTRIBUTES_WITH_HASHTAG = "#/attributes";
-
@Value("${ncmp.policy-executor.enabled:false}")
private boolean enabled;
@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";
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.
}
}
- /**
- * 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,
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;
}
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;
- }
}
--- /dev/null
+/*
+ * ============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) {}
--- /dev/null
+/*
+ * ============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')
+ }
+
+}
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
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
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()
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))