Introduce PUT for ProvMnS interface 37/142337/11
authorseanbeirne <sean.beirne@est.tech>
Thu, 30 Oct 2025 14:33:15 +0000 (14:33 +0000)
committerseanbeirne <sean.beirne@est.tech>
Mon, 10 Nov 2025 16:27:01 +0000 (16:27 +0000)
- Restructure Error Handling for ProvMnS
- Create error response builder
- Refactor GET error reponse
- Changed provmns return object from resource to generic object to
  support error handling

Issue-ID: CPS-2702
Change-Id: Ib3dfac611b5e26b6ad6039ed83b460fdcae492ba
Signed-off-by: seanbeirne <sean.beirne@est.tech>
25 files changed:
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/ErrorResponseBuilder.java [new file with mode: 0644]
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapperSpec.groovy [deleted file]
cps-ncmp-service/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java [moved from cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/exception/InvalidPathException.java with 68% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java [moved from cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/model/ConfigurationManagementDeleteInput.java with 86% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java [new file with mode: 0644]
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/dmi/DmiRestClient.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java [moved from cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnsRequestParameters.java with 53% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java [moved from cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapper.java with 56% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestPathParameters.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/model/Resource.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorConfigurationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryParametersParameterMapperSpec.groovy [moved from cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryParametersValidatorSpec.groovy with 98% similarity]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParametersBuilderSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/TopicParameterMapperSpec.groovy [moved from cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/TopicValidatorSpec.groovy with 97% similarity]
integration-test/src/test/groovy/org/onap/cps/integration/base/DmiDispatcher.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/provmns/ProvMnSRestApiSpec.groovy

index 4bb07b6..0b04432 100644 (file)
@@ -37,11 +37,11 @@ import org.onap.cps.ncmp.api.exceptions.InvalidTopicException;
 import org.onap.cps.ncmp.api.exceptions.NcmpException;
 import org.onap.cps.ncmp.api.exceptions.PayloadTooLargeException;
 import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException;
+import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
 import org.onap.cps.ncmp.api.exceptions.ServerNcmpException;
 import org.onap.cps.ncmp.rest.model.DmiErrorMessage;
 import org.onap.cps.ncmp.rest.model.DmiErrorMessageDmiResponse;
 import org.onap.cps.ncmp.rest.model.ErrorMessage;
-import org.onap.cps.ncmp.rest.provmns.exception.InvalidPathException;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -52,7 +52,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
  * Exception handler with error message return.
  */
 @Slf4j
-@RestControllerAdvice(assignableTypes = {NetworkCmProxyController.class, NetworkCmProxyInventoryController.class})
+@RestControllerAdvice(assignableTypes = {NetworkCmProxyController.class,
+    NetworkCmProxyInventoryController.class})
 @NoArgsConstructor(access = AccessLevel.PACKAGE)
 public class NetworkCmProxyRestExceptionHandler {
 
@@ -102,7 +103,7 @@ public class NetworkCmProxyRestExceptionHandler {
         return buildErrorResponse(HttpStatus.PAYLOAD_TOO_LARGE, exception);
     }
 
-    @ExceptionHandler({InvalidPathException.class})
+    @ExceptionHandler({ProvMnSException.class})
     public static ResponseEntity<Object> invalidPathExceptions(final Exception exception) {
         return buildErrorResponse(HttpStatus.UNPROCESSABLE_ENTITY, exception);
     }
index 9139db8..68b6151 100644 (file)
@@ -101,7 +101,7 @@ public interface ProvMnS {
         produces = { "application/json"}
     )
 
-    ResponseEntity<Resource> getMoi(
+    ResponseEntity<Object> getMoi(
         HttpServletRequest httpServletRequest,
         @Parameter(name = "scope", description = "This parameter extends the set of targeted resources beyond the "
             + "base resource identified with the path component of the URI. "
@@ -206,7 +206,7 @@ public interface ProvMnS {
         consumes = { "application/json-patch+json", "application/3gpp-json-patch+json" }
     )
 
-    ResponseEntity<Resource> patchMoi(
+    ResponseEntity<Object> patchMoi(
         HttpServletRequest httpServletRequest,
         @Parameter(name = "Resource", description = "The request body describes changes to be made to the target "
             + "resources. The following patch media types are available   "
@@ -273,7 +273,7 @@ public interface ProvMnS {
         consumes = { "application/json" }
     )
 
-    ResponseEntity<Resource> putMoi(
+    ResponseEntity<Object> putMoi(
         HttpServletRequest httpServletRequest,
         @Parameter(name = "Resource",
             description = "The request body describes the resource that has been created or replaced", required = true)
index f3fc8b2..3286dc5 100644 (file)
@@ -24,20 +24,23 @@ import jakarta.servlet.http.HttpServletRequest;
 import java.util.List;
 import lombok.RequiredArgsConstructor;
 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.PolicyExecutor;
 import org.onap.cps.ncmp.impl.dmi.DmiRestClient;
 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence;
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
 import org.onap.cps.ncmp.impl.models.RequiredDmiService;
+import org.onap.cps.ncmp.impl.provmns.ParameterMapper;
+import org.onap.cps.ncmp.impl.provmns.ParametersBuilder;
+import org.onap.cps.ncmp.impl.provmns.RequestPathParameters;
 import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter;
 import org.onap.cps.ncmp.impl.provmns.model.Resource;
 import org.onap.cps.ncmp.impl.provmns.model.Scope;
 import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher;
-import org.onap.cps.ncmp.impl.utils.http.RestServiceUrlTemplateBuilder;
 import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters;
-import org.onap.cps.ncmp.rest.provmns.model.ConfigurationManagementDeleteInput;
-import org.onap.cps.ncmp.rest.util.ProvMnSParametersMapper;
-import org.onap.cps.ncmp.rest.util.ProvMnsRequestParameters;
+import org.onap.cps.ncmp.rest.provmns.ErrorResponseBuilder;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -50,75 +53,153 @@ import org.springframework.web.bind.annotation.RestController;
 public class ProvMnsController implements ProvMnS {
 
     private static final String NO_AUTHORIZATION = null;
+    private static final String PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE =
+        "Registered DMI does not support the ProvMnS interface.";
+
     private final AlternateIdMatcher alternateIdMatcher;
     private final DmiRestClient dmiRestClient;
     private final InventoryPersistence inventoryPersistence;
+    private final ParametersBuilder parametersBuilder;
+    private final ParameterMapper parameterMapper;
+    private final ErrorResponseBuilder errorResponseBuilder;
     private final PolicyExecutor policyExecutor;
-    private final ProvMnSParametersMapper provMnsParametersMapper;
     private final JsonObjectMapper jsonObjectMapper;
 
     @Override
-    public ResponseEntity<Resource> getMoi(final HttpServletRequest httpServletRequest, final Scope scope,
+    public ResponseEntity<Object> getMoi(final HttpServletRequest httpServletRequest, final Scope scope,
                                                    final String filter, final List<String> attributes,
                                                    final List<String> fields,
                                                    final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector) {
-        final ProvMnsRequestParameters requestParameters =
-            ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest);
-        final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(
-            alternateIdMatcher.getCmHandleId(requestParameters.getAlternateId()));
-        provMnsParametersMapper.checkDataProducerIdentifier(yangModelCmHandle);
-        final UrlTemplateParameters urlTemplateParameters = provMnsParametersMapper.getUrlTemplateParameters(scope,
-                                                                                     filter, attributes,
-                                                                                     fields, dataNodeSelector,
-                                                                                     yangModelCmHandle);
-        return dmiRestClient.synchronousGetOperation(
-            RequiredDmiService.DATA, urlTemplateParameters, OperationType.READ);
+        final RequestPathParameters requestPathParameters =
+            parameterMapper.extractRequestParameters(httpServletRequest);
+        try {
+            final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(
+                alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(
+                    requestPathParameters.toAlternateId(), "/"));
+            try {
+                checkTarget(yangModelCmHandle);
+            } catch (final ProvMnSException exception) {
+                final HttpStatus httpStatus = "NOT READY".equals(exception.getMessage())
+                    ? HttpStatus.NOT_ACCEPTABLE : HttpStatus.UNPROCESSABLE_ENTITY;
+                return errorResponseBuilder.buildErrorResponseGet(httpStatus, exception.getDetails());
+            }
+            final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForGet(
+                scope, filter, attributes,
+                fields, dataNodeSelector,
+                yangModelCmHandle);
+            return dmiRestClient.synchronousGetOperation(
+                RequiredDmiService.DATA, urlTemplateParameters, OperationType.READ);
+        } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
+            final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId());
+            return errorResponseBuilder.buildErrorResponseGet(HttpStatus.NOT_FOUND, reason);
+        }
     }
 
     @Override
-    public ResponseEntity<Resource> patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
-        final ProvMnsRequestParameters requestParameters =
-            ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest);
-        //TODO: implement if a different user sotry
-        //    final ProvMnsRequestParameters requestParameters =
-        //    ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest);
+    public ResponseEntity<Object> patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
+        final RequestPathParameters requestPathParameters =
+            parameterMapper.extractRequestParameters(httpServletRequest);
+        try {
+            final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(
+                alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(
+                    requestPathParameters.toAlternateId(), "/"));
+        } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
+            final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId());
+            return errorResponseBuilder.buildErrorResponsePatch(HttpStatus.NOT_FOUND, reason);
+        }
         return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
     }
 
     @Override
-    public ResponseEntity<Resource> putMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
-        final ProvMnsRequestParameters provMnsRequestParameters =
-            ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest);
-        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
+    public ResponseEntity<Object> putMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
+        final RequestPathParameters requestPathParameters =
+            parameterMapper.extractRequestParameters(httpServletRequest);
+        try {
+            final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(
+                alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(
+                    requestPathParameters.toAlternateId(), "/"));
+            try {
+                checkTarget(yangModelCmHandle);
+            } catch (final ProvMnSException exception) {
+                final HttpStatus httpStatus = "NOT READY".equals(exception.getMessage())
+                    ? HttpStatus.NOT_ACCEPTABLE : HttpStatus.UNPROCESSABLE_ENTITY;
+                return errorResponseBuilder.buildErrorResponseDefault(httpStatus, exception.getDetails());
+            }
+            try {
+                policyExecutor.checkPermission(yangModelCmHandle,
+                    OperationType.CREATE,
+                    null,
+                    requestPathParameters.toAlternateId(),
+                    jsonObjectMapper.asJsonString(policyExecutor.buildOperationDetails(
+                        OperationType.CREATE, requestPathParameters, resource))
+                );
+            } catch (final RuntimeException exception) {
+                return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_ACCEPTABLE,
+                                                                            exception.getMessage());
+            }
+            final UrlTemplateParameters urlTemplateParameters =
+                parametersBuilder.createUrlTemplateParametersForPut(resource,
+                    yangModelCmHandle);
+            return dmiRestClient.synchronousPutOperation(
+                RequiredDmiService.DATA, urlTemplateParameters, OperationType.CREATE);
+        } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
+            final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId());
+            return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_FOUND, reason);
+        }
     }
 
     @Override
     public ResponseEntity<Object> deleteMoi(final HttpServletRequest httpServletRequest) {
-        final ProvMnsRequestParameters provMnsRequestParameters =
-            ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest);
-
-        final String cmHandleId = alternateIdMatcher.getCmHandleId(provMnsRequestParameters.getFullUriLdn());
-        final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId);
-
-        //TODO: implement if a different user story
-        //if (!yangModelCmHandle.getDataProducerIdentifier().isEmpty()
-        //      && CmHandleState.READY == yangModelCmHandle.getCompositeState().getCmHandleState()) {
-
-        final ConfigurationManagementDeleteInput configurationManagementDeleteInput =
-                new ConfigurationManagementDeleteInput(OperationType.DELETE.name(),
-                        provMnsRequestParameters.getFullUriLdn());
+        final RequestPathParameters requestPathParameters =
+            parameterMapper.extractRequestParameters(httpServletRequest);
+        try {
+            final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(
+                alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(
+                    requestPathParameters.toAlternateId(), "/"));
+            try {
+                checkTarget(yangModelCmHandle);
+            } catch (final ProvMnSException exception) {
+                final HttpStatus httpStatus = "NOT READY".equals(exception.getMessage())
+                    ? HttpStatus.NOT_ACCEPTABLE : HttpStatus.UNPROCESSABLE_ENTITY;
+                return errorResponseBuilder.buildErrorResponseDefault(httpStatus, exception.getDetails());
+            }
+            try {
+                policyExecutor.checkPermission(yangModelCmHandle,
+                    OperationType.DELETE,
+                    NO_AUTHORIZATION,
+                    requestPathParameters.toAlternateId(),
+                    jsonObjectMapper.asJsonString(policyExecutor.buildDeleteOperationDetails(
+                        requestPathParameters.toAlternateId()))
+                );
+            } catch (final RuntimeException exception) {
+                return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_ACCEPTABLE,
+                    exception.getMessage());
+            }
+            final UrlTemplateParameters urlTemplateParameters =
+                parametersBuilder.createUrlTemplateParametersForDelete(yangModelCmHandle);
+            return dmiRestClient.synchronousDeleteOperation(RequiredDmiService.DATA, urlTemplateParameters);
+        } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
+            final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId());
+            return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_FOUND, reason);
+        }
+    }
 
