import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdPatchDefaultResponse;
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.PatchItem;
import org.onap.cps.ncmp.impl.provmns.model.Resource;
import org.onap.cps.ncmp.impl.provmns.model.Scope;
import org.onap.cps.ncmp.rest.model.ErrorMessage;
* and the patch document included in the request message body.
*
* @param httpServletRequest (required)
- * @param resource The request body describes changes to be made to the target resources.
+ * @param patchItems The request body describes changes to be made to the target resources.
* The following patch media types are available
* - "application/json-patch+json" (RFC 6902)
* - "application/3gpp-json-patch+json" (TS 32.158) (required)
+ "resources. The following patch media types are available "
+ "- \"application/json-patch+json\" (RFC 6902) "
+ "- \"application/3gpp-json-patch+json\" (TS 32.158)", required = true) @Valid @RequestBody
- Resource resource
+ List<PatchItem> patchItems
);
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.PatchItem;
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;
? HttpStatus.NOT_ACCEPTABLE : HttpStatus.UNPROCESSABLE_ENTITY;
return errorResponseBuilder.buildErrorResponseGet(httpStatus, exception.getDetails());
}
- final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForGet(
+ final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForRead(
scope, filter, attributes,
fields, dataNodeSelector,
- yangModelCmHandle);
+ yangModelCmHandle, requestPathParameters);
return dmiRestClient.synchronousGetOperation(
- RequiredDmiService.DATA, urlTemplateParameters, OperationType.READ);
+ RequiredDmiService.DATA, urlTemplateParameters);
} catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId());
return errorResponseBuilder.buildErrorResponseGet(HttpStatus.NOT_FOUND, reason);
}
@Override
- public ResponseEntity<Object> patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
+ public ResponseEntity<Object> patchMoi(final HttpServletRequest httpServletRequest,
+ final List<PatchItem> patchItems) {
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.buildErrorResponsePatch(httpStatus, exception.getDetails());
+ }
+ try {
+ policyExecutor.checkPermission(yangModelCmHandle,
+ OperationType.CREATE,
+ NO_AUTHORIZATION,
+ requestPathParameters.toAlternateId(),
+ jsonObjectMapper.asJsonString(policyExecutor.buildPatchOperationDetails(
+ requestPathParameters, patchItems))
+ );
+ } catch (final RuntimeException exception) {
+ // Policy Executor Denied Execution
+ return errorResponseBuilder.buildErrorResponsePatch(HttpStatus.NOT_ACCEPTABLE, exception.getMessage());
+ }
+ final UrlTemplateParameters urlTemplateParameters =
+ parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters);
+ return dmiRestClient.synchronousPatchOperation(RequiredDmiService.DATA, patchItems,
+ urlTemplateParameters, httpServletRequest.getContentType());
} catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId());
return errorResponseBuilder.buildErrorResponsePatch(HttpStatus.NOT_FOUND, reason);
}
- return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
try {
policyExecutor.checkPermission(yangModelCmHandle,
OperationType.CREATE,
- null,
+ NO_AUTHORIZATION,
requestPathParameters.toAlternateId(),
- jsonObjectMapper.asJsonString(policyExecutor.buildOperationDetails(
+ jsonObjectMapper.asJsonString(policyExecutor.buildCreateOperationDetails(
OperationType.CREATE, requestPathParameters, resource))
);
} catch (final RuntimeException exception) {
exception.getMessage());
}
final UrlTemplateParameters urlTemplateParameters =
- parametersBuilder.createUrlTemplateParametersForPut(resource,
- yangModelCmHandle);
- return dmiRestClient.synchronousPutOperation(
- RequiredDmiService.DATA, urlTemplateParameters, OperationType.CREATE);
+ parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters);
+ return dmiRestClient.synchronousPutOperation(RequiredDmiService.DATA, resource, urlTemplateParameters);
} catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId());
return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_FOUND, reason);
exception.getMessage());
}
final UrlTemplateParameters urlTemplateParameters =
- parametersBuilder.createUrlTemplateParametersForDelete(yangModelCmHandle);
+ parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters);
return dmiRestClient.synchronousDeleteOperation(RequiredDmiService.DATA, urlTemplateParameters);
} catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId());
* Default is used by PUT and DELETE
*
* @param httpStatus HTTP response
- * @param reason reason for error response
+ * @param reason reason for error response
* @return response entity
*/
public ResponseEntity<Object> buildErrorResponseDefault(final HttpStatus httpStatus, final String reason) {
* Create response entity for get error response.
*
* @param httpStatus HTTP response
- * @param reason reason for error response
+ * @param reason reason for error response
* @return response entity
*/
public ResponseEntity<Object> buildErrorResponseGet(final HttpStatus httpStatus, final String reason) {
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.exceptions.ProvMnSException
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.provmns.model.PatchItem
+import org.onap.cps.ncmp.impl.provmns.model.PatchOperation
import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf
import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
import org.onap.cps.ncmp.rest.provmns.ErrorResponseBuilder
JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test'))
+ def patchJsonBody = jsonObjectMapper.asJsonString([new PatchItem(op: 'REMOVE', path: 'someUriLdnFirstPart')])
@Value('${rest.api.provmns-base-path}')
def provMnSBasePath
where:
scenario | dataProducerId | state || expectedHttpStatus
'cmHandle state is Ready with populated dataProducerId' | 'someDataProducerId' | READY || HttpStatus.OK
- 'dataProducerId is empty' | '' | READY || HttpStatus.UNPROCESSABLE_ENTITY
+ 'dataProducerId is blank' | ' ' | READY || HttpStatus.UNPROCESSABLE_ENTITY
'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY
'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE
}
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"
- when: 'patch data resource request is performed'
- def response = mvc.perform(patch(patchUrl)
+ def 'Patch request where #scenario'() {
+ given: 'provmns url'
+ def provmnsUrl = "$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.synchronousPatchOperation(*_) >> new ResponseEntity<>('Some response from DMI service', HttpStatus.OK)
+ when: 'patch request is performed'
+ def response = mvc.perform(patch(provmnsUrl)
.contentType(new MediaType('application', 'json-patch+json'))
- .content(jsonBody))
+ .content(patchJsonBody))
.andReturn().response
- then: 'response status is Not Implemented (501)'
- assert response.status == HttpStatus.NOT_IMPLEMENTED.value()
+ 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 blank' | ' ' | READY || HttpStatus.UNPROCESSABLE_ENTITY
+ 'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY
+ 'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE
}
- def 'Patch resource data request with no match for alternate id'() {
+ def 'Patch request with no match for alternate id'() {
given: 'resource data url'
- def patchUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
+ def provmnsUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
and: 'alternate Id cannot be matched'
- alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('/someUriLdnFirstPart/someClassName=someId')}
+ 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(patchUrl)
- .contentType("application/json-patch+json")
- .content(jsonBody))
+ inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY))
+ when: 'patch request is performed'
+ def response = mvc.perform(patch(provmnsUrl)
+ .contentType(new MediaType('application', 'json-patch+json'))
+ .content(patchJsonBody))
.andReturn().response
then: 'response status is NOT_FOUND (404)'
assert response.status == HttpStatus.NOT_FOUND.value()
}
+ def 'Patch request with no permission from coordination management'() {
+ given: 'resource data url'
+ def url = "$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(patch(url)
+ .contentType(new MediaType('application', 'json-patch+json'))
+ .content(patchJsonBody))
+ .andReturn().response
+ then: 'response status is NOT_ACCEPTABLE (406)'
+ assert response.status == HttpStatus.NOT_ACCEPTABLE.value()
+ }
+
def 'Put resource data request where #scenario'() {
given: 'resource data url'
def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"
where:
scenario | dataProducerId | state || expectedHttpStatus
'valid request is made' | 'someDataProducerId' | READY || HttpStatus.OK
- 'dataProducerId is empty' | '' | READY || HttpStatus.UNPROCESSABLE_ENTITY
+ 'dataProducerId is blank' | ' ' | READY || HttpStatus.UNPROCESSABLE_ENTITY
'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY
'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE
}
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
+ scenario | dataProducerId | state || expectedHttpStatus
+ 'valid request is made' | 'someDataProducerId' | READY || HttpStatus.OK
+ 'dataProducerId is blank' | ' ' | 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'() {
import java.util.Map;
@JsonInclude(JsonInclude.Include.NON_NULL)
-public record OperationDetails(String operation,
- String targetIdentifier,
- Map<String, List<OperationEntry>> changeRequest) {}
+public record CreateOperationDetails(String operation,
+ String targetIdentifier,
+ Map<String, List<OperationEntry>> changeRequest) {}
--- /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 java.util.List;
+
+public record PatchOperationsDetails (String permissionId, String changeRequestFormat, List<Object> operations) {}
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 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.Resource;
import org.onap.cps.ncmp.impl.utils.http.RestServiceUrlTemplateBuilder;
import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters;
}
/**
- * Build a OperationDetails object from ProvMnS request details.
+ * Build a PatchOperationDetails 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 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,
+ (Resource) patchItem.getValue()));
+ case REPLACE -> operations.add(
+ buildCreateOperationDetails(OperationType.UPDATE, requestPathParameters,
+ (Resource) patchItem.getValue()));
+ 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 resource provided request resource
- * @return OperationDetails object
+ * @return CreateOperationDetails object
*/
- public OperationDetails buildOperationDetails(final OperationType operationType,
- final RequestPathParameters requestPathParameters,
- final Resource resource) {
+ public CreateOperationDetails buildCreateOperationDetails(final OperationType operationType,
+ final RequestPathParameters requestPathParameters,
+ final Resource resource) {
final Map<String, List<OperationEntry>> changeRequest = new HashMap<>();
final OperationEntry operationEntry = new OperationEntry();
log.debug("JSON processing error: {}", exception);
}
changeRequest.put(className, List.of(operationEntry));
- return new OperationDetails(operationType.name(), requestPathParameters.toAlternateId(), changeRequest);
+ return new CreateOperationDetails(operationType.name(),
+ requestPathParameters.getUriLdnFirstPart(), changeRequest);
}
/**
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
* @param requestBodyAsJsonString JSON data body.
* @param operationType The type of operation being executed (for error reporting only).
* @param authorization Contents of the Authorization header, or null if not present.
- * @return ResponseEntity containing the response from the DMI.
+ * @return ResponseEntity containing the response from the DMI.
* @throws DmiClientRequestException If there is an error during the DMI request.
*/
public ResponseEntity<Object> synchronousPostOperation(final RequiredDmiService requiredDmiService,
* @param operationType An enumeration or object that holds information about the type of operation
* being performed.
* @param authorization The authorization token to be added to the request headers.
- * @return A Mono emitting the response entity containing the server's response.
+ * @return A Mono emitting the response entity containing the server's response.
*/
public Mono<ResponseEntity<Object>> asynchronousPostOperation(final RequiredDmiService requiredDmiService,
final UrlTemplateParameters urlTemplateParameters,
}
/**
- * Sends a synchronous (blocking) GET operation to the DMI.
+ * Sends a synchronous (blocking) GET operation to the DMI without error mapping.
*
* @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.
+ * @return ResponseEntity containing the response from the DMI.
*/
public ResponseEntity<Object> synchronousGetOperation(final RequiredDmiService requiredDmiService,
- final UrlTemplateParameters urlTemplateParameters,
- final OperationType operationType) {
+ final UrlTemplateParameters urlTemplateParameters) {
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();
}
/**
- * Sends a synchronous (blocking) PUT operation to the DMI.
+ * Sends a synchronous (blocking) PUT operation to the DMI without error mapping.
*
* @param requiredDmiService Determines if the required service is for a data or model operation.
+ * @param body resource object to be forwarded.
* @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.
+ * @return ResponseEntity containing the response from the DMI.
*/
public ResponseEntity<Object> synchronousPutOperation(final RequiredDmiService requiredDmiService,
+ final Object body,
+ final UrlTemplateParameters urlTemplateParameters) {
+ return getWebClient(requiredDmiService)
+ .put()
+ .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables())
+ .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION))
+ .bodyValue(body)
+ .retrieve()
+ .toEntity(Object.class)
+ .block();
+ }
+
+ /**
+ * Sends a synchronous (blocking) PATCH operation to the DMI without error mapping.
+ *
+ * @param requiredDmiService Determines if the required service is for a data or model operation.
+ * @param body object
+ * @param urlTemplateParameters The DMI resource URL template with variables.
+ * @param contentType Content type example: application/json
+ * @return ResponseEntity containing the response from the DMI.
+ */
+ public ResponseEntity<Object> synchronousPatchOperation(final RequiredDmiService requiredDmiService,
+ final Object body,
final UrlTemplateParameters urlTemplateParameters,
- final OperationType operationType) {
+ final String contentType) {
return getWebClient(requiredDmiService)
- .get()
+ .patch()
.uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables())
.headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION))
+ .contentType(MediaType.parseMediaType(contentType))
+ .bodyValue(body)
.retrieve()
.toEntity(Object.class)
- .onErrorMap(throwable -> handleDmiClientException(throwable, operationType.getOperationName()))
.block();
}
*
* @param urlTemplateParameters The URL template parameters for the DMI data endpoint.
* @param authorization The authorization token to be added to the request headers.
- * @return A Mono emitting the result of the request as a String.
+ * @return A Mono emitting the result of the request as a String.
*/
public Mono<String> asynchronousDmiDataRequest(final UrlTemplateParameters urlTemplateParameters,
final String authorization) {
}
/**
- * Sends a synchronous (blocking) DELETE operation to the DMI with a JSON body.
+ * Sends a synchronous (blocking) DELETE operation to the DMI with a JSON body without error mapping.
*
* @param requiredDmiService Determines if the required service is for a data or model operation.
* @param urlTemplateParameters The DMI resource URL template with variables.
.headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION))
.retrieve()
.toEntity(Object.class)
- .onErrorMap(throwable -> handleDmiClientException(throwable, OperationType.DELETE.getOperationName()))
.block();
}
package org.onap.cps.ncmp.impl.provmns;
+import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA;
+
import java.util.List;
import lombok.RequiredArgsConstructor;
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;
public class ParametersBuilder {
/**
- * Creates a UrlTemplateParameters object containing the relevant fields for a get.
+ * Creates a UrlTemplateParameters object containing the relevant fields for read requests.
*
* @param scope Provided className parameter.
* @param filter Filter string.
* @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID
* @return UrlTemplateParameters object.
*/
- public UrlTemplateParameters createUrlTemplateParametersForGet(final Scope scope, final String filter,
- final List<String> attributes,
- final List<String> fields,
- final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector,
- final YangModelCmHandle yangModelCmHandle) {
+ public UrlTemplateParameters createUrlTemplateParametersForRead(final Scope scope,
+ final String filter,
+ final List<String> attributes,
+ final List<String> fields,
+ final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector,
+ final YangModelCmHandle yangModelCmHandle,
+ final RequestPathParameters requestPathParameters) {
+ final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(DATA);
+ final String attributesString = removeBrackets(attributes);
+ final String fieldsString = removeBrackets(fields);
return RestServiceUrlTemplateBuilder.newInstance()
- .fixedPathSegment(yangModelCmHandle.getAlternateId())
- .queryParameter("scopeType", scope.getScopeType() != null
- ? scope.getScopeType().getValue() : null)
- .queryParameter("scopeLevel", scope.getScopeLevel() != null
- ? scope.getScopeLevel().toString() : null)
+ .fixedPathSegment(requestPathParameters.toAlternateId())
+ .queryParameter("scopeType", scope.getScopeType() != null ? scope.getScopeType().getValue() : null)
+ .queryParameter("scopeLevel", scope.getScopeLevel() != null ? scope.getScopeLevel().toString() : null)
.queryParameter("filter", filter)
- .queryParameter("attributes", attributes != null ? attributes.toString() : null)
- .queryParameter("fields", fields != null ? fields.toString() : null)
+ .queryParameter("attributes", attributesString.isBlank() ? null : attributesString)
+ .queryParameter("fields", fieldsString.isBlank() ? null : fieldsString)
.queryParameter("dataNodeSelector", dataNodeSelector.getDataNodeSelector())
- .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS");
+ .createUrlTemplateParameters(dmiServiceName, "ProvMnS");
}
/**
- * Creates a UrlTemplateParameters object containing the relevant fields for a put.
+ * Creates a UrlTemplateParameters object containing the relevant fields for a write requests.
*
- * @param resource Provided resource parameter.
- * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID
+ * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID
+ * @param requestPathParameters request path parameters.
* @return UrlTemplateParameters object.
*/
- public UrlTemplateParameters createUrlTemplateParametersForPut(final Resource resource,
- final YangModelCmHandle yangModelCmHandle) {
-
+ public UrlTemplateParameters createUrlTemplateParametersForWrite(final YangModelCmHandle yangModelCmHandle,
+ final RequestPathParameters requestPathParameters) {
+ final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(DATA);
return RestServiceUrlTemplateBuilder.newInstance()
- .fixedPathSegment(yangModelCmHandle.getAlternateId())
- .queryParameter("resource", resource.toString())
- .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS");
+ .fixedPathSegment(requestPathParameters.toAlternateId())
+ .createUrlTemplateParameters(dmiServiceName, "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");
+ private String removeBrackets(final List<String> queryParameterList) {
+ if (queryParameterList != null) {
+ final String queryParameterText = queryParameterList.toString();
+ return queryParameterText.substring(1, queryParameterText.length() - 1);
+ }
+ return "";
}
-}
\ No newline at end of file
+}
import com.fasterxml.jackson.databind.ObjectMapper
import org.onap.cps.ncmp.api.exceptions.NcmpException
import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException
+import org.onap.cps.ncmp.api.exceptions.ProvMnSException
import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
import org.onap.cps.ncmp.impl.provmns.RequestPathParameters
+import org.onap.cps.ncmp.impl.provmns.model.PatchItem
import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf
import org.onap.cps.utils.JsonObjectMapper
import org.slf4j.LoggerFactory
thrownException.cause == webClientRequestException
}
- def 'Build policy executor operation details from ProvMnS request parameters where #scenario.'() {
+ def 'Build policy executor patch operation details from ProvMnS request parameters where #scenario.'() {
+ given: 'a provMnsRequestParameter and a patchItem list'
+ def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
+ def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: objectClass)
+ def patchItemsList = [new PatchItem(op: 'ADD', 'path':'someUriLdnFirstPart', value: resource), new PatchItem(op: 'REPLACE', 'path':'someUriLdnFirstPart', value: resource)]
+ when: 'a configurationManagementOperation is created and converted to JSON'
+ def result = objectUnderTest.buildPatchOperationDetails(path, patchItemsList)
+ then: 'the result is as expected (using json to compare)'
+ def expectedJsonString = '{"permissionId":"Some Permission Id","changeRequestFormat":"cm-legacy","operations":[{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}},{"operation":"UPDATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}]}'
+ assert expectedJsonString == jsonObjectMapper.asJsonString(result)
+ where:
+ scenario | objectClass || changeRequestClassReference
+ 'objectClass is populated' | 'someObjectClass' || 'someObjectClass'
+ 'objectClass is empty' | '' || 'someClassName'
+ 'objectClass is null' | null || 'someClassName'
+ }
+
+ def 'Build policy executor patch operation details from ProvMnS request parameters with invalid op.'() {
+ given: 'a provMnsRequestParameter and a patchItem list'
+ def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
+ def patchItemsList = [new PatchItem(op: 'TEST', 'path':'someUriLdnFirstPart')]
+ 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: '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)
+ def result = objectUnderTest.buildCreateOperationDetails(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"]}]}}'
+ String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}'
assert jsonObjectMapper.asJsonString(result) == expectedJsonString
where:
scenario | objectClass || changeRequestClassReference
def originalException = new JsonProcessingException('some-exception')
spiedObjectMapper.readValue(*_) >> {throw originalException}
when: 'a configurationManagementOperation is created and converted to JSON'
- objectUnderTest.buildOperationDetails(CREATE, path, resource)
+ objectUnderTest.buildCreateOperationDetails(CREATE, path, resource)
then: 'the expected exception is throw and matches the original'
noExceptionThrown()
}
import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE
import static org.onap.cps.ncmp.api.data.models.OperationType.READ
-import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE
import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA
import static org.onap.cps.ncmp.impl.models.RequiredDmiService.MODEL
def result
switch(method) {
case 'get':
- result = objectUnderTest.synchronousGetOperation(DATA, urlTemplateParameters, READ)
+ result = objectUnderTest.synchronousGetOperation(DATA, urlTemplateParameters)
break
case 'post':
result = objectUnderTest.synchronousPostOperation(DATA, urlTemplateParameters, 'body', CREATE, '')
break
case 'put':
- result = objectUnderTest.synchronousPutOperation(DATA, urlTemplateParameters, UPDATE)
+ result = objectUnderTest.synchronousPutOperation(DATA, 'body', urlTemplateParameters)
+ break
+ case 'patch':
+ result = objectUnderTest.synchronousPatchOperation(DATA, 'body', urlTemplateParameters, 'application/json-patch+json')
break
case 'delete':
result = objectUnderTest.synchronousDeleteOperation(DATA, urlTemplateParameters)
then: 'the result has the same status code of 200'
assert result.statusCode.value() == 200
where: 'the following http methods are used'
- method << ['get', 'post', 'put', 'delete']
+ method << ['get', 'post', 'put', 'patch', 'delete']
}
- def 'Synchronous DMI #method request with invalid JSON.'() {
+ def 'Synchronous DMI post request with invalid JSON.'() {
given: 'Web Server wil return OK response'
mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.value)
.setBody('invalid-json:!!')
.addHeader('Content-Type', 'application/json'))
- when: 'synchronous #method request is attempted (on Model service this time for coverage on service selector)'
- switch(method) {
- case 'get':
- objectUnderTest.synchronousGetOperation(MODEL, urlTemplateParameters, READ)
- break;
- case 'post':
- objectUnderTest.synchronousPostOperation(MODEL, urlTemplateParameters, 'body', READ, 'some authorization')
- break
- case 'put':
- objectUnderTest.synchronousPutOperation(MODEL, urlTemplateParameters, UPDATE)
- break
- case 'delete':
- objectUnderTest.synchronousDeleteOperation(MODEL, urlTemplateParameters)
- }
+ when: 'synchronous post request is attempted (on Model service this time for coverage on service selector)'
+ objectUnderTest.synchronousPostOperation(MODEL, urlTemplateParameters, 'body', READ, 'some authorization')
then: 'a dmi client request exception is thrown with the correct error codes'
def thrown = thrown(DmiClientRequestException)
assert thrown.getHttpStatusCode() == 500
assert thrown.ncmpResponseStatus.code == '108'
- where: 'the following http methods are used'
- method << ['get','post','put','delete']
}
def 'DMI Request with non-responding server.'() {
given: 'the web server is shut down'
mockWebServer.shutdown()
- when: 'a synchronous read request is attempted'
- objectUnderTest.synchronousGetOperation(DATA, urlTemplateParameters, READ)
+ when: 'a synchronous post request is attempted'
+ objectUnderTest.synchronousPostOperation(DATA, urlTemplateParameters,'body', CREATE, '' )
then: 'a dmi client request exception is thrown with status code of 503 Service Unavailable'
def thrown = thrown(DmiClientRequestException)
assert thrown.getHttpStatusCode() == 503
def 'DMI Request with #scenario.'() {
given: 'the mock server or exception setup'
mockWebServer.enqueue(new MockResponse().setResponseCode(responseCode.value))
- when: 'a synchronous read request is attempted'
- objectUnderTest.synchronousGetOperation(DATA, urlTemplateParameters, READ)
+ when: 'a synchronous post request is attempted'
+ objectUnderTest.synchronousPostOperation(DATA, urlTemplateParameters,'body', CREATE, '')
then: 'a DMI client request exception is thrown with the right status'
def thrown = thrown(DmiClientRequestException)
assert thrown.httpStatusCode == expectedStatus
given: 'Mock a bad URL that causes IllegalArgumentException before HTTP call'
def badUrlParameters = new UrlTemplateParameters(':://bad url', [someParam: 'value'])
when: 'a synchronous request is attempted'
- objectUnderTest.synchronousGetOperation(DATA, badUrlParameters, READ)
+ objectUnderTest.synchronousGetOperation(DATA, badUrlParameters)
then: 'a invalid url exception is thrown (no mapping)'
thrown(InvalidUrlException)
}
--- /dev/null
+/*
+ * ============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 jakarta.servlet.http.HttpServletRequest
+import org.onap.cps.ncmp.api.exceptions.ProvMnSException
+import spock.lang.Specification
+
+class ParameterMapperSpec extends Specification {
+
+ def objectUnderTest = new ParameterMapper()
+
+ def mockHttpServletRequest = Mock(HttpServletRequest)
+
+ def uriPathAttributeName = 'org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping'
+
+ def 'Extract request parameters with url first part is a FDN with #scenario.'() {
+ given: 'a http request with all the required parts'
+ mockHttpServletRequest.getAttribute(uriPathAttributeName) >> (String) "ProvMnS/v1/${fdnPrefix}/myClass=myId"
+ when: 'the request parameters are extracted'
+ def result = objectUnderTest.extractRequestParameters(mockHttpServletRequest)
+ then: 'the Uri LDN first part is the fdnPrefix preceded with an extra "/"'
+ assert result.uriLdnFirstPart == '/' + fdnPrefix
+ and: 'the class name and id are mapped correctly'
+ assert result.className == 'myClass'
+ assert result.id == 'myId'
+ where: 'The following FDN prefixes are used'
+ scenario | fdnPrefix
+ '1 segment' | 'somePrefix'
+ 'multiple segments' | 'some/prefix'
+ 'empty segment' | ''
+ }
+
+ def 'Attempt to extract request parameters with #scenario.'() {
+ given: 'a http request with all the required parts'
+ mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path
+ when: 'attempt to extract the request parameters'
+ objectUnderTest.extractRequestParameters(mockHttpServletRequest)
+ then: 'a ProvMnS exception is thrown with message about the path being invalid'
+ def thrown = thrown(ProvMnSException)
+ assert thrown.message == 'not a valid path'
+ then: 'the details contain the faulty path'
+ assert thrown.details.contains(path)
+ where: 'the following paths are used'
+ scenario | path
+ 'No / in URI first part' | 'ProvMnS/v1/myClass=myId'
+ 'No = After (last) class name' | 'ProvMnS/v1/someOtherClass=someId/myClass'
+ }
+
+}
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'
+ def 'Create url template parameters for GET'() {
+ when: 'Creating URL parameters for GET with all possible items'
+ def result = objectUnderTest.createUrlTemplateParametersForRead(
+ new Scope(scopeLevel: 1, scopeType: 'BASE_SUBTREE'),
+ 'my filter',
+ ['my attributes'],
+ ['my fields'],
+ new ClassNameIdGetDataNodeSelectorParameter(dataNodeSelector: 'my dataNodeSelector'),
+ new YangModelCmHandle(dmiServiceName: 'myDmiService'),
+ new RequestPathParameters(uriLdnFirstPart:'myPathVariable=myPathValue', className: 'myClassName', id:'myId'))
+ then: 'the template has the correct correct'
+ assert result.urlTemplate.toString().startsWith('myDmiService/ProvMnS/v1/myPathVariable=myPathValue/myClassName=myId')
+ and: 'all url variable have been set correctly'
+ assert result.urlVariables.size() == 6
+ assert result.urlVariables.scopeLevel == '1'
+ assert result.urlVariables.scopeType == 'BASE_SUBTREE'
+ assert result.urlVariables.filter == 'my filter'
+ assert result.urlVariables.attributes == 'my attributes'
+ assert result.urlVariables.fields == 'my fields'
+ assert result.urlVariables.dataNodeSelector == 'my dataNodeSelector'
}
+
+ def 'Create url template parameters for GET without all optional parameters.'() {
+ when: 'Creating URL parameters for GET with null=values where possible'
+ def result = objectUnderTest.createUrlTemplateParametersForRead(
+ new Scope(),
+ null,
+ null,
+ null,
+ new ClassNameIdGetDataNodeSelectorParameter(dataNodeSelector: 'my dataNodeSelector'),
+ new YangModelCmHandle(dmiServiceName: 'myDmiService'),
+ new RequestPathParameters(uriLdnFirstPart:'myPathVariable=myPathValue', className: 'myClassName', id:'myId'))
+ then: 'The url variables contains only a data node selector'
+ assert result.urlVariables.size() == 1
+ assert result.urlVariables.keySet()[0] == 'dataNodeSelector'
+ }
+
+ def 'Create url template parameters for PUT and PATCH.'() {
+ when: 'Creating URL parameters for PUT (or PATCH)'
+ def result = objectUnderTest.createUrlTemplateParametersForWrite(
+ new YangModelCmHandle(dmiServiceName: 'myDmiService'),
+ new RequestPathParameters(uriLdnFirstPart:'myPathVariable=myPathValue', className: 'myClassName', id:'myId'))
+ then: 'the template has the correct correct'
+ assert result.urlTemplate.toString().startsWith('myDmiService/ProvMnS/v1/myPathVariable=myPathValue/myClassName=myId')
+ and: 'no url variables have been set'
+ assert result.urlVariables.isEmpty()
+ }
+
+
}
package org.onap.cps.integration.functional.ncmp.provmns
import org.onap.cps.integration.base.CpsIntegrationSpecBase
+import org.onap.cps.ncmp.impl.provmns.model.PatchItem
import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf
import org.springframework.http.MediaType
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'))
+ def jsonBody = jsonObjectMapper.asJsonString([new PatchItem(op: 'REMOVE', path: 'someUriLdnFirstPart')])
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())
+ .andExpect(status().isOk())
cleanup: 'deregister CM handles'
deregisterCmHandle(DMI1_URL, 'ch-1')
}