package org.onap.cps.ncmp.rest.controller;
import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE;
+import static org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetailsFactory.DELETE_OPERATION_DETAILS;
import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA;
-import static org.onap.cps.ncmp.impl.provmns.ParameterMapper.NO_OP;
+import static org.onap.cps.ncmp.impl.provmns.ParameterHelper.NO_OP;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.CONFLICT;
import static org.springframework.http.HttpStatus.GATEWAY_TIMEOUT;
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.CreateOperationDetails;
-import org.onap.cps.ncmp.impl.data.policyexecutor.DeleteOperationDetails;
import org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetails;
import org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetailsFactory;
import org.onap.cps.ncmp.impl.data.policyexecutor.PolicyExecutor;
import org.onap.cps.ncmp.impl.dmi.DmiRestClient;
import org.onap.cps.ncmp.impl.inventory.InventoryPersistence;
import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
-import org.onap.cps.ncmp.impl.provmns.ParameterMapper;
+import org.onap.cps.ncmp.impl.provmns.ParameterHelper;
import org.onap.cps.ncmp.impl.provmns.ParametersBuilder;
import org.onap.cps.ncmp.impl.provmns.RequestParameters;
import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter;
private final DmiRestClient dmiRestClient;
private final InventoryPersistence inventoryPersistence;
private final ParametersBuilder parametersBuilder;
- private final ParameterMapper parameterMapper;
private final PolicyExecutor policyExecutor;
private final JsonObjectMapper jsonObjectMapper;
private final OperationDetailsFactory operationDetailsFactory;
final List<String> attributes,
final List<String> fields,
final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector) {
- final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
+ final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest);
try {
final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters);
- final String targetFdn = requestParameters.toTargetFdn();
final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForRead(
- yangModelCmHandle, targetFdn, scope, filter, attributes, fields, dataNodeSelector);
+ yangModelCmHandle, requestParameters.fdn(), scope, filter, attributes, fields, dataNodeSelector);
return dmiRestClient.synchronousGetOperation(DATA, urlTemplateParameters);
} catch (final Exception exception) {
throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
+ maxNumberOfPatchOperations;
throw new ProvMnSException(httpServletRequest.getMethod(), PAYLOAD_TOO_LARGE, title, NO_OP);
}
- final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
+ final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest);
try {
final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters);
- checkPermissionForEachPatchItem(requestParameters, patchItems, yangModelCmHandle);
- final String targetFdn = requestParameters.toTargetFdn();
+ checkPermissionForEachPatchItem(requestParameters.fdn(), patchItems, yangModelCmHandle);
final UrlTemplateParameters urlTemplateParameters =
- parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
+ parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn());
return dmiRestClient.synchronousPatchOperation(DATA, patchItems, urlTemplateParameters,
httpServletRequest.getContentType());
} catch (final Exception exception) {
@Override
public ResponseEntity<Object> putMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
- final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
+ final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest);
try {
final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters);
- final CreateOperationDetails createOperationDetails =
- operationDetailsFactory.buildCreateOperationDetails(CREATE, requestParameters, resource);
- checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), createOperationDetails);
- final String targetFdn = requestParameters.toTargetFdn();
+ final OperationDetails operationDetails =
+ operationDetailsFactory.buildOperationDetails(CREATE, requestParameters, resource);
+ checkPermission(yangModelCmHandle, requestParameters.fdn(), operationDetails);
final UrlTemplateParameters urlTemplateParameters =
- parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
+ parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn());
return dmiRestClient.synchronousPutOperation(DATA, resource, urlTemplateParameters);
} catch (final Exception exception) {
throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
@Override
public ResponseEntity<Object> deleteMoi(final HttpServletRequest httpServletRequest) {
- final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest);
+ final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest);
try {
final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters);
- final DeleteOperationDetails deleteOperationDetails =
- operationDetailsFactory.buildDeleteOperationDetails(requestParameters.toTargetFdn());
- checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), deleteOperationDetails);
- final String targetFdn = requestParameters.toTargetFdn();
+ checkPermission(yangModelCmHandle, requestParameters.fdn(), DELETE_OPERATION_DETAILS);
final UrlTemplateParameters urlTemplateParameters =
- parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn);
+ parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn());
return dmiRestClient.synchronousDeleteOperation(DATA, urlTemplateParameters);
} catch (final Exception exception) {
throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP);
private YangModelCmHandle getAndValidateYangModelCmHandle(final RequestParameters requestParameters)
throws ProvMnSException {
- final String alternateId = requestParameters.toTargetFdn();
+ final String fdn = requestParameters.fdn();
try {
- final String cmHandleId = alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(alternateId, "/");
+ final String cmHandleId = alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(fdn, "/");
final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId);
if (!StringUtils.hasText(yangModelCmHandle.getDataProducerIdentifier())) {
- throw new ProvMnSException(requestParameters.getHttpMethodName(), UNPROCESSABLE_ENTITY,
+ throw new ProvMnSException(requestParameters.httpMethodName(), UNPROCESSABLE_ENTITY,
PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE, NO_OP);
}
if (yangModelCmHandle.getCompositeState().getCmHandleState() != CmHandleState.READY) {
final String title = yangModelCmHandle.getId() + " is not in READY state. Current state: "
+ yangModelCmHandle.getCompositeState().getCmHandleState().name();
- throw new ProvMnSException(requestParameters.getHttpMethodName(), NOT_ACCEPTABLE, title, NO_OP);
+ throw new ProvMnSException(requestParameters.httpMethodName(), NOT_ACCEPTABLE, title, NO_OP);
}
return yangModelCmHandle;
} catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) {
- final String title = alternateId + " not found";
- throw new ProvMnSException(requestParameters.getHttpMethodName(), NOT_FOUND, title, NO_OP);
+ throw new ProvMnSException(requestParameters.httpMethodName(), NOT_FOUND, fdn + " not found", NO_OP);
}
}
private void checkPermission(final YangModelCmHandle yangModelCmHandle,
- final String alternateId,
+ final String resourceIdentifier,
final OperationDetails operationDetails) {
final OperationType operationType = OperationType.fromOperationName(operationDetails.operation());
final String operationDetailsAsJson = jsonObjectMapper.asJsonString(operationDetails);
- policyExecutor.checkPermission(yangModelCmHandle, operationType, NO_AUTHORIZATION, alternateId,
+ policyExecutor.checkPermission(yangModelCmHandle, operationType, NO_AUTHORIZATION, resourceIdentifier,
operationDetailsAsJson);
}
- private void checkPermissionForEachPatchItem(final RequestParameters requestParameters,
+ private void checkPermissionForEachPatchItem(final String baseFdn,
final List<PatchItem> patchItems,
final YangModelCmHandle yangModelCmHandle) {
+ int patchItemCounter = 0;
for (final PatchItem patchItem : patchItems) {
+ final String extendedPath = baseFdn + patchItem.getPath();
+ final RequestParameters requestParameters = ParameterHelper.createRequestParametersForPatch(extendedPath);
final OperationDetails operationDetails =
operationDetailsFactory.buildOperationDetails(requestParameters, patchItem);
try {
- checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), operationDetails);
+ checkPermission(yangModelCmHandle, requestParameters.fdn(), operationDetails);
+ patchItemCounter++;
} catch (final Exception exception) {
final String httpMethodName = "PATCH";
- throw toProvMnSException(httpMethodName, exception, patchItem.getOp().getValue());
+ throw toProvMnSException(httpMethodName, exception, "/" + patchItemCounter);
}
}
}
package org.onap.cps.ncmp.rest.controller;
-import static org.onap.cps.ncmp.impl.provmns.ParameterMapper.NO_OP;
+import static org.onap.cps.ncmp.impl.provmns.ParameterHelper.NO_OP;
import java.util.Map;
import java.util.Objects;
import com.fasterxml.jackson.databind.ObjectMapper
import io.netty.handler.timeout.TimeoutException
+import org.onap.cps.api.exceptions.DataValidationException
+import org.onap.cps.ncmp.api.data.models.OperationType
import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException
import org.onap.cps.ncmp.api.inventory.models.CompositeState
import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException
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.ParameterMapper
import org.onap.cps.ncmp.impl.provmns.ParametersBuilder
import org.onap.cps.ncmp.impl.provmns.model.PatchItem
import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
import static org.springframework.http.HttpStatus.NOT_FOUND
import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE
-import static org.springframework.http.HttpStatus.SEE_OTHER
import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY
import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
@Autowired
OperationDetailsFactory operationDetailsFactory
- @SpringBean
- ParameterMapper parameterMapper = new ParameterMapper()
-
@SpringBean
PolicyExecutor mockPolicyExecutor = Mock()
ObjectMapper objectMapper = new ObjectMapper()
@SpringBean
- JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
+ JsonObjectMapper spiedJsonObjectMapper = Spy(new JsonObjectMapper(objectMapper))
- static def resourceAsJson = '{"id":"test"}'
+ static def resourceAsJson = '{"id":"test", "objectClass": "Test", "attributes": { "attr1": "value1"} }'
static def validCmHandle = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY))
static def cmHandleWithoutDataProducer = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', compositeState: new CompositeState(cmHandleState: READY))
static def cmHandleNotReady = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: ADVISED))
static def patchMediaType = new MediaType('application', 'json-patch+json')
static def patchMediaType3gpp = new MediaType('application', '3gpp-json-patch+json')
- static def patchJsonBody = '[{"op":"replace","path":"className/attributes/attr1","value":{"id":"test"}}]'
- static def patchJsonBody3gpp = '[{"op":"replace","path":"className#/attributes/attr1","value":{"id":"test"}}]'
- static def patchJsonBodyInvalid = '[{"op":"replace","path":"/test","value":"INVALID"}]'
+ static def patchJsonBody = '[{"op":"replace","path":"/child=id2/attributes","value":{"attr1":"test"}}]'
+ static def patchJsonBody3gpp = '[{"op":"replace","path":"/child=id2#/attributes/attr1","value":"test"}]'
+ static def patchJsonBodyInvalid = '[{"op":"replace","path":"/test","value":{}}]'
+
+ static def expectedDeleteOperationDetails = '{"operation":"delete","targetIdentifier":"","changeRequest":{}}'
@Value('${rest.api.provmns-base-path}')
def provMnSBasePath
int maxNumberOfPatchOperations
static def NO_CONTENT = ''
- static def STATUS_NOT_RELEVANT = SEE_OTHER
def 'Get resource data #scenario.'() {
given: 'resource data url'
new Exception("my message") || INTERNAL_SERVER_ERROR | 'APPLICATION_LAYER_ERROR' | 'my message'
}
-
def 'Get resource data request with invalid URL: #scenario.'() {
given: 'resource data url'
def getUrl = "$provMnSBasePath/$version/$fdn$queryParameter"
def provmnsUrl = "$provMnSBasePath/v1/myClass=id1"
and: 'alternate Id can be matched'
mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+ and: 'resource id for policy executor points to child node'
+ def expectedResourceIdForPolicyExecutor = '/myClass=id1/child=id2'
+ and: 'operation details has correct class and attributes, target identifier points to parent'
+ def expectedOperationDetails = '{"operation":"update","targetIdentifier":"/myClass=id1","changeRequest":{"child":[{"id":"id2","attributes":{"attr1":"test"}}]}}'
and: 'persistence service returns yangModelCmHandle'
mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
and: 'dmi provides a response'
.content(jsonBody))
.andReturn().response
then: 'response status is the same as what DMI gave'
- assert response.status == expectedRespinsStatusFromProvMnS.value()
+ assert response.status == expectedResponseStatusFromProvMnS.value()
and: 'the response contains the expected content'
- assert response.contentAsString.contains(expectedResponseContent)
+ assert response.contentAsString.contains('content from DMI')
+ and: 'policy executor was invoked with the expected parameters'
+ 1 * mockPolicyExecutor.checkPermission(_, OperationType.UPDATE, _, expectedResourceIdForPolicyExecutor, expectedOperationDetails)
where: 'following scenarios are applied'
- scenario | contentMediaType | jsonBody | responseStatusFromDmi || expectedResponseContent | expectedRespinsStatusFromProvMnS
- 'happy flow 3gpp' | patchMediaType3gpp | patchJsonBody3gpp | OK || 'content from DMI' | OK
- 'happy flow' | patchMediaType | patchJsonBody | OK || 'content from DMI' | OK
- 'error from DMI' | patchMediaType | patchJsonBody | I_AM_A_TEAPOT || 'content from DMI' | I_AM_A_TEAPOT
- 'invalid Json' | patchMediaType | patchJsonBodyInvalid | STATUS_NOT_RELEVANT || '"type":"VALIDATION_ERROR"' | BAD_REQUEST
- 'malformed Json' | patchMediaType | '{malformed]' | STATUS_NOT_RELEVANT || NO_CONTENT | BAD_REQUEST
+ scenario | contentMediaType | jsonBody | responseStatusFromDmi || expectedResponseStatusFromProvMnS
+ 'happy flow 3gpp' | patchMediaType3gpp | patchJsonBody3gpp | OK || OK
+ 'happy flow' | patchMediaType | patchJsonBody | OK || OK
+ 'error from DMI' | patchMediaType | patchJsonBody | I_AM_A_TEAPOT || I_AM_A_TEAPOT
+ }
+
+ def 'Attempt Patch request with malformed json.'() {
+ given: 'provmns url'
+ def provmnsUrl = "$provMnSBasePath/v1/myClass=id1"
+ and: 'alternate Id can be matched'
+ mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+ and: 'persistence service returns yangModelCmHandle'
+ mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
+ when: 'patch request is performed'
+ def response = mvc.perform(patch(provmnsUrl)
+ .contentType(patchMediaType)
+ .content('{malformed}'))
+ .andReturn().response
+ then: 'response status is Bad Request'
+ assert response.status == BAD_REQUEST.value()
+ and: 'the response content is empty'
+ assert response.contentAsString.isEmpty()
+ }
+
+ def 'Attempt Patch request with json exception during processing.'() {
+ given: 'provmns url'
+ def provmnsUrl = "$provMnSBasePath/v1/myClass=id1"
+ and: 'alternate Id can be matched'
+ mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+ and: 'persistence service returns yangModelCmHandle'
+ mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
+ and:
+ spiedJsonObjectMapper.asJsonString(_) >> { throw new DataValidationException('my message','some details') }
+ when: 'patch request is performed'
+ def response = mvc.perform(patch(provmnsUrl)
+ .contentType(patchMediaType)
+ .content(patchJsonBody))
+ .andReturn().response
+ then: 'response status is Bad Request'
+ assert response.status == BAD_REQUEST.value()
+ and: 'the response contains the correct type and original exception message'
+ assert response.contentAsString.contains('"type":"VALIDATION_ERROR"')
+ assert response.contentAsString.contains('my message')
+ }
+
+ def 'Patch remove request.'() {
+ given: 'resource data url'
+ def url = "$provMnSBasePath/v1/myClass=id1"
+ and: 'alternate Id can be matched'
+ mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+ and: 'persistence service returns valid yangModelCmHandle'
+ mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
+ def expectedResourceIdentifier = '/myClass=id1/childClass=1/grandchildClass=1'
+ when: 'patch data resource request is performed'
+ def response = mvc.perform(patch(url)
+ .contentType(patchMediaType)
+ .content('[{"op":"remove","path":"/childClass=1/grandchildClass=1"}]'))
+ .andReturn().response
+ then: 'response status is OK'
+ assert response.status == OK.value()
+ and: 'Policy Executor was invoked with correct details'
+ 1 * mockPolicyExecutor.checkPermission(_, OperationType.DELETE, _, expectedResourceIdentifier, expectedDeleteOperationDetails)
}
def 'Patch request with no permission from Coordination Management (aka Policy Executor).'() {
assert response.status == CONFLICT.value()
and: 'response contains the correct type'
assert response.contentAsString.contains('"type":"APPLICATION_LAYER_ERROR"')
- and: 'response contains the bad operation'
- assert response.contentAsString.contains('"badOp":"replace"')
+ and: 'response contains the bad operation index'
+ assert response.contentAsString.contains('"badOp":"/0"')
and: 'response contains the message from Policy Executor (as title)'
assert response.contentAsString.contains('"title":"denied for test"')
}
for (def i = 0; i <= maxNumberOfPatchOperations; i++) {
patchItems.add(new PatchItem(op: 'REMOVE', path: 'somePath'))
}
- def patchItemsJsonRequestBody = jsonObjectMapper.asJsonString(patchItems)
+ def patchItemsJsonRequestBody = spiedJsonObjectMapper.asJsonString(patchItems)
when: 'patch data resource request is performed'
def response = mvc.perform(patch(url)
.contentType(patchMediaType)
def 'Put resource data request with #scenario.'() {
given: 'resource data url'
- def putUrl = "$provMnSBasePath/v1/myClass=id1"
+ def putUrl = "$provMnSBasePath/v1/myClass=id1/childClass=1/grandChildClass=2"
and: 'alternate Id can be matched'
- mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+ mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1/childClass=1/grandChildClass=2', "/") >> 'ch-1'
and: 'persistence service returns yangModelCmHandle'
mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
and: 'dmi provides a response'
mockDmiRestClient.synchronousPutOperation(*_) >> new ResponseEntity<>(responseContentFromDmi, responseStatusFromDmi)
+ and: 'The expected resource identifier for policy executor is the FDN to grandchild'
+ def expectedResourceIdentifier = '/myClass=id1/childClass=1/grandChildClass=2'
+ and: 'The change request target identifier is the FDN to parent and last class as object name in change request'
+ def expectedChangeRequest = '{"operation":"create","targetIdentifier":"/myClass=id1/childClass=1","changeRequest":{"grandChildClass":[{"id":"2","attributes":{"attr1":"value1"}}]}}'
when: 'put data resource request is performed'
def response = mvc.perform(put(putUrl)
.contentType(MediaType.APPLICATION_JSON)
assert response.status == responseStatusFromDmi.value()
and: 'the content is whatever the DMI returned'
assert response.contentAsString == responseContentFromDmi
+ and: 'The policy executor was invoked with the expected parameters'
+ 1 * mockPolicyExecutor.checkPermission(_, OperationType.CREATE, _, expectedResourceIdentifier, expectedChangeRequest)
where: 'following responses returned by DMI'
scenario | responseStatusFromDmi | responseContentFromDmi
'happy flow' | OK | 'content from DMI'
def 'Delete resource data request with #scenario.'() {
given: 'resource data url'
- def deleteUrl = "$provMnSBasePath/v1/myClass=id1"
+ def deleteUrl = "$provMnSBasePath/v1/myClass=id1/childClass=1/grandChildClass=2"
and: 'alternate Id can be matched'
- mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1'
+ mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1/childClass=1/grandChildClass=2', "/") >> 'ch-1'
and: 'persistence service returns yangModelCmHandle'
mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle
and: 'dmi provides a response'
assert response.status == responseStatusFromDmi.value()
and: 'the content is whatever the DMI returned'
assert response.contentAsString == responseContentFromDmi
+ and: 'Policy Executor was invoked with correct resource identifier and almost empty operation details (not used for delete!)'
+ 1 * mockPolicyExecutor.checkPermission(_, OperationType.DELETE, _, '/myClass=id1/childClass=1/grandChildClass=2', expectedDeleteOperationDetails)
where: 'following responses returned by DMI'
scenario | responseStatusFromDmi | responseContentFromDmi
'happy flow' | OK | 'content from DMI'
+++ /dev/null
-/*
- * ============LICENSE_START=======================================================
- * Copyright (C) 2025 OpenInfra Foundation Europe
- * ================================================================================
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.ncmp.impl.data.policyexecutor;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import java.util.List;
-import java.util.Map;
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public record CreateOperationDetails(String operation,
- String targetIdentifier,
- Map<String, List<OperationEntry>> changeRequest) implements OperationDetails {}
+++ /dev/null
-/*
- * ============LICENSE_START=======================================================
- * Copyright (C) 2025 OpenInfra Foundation Europe
- * ================================================================================
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.ncmp.impl.data.policyexecutor;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public record DeleteOperationDetails(String operation,
- String targetIdentifier) implements OperationDetails {}
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2025 OpenInfra Foundation Europe
+ * Copyright (C) 2025-2026 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.
package org.onap.cps.ncmp.impl.data.policyexecutor;
-public interface OperationDetails {
- String operation();
-}
+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) {}
package org.onap.cps.ncmp.impl.data.policyexecutor;
+import static java.util.Collections.emptyMap;
import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE;
-import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE;
import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE;
-import com.google.common.base.Strings;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.ncmp.api.data.models.OperationType;
import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
+import org.onap.cps.ncmp.impl.provmns.ParameterHelper;
import org.onap.cps.ncmp.impl.provmns.RequestParameters;
import org.onap.cps.ncmp.impl.provmns.model.PatchItem;
import org.onap.cps.utils.JsonObjectMapper;
@RequiredArgsConstructor
public class OperationDetailsFactory {
+ public static final OperationDetails DELETE_OPERATION_DETAILS = new OperationDetails("delete", "", emptyMap());
+
private static final String ATTRIBUTE_NAME_SEPARATOR = "/";
private static final String REGEX_FOR_LEADING_AND_TRAILING_SEPARATORS = "(^/)|(/$)";
/**
* Create OperationDetails object from ProvMnS request details.
*
- * @param requestParameters request parameters including uri-ldn-first-part, className and id
- * @param patchItem provided request payload
+ * @param requestParameters request parameters including uri-ldn-first-part, className and id
+ * @param patchItem provided request payload
* @return OperationDetails object
*/
public OperationDetails buildOperationDetails(final RequestParameters requestParameters,
final OperationDetails operationDetails;
switch (patchItem.getOp()) {
case ADD:
- operationDetails = buildCreateOperationDetails(CREATE, requestParameters, patchItem.getValue());
+ operationDetails = buildOperationDetailsForPatchItem(CREATE, requestParameters, patchItem);
break;
case REPLACE:
if (patchItem.getPath().contains("#/attributes")) {
- operationDetails = buildCreateOperationDetailsForUpdateWithHash(requestParameters, patchItem);
+ operationDetails = buildOperationDetailsForPatchItemWithHash(requestParameters, patchItem);
} else {
- operationDetails = buildCreateOperationDetails(UPDATE, requestParameters, patchItem.getValue());
+ operationDetails = buildOperationDetailsForPatchItem(UPDATE, requestParameters, patchItem);
}
break;
case REMOVE:
- operationDetails = buildDeleteOperationDetails(requestParameters.toTargetFdn());
+ operationDetails = DELETE_OPERATION_DETAILS;
break;
default:
throw new ProvMnSException("PATCH", HttpStatus.UNPROCESSABLE_ENTITY,
}
/**
- * Build a CreateOperationDetails object from ProvMnS request details.
+ * Build a OperationDetails object from ProvMnS request details.
*
- * @param operationType Type of operation create, update.
- * @param requestParameters request parameters including uri-ldn-first-part, className and id
- * @param resourceAsObject provided request payload
- * @return CreateOperationDetails object
+ * @param operationType Type of operation create, update.
+ * @param requestParameters request parameters including uri-ldn-first-part, className and id
+ * @param resourceAsObject provided request payload
+ * @return OperationDetails object
*/
- public CreateOperationDetails buildCreateOperationDetails(final OperationType operationType,
- final RequestParameters requestParameters,
- final Object resourceAsObject) {
+ public OperationDetails buildOperationDetails(final OperationType operationType,
+ final RequestParameters requestParameters,
+ final Object resourceAsObject) {
final ResourceObjectDetails resourceObjectDetails = createResourceObjectDetails(resourceAsObject,
requestParameters);
final OperationEntry operationEntry = new OperationEntry(resourceObjectDetails.id(),
resourceObjectDetails.attributes());
- return new CreateOperationDetails(operationType.name(),
- requestParameters.getUriLdnFirstPart(),
- Map.of(resourceObjectDetails.objectClass(), List.of(operationEntry)));
+ final Map<String, List<OperationEntry>> changeRequestAsMap =
+ Map.of(resourceObjectDetails.objectClass(), List.of(operationEntry));
+ final String targetIdentifier = ParameterHelper.extractParentFdn(requestParameters.fdn());
+ return new OperationDetails(operationType.getOperationName(), targetIdentifier, changeRequestAsMap);
}
/**
- * Builds a DeleteOperationDetails object from provided alternate id.
+ * Build OperationDetails for a specific patch item.
*
- * @param alternateId alternate id for request
- * @return DeleteOperationDetails object
+ * @param operationType the type of operation (CREATE, UPDATE)
+ * @param requestParameters request parameters including uri-ldn-first-part, className and id
+ * @param patchItem the patch item containing operation details
+ * @return OperationDetails object for the patch item
*/
- public DeleteOperationDetails buildDeleteOperationDetails(final String alternateId) {
- return new DeleteOperationDetails(DELETE.name(), alternateId);
+ public OperationDetails buildOperationDetailsForPatchItem(final OperationType operationType,
+ final RequestParameters requestParameters,
+ final PatchItem patchItem) {
+ final Map<String, Object> resourceAsObject = new HashMap<>(2);
+ resourceAsObject.put("id", requestParameters.id());
+ resourceAsObject.put("attributes", patchItem.getValue());
+ return buildOperationDetails(operationType, requestParameters, resourceAsObject);
+ }
+
+ private OperationDetails buildOperationDetailsForPatchItemWithHash(final RequestParameters requestParameters,
+ final PatchItem patchItem) {
+ final Map<String, Object> attributeHierarchyAsMap = createNestedMap(patchItem);
+ final OperationEntry operationEntry = new OperationEntry(requestParameters.id(), attributeHierarchyAsMap);
+ final String targetIdentifier = ParameterHelper.extractParentFdn(requestParameters.fdn());
+ final Map<String, List<OperationEntry>> operationEntriesPerObjectClass = new HashMap<>();
+ operationEntriesPerObjectClass.put(requestParameters.className(), List.of(operationEntry));
+ return new OperationDetails(UPDATE.getOperationName(), targetIdentifier, operationEntriesPerObjectClass);
}
@SuppressWarnings("unchecked")
final RequestParameters requestParameters) {
final String resourceAsJson = jsonObjectMapper.asJsonString(resourceAsObject);
final Map<String, Object> resourceAsMap = jsonObjectMapper.convertJsonString(resourceAsJson, Map.class);
- return new ResourceObjectDetails(requestParameters.getId(),
- extractObjectClass(resourceAsMap, requestParameters),
+ return new ResourceObjectDetails(requestParameters.id(),
+ requestParameters.className(),
resourceAsMap.get("attributes"));
-
- }
-
- private static String extractObjectClass(final Map<String, Object> resourceAsMap,
- final RequestParameters requestParameters) {
- final String objectClass = (String) resourceAsMap.get("objectClass");
- if (Strings.isNullOrEmpty(objectClass)) {
- return requestParameters.getClassName();
- }
- return objectClass;
- }
-
- private CreateOperationDetails buildCreateOperationDetailsForUpdateWithHash(
- final RequestParameters requestParameters,
- final PatchItem patchItem) {
- final Map<String, List<OperationEntry>> operationEntriesPerObjectClass = new HashMap<>();
- final String className = requestParameters.getClassName();
- final Map<String, Object> attributeHierarchyAsMap = createNestedMap(patchItem);
- final OperationEntry operationEntry = new OperationEntry(requestParameters.getId(), attributeHierarchyAsMap);
- operationEntriesPerObjectClass.put(className, List.of(operationEntry));
- return new CreateOperationDetails(UPDATE.getOperationName(), requestParameters.getUriLdnFirstPart(),
- operationEntriesPerObjectClass);
}
private Map<String, Object> createNestedMap(final PatchItem patchItem) {
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
+ * Copyright (C) 2021-2026 OpenInfra Foundation Europe. All rights reserved.
* Modifications Copyright (C) 2022 Bell Canada
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* @return ResponseEntity containing the response from the DMI.
*/
public ResponseEntity<Object> synchronousPatchOperation(final RequiredDmiService requiredDmiService,
- final Object body,
- final UrlTemplateParameters urlTemplateParameters,
- final String contentType) {
+ final Object body,
+ final UrlTemplateParameters urlTemplateParameters,
+ final String contentType) {
return getWebClient(requiredDmiService)
.patch()
.uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables())
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025-2026 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 jakarta.servlet.http.HttpServletRequest;
+import lombok.NoArgsConstructor;
+import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
+import org.springframework.http.HttpStatus;
+
+@NoArgsConstructor
+public class ParameterHelper {
+
+ public static final String NO_OP = null;
+ private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/";
+ private static final String INVALID_PATH_DETAILS_TEMPLATE = "%s not a valid path";
+ private static final int PATH_VARIABLES_EXPECTED_LENGTH = 2;
+ private static final int REQUEST_FDN_INDEX = 1;
+
+ /**
+ * Converts HttpServletRequest to RequestParameters.
+ *
+ * @param httpServletRequest HttpServletRequest object containing the path
+ * @return RequestParameters object containing http method and FDN parameters
+ */
+ public static RequestParameters extractRequestParameters(final HttpServletRequest httpServletRequest) {
+ final String uriPath = (String) httpServletRequest.getAttribute(
+ "org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping");
+ final String[] pathVariables = uriPath.split(PROVMNS_BASE_PATH);
+ if (pathVariables.length != PATH_VARIABLES_EXPECTED_LENGTH) {
+ throw createProvMnSException(httpServletRequest.getMethod(), uriPath);
+ }
+ final String fdn = "/" + pathVariables[REQUEST_FDN_INDEX];
+ return createRequestParameters(httpServletRequest.getMethod(), fdn);
+ }
+
+ /**
+ * Create RequestParameters object for PATCH operations.
+ *
+ * @param pathWithAttributes the path a fdn possibly with containing attributes
+ * @return RequestParameters object for PATCH operation
+ */
+ public static RequestParameters createRequestParametersForPatch(final String pathWithAttributes) {
+ final String fdn = removeTrailingHash(extractFdn(pathWithAttributes));
+ return createRequestParameters("PATCH", fdn);
+ }
+
+ /**
+ * Extract parent FDN from the given path allowing only className=id pairs.
+ *
+ * @param path the path to convert
+ * @return parent FDN
+ */
+ public static String extractParentFdn(final String path) {
+ return extractFdn(path, 2);
+ }
+
+ /**
+ * Extract FDN from the given path allowing only className=id pairs.
+ *
+ * @param path the path to convert
+ * @return FDN
+ */
+ public static String extractFdn(final String path) {
+ return extractFdn(path, 1);
+ }
+
+ private static String extractFdn(final String path, final int indexFromEnd) {
+ final String[] segments = path.split("/");
+ int count = 0;
+ for (int i = segments.length - 1; i >= 0; i--) {
+ if (segments[i].contains("=") && ++count == indexFromEnd) {
+ return String.join("/", java.util.Arrays.copyOfRange(segments, 0, i + 1));
+ }
+ }
+ return "";
+ }
+
+ private static String removeTrailingHash(final String string) {
+ return string.endsWith("#") ? string.substring(0, string.length() - 1) : string;
+ }
+
+ private static RequestParameters createRequestParameters(final String httpMethodName,
+ final String fdn) {
+ final int lastSlashIndex = fdn.lastIndexOf('/');
+ final String classNameAndId;
+ final String uriLdnFirstPart;
+ uriLdnFirstPart = fdn.substring(0, lastSlashIndex);
+ classNameAndId = fdn.substring(lastSlashIndex + 1);
+ final String[] splitClassNameId = classNameAndId.split("=", 2);
+ if (splitClassNameId.length != 2) {
+ throw createProvMnSException(httpMethodName, fdn);
+ }
+ final String className = splitClassNameId[0];
+ final String id = removeTrailingHash(splitClassNameId[1]);
+ return new RequestParameters(httpMethodName, fdn, uriLdnFirstPart, className, id);
+ }
+
+ private static ProvMnSException createProvMnSException(final String httpMethodName, final String uriPath) {
+ final String title = String.format(INVALID_PATH_DETAILS_TEMPLATE, uriPath);
+ return new ProvMnSException(httpMethodName, HttpStatus.UNPROCESSABLE_ENTITY, title, NO_OP);
+ }
+
+}
+++ /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.provmns;
-
-import jakarta.servlet.http.HttpServletRequest;
-import lombok.RequiredArgsConstructor;
-import org.onap.cps.ncmp.api.exceptions.ProvMnSException;
-import org.springframework.http.HttpStatus;
-import org.springframework.stereotype.Service;
-
-@Service
-@RequiredArgsConstructor
-public class ParameterMapper {
-
- public static final String NO_OP = null;
- private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/";
- private static final String INVALID_PATH_DETAILS_TEMPLATE = "%s not a valid path";
- private static final int PATH_VARIABLES_EXPECTED_LENGTH = 2;
- private static final int OBJECT_INSTANCE_INDEX = 1;
-
- /**
- * Converts HttpServletRequest to RequestParameters.
- *
- * @param httpServletRequest HttpServletRequest object containing the path
- * @return RequestParameters object containing http method and parsed parameters
- */
- public RequestParameters extractRequestParameters(final HttpServletRequest httpServletRequest) {
- final String uriPath = (String) httpServletRequest.getAttribute(
- "org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping");
- final String[] pathVariables = uriPath.split(PROVMNS_BASE_PATH);
- if (pathVariables.length != PATH_VARIABLES_EXPECTED_LENGTH) {
- throw createProvMnSException(httpServletRequest.getMethod(), uriPath);
- }
- final int lastSlashIndex = pathVariables[1].lastIndexOf('/');
- final RequestParameters requestParameters = new RequestParameters();
- requestParameters.setHttpMethodName(httpServletRequest.getMethod());
- final String classNameAndId;
- if (lastSlashIndex < 0) {
- requestParameters.setUriLdnFirstPart("");
- classNameAndId = pathVariables[OBJECT_INSTANCE_INDEX];
- } else {
- final String uriLdnFirstPart = "/" + pathVariables[OBJECT_INSTANCE_INDEX].substring(0, lastSlashIndex);
- requestParameters.setUriLdnFirstPart(uriLdnFirstPart);
- classNameAndId = pathVariables[OBJECT_INSTANCE_INDEX].substring(lastSlashIndex + 1);
- }
- final String[] splitClassNameId = classNameAndId.split("=", 2);
- if (splitClassNameId.length != 2) {
- throw createProvMnSException(httpServletRequest.getMethod(), uriPath);
- }
- requestParameters.setClassName(splitClassNameId[0]);
- requestParameters.setId(splitClassNameId[1]);
- return requestParameters;
- }
-
- private ProvMnSException createProvMnSException(final String httpMethodName, final String uriPath) {
- final String title = String.format(INVALID_PATH_DETAILS_TEMPLATE, uriPath);
- return new ProvMnSException(httpMethodName, HttpStatus.UNPROCESSABLE_ENTITY, title, NO_OP);
- }
-
-}
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2025 OpenInfra Foundation Europe
+ * Copyright (C) 2025-2026 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.
/**
* Creates a UrlTemplateParameters object containing the relevant fields for read requests.
*
- * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID
- * @param targetFdn Target FDN for the resource
- * @param scope Provided className parameter
- * @param filter Filter string
- * @param attributes Attributes List
- * @param fields Fields list
- * @param dataNodeSelector dataNodeSelector parameter
+ * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID
+ * @param resourceIdentifier Target FDN for the resource
+ * @param scope Provided className parameter
+ * @param filter Filter string
+ * @param attributes Attributes List
+ * @param fields Fields list
+ * @param dataNodeSelector dataNodeSelector parameter
* @return UrlTemplateParameters object.
*/
public UrlTemplateParameters createUrlTemplateParametersForRead(final YangModelCmHandle yangModelCmHandle,
- final String targetFdn,
+ final String resourceIdentifier,
final Scope scope,
final String filter,
final List<String> attributes,
final ClassNameIdGetDataNodeSelectorParameter
dataNodeSelector) {
final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(DATA);
- final String targetFdnWithoutPrecedingSlash = targetFdn.substring(1);
+ final String resourceIdentifierWithoutPrecedingSlash = resourceIdentifier.substring(1);
return RestServiceUrlTemplateBuilder.newInstance()
- .fixedPathSegment(targetFdnWithoutPrecedingSlash)
+ .fixedPathSegment(resourceIdentifierWithoutPrecedingSlash)
.queryParameter("scopeType", scope.getScopeType() != null ? scope.getScopeType().getValue() : null)
.queryParameter("scopeLevel", scope.getScopeLevel() != null ? scope.getScopeLevel().toString() : null)
.queryParameter("filter", filter)
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2025 OpenInfra Foundation Europe
+ * Copyright (C) 2025-2026 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.
package org.onap.cps.ncmp.impl.provmns;
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class RequestParameters {
-
- private String httpMethodName;
- private String uriLdnFirstPart;
- private String className;
- private String id;
-
- /**
- * Gets target FDN by combining URI-LDN-First-Part, className and id.
- *
- * @return String of FDN
- */
- public String toTargetFdn() {
- return uriLdnFirstPart + "/" + className + "=" + id;
- }
-}
+public record RequestParameters(
+ String httpMethodName,
+ String fdn,
+ String uriLdnFirstPart,
+ String className,
+ String id) {}
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved.
+ * Copyright (C) 2024-2026 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.
package org.onap.cps.ncmp.impl.data.policyexecutor
-
import com.fasterxml.jackson.databind.ObjectMapper
import org.onap.cps.ncmp.api.exceptions.ProvMnSException
import org.onap.cps.ncmp.impl.provmns.RequestParameters
class OperationDetailsFactorySpec extends Specification {
def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+ def requestPathParameters = new RequestParameters('some method', '/parent=1/child=2','some uri', 'class in uri', 'id in uri')
OperationDetailsFactory objectUnderTest = new OperationDetailsFactory(jsonObjectMapper)
- static def complexValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['myAttribute1:myValue1', 'myAttribute2:myValue2'], objectClass: 'myClassName')
- static def simpleValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['simpleAttribute:1'], objectClass: 'myClassName')
-
def 'Build create operation details with all properties.'() {
- given: 'request parameters and resource'
- def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'class in uri', id: 'my id')
- def resource = new ResourceOneOf(id: 'some resource id', objectClass: 'class in resource')
+ given: 'a resource'
+ def resource = new ResourceOneOf(id: 'id in resource', objectClass: 'class in resource')
when: 'create operation details are built'
- def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
+ def result = objectUnderTest.buildOperationDetails(CREATE, requestPathParameters, resource)
then: 'all details are correct'
- assert result.targetIdentifier == 'my uri'
- assert result.changeRequest.keySet()[0] == 'class in resource'
- assert result.changeRequest['class in resource'][0].id == 'my id'
+ assert result.targetIdentifier == '/parent=1'
+ assert result.changeRequest.keySet()[0] == 'class in uri'
+ assert result.changeRequest['class in uri'][0].id == 'id in uri'
}
- def 'Build replace operation details with all properties where class name in body is #scenario.'() {
- given: 'request parameters and resource'
- def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'class in uri', id: 'some id')
+ def 'Build replace (~create) operation details with all properties where class name in body is #scenario.'() {
+ given: 'a resource'
def resource = new ResourceOneOf(id: 'some resource id', objectClass: classNameInBody)
when: 'replace operation details are built'
- def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
+ def result = objectUnderTest.buildOperationDetails(CREATE, requestPathParameters, resource)
then: 'all details are correct'
- assert result.targetIdentifier == 'my uri'
- assert result.changeRequest.keySet()[0] == expectedChangeRequestKey
+ assert result.targetIdentifier == '/parent=1'
+ assert result.changeRequest.keySet()[0] == 'class in uri'
where:
- scenario | classNameInBody || expectedChangeRequestKey
- 'populated' | 'class in body' || 'class in body'
- 'empty' | '' || 'class in uri'
- 'null' | null || 'class in uri'
- }
-
- def 'Build delete operation details with all properties'() {
- given: 'request parameters'
- def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'classNameInUri', id: 'myId')
- when: 'delete operation details are built'
- def result = objectUnderTest.buildDeleteOperationDetails(requestPathParameters.toTargetFdn())
- then: 'all details are correct'
- assert result.targetIdentifier == 'my uri/classNameInUri=myId'
+ scenario | classNameInBody
+ 'populated' | 'class in body'
+ 'empty' | ''
+ 'null' | null
}
def 'Single patch operation with #patchOperationType checks correct operation type.'() {
- given: 'request parameters and single patch item'
- def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class')
+ given: 'a resource and single patch item'
def resource = new ResourceOneOf(id: 'some resource id')
def patchItem = new PatchItem(op: patchOperationType, 'path':'some uri', value: resource)
when: 'operation details is created'
def result = objectUnderTest.buildOperationDetails(requestPathParameters, patchItem)
then: 'it has the correct operation type (for Policy Executor check)'
- assert result.operation() == expectedPolicyExecutorOperationType.name()
+ assert result.operation() == expectedPolicyExecutorOperationType.operationName
where: 'following operations are used'
patchOperationType | expectedPolicyExecutorOperationType
'ADD' | CREATE
}
def 'Build policy executor patch operation details with single replace operation and #scenario.'() {
- given: 'a requestParameter and a patchItem list'
- def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class')
- def pathItem = new PatchItem(op: 'REPLACE', 'path':"some uri${suffix}", value: value)
+ given: 'a patchItem'
+ def patchItem = new PatchItem(op: 'REPLACE', 'path':"some uri${suffix}", value: value)
when: 'patch operation details are checked'
- def result = objectUnderTest.buildOperationDetails(requestPathParameters, pathItem)
+ def result = objectUnderTest.buildOperationDetails(requestPathParameters, patchItem)
then: 'Attribute Value in operation is correct'
result.changeRequest.values()[0].attributes[0] == expectedAttributesValueInOperation
where: 'attributes are set using # or resource'
- scenario | suffix | value || expectedAttributesValueInOperation
- 'set simple value using #' | '#/attributes/simpleAttribute' | 1 || [simpleAttribute:1]
- 'set simple value using resource' | '' | simpleValueAsResource || ['simpleAttribute:1']
- 'set complex value using resource' | '' | complexValueAsResource || ["myAttribute1:myValue1","myAttribute2:myValue2"]
+ scenario | suffix | value || expectedAttributesValueInOperation
+ 'set simple value using #' | '#/attributes/attr1' | 1 || [attr1:1]
+ 'set complex value using resource' | '' | '{"attr1":"abc","attr2":123}' || '{"attr1":"abc","attr2":123}'
}
def 'Build an attribute map with different depths of hierarchy with #scenario.'() {
}
def 'Attempt to Build Operation details with unsupported op (MOVE).'() {
- given: 'a provMnsRequestParameter and a patchItem'
- def path = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class')
+ given: 'a patchItem'
def patchItem = new PatchItem(op: 'MOVE', 'path':'some uri')
when: 'a build is attempted with an unsupported op'
- objectUnderTest.buildOperationDetails(path, patchItem)
+ objectUnderTest.buildOperationDetails(requestPathParameters, patchItem)
then: 'the result is as expected (exception thrown)'
def exceptionThrown = thrown(ProvMnSException)
assert exceptionThrown.title == 'Unsupported Patch Operation Type: move'
}
- def 'Build policy executor create operation details from ProvMnS request parameters where objectClass in resource #scenario.'() {
- given: 'a provMnsRequestParameter and a resource'
- def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'class in uri', id:'my id')
- def resource = new ResourceOneOf(id: 'some resource id', objectClass: objectInResouce)
- when: 'a configurationManagementOperation is created and converted to JSON'
- def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
- then: 'the result is as expected (using json to compare)'
- String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"some uri","changeRequest":{"' + changeRequestClassReference + '":[{"id":"my id","attributes":null}]}}'
- assert jsonObjectMapper.asJsonString(result) == expectedJsonString
- where:
- scenario | objectInResouce || changeRequestClassReference
- 'populated' | 'class in resource' || 'class in resource'
- 'empty' | '' || 'class in uri'
- 'null' | null || 'class in uri'
- }
-
}
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved.
+ * Copyright (C) 2023-2026 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.
JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
@SpringBean
- EventProducer eventsProducer = new EventProducer(legacyEventKafkaTemplate, cloudEventKafkaTemplate)
+ EventProducer eventProducer = new EventProducer(legacyEventKafkaTemplate, cloudEventKafkaTemplate)
def 'Process per data operation request with #serviceName.'() {
given: 'data operation request with 3 operations'
--- /dev/null
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025-2026 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 ParameterHelperSpec extends Specification {
+
+ def objectUnderTest = new ParameterHelper()
+
+ 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) >> path
+ when: 'the request parameters are extracted'
+ def result = objectUnderTest.extractRequestParameters(mockHttpServletRequest)
+ then: 'the Uri LDN first part is as expected'
+ assert result.uriLdnFirstPart == expectedUriLdnFirstPart
+ and: 'the class name and id are mapped correctly'
+ assert result.className == 'myClass'
+ assert result.id == 'id'
+ where: 'The following URIs are used'
+ scenario | path || expectedUriLdnFirstPart
+ '1 segment' | 'ProvMnS/v1/segment1/myClass=id' || '/segment1'
+ '2 segments' | 'ProvMnS/v1/segment1/segment2/myClass=id' || '/segment1/segment2'
+ 'multiple segments' | 'ProvMnS/v1/segment1/segment2/segment3/segment4/myClass=id' || '/segment1/segment2/segment3/segment4'
+ 'no slash' | 'ProvMnS/v1/myClass=id' || ''
+ }
+
+ def 'Extract request parameters for Patch Path with attributes.'() {
+ when: 'the request parameters are extracted from the path'
+ def result = objectUnderTest.createRequestParametersForPatch(path)
+ then: 'the FDN is as expected'
+ assert result.fdn == expectedFdn
+ and: 'the class name and id are mapped correctly'
+ assert result.className == 'myClass'
+ assert result.id == 'id'
+ where: 'the following paths are used'
+ scenario | path || expectedFdn
+ 'attributes in path' | '/myClass=id/attributes' || '/myClass=id'
+ 'attributes with parent' | '/parent=p/myClass=id/attributes' || '/parent=p/myClass=id'
+ '#/attributes in path' | '/myClass=id#/attributes' || '/myClass=id'
+ }
+
+ def 'Attempt to extract request parameters with #scenario.'() {
+ given: 'a http request with invalid path'
+ mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path
+ mockHttpServletRequest.getMethod() >> 'GET'
+ when: 'attempt to extract the request parameters'
+ objectUnderTest.extractRequestParameters(mockHttpServletRequest)
+ then: 'a ProvMnS exception is thrown'
+ def thrown = thrown(ProvMnSException)
+ assert thrown.message == 'GET failed'
+ and: 'the title contains the expected error message'
+ assert thrown.title == expectedPathInError + ' not a valid path'
+ where: 'the following invalid paths are used'
+ scenario | path || expectedPathInError
+ 'no = After (last) class name' | 'ProvMnS/v1/myClass1=id/Class2' || '/myClass1=id/Class2'
+ 'attributes in path' | 'ProvMnS/v1/myClass=id/attributes' || '/myClass=id/attributes'
+ '#/attributes in path' | 'ProvMnS/v1/myClass=id#/attributes' || '/myClass=id#/attributes'
+ 'missing ProvMnS prefix' | 'v1/segment1/myClass=id' || 'v1/segment1/myClass=id'
+ 'wrong version' | 'ProvMnS/wrongVersion/myClass=id' || 'ProvMnS/wrongVersion/myClass=id'
+ 'empty path' | '' || ''
+ }
+
+ def 'Extract Fdn.'() {
+ expect: 'Only valid name-id pairs are retuned up to the required index'
+ assert objectUnderTest.extractFdn('/a=1/b=2/c=3/d/e/f', indexFromEnd) == expectedResult
+ where: 'following fdns are used'
+ indexFromEnd || expectedResult
+ 0 || ''
+ 1 || '/a=1/b=2/c=3'
+ 2 || '/a=1/b=2'
+ 3 || '/a=1'
+ 4 || ''
+ }
+
+ def 'Extract Parent Fdn.'() {
+ expect: 'Teh cortect Parent FDN (up to 2nd last name-id pair)) is returned'
+ assert objectUnderTest.extractParentFdn('/a=1/b=2/c=3/d/e/f') == '/a=1/b=2'
+ }
+
+}
+++ /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) >> path
- when: 'the request parameters are extracted'
- def result = objectUnderTest.extractRequestParameters(mockHttpServletRequest)
- then: 'the Uri LDN first part is as expected'
- assert result.uriLdnFirstPart == expectedUriLdnFirstPart
- 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 | path || expectedUriLdnFirstPart
- '1 segment' | 'ProvMnS/v1/segment1/myClass=myId' || '/segment1'
- '2 segments' | 'ProvMnS/v1/segment1/segment2/myClass=myId' || '/segment1/segment2'
- 'multiple segments' | 'ProvMnS/v1/segment1/segment2/segment3/segment4/myClass=myId' || '/segment1/segment2/segment3/segment4'
- 'no slash' | 'ProvMnS/v1/myClass=myId' || ''
- }
-
- def 'Attempt to extract request parameters with #scenario.'() {
- given: 'a http request with invalid path'
- mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path
- mockHttpServletRequest.getMethod() >> 'GET'
- when: 'attempt to extract the request parameters'
- objectUnderTest.extractRequestParameters(mockHttpServletRequest)
- then: 'a ProvMnS exception is thrown'
- def thrown = thrown(ProvMnSException)
- assert thrown.message == 'GET failed'
- and: 'the title contains the expected error message'
- assert thrown.title == path + ' not a valid path'
- where: 'the following invalid paths are used'
- scenario | path
- 'no = After (last) class name'| 'ProvMnS/v1/someOtherClass=someId/myClass'
- 'missing ProvMnS prefix' | 'v1/segment1/myClass=myId'
- 'wrong version' | 'ProvMnS/wrongVersion/myClass=myId'
- 'empty path' | ''
- 'multiple ProvMnS segments' | 'ProvMnS/v1/myClass=myId/ProvMnS/v2/otherSegment'
- }
-}
+++ /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 spock.lang.Specification
-
-class RequestParametersSpec extends Specification {
-
- def objectUnderTest = new RequestParameters()
-
- def 'Generate target FDN #scenario.'() {
- given: 'request parameters with URI LDN first part, class name and id'
- objectUnderTest.uriLdnFirstPart = uriLdnFirstPart
- objectUnderTest.className = 'myClass'
- objectUnderTest.id = 'myId'
- when: 'target FDN is generated'
- def result = objectUnderTest.toTargetFdn()
- then: 'the target FDN is as expected'
- result == expectedTargetFdn
- where: 'the following uri first part is used'
- scenario | uriLdnFirstPart || expectedTargetFdn
- 'with segments' | '/segment1' || '/segment1/myClass=myId'
- 'empty first part' | '' || '/myClass=myId'
- }
-}
image: ${DOCKER_REPO:-nexus3.onap.org:10003}/onap/policy-executor-stub:latest
ports:
- ${POLICY_EXECUTOR_STUB_PORT:-8785}:8093
- ### DEBUG: Uncomment next lines to enable java debugging in Policy Executor Stub
- ### - ${POLICY_EXECUTOR_STUB_DEBUG_PORT:-5005}:5005
- ### environment:
+ ### DEBUG: Uncomment next lines to enable java debugging in Policy Executor Stub
+ ### - ${POLICY_EXECUTOR_STUB_DEBUG_PORT:-5005}:5005
+ ### environment:
#### JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
restart: unless-stopped
# Note policy-executor-stub does not have a healthcheck as it does not expose /actuator/health endpoint