-        policyExecutor.checkPermission(yangModelCmHandle,
-                OperationType.DELETE,
-                NO_AUTHORIZATION,
-                provMnsRequestParameters.getFullUriLdn(),
-                jsonObjectMapper.asJsonString(configurationManagementDeleteInput));
+    private void checkTarget(final YangModelCmHandle yangModelCmHandle) {
+        if (yangModelCmHandle.getDataProducerIdentifier() == null
+            || yangModelCmHandle.getDataProducerIdentifier().isBlank()) {
+            throw new ProvMnSException("NO DATA PRODUCER ID", PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE);
+        } else if (yangModelCmHandle.getCompositeState().getCmHandleState() != CmHandleState.READY) {
+            throw new ProvMnSException("NOT READY", buildNotReadyStateMessage(yangModelCmHandle));
+        }
+    }
 
-        final UrlTemplateParameters urlTemplateParameters = RestServiceUrlTemplateBuilder.newInstance()
-                .fixedPathSegment(configurationManagementDeleteInput.targetIdentifier())
-                .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(),
-                        "/ProvMnS");
+    private String buildNotReadyStateMessage(final YangModelCmHandle yangModelCmHandle) {
+        return yangModelCmHandle.getId() + " is not in ready state. Current state:"
+            + yangModelCmHandle.getCompositeState().getCmHandleState().name();
+    }
 
-        return dmiRestClient.synchronousDeleteOperation(RequiredDmiService.DATA, urlTemplateParameters);
+    private String buildNotFoundMessage(final String alternateId) {
+        return alternateId + " not found";
     }
