import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@RequiredArgsConstructor
public class PolicyExecutor {
+ public static final String ATTRIBUTES_WITH_HASHTAG = "#/attributes";
+
@Value("${ncmp.policy-executor.enabled:false}")
private boolean enabled;
buildCreateOperationDetails(OperationType.CREATE, requestPathParameters,
(Resource) patchItem.getValue()));
case REPLACE -> operations.add(
- buildCreateOperationDetails(OperationType.UPDATE, requestPathParameters,
- (Resource) patchItem.getValue()));
+ buildCreateOperationDetailsForUpdate(OperationType.UPDATE, requestPathParameters, patchItem));
case REMOVE -> operations.add(
buildDeleteOperationDetails(requestPathParameters.toAlternateId()));
default -> log.warn("Unsupported Patch Operation Type:{}", patchItem.getOp().getValue());
final Map<String, List<OperationEntry>> changeRequest = new HashMap<>();
final OperationEntry operationEntry = new OperationEntry();
- final String resourceJson = jsonObjectMapper.asJsonString(resource);
+ final String resourceAsJson = jsonObjectMapper.asJsonString(resource);
String className = requestPathParameters.getClassName();
try {
final TypeReference<HashMap<String, Object>> typeReference =
new TypeReference<HashMap<String, Object>>() {};
- final Map<String, Object> fullValue = objectMapper.readValue(resourceJson, typeReference);
+ final Map<String, Object> fullValue = objectMapper.readValue(resourceAsJson, typeReference);
operationEntry.setId(requestPathParameters.getId());
operationEntry.setAttributes(fullValue.get("attributes"));
requestPathParameters.getUriLdnFirstPart(), changeRequest);
}
+ /**
+ * Build a CreateOperationDetails object from ProvMnS request details.
+ *
+ * @param operationType Type of operation create, update.
+ * @param requestPathParameters request parameters including uri-ldn-first-part, className and id
+ * @param patchItem provided request
+ * @return CreateOperationDetails object
+ */
+ public CreateOperationDetails buildCreateOperationDetailsForUpdate(final OperationType operationType,
+ final RequestPathParameters requestPathParameters,
+ final PatchItem patchItem) {
+ if (patchItem.getPath().contains(ATTRIBUTES_WITH_HASHTAG)) {
+ return buildCreateOperationDetailsForUpdateWithHash(operationType, requestPathParameters, patchItem);
+ } else {
+ return buildCreateOperationDetails(operationType, requestPathParameters, (Resource) patchItem.getValue());
+ }
+ }
+
+ private CreateOperationDetails buildCreateOperationDetailsForUpdateWithHash(final OperationType operationType,
+ final RequestPathParameters requestPathParameters,
+ final PatchItem patchItem) {
+ final Map<String, List<OperationEntry>> changeRequest = new HashMap<>();
+ final OperationEntry operationEntry = new OperationEntry();
+ final String className = requestPathParameters.getClassName();
+
+ final Map<String, Object> attributeHiearchyAsMap = getAttributeHierarchyMap(patchItem);
+
+ operationEntry.setId(requestPathParameters.getId());
+ operationEntry.setAttributes(attributeHiearchyAsMap);
+ changeRequest.put(className, List.of(operationEntry));
+
+ return new CreateOperationDetails(operationType.getOperationName(),
+ requestPathParameters.getUriLdnFirstPart(),
+ changeRequest);
+ }
+
+ private Map<String, Object> getAttributeHierarchyMap(final PatchItem patchItem) {
+ final String[] parts = patchItem.getPath().split(ATTRIBUTES_WITH_HASHTAG);
+
+ final String attributeHierarchy = parts[1];
+ final String[] attributeHierarchyAsArray = Arrays.stream(attributeHierarchy.split("/"))
+ .filter(attributeName -> !attributeName.isEmpty())
+ .toArray(String[]::new);
+
+ return buildAttributeHiearchyAsMap(attributeHierarchyAsArray, 0, patchItem.getValue());
+ }
+
+ private Map<String, Object> buildAttributeHiearchyAsMap(final String[] parts,
+ final int index,
+ final Object value) {
+ if (index == parts.length - 1) {
+ return Map.of(parts[index], value);
+ }
+
+ return Map.of(parts[index], buildAttributeHiearchyAsMap(parts, index + 1, value));
+ }
+
/**
* Builds a DeleteOperationDetails object from provided alternate id.
*
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
def logAppender = Spy(ListAppender<ILoggingEvent>)
def someValidJson = '{"Hello":"World"}'
+ static def complexValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['myAttribute1:myValue1', 'myAttribute2:myValue2'], objectClass: 'myClassName')
+ static def simpleValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['simpleAttribute:1'], objectClass: 'myClassName')
+
def setup() {
setupLogger()
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 path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'classNameInUri', id: 'myId')
+ def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: classNameInBody)
+ def patchItemsList = [new PatchItem(op: 'ADD', 'path':'myUriLdnFirstPart', value: resource), new PatchItem(op: 'REPLACE', 'path':'myUriLdnFirstPart', value: resource), new PatchItem(op: 'REMOVE', 'path':'myUriLdnFirstPart'),]
+ when: 'patch operation details are created'
def result = objectUnderTest.buildPatchOperationDetails(path, patchItemsList)
- then: 'the result 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'
+ then: 'the result contain 3 operations of the correct types in the correct order'
+ result.operations.size() == 3
+ and: 'note that Add and Replace both are defined using Create Operation Details'
+ assert result.operations[0] instanceof CreateOperationDetails
+ assert result.operations[1] instanceof CreateOperationDetails
+ assert result.operations[2] instanceof DeleteOperationDetails
+ and: 'the add operation target identifier is just the uri first part'
+ assert result.operations[0]['targetIdentifier'] == 'myUriLdnFirstPart'
+ and: 'the replace operation target identifier is just the uri first part'
+ assert result.operations[1]['targetIdentifier'] == 'myUriLdnFirstPart'
+ and: 'the replace change request has the correct class name'
+ assert result.operations[1].changeRequest.keySet()[0] == expectedChangeRequestKey
+ and: 'the delete operation target identifier includes the target class and id'
+ assert result.operations[2]['targetIdentifier'] == 'myUriLdnFirstPart/classNameInUri=myId'
+ where: 'the following class names are used in the body'
+ scenario | classNameInBody || expectedChangeRequestKey
+ 'class name in body is populated' | 'myClass' || 'myClass'
+ 'class name in body is empty' | '' || 'classNameInUri'
+ 'class name in body is null' | null || 'classNameInUri'
+ }
+
+ def 'Build policy executor patch operation details with single replace operation and #scenario.'() {
+ given: 'a requestParameter and a patchItem list'
+ def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'myClassName', id: 'myId')
+ def pathItems = [new PatchItem(op: 'REPLACE', 'path':"myUriLdnFirstPart${suffix}", value: value)]
+ when: 'patch operation details are created'
+ def result = objectUnderTest.buildPatchOperationDetails(path, pathItems)
+ then: 'the result has the correct type'
+ assert result instanceof PatchOperationsDetails
+ and: 'the change request contains the correct attributes value'
+ assert result.operations[0]['changeRequest']['myClassName'][0]['attributes'].toString() == attributesValueInOperation
+ where: 'attributes are set using # or resource'
+ scenario | suffix | value || attributesValueInOperation
+ 'set simple value using #' | '#/attributes/simpleAttribute' | 1 || '[simpleAttribute:1]'
+ 'set simple value using resource' | '' | simpleValueAsResource || '[simpleAttribute:1]'
+ 'set complex value using resource' | '' | complexValueAsResource || '[myAttribute1:myValue1, myAttribute2:myValue2]'
+ }
+
+ def 'Build an attribute map with different depths of hierarchy with #scenario.'() {
+ given: 'a patch item with a path'
+ def patchItem = new PatchItem(op: 'REPLACE', 'path':path, value: 123)
+ when: 'transforming the attributes'
+ def hierarchyMap = objectUnderTest.getAttributeHierarchyMap(patchItem)
+ then: 'the map depth is equal to the expected number of attributes'
+ assert hierarchyMap.get(expectedAttributeName).toString() == expectedAttributeValue
+ where: 'simple and complex attributes are tested'
+ scenario | path || expectedAttributeName || expectedAttributeValue
+ 'set a simple attribute' | 'myUriLdnFirstPart#/attributes/simpleAttribute' || 'simpleAttribute' || '123'
+ 'set a complex attribute' | 'myUriLdnFirstPart#/attributes/complexAttribute/simpleAttribute' || 'complexAttribute' || '[simpleAttribute:123]'
}
def 'Build policy executor patch operation details from ProvMnS request parameters with invalid op.'() {
given: 'a provMnsRequestParameter and a patchItem list'
- def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
- def patchItemsList = [new PatchItem(op: 'TEST', 'path':'someUriLdnFirstPart')]
+ def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'someClassName', id: 'someId')
+ def patchItemsList = [new PatchItem(op: 'TEST', 'path':'myUriLdnFirstPart')]
when: 'a configurationManagementOperation is created and converted to JSON'
def result = objectUnderTest.buildPatchOperationDetails(path, patchItemsList)
then: 'the result is as expected (using json to compare)'
def '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 path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'someClassName', id: 'someId')
def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: objectClass)
when: 'a configurationManagementOperation is created and converted to JSON'
def result = objectUnderTest.buildCreateOperationDetails(CREATE, path, resource)
then: 'the result is as expected (using json to compare)'
- String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}'
+ String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"myUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}'
assert jsonObjectMapper.asJsonString(result) == expectedJsonString
where:
scenario | objectClass || changeRequestClassReference
def 'Build Policy Executor Operation Details with a exception during conversion'() {
given: 'a provMnsRequestParameter and a resource'
- def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId')
+ def path = new RequestPathParameters(uriLdnFirstPart: 'myUriLdnFirstPart', className: 'someClassName', id: 'someId')
def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'])
and: 'json object mapper throws an exception'
def originalException = new JsonProcessingException('some-exception')