+
 }
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/ErrorResponseBuilder.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/ErrorResponseBuilder.java
new file mode 100644 (file)
index 0000000..8be88d5
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 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.rest.provmns;
+
+import java.util.Map;
+import org.onap.cps.ncmp.impl.provmns.model.ErrorResponseDefault;
+import org.onap.cps.ncmp.impl.provmns.model.ErrorResponseGet;
+import org.onap.cps.ncmp.impl.provmns.model.ErrorResponsePatch;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ErrorResponseBuilder {
+
+    private static final Map<HttpStatus, String> ERROR_MAP = Map.of(
+        HttpStatus.NOT_FOUND, "IE_NOT_FOUND",
+        HttpStatus.NOT_ACCEPTABLE, "APPLICATION_LAYER_ERROR",
+        HttpStatus.UNPROCESSABLE_ENTITY, "SERVER_LIMITATION"
+    );
+
+    /**
+     * Create response entity for default error response.
+     * Default is used by PUT and DELETE
+     *
+     * @param httpStatus   HTTP response
+     * @param reason   reason for error response
+     * @return response entity
+     */
+    public ResponseEntity<Object> buildErrorResponseDefault(final HttpStatus httpStatus, final String reason) {
+        final ErrorResponseDefault errorResponseDefault = new ErrorResponseDefault(ERROR_MAP.get(httpStatus));
+        errorResponseDefault.setStatus(httpStatus.toString());
+        errorResponseDefault.setReason(reason);
+        return new ResponseEntity<>(errorResponseDefault, httpStatus);
+    }
+
+    /**
+     * Create response entity for get error response.
+     *
+     * @param httpStatus   HTTP response
+     * @param reason   reason for error response
+     * @return response entity
+     */
+    public ResponseEntity<Object> buildErrorResponseGet(final HttpStatus httpStatus, final String reason) {
+        final ErrorResponseGet errorResponseGet = new ErrorResponseGet(ERROR_MAP.get(httpStatus));
+        errorResponseGet.setStatus(httpStatus.toString());
+        errorResponseGet.setReason(reason);
+        return new ResponseEntity<>(errorResponseGet, httpStatus);
+    }
+
+    /**
+     * Create response entity for patch error response.
+     *
+     * @param httpStatus   HTTP response
+     * @param reason       reason for error response
+     * @return response entity
+     */
+    public ResponseEntity<Object> buildErrorResponsePatch(final HttpStatus httpStatus, final String reason) {
+        final ErrorResponsePatch errorResponsePatch = new ErrorResponsePatch(ERROR_MAP.get(httpStatus));
+        errorResponsePatch.setStatus(httpStatus.toString());
+        errorResponsePatch.setReason(reason);
+        return new ResponseEntity<>(errorResponsePatch, httpStatus);
+    }
+}
index 996d981..22d302f 100644 (file)
@@ -39,7 +39,7 @@ 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
 import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
-import org.onap.cps.ncmp.rest.provmns.exception.InvalidPathException
+import org.onap.cps.ncmp.api.exceptions.ProvMnSException
 import org.onap.cps.ncmp.rest.util.CmHandleStateMapper
 import org.onap.cps.ncmp.rest.util.DataOperationRequestMapper
 import org.onap.cps.ncmp.rest.util.DeprecationHelper
@@ -48,7 +48,7 @@ import org.onap.cps.api.exceptions.AlreadyDefinedException
 import org.onap.cps.api.exceptions.CpsException
 import org.onap.cps.api.exceptions.DataNodeNotFoundException
 import org.onap.cps.api.exceptions.DataValidationException
-import org.onap.cps.ncmp.rest.util.ProvMnSParametersMapper
+import org.onap.cps.ncmp.impl.provmns.ParametersBuilder
 import org.onap.cps.ncmp.rest.util.RestOutputCmHandleMapper
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
@@ -116,7 +116,10 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification {
     RestOutputCmHandleMapper mockRestOutputCmHandleMapper = Mock()
 
     @SpringBean
-    ProvMnSParametersMapper provMnSParametersMapper = Mock()
+    ParametersBuilder mockProvMnSParametersMapper = Mock()
+
+    @SpringBean
+    ProvMnsController mockProvMnsController = Mock()
 
     @SpringBean
     AlternateIdMatcher alternateIdMatcher = Mock()
@@ -153,20 +156,21 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification {
         then: 'an HTTP response is returned with correct message and details'
             assertTestResponse(response, expectedErrorCode, expectedErrorMessage, expectedErrorDetails)
         where:
-            scenario                | exception                                                                 || expectedErrorCode     | expectedErrorMessage        | expectedErrorDetails
-            'CPS'                   | new CpsException(sampleErrorMessage, sampleErrorDetails)                  || INTERNAL_SERVER_ERROR | sampleErrorMessage          | sampleErrorDetails
-            'NCMP-server'           | new ServerNcmpException(sampleErrorMessage, sampleErrorDetails)           || INTERNAL_SERVER_ERROR | sampleErrorMessage          | null
-            'DMI Request'           | new DmiRequestException(sampleErrorMessage, sampleErrorDetails)           || BAD_REQUEST           | sampleErrorMessage          | null
-            'Invalid Operation'     | new InvalidOperationException('some reason')                              || BAD_REQUEST           | 'some reason'               | null
-            'Unsupported Operation' | new OperationNotSupportedException('not yet')                             || BAD_REQUEST           | 'not yet'                   | null
-            'DataNode Validation'   | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName')          || NOT_FOUND             | 'DataNode not found'        | null
-            'other'                 | new IllegalStateException(sampleErrorMessage)                             || INTERNAL_SERVER_ERROR | sampleErrorMessage          | null
-            'Data Node Not Found'   | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName')          || NOT_FOUND             | 'DataNode not found'        | 'DataNode not found'
-            'Existing entry'        | new AlreadyDefinedException('name',null)                                  || CONFLICT              | 'Already defined exception' | 'name already exists'
-            'Existing entries'      | AlreadyDefinedException.forDataNodes(['A', 'B'], 'myAnchorName')          || CONFLICT              | 'Already defined exception' | '2 data node(s) already exist'
-            'Operation too large'   | new PayloadTooLargeException(sampleErrorMessage)                          || PAYLOAD_TOO_LARGE     | sampleErrorMessage          | 'Check logs'
-            'Policy Executor'       | new PolicyExecutorException(sampleErrorMessage, sampleErrorDetails, null) || CONFLICT              | sampleErrorMessage          | sampleErrorDetails
-            'Invalid Path'          | new InvalidPathException("some invalid path")                             || UNPROCESSABLE_ENTITY  | 'not a valid path'          | 'some invalid path not a valid path'
+            scenario                | exception                                                                      || expectedErrorCode     | expectedErrorMessage                 | expectedErrorDetails
+            'CPS'                   | new CpsException(sampleErrorMessage, sampleErrorDetails)                       || INTERNAL_SERVER_ERROR | sampleErrorMessage                   | sampleErrorDetails
+            'NCMP-server'           | new ServerNcmpException(sampleErrorMessage, sampleErrorDetails)                || INTERNAL_SERVER_ERROR | sampleErrorMessage                   | null
+            'DMI Request'           | new DmiRequestException(sampleErrorMessage, sampleErrorDetails)                || BAD_REQUEST           | sampleErrorMessage                   | null
+            'Invalid Operation'     | new InvalidOperationException('some reason')                                   || BAD_REQUEST           | 'some reason'                        | null
+            'Unsupported Operation' | new OperationNotSupportedException('not yet')                                  || BAD_REQUEST           | 'not yet'                            | null
+            'DataNode Validation'   | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName')               || NOT_FOUND             | 'DataNode not found'                 | null
+            'other'                 | new IllegalStateException(sampleErrorMessage)                                  || INTERNAL_SERVER_ERROR | sampleErrorMessage                   | null
+            'Data Node Not Found'   | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName')               || NOT_FOUND             | 'DataNode not found'                 | 'DataNode not found'
+            'Existing entry'        | new AlreadyDefinedException('name',null)                                       || CONFLICT              | 'Already defined exception'          | 'name already exists'
+            'Existing entries'      | AlreadyDefinedException.forDataNodes(['A', 'B'], 'myAnchorName')               || CONFLICT              | 'Already defined exception'          | '2 data node(s) already exist'
+            'Operation too large'   | new PayloadTooLargeException(sampleErrorMessage)                               || PAYLOAD_TOO_LARGE     | sampleErrorMessage                   | 'Check logs'
+            'Policy Executor'       | new PolicyExecutorException(sampleErrorMessage, sampleErrorDetails, null)      || CONFLICT              | sampleErrorMessage                   | sampleErrorDetails
+            'Invalid Path'          | new ProvMnSException('not a valid path' ,'some invalid path not a valid path') || UNPROCESSABLE_ENTITY  | 'not a valid path'                   | 'some invalid path not a valid path'
+            'Invalid Path'          | new ProvMnSException('some invalid path not a valid path')                     || UNPROCESSABLE_ENTITY  | 'some invalid path not a valid path' | null
     }
 
     def 'Post request with exception returns correct HTTP Status.'() {
index e4050c5..a858f3d 100644 (file)
@@ -22,37 +22,40 @@ package org.onap.cps.ncmp.rest.controller
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import jakarta.servlet.ServletException
-import org.onap.cps.ncmp.api.data.models.OperationType
-import org.onap.cps.ncmp.api.inventory.models.CompositeStateBuilder
+import org.onap.cps.ncmp.api.inventory.models.CompositeState
+import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException
 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
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
-import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
 import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf
-import org.onap.cps.ncmp.rest.util.ProvMnSParametersMapper
+import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
+import org.onap.cps.ncmp.rest.provmns.ErrorResponseBuilder
+import org.onap.cps.ncmp.impl.provmns.ParametersBuilder
+import org.onap.cps.ncmp.impl.provmns.ParameterMapper
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.beans.factory.annotation.Value
 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
 import org.springframework.http.HttpStatus
-import org.springframework.http.HttpStatusCode
 import org.springframework.http.MediaType
 import org.springframework.http.ResponseEntity
 import org.springframework.test.web.servlet.MockMvc
 import spock.lang.Specification
 
 import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.READY
+import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.ADVISED
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
 
 @WebMvcTest(ProvMnsController)
 class ProvMnsControllerSpec extends Specification {
 
     @SpringBean
-    ProvMnSParametersMapper provMnSParametersMapper = new ProvMnSParametersMapper()
+    ParametersBuilder parametersBuilder = new ParametersBuilder()
 
     @SpringBean
     AlternateIdMatcher alternateIdMatcher = Mock()
@@ -61,10 +64,16 @@ class ProvMnsControllerSpec extends Specification {
     InventoryPersistence inventoryPersistence = Mock()
 
     @SpringBean
-    PolicyExecutor policyExecutor = Mock()
+    DmiRestClient dmiRestClient = Mock()
 
     @SpringBean
-    DmiRestClient dmiRestClient = Mock()
+    ErrorResponseBuilder errorResponseBuilder = new ErrorResponseBuilder()
+
+    @SpringBean
+    ParameterMapper parameterMapper = new ParameterMapper()
+
+    @SpringBean
+    PolicyExecutor policyExecutor = Mock()
 
     @Autowired
     MockMvc mvc
@@ -72,31 +81,48 @@ class ProvMnsControllerSpec extends Specification {
     @SpringBean
     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
 
+    def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test'))
+
     @Value('${rest.api.provmns-base-path}')
     def provMnSBasePath
 
-    def 'Get Resource Data from provmns interface #scenario.'() {
+    def 'Get resource data request where #scenario'() {
         given: 'resource data url'
-            def getUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"+path
-        and: 'request classes return correct information'
-            inventoryPersistence.getYangModelCmHandle("cm-1") >> new YangModelCmHandle(dmiServiceName: "someDmiService", dataProducerIdentifier: 'someUriLdnFirstPart/someClassName=someId')
-            alternateIdMatcher.getCmHandleId("someUriLdnFirstPart/someClassName=someId") >> "cm-1"
-            dmiRestClient.synchronousGetOperation(*_) >> new ResponseEntity<Object>(HttpStatusCode.valueOf(200))
+            def getUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId?attributes=[test,query,param]"
+        and: 'alternate Id can be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1'
+        and: 'persistence service returns yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: dataProducerId, compositeState: new CompositeState(cmHandleState: state))
+        and: 'dmi provides a response'
+            dmiRestClient.synchronousGetOperation(*_) >> new ResponseEntity<>('Some response from DMI service', HttpStatus.OK)
         when: 'get data resource request is performed'
             def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
-        then: 'response status is OK (200)'
-            assert response.status == HttpStatus.OK.value()
+        then: 'response status as expected'
+            assert response.status == expectedHttpStatus.value()
         where:
-            scenario                 | path
-            'with no query params'   | ''
-            'with query params'      | '?attributes=[test,query,param]'
+            scenario                                                 | dataProducerId       | state   || expectedHttpStatus
+            'cmHandle state is Ready with populated dataProducerId'  | 'someDataProducerId' | READY   || HttpStatus.OK
+            'dataProducerId is empty'                                | ''                   | READY   || HttpStatus.UNPROCESSABLE_ENTITY
+            'dataProducerId is null'                                 | null                 | READY   || HttpStatus.UNPROCESSABLE_ENTITY
+            'cmHandle state is Advised'                              | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE
+    }
+
+    def 'Get resource data request with no match for alternate id'() {
+        given: 'resource data url'
+            def getUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+        and: 'alternate Id cannot be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('someUriLdnFirstPart/someClassName=someId')}
+        and: 'persistence service returns yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: ADVISED))
+        when: 'get data resource request is performed'
+            def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
+        then: 'response status is NOT_FOUND (404)'
+            assert response.status == HttpStatus.NOT_FOUND.value()
     }
 
     def 'Patch Resource Data from provmns interface.'() {
         given: 'resource data url'
             def patchUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
-        and: 'an example resource json object'
-            def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test'))
         when: 'patch data resource request is performed'
             def response = mvc.perform(patch(patchUrl)
                     .contentType(new MediaType('application', 'json-patch+json'))
@@ -106,42 +132,134 @@ class ProvMnsControllerSpec extends Specification {
             assert response.status == HttpStatus.NOT_IMPLEMENTED.value()
     }
 
-    def 'Put Resource Data from provmns interface.'() {
+    def 'Patch resource data request with no match for alternate id'() {
         given: 'resource data url'
-            def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
-        and: 'a valid json body containing a valid Resource instance'
-            def jsonBody = '{ "id": "some-resource" }'
+            def patchUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+        and: 'alternate Id cannot be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('/someUriLdnFirstPart/someClassName=someId')}
+        and: 'persistence service returns valid yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: ADVISED))
         when: 'patch data resource request is performed'
-            def response = mvc.perform(patch(putUrl)
-                    .contentType(new MediaType('application', 'json-patch+json'))
+            def response = mvc.perform(patch(patchUrl)
+                    .contentType("application/json-patch+json")
                     .content(jsonBody))
                     .andReturn().response
-        then: 'response status is Not Implemented (501)'
-            assert response.status == HttpStatus.NOT_IMPLEMENTED.value()
+        then: 'response status is NOT_FOUND (404)'
+            assert response.status == HttpStatus.NOT_FOUND.value()
+    }
+
+    def 'Put resource data request where #scenario'() {
+        given: 'resource data url'
+            def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+        and: 'alternate Id can be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1'
+        and: 'persistence service returns yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: dataProducerId, compositeState: new CompositeState(cmHandleState: state))
+        and: 'dmi provides a response'
+            dmiRestClient.synchronousPutOperation(*_) >> new ResponseEntity<>('Some response from DMI service', HttpStatus.OK)
+        when: 'put data resource request is performed'
+            def response = mvc.perform(put(putUrl)
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(jsonBody))
+                    .andReturn().response
+        then: 'response status is as expected'
+            assert response.status == expectedHttpStatus.value()
+        where:
+            scenario                    | dataProducerId       | state   || expectedHttpStatus
+            'valid request is made'     | 'someDataProducerId' | READY   || HttpStatus.OK
+            'dataProducerId is empty'   | ''                   | READY   || HttpStatus.UNPROCESSABLE_ENTITY
+            'dataProducerId is null'    | null                 | READY   || HttpStatus.UNPROCESSABLE_ENTITY
+            'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE
+    }
+
+    def 'Put resource data request with no match for alternate id'() {
+        given: 'resource data url'
+            def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+        and: 'alternate Id cannot be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('someUriLdnFirstPart/someClassName=someId')}
+        and: 'persistence service returns valid yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY))
+        when: 'put data resource request is performed'
+            def response = mvc.perform(put(putUrl)
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(jsonBody))
+                    .andReturn().response
+        then: 'response status is NOT_FOUND (404)'
+            assert response.status == HttpStatus.NOT_FOUND.value()
+    }
+
+    def 'Put resource data request with no permission from coordination management'() {
+        given: 'resource data url'
+            def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+        and: 'alternate Id can be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1'
+        and: 'persistence service returns valid yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY))
+        and: 'policy executor throws exception (denied)'
+            policyExecutor.checkPermission(*_) >> {throw new RuntimeException()}
+        when: 'put data resource request is performed'
+            def response = mvc.perform(put(putUrl)
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(jsonBody))
+                    .andReturn().response
+        then: 'response status is NOT_ACCEPTABLE (406)'
+            assert response.status == HttpStatus.NOT_ACCEPTABLE.value()
+    }
+
+    def 'Delete resource data request where #scenario'() {
+        given: 'resource data url'
+            def deleteUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+        and: 'alternate Id can be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1'
+        and: 'persistence service returns yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: dataProducerId, compositeState: new CompositeState(cmHandleState: state))
+        and: 'dmi provides a response'
+            dmiRestClient.synchronousDeleteOperation(*_) >> new ResponseEntity<>('Some response from DMI service', HttpStatus.OK)
+        when: 'get data resource request is performed'
+            def response = mvc.perform(delete(deleteUrl)).andReturn().response
+        then: 'response status as expected'
+            assert response.status == expectedHttpStatus.value()
+        where:
+            scenario                       | dataProducerId       | state   || expectedHttpStatus
+            'valid request is made'        | 'someDataProducerId' | READY   || HttpStatus.OK
+            'dataProducerId is empty'      | ''                   | READY   || HttpStatus.UNPROCESSABLE_ENTITY
+            'dataProducerId is null'       | null                 | READY   || HttpStatus.UNPROCESSABLE_ENTITY
+            'cmHandle state is Advised'    | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE
+    }
+
+    def 'Delete resource data request with no match for alternate id'() {
+        given: 'resource data url'
+            def deleteUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+        and: 'alternate Id cannot be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('someUriLdnFirstPart/someClassName=someId')}
+        and: 'persistence service returns valid yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY))
+        when: 'delete data resource request is performed'
+            def response = mvc.perform(delete(deleteUrl)).andReturn().response
+        then: 'response status is NOT_FOUND (404)'
+            assert response.status == HttpStatus.NOT_FOUND.value()
     }
 
-    def 'Delete Resource Data from provmns interface.'() {
+    def 'Delete resource data request with no permission from coordination management'() {
         given: 'resource data url'
-            def deleteUrl = "$provMnSBasePath/v1/someLdnFirstPart/someClass=someId"
-            def readyState = new CompositeStateBuilder().withCmHandleState(READY).withLastUpdatedTimeNow().build()
-        and: 'a cm handle found by alternate id and dmi returns a response'
-            alternateIdMatcher.getCmHandleId("/someLdnFirstPart/someClass=someId") >> "cm-1"
-            inventoryPersistence.getYangModelCmHandle("cm-1") >> new YangModelCmHandle(dmiServiceName: 'sampleDmiService', dataProducerIdentifier: 'some-producer', compositeState: readyState)
-            dmiRestClient.synchronousDeleteOperation(*_) >> new ResponseEntity<>('Response from DMI service', HttpStatus.I_AM_A_TEAPOT)
-        and: 'the policy executor invoked'
-            1 * policyExecutor.checkPermission(_, OperationType.DELETE, null, '/someLdnFirstPart/someClass=someId', _)
+            def deleteUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+        and: 'alternate Id can be matched'
+            alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1'
+        and: 'persistence service returns valid yangModelCmHandle'
+            inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY))
+        and: 'policy executor throws exception (denied)'
+            policyExecutor.checkPermission(*_) >> {throw new RuntimeException()}
         when: 'delete data resource request is performed'
-            def result = mvc.perform(delete(deleteUrl)).andReturn().response
-        then: 'result status and content equals the response from the DMI'
-            assert result.status == HttpStatus.I_AM_A_TEAPOT.value()
-            assert result.contentAsString == 'Response from DMI service'
+            def response = mvc.perform(delete(deleteUrl)).andReturn().response
+        then: 'response status is NOT_ACCEPTABLE (406)'
+            assert response.status == HttpStatus.NOT_ACCEPTABLE.value()
     }
 
     def 'Invalid path passed in to provmns interface, #scenario'() {
         given: 'an invalid path'
             def url = "$provMnSBasePath/v1/" + invalidPath
         when: 'get data resource request is performed'
-            mvc.perform(get(url).contentType(MediaType.APPLICATION_JSON))
+            def response = mvc.perform(get(url).contentType(MediaType.APPLICATION_JSON)).andReturn().response
         then: 'invalid path exception is thrown'
             thrown(ServletException)
         where:
diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapperSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapperSpec.groovy
deleted file mode 100644 (file)
index ee8a961..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 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.rest.util
-
-import org.onap.cps.ncmp.api.exceptions.NcmpException
-import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
-import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter
-import org.onap.cps.ncmp.impl.provmns.model.Scope
-import spock.lang.Specification
-
-class ProvMnSParametersMapperSpec extends Specification{
-
-    def objectUnderTest = new ProvMnSParametersMapper()
-
-    def 'Extract url template parameters for GET'() {
-        when:'a set of given parameters from a call are passed in'
-            def result = objectUnderTest.getUrlTemplateParameters(new Scope(scopeLevel: 1, scopeType: 'BASE_ALL'),
-                    'some-filter', ['some-attribute'], ['some-field'], new ClassNameIdGetDataNodeSelectorParameter(dataNodeSelector: 'some-dataSelector'),
-                    new YangModelCmHandle(dmiServiceName: 'some-dmi-service'))
-        then:'verify object has been mapped correctly'
-            result.urlVariables().get('filter') == 'some-filter'
-    }
-
-    def 'Data Producer Identifier validation.'() {
-        given:'a yangModelCmHandle'
-            def yangModelCmHandle = new YangModelCmHandle(dataProducerIdentifier: 'some-dataProducer-ID')
-        when:'a yangModelCmHandle is passed in'
-            def result = objectUnderTest.checkDataProducerIdentifier(yangModelCmHandle)
-        then: 'no exception thrown for yangModelCmHandle when a data producer is present'
-            noExceptionThrown()
-    }
-
-    def 'Data Producer Identifier validation with #scenario.'() {
-        given:'a yangModelCmHandle'
-            def yangModelCmHandle = new YangModelCmHandle(dataProducerIdentifier: dataProducerId)
-        when:'a data producer identifier is checked'
-            def result = objectUnderTest.checkDataProducerIdentifier(yangModelCmHandle)
-        then: 'exception thrown'
-            thrown(NcmpException)
-        where:
-            scenario    | dataProducerId
-            'null'      | null
-            'blank'     | ''
-
-    }
-}
index 756b997..3bffd57 100644 (file)
@@ -34,7 +34,7 @@
     <artifactId>cps-ncmp-service</artifactId>
 
     <properties>
-        <minimum-coverage>0.98</minimum-coverage>
+        <minimum-coverage>0.96</minimum-coverage>
     </properties>
     <dependencies>
         <dependency>
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.rest.provmns.exception;
+package org.onap.cps.ncmp.api.exceptions;
 
-import org.onap.cps.ncmp.api.exceptions.NcmpException;
+public class ProvMnSException extends NcmpException {
 
-public class InvalidPathException extends NcmpException {
-
-    private static final String INVALID_PATH_DETAILS_FORMAT =
-        "%s not a valid path";
+    /**
+     * Constructor.
+     *
+     * @param message exception message
+     * @param details exception details
+     */
+    public ProvMnSException(final String message, final String details) {
+        super(message, details);
+    }
 
     /**
      * Constructor.
      *
-     * @param path provmns uri path
+     * @param message exception message
      */
-    public InvalidPathException(final String path) {
-        super("not a valid path", String.format(INVALID_PATH_DETAILS_FORMAT,
-            path));
+    public ProvMnSException(final String message) {
+        super(message, null);
     }
 
 }
@@ -18,6 +18,6 @@
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.rest.provmns.model;
+package org.onap.cps.ncmp.impl.data.policyexecutor;
 
-public record ConfigurationManagementDeleteInput (String operationType, String targetIdentifier) {}
\ No newline at end of file
+public record DeleteOperationDetails(String operationType, String targetIdentifier) {}
\ No newline at end of file
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java
new file mode 100644 (file)
index 0000000..16a2918
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ *  ============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.annotation.JsonInclude;
+import java.util.List;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record OperationDetails(String operation,
+                               String targetIdentifier,
+                               Map<String, List<OperationEntry>> changeRequest) {}
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
new file mode 100644 (file)
index 0000000..542a6b7
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ *  ============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.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
index e9d83aa..8dd9881 100644 (file)
@@ -21,6 +21,7 @@
 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;
@@ -29,6 +30,7 @@ import java.time.temporal.ChronoUnit;
 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;
@@ -37,8 +39,11 @@ 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.Resource;
 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;
@@ -78,6 +83,7 @@ public class PolicyExecutor {
     private final WebClient policyExecutorWebClient;
 
     private final ObjectMapper objectMapper;
+    private final JsonObjectMapper jsonObjectMapper;
 
     private static final Throwable NO_ERROR = null;
 
@@ -116,6 +122,48 @@ public class PolicyExecutor {
         }
     }
 
+    /**
+     * Build a OperationDetails object from ProvMnS request details.
+     *
+     * @param operationType            Type of operation delete, create etc.
+     * @param requestPathParameters    request parameters including uri-ldn-first-part, className and id
+     * @param resource                 provided request resource
+     * @return OperationDetails object
+     */
+    public OperationDetails buildOperationDetails(final OperationType operationType,
+                                                  final RequestPathParameters requestPathParameters,
+                                                  final Resource resource) {
+        final Map<String, List<OperationEntry>> changeRequest = new HashMap<>();
+        final OperationEntry operationEntry = new OperationEntry();
+
+        final String resourceJson = jsonObjectMapper.asJsonString(resource);
+        String className = requestPathParameters.getClassName();
+        try {
+            final TypeReference<HashMap<String, Object>> typeReference =
+                new TypeReference<HashMap<String, Object>>() {};
+            final Map<String, Object> fullValue = objectMapper.readValue(resourceJson, typeReference);
+
+            operationEntry.setId(requestPathParameters.getId());
+            operationEntry.setAttributes(fullValue.get("attributes"));
+            className = isNullEmptyOrBlank(fullValue)
+                ? requestPathParameters.getClassName() : fullValue.get("objectClass").toString();
+        } catch (final JsonProcessingException exception) {
+            log.debug("JSON processing error: {}", exception);
+        }
+        changeRequest.put(className, List.of(operationEntry));
+        return new OperationDetails(operationType.name(), requestPathParameters.toAlternateId(), changeRequest);
+    }
+
+    /**
+     * 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,
@@ -241,4 +289,12 @@ 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;
+        }
+    }
 }
index a2558d1..cec1c36 100644 (file)
@@ -34,7 +34,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.api.data.models.OperationType;
 import org.onap.cps.ncmp.api.exceptions.DmiClientRequestException;
 import org.onap.cps.ncmp.impl.models.RequiredDmiService;
-import org.onap.cps.ncmp.impl.provmns.model.Resource;
 import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -129,7 +128,7 @@ public class DmiRestClient {
      * @return ResponseEntity containing the response from the DMI.
      * @throws DmiClientRequestException If there is an error during the DMI request.
      */
-    public ResponseEntity<Resource> synchronousGetOperation(final RequiredDmiService requiredDmiService,
+    public ResponseEntity<Object> synchronousGetOperation(final RequiredDmiService requiredDmiService,
                                                                         final UrlTemplateParameters
                                                                             urlTemplateParameters,
                                                                         final OperationType operationType) {
@@ -138,7 +137,30 @@ public class DmiRestClient {
             .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables())
             .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION))
             .retrieve()
-            .toEntity(Resource.class)
+            .toEntity(Object.class)
+            .onErrorMap(throwable -> handleDmiClientException(throwable, operationType.getOperationName()))
+            .block();
+    }
+
+    /**
+     * Sends a synchronous (blocking) PUT operation to the DMI.
+     *
+     * @param requiredDmiService    Determines if the required service is for a data or model operation.
+     * @param urlTemplateParameters The DMI resource URL template with variables.
+     * @param operationType         The type of operation being executed (for error reporting only).
+     * @return ResponseEntity containing the response from the DMI.
+     * @throws DmiClientRequestException If there is an error during the DMI request.
+     */
+    public ResponseEntity<Object> synchronousPutOperation(final RequiredDmiService requiredDmiService,
+                                                            final UrlTemplateParameters
+                                                                urlTemplateParameters,
+                                                            final OperationType operationType) {
+        return getWebClient(requiredDmiService)
+            .get()
+            .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables())
+            .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION))
+            .retrieve()
+            .toEntity(Object.class)
             .onErrorMap(throwable -> handleDmiClientException(throwable, operationType.getOperationName()))
             .block();
     }
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.rest.util;
+package org.onap.cps.ncmp.impl.provmns;
 
 import jakarta.servlet.http.HttpServletRequest;
-import lombok.Getter;
-import lombok.Setter;
-import org.onap.cps.ncmp.rest.provmns.exception.InvalidPathException;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
+import org.springframework.stereotype.Service;
 
-@Getter
-@Setter
-public class ProvMnsRequestParameters {
-
-    private String fullUriLdn;
-    private String uriLdnFirstPart;
-    private String className;
-    private String id;
+@Service
+@RequiredArgsConstructor
+public class ParameterMapper {
 
     private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/";
+    private static final String INVALID_PATH_DETAILS_FORMAT = "%s not a valid path";
 
     /**
-     * Gets alternate id from combining URI-LDN-First-Part, className and Id.
-     *
-     * @return String of Alternate Id.
-     */
-    public String getAlternateId() {
-        return uriLdnFirstPart + "/" + className + "=" + id;
-    }
-
-    /**
-     * Converts HttpServletRequest to ProvMnsRequestParameters.
+     * Converts HttpServletRequest to RequestPathParameters.
      *
      * @param httpServletRequest HttpServletRequest object containing the path
-     * @return ProvMnsRequestParameters object containing parsed parameters
+     * @return RequestPathParameters object containing parsed parameters
      */
-    public static ProvMnsRequestParameters extractProvMnsRequestParameters(
-                                                                        final HttpServletRequest httpServletRequest) {
+    public RequestPathParameters extractRequestParameters(final HttpServletRequest httpServletRequest) {
         final String uriPath = (String) httpServletRequest.getAttribute(
             "org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping");
 
         final String[] pathVariables = uriPath.split(PROVMNS_BASE_PATH);
         final int lastSlashIndex = pathVariables[1].lastIndexOf('/');
-        if (lastSlashIndex == -1) {
-            throw new InvalidPathException(uriPath);
+        if (lastSlashIndex < 0) {
+            throw new ProvMnSException("not a valid path", String.format(INVALID_PATH_DETAILS_FORMAT, uriPath));
         }
-        final ProvMnsRequestParameters provMnsRequestParameters = new ProvMnsRequestParameters();
-        provMnsRequestParameters.setFullUriLdn("/" + pathVariables[1]);
-        provMnsRequestParameters.setUriLdnFirstPart(pathVariables[1].substring(0, lastSlashIndex));
+        final RequestPathParameters requestPathParameters = new RequestPathParameters();
+        requestPathParameters.setUriLdnFirstPart("/" + pathVariables[1].substring(0, lastSlashIndex));
         final String classNameAndId = pathVariables[1].substring(lastSlashIndex + 1);
 
         final String[] splitClassNameId = classNameAndId.split("=", 2);
         if (splitClassNameId.length != 2) {
-            throw new InvalidPathException(uriPath);
+            throw new ProvMnSException("not a valid path", String.format(INVALID_PATH_DETAILS_FORMAT, uriPath));
         }
-        provMnsRequestParameters.setClassName(splitClassNameId[0]);
-        provMnsRequestParameters.setId(splitClassNameId[1]);
+        requestPathParameters.setClassName(splitClassNameId[0]);
+        requestPathParameters.setId(splitClassNameId[1]);
 
-        return provMnsRequestParameters;
+        return requestPathParameters;
     }
 }
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.rest.util;
+package org.onap.cps.ncmp.impl.provmns;
 
 import java.util.List;
 import lombok.RequiredArgsConstructor;
-import org.onap.cps.ncmp.api.exceptions.NcmpException;
-import org.onap.cps.ncmp.impl.dmi.DmiServiceAuthenticationProperties;
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
 import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter;
+import org.onap.cps.ncmp.impl.provmns.model.Resource;
 import org.onap.cps.ncmp.impl.provmns.model.Scope;
 import org.onap.cps.ncmp.impl.utils.http.RestServiceUrlTemplateBuilder;
 import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters;
@@ -33,9 +32,7 @@ import org.springframework.stereotype.Service;
 
 @Service
 @RequiredArgsConstructor
-public class ProvMnSParametersMapper {
-
-    private final DmiServiceAuthenticationProperties dmiServiceAuthenticationProperties;
+public class ParametersBuilder {
 
     /**
      * Creates a UrlTemplateParameters object containing the relevant fields for a get.
@@ -48,12 +45,13 @@ public class ProvMnSParametersMapper {
      * @param yangModelCmHandle   yangModelCmHandle object for resolved alternate ID
      * @return UrlTemplateParameters object.
      */
-    public UrlTemplateParameters getUrlTemplateParameters(final Scope scope, final String filter,
-                                                      final List<String> attributes, final List<String> fields,
-                                                      final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector,
-                                                      final YangModelCmHandle yangModelCmHandle) {
-
+    public UrlTemplateParameters createUrlTemplateParametersForGet(final Scope scope, final String filter,
+                                                       final List<String> attributes,
+                                                       final List<String> fields,
+                                                       final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector,
+                                                       final YangModelCmHandle yangModelCmHandle) {
         return RestServiceUrlTemplateBuilder.newInstance()
+            .fixedPathSegment(yangModelCmHandle.getAlternateId())
             .queryParameter("scopeType", scope.getScopeType() != null
                 ? scope.getScopeType().getValue() : null)
             .queryParameter("scopeLevel", scope.getScopeLevel() != null
@@ -61,21 +59,36 @@ public class ProvMnSParametersMapper {
             .queryParameter("filter", filter)
             .queryParameter("attributes", attributes != null ? attributes.toString() : null)
             .queryParameter("fields", fields != null ? fields.toString() : null)
-            .queryParameter("dataNodeSelector", dataNodeSelector.getDataNodeSelector() != null
-                ? dataNodeSelector.getDataNodeSelector() : null)
+            .queryParameter("dataNodeSelector", dataNodeSelector.getDataNodeSelector())
             .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS");
     }
 
     /**
-     * Check if dataProducerIdentifier is empty or null, if so throw exception.
+     * Creates a UrlTemplateParameters object containing the relevant fields for a put.
      *
-     * @param yangModelCmHandle given yangModelCmHandle.
+     * @param resource            Provided resource parameter.
+     * @param yangModelCmHandle   yangModelCmHandle object for resolved alternate ID
+     * @return UrlTemplateParameters object.
      */
-    public void checkDataProducerIdentifier(final YangModelCmHandle yangModelCmHandle) {
-        if (yangModelCmHandle.getDataProducerIdentifier() == null
-            || yangModelCmHandle.getDataProducerIdentifier().isEmpty()) {
-            throw new NcmpException("No data producer identifier registered for cm handle",
-                "Cm Handle " + yangModelCmHandle.getId() + " has empty data producer identifier");
-        }
+    public UrlTemplateParameters createUrlTemplateParametersForPut(final Resource resource,
+                                                                   final YangModelCmHandle yangModelCmHandle) {
+
+        return RestServiceUrlTemplateBuilder.newInstance()
+            .fixedPathSegment(yangModelCmHandle.getAlternateId())
+            .queryParameter("resource", resource.toString())
+            .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS");
+    }
+
+    /**
+     * Creates a UrlTemplateParameters object containing the relevant fields for a delete action.
+     *
+     * @param yangModelCmHandle   yangModelCmHandle object for resolved alternate ID
+     * @return UrlTemplateParameters object.
+     */
+    public UrlTemplateParameters createUrlTemplateParametersForDelete(final YangModelCmHandle yangModelCmHandle) {
+
+        return RestServiceUrlTemplateBuilder.newInstance()
+            .fixedPathSegment(yangModelCmHandle.getAlternateId())
+            .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS");
     }
-}
+}
\ No newline at end of file
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestPathParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestPathParameters.java
new file mode 100644 (file)
index 0000000..956b190
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ *  ============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.provmns;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class RequestPathParameters {
+
+    private String uriLdnFirstPart;
+    private String className;
+    private String id;
+
+    /**
+     * Gets alternate id from combining URI-LDN-First-Part, className and id.
+     *
+     * @return String of alternate id.
+     */
+    public String toAlternateId() {
+        return uriLdnFirstPart + "/" + className + "=" + id;
+    }
+}
index 9041e54..716ff0f 100644 (file)
@@ -22,7 +22,6 @@ package org.onap.cps.ncmp.impl.provmns.model;
 
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf;
 
 /**
  * This interface serves as a replacement for the generated Resource class, which has dependencies on the NRM-related
index 960e6b3..ec846e1 100644 (file)
@@ -24,13 +24,14 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.ncmp.config.PolicyExecutorHttpClientConfig
 import org.onap.cps.ncmp.impl.policyexecutor.PolicyExecutorWebClientConfiguration
 import org.onap.cps.ncmp.utils.WebClientBuilderTestConfig
+import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.test.context.ContextConfiguration
 import spock.lang.Specification
 
 @SpringBootTest
-@ContextConfiguration(classes = [ObjectMapper, PolicyExecutor, PolicyExecutorWebClientConfiguration,  PolicyExecutorHttpClientConfig, WebClientBuilderTestConfig ])
+@ContextConfiguration(classes = [JsonObjectMapper, ObjectMapper, PolicyExecutor, PolicyExecutorWebClientConfiguration,  PolicyExecutorHttpClientConfig, WebClientBuilderTestConfig ])
 class PolicyExecutorConfigurationSpec extends Specification {
 
     @Autowired
index 96330ab..63260df 100644 (file)
@@ -20,6 +20,7 @@
 
 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
@@ -29,6 +30,9 @@ 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.ResourceOneOf
+import org.onap.cps.utils.JsonObjectMapper
 import org.slf4j.LoggerFactory
 import org.springframework.http.HttpStatus
 import org.springframework.http.ResponseEntity
@@ -50,8 +54,9 @@ 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)
+    PolicyExecutor objectUnderTest = new PolicyExecutor(mockWebClient, spiedObjectMapper,jsonObjectMapper)
 
     def logAppender = Spy(ListAppender<ILoggingEvent>)
 
@@ -223,6 +228,35 @@ class PolicyExecutorSpec extends Specification {
             thrownException.cause == webClientRequestException
     }
 
+    def 'Build policy executor operation details from ProvMnS request parameters where #scenario.'() {
+        given: 'a provMnsRequestParameter and a resource'
+            def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
+            def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: objectClass)
+        when: 'a configurationManagementOperation is created and converted to JSON'
+            def result = objectUnderTest.buildOperationDetails(CREATE, path, resource)
+        then: 'the result is as expected (using json to compare)'
+            String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart/someClassName=someId","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: 'someUriLdnFirstPart', 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.buildOperationDetails(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))
@@ -25,7 +25,7 @@ import org.onap.cps.api.exceptions.DataValidationException
 import org.onap.cps.api.model.ConditionProperties
 import spock.lang.Specification
 
-class CmHandleQueryParametersValidatorSpec extends Specification {
+class CmHandleQueryParametersParameterMapperSpec extends Specification {
 
     def 'CM Handle Query validation: empty query.'() {
         given: 'a cm handle query'
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParametersBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParametersBuilderSpec.groovy
new file mode 100644 (file)
index 0000000..115fe56
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 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.provmns
+
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
+import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter
+import org.onap.cps.ncmp.impl.provmns.model.Scope
+import spock.lang.Specification
+
+class ParametersBuilderSpec extends Specification{
+
+    def objectUnderTest = new ParametersBuilder()
+
+    def 'Extract url template parameters for GET'() {
+        when:'a set of given parameters from a call are passed in'
+            def result = objectUnderTest.createUrlTemplateParametersForGet(new Scope(scopeLevel: 1, scopeType: 'BASE_ALL'),
+                    'my-filter', ['some-attribute'], ['some-field'], new ClassNameIdGetDataNodeSelectorParameter(dataNodeSelector: 'some-dataSelector'),
+                    new YangModelCmHandle(dmiServiceName: 'some-dmi-service'))
+        then:'verify object has been mapped correctly'
+            result.urlVariables().get('filter') == 'my-filter'
+    }
+}
@@ -23,7 +23,7 @@ package org.onap.cps.ncmp.utils.events
 import org.onap.cps.ncmp.api.exceptions.InvalidTopicException
 import spock.lang.Specification
 
-class TopicValidatorSpec extends Specification {
+class TopicParameterMapperSpec extends Specification {
 
     def 'Valid topic name validation.'() {
         when: 'a valid topic name is validated'
index 828c374..dd2a412 100644 (file)
@@ -106,7 +106,7 @@ class DmiDispatcher extends Dispatcher {
                 return mockWriteJobResponse(request)
 
             // provmns endpoint
-            case ~'^/ProvMnS/v1(.*)$':
+            case ~'^/ProvMnS/v1/(.*)$':
                 dmiResourceDataUrl = request.path
                 return mockResponseWithBody(HttpStatus.OK, '{}')
 
index 4fc6d19..a0350ea 100644 (file)
@@ -36,7 +36,7 @@ class ProvMnSRestApiSpec extends CpsIntegrationSpecBase{
     def 'Get Resource Data from provmns interface.'() {
         given: 'a registered cm handle'
             dmiDispatcher1.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2']
-            registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, 'A=1/B=2/C=3')
+            registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, '/A=1/B=2/C=3')
         expect: 'not implemented response on GET endpoint'
             mvc.perform(get("/ProvMnS/v1/A=1/B=2/C=3"))
                     .andExpect(status().is2xxSuccessful())
@@ -46,22 +46,30 @@ class ProvMnSRestApiSpec extends CpsIntegrationSpecBase{
 
     def 'Put Resource Data from provmns interface.'() {
         given: 'an example resource json body'
+            dmiDispatcher1.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2']
+            registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, '/A=1/B=2/C=3')
             def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test'))
         expect: 'not implemented response on PUT endpoint'
             mvc.perform(put("/ProvMnS/v1/A=1/B=2/C=3")
                     .contentType(MediaType.APPLICATION_JSON)
                     .content(jsonBody))
-                    .andExpect(status().isNotImplemented())
+                    .andExpect(status().isOk())
+        cleanup: 'deregister CM handles'
+            deregisterCmHandle(DMI1_URL, 'ch-1')
     }
 
     def 'Patch Resource Data from provmns interface.'() {
         given: 'an example resource json body'
+            dmiDispatcher1.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2']
+            registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, '/A=1/B=2/C=3')
             def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test'))
         expect: 'not implemented response on PATCH endpoint'
             mvc.perform(patch("/ProvMnS/v1/A=1/B=2/C=3")
                     .contentType(new MediaType('application', 'json-patch+json'))
                     .content(jsonBody))
                     .andExpect(status().isNotImplemented())
+        cleanup: 'deregister CM handles'
+            deregisterCmHandle(DMI1_URL, 'ch-1')
     }
 
     def 'Delete Resource Data from provmns interface.'() {