diff options
59 files changed, 1809 insertions, 392 deletions
diff --git a/cps-ncmp-events/src/main/resources/schemas/async/batch-event-headers-1.0.0.json b/cps-ncmp-events/src/main/resources/schemas/async/batch-event-headers-1.0.0.json new file mode 100644 index 0000000000..bbcadcd0f4 --- /dev/null +++ b/cps-ncmp-events/src/main/resources/schemas/async/batch-event-headers-1.0.0.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "urn:cps:org.onap.cps.ncmp.events.async:batch-event-headers:1.0.0", + "$ref": "#/definitions/BatchEventHeaders", + "definitions": { + "BatchEventHeaders": { + "description": "The header information of the Batch event.", + "type": "object", + "javaType" : "org.onap.cps.ncmp.events.async.BatchEventHeadersV1", + "properties": { + "eventId": { + "description": "The unique id for identifying the event.", + "type": "string" + }, + "eventCorrelationId": { + "description": "The request id received by NCMP as an acknowledgement.", + "type": "string" + }, + "eventTime": { + "description": "The time of the event. It should be in RFC format ('yyyy-MM-dd'T'HH:mm:ss.SSSZ').", + "type": "string" + }, + "eventTarget": { + "description": "The destination topic to forward the consumed event.", + "type": "string" + }, + "eventSource": { + "description": "The source of the event.", + "type": "string" + }, + "eventType": { + "description": "The type of the Batch event.", + "type": "string" + }, + "eventSchema": { + "description": "The schema of the Batch event payload.", + "type": "string" + }, + "eventSchemaVersion": { + "description": "The schema version of the Batch event payload.", + "type": "string" + } + }, + "required": [ + "eventId", + "eventCorrelationId", + "eventTarget", + "eventType", + "eventSchema", + "eventSchemaVersion" + ], + "additionalProperties": false + } + } +}
\ No newline at end of file diff --git a/cps-ncmp-events/src/main/resources/schemas/async/batch-event-schema-1.0.0.json b/cps-ncmp-events/src/main/resources/schemas/async/batch-event-schema-1.0.0.json new file mode 100644 index 0000000000..da836ff167 --- /dev/null +++ b/cps-ncmp-events/src/main/resources/schemas/async/batch-event-schema-1.0.0.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "urn:cps:org.onap.cps.ncmp.events.async:batch-event-schema:1.0.0", + "$ref": "#/definitions/BatchDataResponseEvent", + "definitions": { + "BatchDataResponseEvent": { + "description": "The payload of batch event.", + "type": "object", + "javaType" : "org.onap.cps.ncmp.events.async.BatchDataResponseEventV1", + "properties": { + "event": { + "description": "The payload content of the requested data.", + "type": "object", + "javaType" : "org.onap.cps.ncmp.events.async.BatchDataEvent", + "properties": { + "batch-responses": { + "description": "An array of batch responses which contains both success and failure", + "type": "array", + "items": { + "type": "object", + "properties": { + "operationId": { + "description": "Used to distinguish multiple operations using same cmhandleId", + "type": "string" + }, + "ids": { + "description": "Id's of the cmhandles", + "type": "array" + }, + "status-code": { + "description": "which says success or failure (0-99) are for success and (100-199) are for failure", + "type": "string" + }, + "status-message": { + "description": "Human readable message, Which says what the response has", + "type": "string" + }, + "data": { + "description": "Contains the requested data response.", + "type": "object", + "existingJavaType": "java.lang.Object", + "additionalProperties": false + } + }, + "required": [ + "operationId", + "ids", + "status-code", + "status-message" + ], + "additionalProperties": false + } + } + }, + "required": [ + "batch-responses" + ], + "additionalProperties": false + } + }, + "required": [ + "event" + ], + "additionalProperties": false + } + } +}
\ No newline at end of file diff --git a/cps-ncmp-events/src/main/resources/schemas/dmidataavc/avc-event-headers-v1.json b/cps-ncmp-events/src/main/resources/schemas/dmidataavc/avc-event-header-v1.json index caae82bb23..ea1e617c82 100644 --- a/cps-ncmp-events/src/main/resources/schemas/dmidataavc/avc-event-headers-v1.json +++ b/cps-ncmp-events/src/main/resources/schemas/dmidataavc/avc-event-header-v1.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "urn:cps:org.onap.cps.ncmp.events:avc-event-headers-schema:v1", + "$id": "urn:cps:org.onap.cps.ncmp.events:avc-event-header-schema:v1", "$ref": "#/definitions/AvcEventHeader", "definitions": { "AvcEventHeader": { diff --git a/cps-ncmp-events/src/main/resources/schemas/dmidataavc/avc-event-schema-v1.json b/cps-ncmp-events/src/main/resources/schemas/dmidataavc/avc-event-schema-v1.json index 407551f4fd..7e975c9b93 100644 --- a/cps-ncmp-events/src/main/resources/schemas/dmidataavc/avc-event-schema-v1.json +++ b/cps-ncmp-events/src/main/resources/schemas/dmidataavc/avc-event-schema-v1.json @@ -3,16 +3,99 @@ "$id": "urn:cps:org.onap.cps.ncmp.events:avc-event-schema:v1", "$ref": "#/definitions/AvcEvent", "definitions": { + "Edit": { + "javaType": "org.onap.cps.ncmp.events.avc.v1.Edit", + "additionalProperties": false, + "properties": { + "edit-id": { + "type": "string" + }, + "operation": { + "type": "string" + }, + "target": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/Value" + } + }, + "required": [ + "edit-id", + "operation", + "target" + ] + }, + "Value": { + "type": "object", + "additionalProperties": false, + "properties": { + "attributes": { + "type": "array", + "items": { + "type": "object", + "existingJavaType": "java.util.Map<String,Object>", + "additionalProperties": false, + "properties": { + "isHoAllowed": { + "type": "boolean" + } + } + } + } + } + }, "AvcEvent": { "description": "The payload for AVC event.", "type": "object", - "javaType" : "org.onap.cps.ncmp.events.avc.v1.AvcEvent", + "javaType": "org.onap.cps.ncmp.events.avc.v1.AvcEvent", "properties": { "event": { - "description": "The AVC event content.", + "description": "The AVC event content compliant with RFC8641 format", "type": "object", - "existingJavaType": "java.lang.Object", - "additionalProperties": false + "additionalProperties": false, + "properties": { + "push-change-update": { + "type": "object", + "additionalProperties": false, + "properties": { + "datastore-changes": { + "type": "object", + "additionalProperties": false, + "properties": { + "ietf-yang-patch:yang-patch": { + "type": "object", + "additionalProperties": false, + "properties": { + "patch-id": { + "type": "string" + }, + "edit": { + "type": "array", + "items": { + "$ref": "#/definitions/Edit" + } + } + }, + "required": [ + "patch-id", + "edit" + ] + } + }, + "required": [ + "ietf-yang-patch:yang-patch" + ] + } + }, + "required": [ + "datastore-changes" + ] + } + }, + "required": [ + "push-change-update" + ] } }, "required": [ diff --git a/cps-ncmp-events/src/main/resources/schemas/lcm/lcm-event-header-v1.json b/cps-ncmp-events/src/main/resources/schemas/lcm/lcm-event-header-v1.json new file mode 100644 index 0000000000..8c9922ef7e --- /dev/null +++ b/cps-ncmp-events/src/main/resources/schemas/lcm/lcm-event-header-v1.json @@ -0,0 +1,56 @@ +{ + + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "urn:cps:org.onap.ncmp.cmhandle.lcm-event-header:v1", + "$ref": "#/definitions/LcmEventHeader", + + "definitions": { + "LcmEventHeader": { + "description": "The header for LCM event", + "type": "object", + "javaType" : "org.onap.cps.ncmp.events.lcm.v1.LcmEventHeader", + "properties": { + "eventId": { + "description": "The unique id identifying the event", + "type": "string" + }, + "eventCorrelationId": { + "description": "The id identifying the event", + "type": "string" + }, + "eventTime": { + "description": "The timestamp when original event occurred", + "type": "string" + }, + "eventSource": { + "description": "The source of the event", + "type": "string" + }, + "eventType": { + "description": "The type of the event", + "type": "string" + }, + "eventSchema": { + "description": "The schema that this event adheres to", + "type": "string" + }, + "eventSchemaVersion": { + "description": "The version of the schema that this event adheres to", + "type": "string" + } + }, + "required": [ + "eventId", + "eventCorrelationId", + "eventTime", + "eventSource", + "eventType", + "eventSchema", + "eventSchemaVersion", + "event" + ], + "additionalProperties": false + } + + } +} diff --git a/cps-ncmp-events/src/main/resources/schemas/lcm-event-schema-v1.json b/cps-ncmp-events/src/main/resources/schemas/lcm/lcm-event-schema-v1.json index 97c0fbee22..7006b78360 100644 --- a/cps-ncmp-events/src/main/resources/schemas/lcm-event-schema-v1.json +++ b/cps-ncmp-events/src/main/resources/schemas/lcm/lcm-event-schema-v1.json @@ -55,7 +55,7 @@ "LcmEvent": { "description": "The payload for LCM event", "type": "object", - "javaType" : "org.onap.ncmp.cmhandle.event.lcm.LcmEvent", + "javaType" : "org.onap.cps.ncmp.events.lcm.v1.LcmEvent", "properties": { "eventId": { "description": "The unique id identifying the event", diff --git a/cps-ncmp-rest-stub/src/main/java/org/onap/cps/ncmp/rest/stub/handlers/NetworkCmProxyApiStubDefaultImpl.java b/cps-ncmp-rest-stub/src/main/java/org/onap/cps/ncmp/rest/stub/handlers/NetworkCmProxyApiStubDefaultImpl.java index 6e28dbc44c..7bd3acbf65 100644 --- a/cps-ncmp-rest-stub/src/main/java/org/onap/cps/ncmp/rest/stub/handlers/NetworkCmProxyApiStubDefaultImpl.java +++ b/cps-ncmp-rest-stub/src/main/java/org/onap/cps/ncmp/rest/stub/handlers/NetworkCmProxyApiStubDefaultImpl.java @@ -23,6 +23,7 @@ package org.onap.cps.ncmp.rest.stub.handlers; import java.util.List; import org.onap.cps.ncmp.rest.api.NetworkCmProxyApi; import org.onap.cps.ncmp.rest.model.CmHandleQueryParameters; +import org.onap.cps.ncmp.rest.model.ResourceDataBatchRequest; import org.onap.cps.ncmp.rest.model.RestModuleDefinition; import org.onap.cps.ncmp.rest.model.RestModuleReference; import org.onap.cps.ncmp.rest.model.RestOutputCmHandle; @@ -46,12 +47,9 @@ public interface NetworkCmProxyApiStubDefaultImpl extends NetworkCmProxyApi { } @Override - default ResponseEntity<Object> getResourceDataForCmHandleBatch(final String resourceIdentifier, - final String topicParamInQuery, - final String datastoreName, - final Object requestBody, - final String optionsParamInQuery, - final Boolean includeDescendants) { + default ResponseEntity<Object> getResourceDataForCmHandleBatch(final String topicParamInQuery, + final ResourceDataBatchRequest + resourceDataBatchRequest) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 7fc1063a12..2781f572f2 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -285,6 +285,43 @@ components: properties: state: $ref: '#/components/schemas/CmHandleCompositeState' + # Batch Request Schemas + ResourceDataBatchRequest: + type: object + title: get resource data for given array of operations + properties: + operations: + type: array + items: + type: object + $ref: '#/components/schemas/BatchOperationDefinition' + description: contains batch request details + BatchOperationDefinition: + required: + - operation + - datastore + - operationId + properties: + operation: + type: string + example: 'read' + operationId: + type: string + example: '12' + datastore: + type: string + example: 'ncmp-datastore:passthrough-operational' + options: + type: string + example: '(fields=schemas/schema)' + resourceIdentifier: + type: string + example: 'parent/child' + targetIds: + type: array + items: + type: string + example: [ "da310eecdb8d44c2acc0ddaae01174b1","c748c58f8e0b438f9fd1f28370b17d47" ] examples: dataSampleRequest: diff --git a/cps-ncmp-rest/docs/openapi/ncmp.yml b/cps-ncmp-rest/docs/openapi/ncmp.yml index 2b70d94892..957a3b8735 100755 --- a/cps-ncmp-rest/docs/openapi/ncmp.yml +++ b/cps-ncmp-rest/docs/openapi/ncmp.yml @@ -202,17 +202,13 @@ getResourceDataForCmHandleBatch: description: This request will be handled asynchronously using messaging to the supplied topic. The rest response will be an acknowledge with a requestId to identify the relevant messages. operationId: getResourceDataForCmHandleBatch parameters: - - $ref: 'components.yaml#/components/parameters/datastoreName' - - $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery' - - $ref: 'components.yaml#/components/parameters/optionsParamInQuery' - $ref: 'components.yaml#/components/parameters/requiredTopicParamInQuery' - - $ref: 'components.yaml#/components/parameters/includeDescendantsOptionInQuery' requestBody: required: true content: application/json: schema: - type: object + $ref: 'components.yaml#/components/schemas/ResourceDataBatchRequest' responses: 200: description: OK diff --git a/cps-ncmp-rest/docs/openapi/openapi.yml b/cps-ncmp-rest/docs/openapi/openapi.yml index 5b4c0d3496..b63b568234 100755 --- a/cps-ncmp-rest/docs/openapi/openapi.yml +++ b/cps-ncmp-rest/docs/openapi/openapi.yml @@ -34,7 +34,7 @@ paths: /v1/ch/{cm-handle}/data/ds/{datastore-name}: $ref: 'ncmp.yml#/resourceDataForCmHandle' - /v1/batch/data/ds/{datastore-name}: + /v1/data: $ref: 'ncmp.yml#/getResourceDataForCmHandleBatch' /v1/ch/{cm-handle}/data/ds/{datastore-name}/query: diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java index fca1d6310f..1b78fa0343 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java @@ -23,10 +23,12 @@ package org.onap.cps.ncmp.rest.controller; -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.CREATE; -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.DELETE; -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.PATCH; -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.UPDATE; +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL; +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING; +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE; +import static org.onap.cps.ncmp.api.impl.operations.OperationType.DELETE; +import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH; +import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE; import java.util.Collection; import java.util.List; @@ -44,8 +46,10 @@ import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandl import org.onap.cps.ncmp.rest.controller.handlers.NcmpDatastoreRequestHandler; import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler; import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper; +import org.onap.cps.ncmp.rest.mapper.ResourceDataBatchRequestMapper; import org.onap.cps.ncmp.rest.model.CmHandlePublicProperties; import org.onap.cps.ncmp.rest.model.CmHandleQueryParameters; +import org.onap.cps.ncmp.rest.model.ResourceDataBatchRequest; import org.onap.cps.ncmp.rest.model.RestModuleDefinition; import org.onap.cps.ncmp.rest.model.RestModuleReference; import org.onap.cps.ncmp.rest.model.RestOutputCmHandle; @@ -72,6 +76,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { private final CmHandleStateMapper cmHandleStateMapper; private final NcmpCachedResourceRequestHandler ncmpCachedResourceRequestHandler; private final NcmpPassthroughResourceRequestHandler ncmpPassthroughResourceRequestHandler; + private final ResourceDataBatchRequestMapper resourceDataBatchRequestMapper; /** * Get resource data from datastore. @@ -100,19 +105,11 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { } @Override - public ResponseEntity<Object> getResourceDataForCmHandleBatch(final String resourceIdentifier, - final String topicParamInQuery, - final String datastoreName, - final Object requestBody, - final String optionsParamInQuery, - final Boolean includeDescendants) { - - final NcmpDatastoreRequestHandler ncmpDatastoreRequestHandler = getNcmpDatastoreRequestHandler(datastoreName); - - final List<String> cmHandleIds = jsonObjectMapper.convertJsonString(jsonObjectMapper.asJsonString(requestBody), - List.class); - return ncmpDatastoreRequestHandler.executeRequest(datastoreName, cmHandleIds, resourceIdentifier, - optionsParamInQuery, topicParamInQuery, includeDescendants); + public ResponseEntity<Object> getResourceDataForCmHandleBatch(final String topicParamInQuery, + final ResourceDataBatchRequest + resourceDataBatchRequest) { + return ncmpPassthroughResourceRequestHandler.executeRequest(topicParamInQuery, + resourceDataBatchRequestMapper.toResourceDataBatchRequest(resourceDataBatchRequest)); } /** @@ -134,7 +131,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { final String optionsParamInQuery, final String topicParamInQuery, final Boolean includeDescendants) { - validateDataStore(DatastoreType.OPERATIONAL, datastoreName); + validateDataStore(OPERATIONAL, datastoreName); return ncmpCachedResourceRequestHandler.executeRequest(cmHandle, cpsPath, includeDescendants); } @@ -156,7 +153,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { final Object requestBody, final String contentType) { - validateDataStore(DatastoreType.PASSTHROUGH_RUNNING, datastoreName); + validateDataStore(PASSTHROUGH_RUNNING, datastoreName); final Object responseObject = networkCmProxyDataService .writeResourceDataPassThroughRunningForCmHandle( @@ -182,7 +179,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { final Object requestBody, final String contentType) { - validateDataStore(DatastoreType.PASSTHROUGH_RUNNING, datastoreName); + validateDataStore(PASSTHROUGH_RUNNING, datastoreName); networkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle(cmHandle, resourceIdentifier, CREATE, jsonObjectMapper.asJsonString(requestBody), contentType); @@ -206,7 +203,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { final String cmHandle, final Object requestBody, final String contentType) { - validateDataStore(DatastoreType.PASSTHROUGH_RUNNING, datastoreName); + validateDataStore(PASSTHROUGH_RUNNING, datastoreName); networkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle(cmHandle, resourceIdentifier, UPDATE, jsonObjectMapper.asJsonString(requestBody), contentType); @@ -228,7 +225,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { final String resourceIdentifier, final String contentType) { - validateDataStore(DatastoreType.PASSTHROUGH_RUNNING, datastoreName); + validateDataStore(PASSTHROUGH_RUNNING, datastoreName); networkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle(cmHandle, resourceIdentifier, DELETE, NO_BODY, contentType); @@ -381,7 +378,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { } private NcmpDatastoreRequestHandler getNcmpDatastoreRequestHandler(final String datastoreName) { - if (DatastoreType.OPERATIONAL.equals(DatastoreType.fromDatastoreName(datastoreName))) { + if (OPERATIONAL.equals(DatastoreType.fromDatastoreName(datastoreName))) { return ncmpCachedResourceRequestHandler; } return ncmpPassthroughResourceRequestHandler; diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java index a32c462e76..a8ca13a752 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java @@ -20,12 +20,19 @@ package org.onap.cps.ncmp.rest.controller.handlers; -import java.util.List; +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL; +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ; + import java.util.Map; import java.util.UUID; import java.util.function.Supplier; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException; +import org.onap.cps.ncmp.api.impl.operations.DatastoreType; +import org.onap.cps.ncmp.api.impl.operations.OperationType; +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest; +import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException; import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor; import org.onap.cps.ncmp.rest.util.TopicValidator; import org.springframework.beans.factory.annotation.Value; @@ -67,7 +74,7 @@ public class NcmpDatastoreRequestHandler implements TaskManagementDefaultHandler final boolean asyncResponseRequested = topicParamInQuery != null; if (asyncResponseRequested && notificationFeatureEnabled) { return executeAsyncTaskAndGetResponseEntity(datastoreName, cmHandleId, resourceIdentifier, - optionsParamInQuery, topicParamInQuery, includeDescendants, false); + optionsParamInQuery, topicParamInQuery, includeDescendants); } if (asyncResponseRequested) { @@ -98,26 +105,21 @@ public class NcmpDatastoreRequestHandler implements TaskManagementDefaultHandler } /** - * Executes synchronous/asynchronous request for batch of cm handles. + * Executes asynchronous request for batch of cm handles to resource data. * - * @param datastoreName the name of the datastore - * @param cmHandleIds list of cm handles - * @param resourceIdentifier the resource identifier - * @param optionsParamInQuery the options param in query - * @param topicParamInQuery the topic param in query - * @param includeDescendants whether to include descendants or not + * @param topicParamInQuery the topic param in query + * @param resourceDataBatchRequest batch request details for resource data * @return the response entity */ - public ResponseEntity<Object> executeRequest(final String datastoreName, - final List<String> cmHandleIds, - final String resourceIdentifier, - final String optionsParamInQuery, - final String topicParamInQuery, - final boolean includeDescendants) { - - return executeAsyncTaskAndGetResponseEntity(datastoreName, cmHandleIds, resourceIdentifier, optionsParamInQuery, - topicParamInQuery, includeDescendants, true); - + public ResponseEntity<Object> executeRequest(final String topicParamInQuery, + final ResourceDataBatchRequest + resourceDataBatchRequest) { + validateBatchRequest(topicParamInQuery, resourceDataBatchRequest); + if (!notificationFeatureEnabled) { + return ResponseEntity.ok(Map.of("status", + "Asynchronous request is unavailable as notification feature is currently disabled.")); + } + return getRequestIdAndSendBatchRequestToDmiService(topicParamInQuery, resourceDataBatchRequest); } protected ResponseEntity<Object> executeTaskAsync(final String topicParamInQuery, @@ -127,7 +129,6 @@ public class NcmpDatastoreRequestHandler implements TaskManagementDefaultHandler TopicValidator.validateTopicName(topicParamInQuery); log.debug("Received Async request with id {}", requestId); cpsNcmpTaskExecutor.executeTask(taskSupplier, timeOutInMilliSeconds); - return ResponseEntity.ok(Map.of("requestId", requestId)); } @@ -136,25 +137,43 @@ public class NcmpDatastoreRequestHandler implements TaskManagementDefaultHandler } private ResponseEntity<Object> executeAsyncTaskAndGetResponseEntity(final String datastoreName, - final Object targetObject, + final String cmHandleId, final String resourceIdentifier, final String optionsParamInQuery, final String topicParamInQuery, - final boolean includeDescendants, - final boolean isBulkRequest) { + final boolean includeDescendants) { final String requestId = UUID.randomUUID().toString(); - final Supplier<Object> taskSupplier; - if (isBulkRequest) { - taskSupplier = getTaskSupplierForBulkRequest(datastoreName, (List<String>) targetObject, - resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId, includeDescendants); - } else { - taskSupplier = getTaskSupplierForGetRequest(datastoreName, targetObject.toString(), resourceIdentifier, - optionsParamInQuery, topicParamInQuery, requestId, includeDescendants); - } + final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(datastoreName, cmHandleId, + resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId, includeDescendants); if (taskSupplier == NO_OBJECT_SUPPLIER) { return new ResponseEntity<>(Map.of("status", "Unable to execute request as " + "datastore is not implemented."), HttpStatus.NOT_IMPLEMENTED); } return executeTaskAsync(topicParamInQuery, requestId, taskSupplier); } + + private ResponseEntity<Object> getRequestIdAndSendBatchRequestToDmiService(final String topicParamInQuery, + final ResourceDataBatchRequest + resourceDataBatchRequest) { + final String requestId = UUID.randomUUID().toString(); + sendResourceDataBatchRequestAsynchronously(topicParamInQuery, resourceDataBatchRequest, requestId); + return ResponseEntity.ok(Map.of("requestId", requestId)); + } + + private void validateBatchRequest(final String topicParamInQuery, + final ResourceDataBatchRequest + resourceDataBatchRequest) { + TopicValidator.validateTopicName(topicParamInQuery); + resourceDataBatchRequest.getBatchOperationDefinitions().forEach(batchOperationDetail -> { + if (OperationType.fromOperationName(batchOperationDetail.getOperation()) != READ) { + throw new OperationNotSupportedException( + batchOperationDetail.getOperation() + " operation not yet supported for target ids :" + + batchOperationDetail.getCmHandleIds()); + } else if (DatastoreType.fromDatastoreName(batchOperationDetail.getDatastore()) == OPERATIONAL) { + throw new InvalidDatastoreException(batchOperationDetail.getDatastore() + + " datastore is not supported for target ids : " + + batchOperationDetail.getCmHandleIds()); + } + }); + } } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java index 18e5a9f5ac..5c35818a3a 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java @@ -20,10 +20,11 @@ package org.onap.cps.ncmp.rest.controller.handlers; -import java.util.List; import java.util.function.Supplier; import org.onap.cps.ncmp.api.NetworkCmProxyDataService; +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest; import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Component @@ -56,17 +57,14 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH datastoreName, cmHandleId, resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId); } + @Async @Override - public Supplier<Object> getTaskSupplierForBulkRequest(final String datastoreName, - final List<String> cmHandleIds, - final String resourceIdentifier, - final String optionsParamInQuery, - final String topicParamInQuery, - final String requestId, - final boolean includeDescendants) { + public void sendResourceDataBatchRequestAsynchronously(final String topicParamInQuery, + final ResourceDataBatchRequest + resourceDataBatchRequest, + final String requestId) { + networkCmProxyDataService.requestResourceDataForCmHandleBatch(topicParamInQuery, resourceDataBatchRequest, + requestId); - return () -> networkCmProxyDataService.getResourceDataForCmHandleBatch( - datastoreName, cmHandleIds, resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId); } - } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/TaskManagementDefaultHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/TaskManagementDefaultHandler.java index 6d68f76802..937935bec4 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/TaskManagementDefaultHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/TaskManagementDefaultHandler.java @@ -20,8 +20,8 @@ package org.onap.cps.ncmp.rest.controller.handlers; -import java.util.List; import java.util.function.Supplier; +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest; import org.onap.cps.spi.FetchDescendantsOption; public interface TaskManagementDefaultHandler { @@ -46,14 +46,10 @@ public interface TaskManagementDefaultHandler { return NO_OBJECT_SUPPLIER; } - default Supplier<Object> getTaskSupplierForBulkRequest(final String datastoreName, - final List<String> cmHandleIds, - final String resourceIdentifier, - final String optionsParamInQuery, - final String topicParamInQuery, - final String requestId, - final boolean includeDescendant) { - return NO_OBJECT_SUPPLIER; + default void sendResourceDataBatchRequestAsynchronously(final String topicParamInQuery, + final ResourceDataBatchRequest + resourceDataBatchRequest, + final String requestId) { } static FetchDescendantsOption getFetchDescendantsOption(final boolean includeDescendants) { diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java index 5faeee69fc..f459acec25 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java @@ -77,7 +77,7 @@ public class NetworkCmProxyRestExceptionHandler { return wrapDmiErrorResponse(HttpStatus.BAD_GATEWAY, httpClientRequestException); } - @ExceptionHandler({DmiRequestException.class, DataValidationException.class, + @ExceptionHandler({DmiRequestException.class, DataValidationException.class, OperationNotSupportedException.class, HttpMessageNotReadableException.class, InvalidTopicException.class, InvalidDatastoreException.class}) public static ResponseEntity<Object> handleDmiRequestExceptions(final Exception exception) { return buildErrorResponse(HttpStatus.BAD_REQUEST, exception); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationEnum.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/OperationNotSupportedException.java index 796cef23d0..e1daf3df6f 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationEnum.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/OperationNotSupportedException.java @@ -18,26 +18,15 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.impl.operations; +package org.onap.cps.ncmp.rest.exceptions; -import com.fasterxml.jackson.annotation.JsonValue; - -public enum OperationEnum { - - READ("read"), - CREATE("create"), - UPDATE("update"), - PATCH("patch"), - DELETE("delete"); - private final String value; - - OperationEnum(final String value) { - this.value = value; - } - - @Override - @JsonValue - public String toString() { - return String.valueOf(value); +public class OperationNotSupportedException extends RuntimeException { + /** + * Instantiates a new not implemented operation exception. + * + * @param message the message + */ + public OperationNotSupportedException(final String message) { + super(message); } } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/ResourceDataBatchRequestMapper.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/ResourceDataBatchRequestMapper.java new file mode 100644 index 0000000000..d045e31610 --- /dev/null +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/ResourceDataBatchRequestMapper.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.rest.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.onap.cps.ncmp.api.models.BatchOperationDefinition; +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest; + +@Mapper(componentModel = "spring", nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT) +public interface ResourceDataBatchRequestMapper { + + @Mapping(source = "operations", target = "batchOperationDefinitions") + ResourceDataBatchRequest toResourceDataBatchRequest( + org.onap.cps.ncmp.rest.model.ResourceDataBatchRequest resourceDataBatchRequest); + + @Mapping(source = "targetIds", target = "cmHandleIds") + BatchOperationDefinition toBatchOperationDefinition( + org.onap.cps.ncmp.rest.model.BatchOperationDefinition batchOperationDefinition); +} diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy index fb411c054a..31e83aa7a1 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy @@ -32,11 +32,14 @@ import org.onap.cps.ncmp.api.inventory.CmHandleState import org.onap.cps.ncmp.api.inventory.CompositeState import org.onap.cps.ncmp.api.inventory.DataStoreSyncState import org.onap.cps.ncmp.api.inventory.LockReasonCategory +import org.onap.cps.ncmp.rest.model.BatchOperationDefinition import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper +import org.onap.cps.ncmp.rest.mapper.ResourceDataBatchRequestMapper +import org.onap.cps.ncmp.rest.model.ResourceDataBatchRequest import org.onap.cps.ncmp.rest.util.DeprecationHelper import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.model.ModuleDefinition @@ -62,10 +65,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.CREATE -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.UPDATE -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.PATCH -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.DELETE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH +import static org.onap.cps.ncmp.api.impl.operations.OperationType.DELETE import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL @@ -98,6 +101,9 @@ class NetworkCmProxyControllerSpec extends Specification { CmHandleStateMapper cmHandleStateMapper = Mappers.getMapper(CmHandleStateMapper) @SpringBean + ResourceDataBatchRequestMapper resourceDataBatchRequestMapper = Mappers.getMapper(ResourceDataBatchRequestMapper) + + @SpringBean CpsNcmpTaskExecutor spiedCpsTaskExecutor = Spy() @SpringBean @@ -113,7 +119,6 @@ class NetworkCmProxyControllerSpec extends Specification { def ncmpBasePathV1 def requestBody = '{"some-key":"some-value"}' - def bulkRequestBody = '["testCmHandle"]' def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC)) @@ -200,44 +205,69 @@ class NetworkCmProxyControllerSpec extends Specification { 'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#' } - def 'Get (async) bulk resource data from dmi service.'() { - given: 'bulk resource data url' - def getUrl = "$ncmpBasePathV1/batch/data/ds/${datastore.datastoreName}" + - "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=myTopic" + def 'Get (async) batch resource data from dmi service.'() { + given: 'batch resource data url' + def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name" + def resourceDataBatchRequestJsonData = jsonObjectMapper.asJsonString( + getResourceDataBatchRequest("read", datastore.datastoreName)) + def expectedDmiResourceDataBatchRequest + = jsonObjectMapper.convertJsonString(resourceDataBatchRequestJsonData, org.onap.cps.ncmp.api.models.ResourceDataBatchRequest.class) when: 'post data resource request is performed' def response = mvc.perform( post(getUrl) .contentType(MediaType.APPLICATION_JSON) - .content(bulkRequestBody) + .content(resourceDataBatchRequestJsonData) ).andReturn().response then: 'response status is Ok' response.status == HttpStatus.OK.value() and: 'async request id is generated' assert response.contentAsString.contains("requestId") then: 'wait a little to allow execution of service method by task executor (on separate thread)' - Thread.sleep(100); + Thread.sleep(100) then: 'the service has been invoked with the correct parameters ' - 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandleBatch(datastore.datastoreName, ['testCmHandle'], - 'parent/child', - '(a=1,b=2)', - 'myTopic', - _) + 1 * mockNetworkCmProxyDataService.requestResourceDataForCmHandleBatch('my-topic-name', expectedDmiResourceDataBatchRequest, _) where: 'the following data stores are used' datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL] } - def 'Get bulk resource data for non-supported #datastoreName from dmi service.'() { - given: 'bulk resource data url' - def getUrl = "$ncmpBasePathV1/batch/data/ds/ncmp-datastore:operational" + - "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=myTopic" + def 'Get batch resource data for #scenario from dmi service.'() { + given: 'batch resource data url' + def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name" + def resourceDataBatchRequestJsonData = jsonObjectMapper.asJsonString( + getResourceDataBatchRequest(operation, datastore)) + when: 'post data resource request is performed' + def response = mvc.perform( + post(getUrl) + .contentType(MediaType.APPLICATION_JSON) + .content(resourceDataBatchRequestJsonData) + ).andReturn().response + then: 'response status is BAD_REQUEST' + response.status == HttpStatus.BAD_REQUEST.value() + where: 'the following parameters are used' + scenario | datastore | operation + 'non-supported datastoreName' | OPERATIONAL.datastoreName | 'read' + 'non-supported operation (passthrough-running)' | PASSTHROUGH_RUNNING.datastoreName | 'create' + 'non-supported operation (passthrough-operational)' | PASSTHROUGH_OPERATIONAL.datastoreName | 'create' + } + + def 'Get batch resource data when notification feature is disabled for datastore: #datastore.'() { + given: 'batch resource data url' + def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name" + def resourceDataBatchRequestJsonData = jsonObjectMapper.asJsonString( + getResourceDataBatchRequest("read", datastore.datastoreName)) + ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = false when: 'post data resource request is performed' def response = mvc.perform( post(getUrl) .contentType(MediaType.APPLICATION_JSON) - .content(bulkRequestBody) + .content(resourceDataBatchRequestJsonData) ).andReturn().response - then: 'response status code is 501 not implemented' - response.status == HttpStatus.NOT_IMPLEMENTED.value() + then: 'response status is Ok' + response.status == HttpStatus.OK.value() + and: 'async request id is unavailable' + assert response.contentAsString == '{"status":"Asynchronous request is unavailable as notification feature is currently disabled."}' + where: 'the following data stores are used' + datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL] } def 'Query Resource Data from operational.'() { @@ -656,5 +686,23 @@ class NetworkCmProxyControllerSpec extends Specification { return assertContainsAll(response, expectedContent) } + def getResourceDataBatchRequest(operation, datastore) { + def resourceDataBatchRequest = new ResourceDataBatchRequest() + def batchOperationDefinitions = new ArrayList() + batchOperationDefinitions.add(getBatchOperationDefinition(operation, datastore)) + resourceDataBatchRequest.addOperationsItem(batchOperationDefinitions) + } + + def getBatchOperationDefinition(operation, datastore) { + def batchOperationDefinition = new BatchOperationDefinition() + batchOperationDefinition.setOperation(operation) + batchOperationDefinition.setOperationId("operational-12") + batchOperationDefinition.setDatastore(datastore) + batchOperationDefinition.setOptions("some option") + batchOperationDefinition.setResourceIdentifier("some resource identifier") + batchOperationDefinition.addTargetIdsItem("some-cm-handle") + return batchOperationDefinition + } + } diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy index f44d6c9907..a3afc5546f 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy @@ -33,6 +33,7 @@ import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandl import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper +import org.onap.cps.ncmp.rest.mapper.ResourceDataBatchRequestMapper import org.onap.cps.ncmp.rest.util.DeprecationHelper import org.onap.cps.spi.exceptions.AlreadyDefinedException import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch @@ -75,6 +76,9 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification { CmHandleStateMapper cmHandleStateMapper = Mappers.getMapper(CmHandleStateMapper) @SpringBean + ResourceDataBatchRequestMapper resourceDataBatchRequestMapper = Mappers.getMapper(ResourceDataBatchRequestMapper) + + @SpringBean CpsNcmpTaskExecutor stubbedCpsTaskExecutor = Stub() @SpringBean diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml index 0a732ef873..b87fe64366 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -38,6 +38,10 @@ </properties> <dependencies> <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> <groupId>${project.groupId}</groupId> <artifactId>cps-service</artifactId> </dependency> @@ -67,6 +71,17 @@ </dependency> <!-- T E S T - D E P E N D E N C I E S --> <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <scope>test</scope> @@ -86,16 +101,5 @@ <artifactId>spock</artifactId> <scope>test</scope> </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> - <exclusions> - <exclusion> - <groupId>org.junit.vintage</groupId> - <artifactId>junit-vintage-engine</artifactId> - </exclusion> - </exclusions> - </dependency> </dependencies> </project> diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java index 05490d8fc9..046c78879b 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java @@ -24,15 +24,15 @@ package org.onap.cps.ncmp.api; import java.util.Collection; -import java.util.List; import java.util.Map; -import org.onap.cps.ncmp.api.impl.operations.OperationEnum; +import org.onap.cps.ncmp.api.impl.operations.OperationType; import org.onap.cps.ncmp.api.inventory.CompositeState; import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters; import org.onap.cps.ncmp.api.models.DmiPluginRegistration; import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.ModuleDefinition; import org.onap.cps.spi.model.ModuleReference; @@ -83,22 +83,15 @@ public interface NetworkCmProxyDataService { FetchDescendantsOption fetchDescendantsOption); /** - * Get resource data for given batch of cm handles using dmi. + * Get resource data for batch of cm handles using dmi. * - * @param datastoreName datastore name - * @param cmHandleIds cm handle identifiers - * @param resourceIdentifier resource identifier - * @param optionsParamInQuery options query - * @param topicParamInQuery topic name for (triggering) async responses - * @param requestId unique requestId for async request - * @return {@code Object} resource data + * @param topicParamInQuery topic name for (triggering) async responses + * @param resourceDataBatchRequest cm handle identifiers */ - Object getResourceDataForCmHandleBatch(String datastoreName, - List<String> cmHandleIds, - String resourceIdentifier, - String optionsParamInQuery, - String topicParamInQuery, - String requestId); + void requestResourceDataForCmHandleBatch(String topicParamInQuery, + ResourceDataBatchRequest + resourceDataBatchRequest, + String requestId); /** @@ -106,14 +99,14 @@ public interface NetworkCmProxyDataService { * * @param cmHandleId cm handle identifier * @param resourceIdentifier resource identifier - * @param operation required operation + * @param operationType required operation type * @param requestBody request body to create resource * @param contentType content type in body * @return {@code Object} return data */ Object writeResourceDataPassThroughRunningForCmHandle(String cmHandleId, String resourceIdentifier, - OperationEnum operation, + OperationType operationType, String requestBody, String contentType); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java index e478b0053b..536775ec5c 100755 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java @@ -45,7 +45,7 @@ import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService; import org.onap.cps.ncmp.api.NetworkCmProxyDataService; import org.onap.cps.ncmp.api.impl.events.lcm.LcmEventsCmHandleStateHandler; import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations; -import org.onap.cps.ncmp.api.impl.operations.OperationEnum; +import org.onap.cps.ncmp.api.impl.operations.OperationType; import org.onap.cps.ncmp.api.impl.utils.CmHandleQueryConditions; import org.onap.cps.ncmp.api.impl.utils.InventoryQueryConditions; import org.onap.cps.ncmp.api.impl.utils.YangDataConverter; @@ -63,6 +63,7 @@ import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationErr import org.onap.cps.ncmp.api.models.DmiPluginRegistration; import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch; import org.onap.cps.spi.exceptions.CpsException; @@ -138,28 +139,21 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService } @Override - public Object getResourceDataForCmHandleBatch(final String datastoreName, - final List<String> cmHandleIds, - final String resourceIdentifier, - final String optionsParamInQuery, - final String topicParamInQuery, - final String requestId) { - final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(datastoreName, cmHandleIds, - resourceIdentifier, - optionsParamInQuery, - topicParamInQuery, - requestId); - return responseEntity.getBody(); + public void requestResourceDataForCmHandleBatch(final String topicParamInQuery, + final ResourceDataBatchRequest + resourceDataBatchRequest, + final String requestId) { + dmiDataOperations.requestResourceDataFromDmi(topicParamInQuery, resourceDataBatchRequest, requestId); } @Override public Object writeResourceDataPassThroughRunningForCmHandle(final String cmHandleId, final String resourceIdentifier, - final OperationEnum operation, + final OperationType operationType, final String requestData, final String dataType) { - return dmiDataOperations.writeResourceDataPassThroughRunningFromDmi(cmHandleId, resourceIdentifier, operation, - requestData, dataType); + return dmiDataOperations.writeResourceDataPassThroughRunningFromDmi(cmHandleId, resourceIdentifier, + operationType, requestData, dataType); } @Override diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/async/BatchRecordFilterStrategy.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/async/BatchRecordFilterStrategy.java new file mode 100644 index 0000000000..088e96564c --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/async/BatchRecordFilterStrategy.java @@ -0,0 +1,50 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.async; + +import org.apache.commons.lang3.SerializationUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.listener.adapter.RecordFilterStrategy; + +/** + * Batch Record filter strategy, which helps to filter the consumer records. + * + */ +@Configuration +public class BatchRecordFilterStrategy { + + /** + * Filtering the consumer records based on the eventType header, It + * returns boolean, true means filter the consumer record and false + * means not filter the consumer record. + * @return boolean value. + */ + @Bean + public RecordFilterStrategy<Object, Object> filterBatchDataResponseEvent() { + return consumedRecord -> { + final String headerValue = SerializationUtils + .deserialize(consumedRecord.headers().lastHeader("eventType").value()); + return !(headerValue != null + && headerValue.startsWith("org.onap.cps.ncmp.events.async.BatchDataResponseEvent")); + }; + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/async/NcmpAsyncBatchEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/async/NcmpAsyncBatchEventConsumer.java new file mode 100644 index 0000000000..2a332d0037 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/async/NcmpAsyncBatchEventConsumer.java @@ -0,0 +1,64 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.async; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.SerializationUtils; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.onap.cps.ncmp.api.impl.events.EventsPublisher; +import org.onap.cps.ncmp.events.async.BatchDataResponseEventV1; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +/** + * Listener for cps-ncmp async batch events. + */ +@Component +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(name = "notification.enabled", havingValue = "true", matchIfMissing = true) +public class NcmpAsyncBatchEventConsumer { + + private final EventsPublisher<BatchDataResponseEventV1> eventsPublisher; + + /** + * Consume the BatchDataResponseEvent published by producer to topic 'async-m2m.topic' + * and publish the same to the client specified topic. + * + * @param batchEventConsumerRecord consuming event as a ConsumerRecord. + */ + @KafkaListener( + topics = "${app.ncmp.async-m2m.topic}", + filter = "filterBatchDataResponseEvent", + groupId = "ncmp-batch-event-group", + properties = {"spring.json.value.default.type=org.onap.cps.ncmp.events.async.BatchDataResponseEventV1"}) + public void consumeAndPublish(final ConsumerRecord<String, BatchDataResponseEventV1> batchEventConsumerRecord) { + log.info("Consuming event payload {} ...", batchEventConsumerRecord.value()); + final String eventTarget = SerializationUtils + .deserialize(batchEventConsumerRecord.headers().lastHeader("eventTarget").value()); + final String eventId = SerializationUtils + .deserialize(batchEventConsumerRecord.headers().lastHeader("eventId").value()); + eventsPublisher.publishEvent(eventTarget, eventId, batchEventConsumerRecord.headers(), + batchEventConsumerRecord.value()); + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java index 9d087806c0..136935ba53 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java @@ -24,7 +24,7 @@ package org.onap.cps.ncmp.api.impl.client; import lombok.AllArgsConstructor; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration.DmiProperties; import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; -import org.onap.cps.ncmp.api.impl.operations.OperationEnum; +import org.onap.cps.ncmp.api.impl.operations.OperationType; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -44,17 +44,17 @@ public class DmiRestClient { * Sends POST operation to DMI with json body containing module references. * @param dmiResourceUrl dmi resource url * @param requestBodyAsJsonString json data body - * @param operation the type of operation being executed (for error reporting only) + * @param operationType the type of operation being executed (for error reporting only) * @return response entity of type String */ public ResponseEntity<Object> postOperationWithJsonData(final String dmiResourceUrl, final String requestBodyAsJsonString, - final OperationEnum operation) { + final OperationType operationType) { final var httpEntity = new HttpEntity<>(requestBodyAsJsonString, configureHttpHeaders(new HttpHeaders())); try { return restTemplate.postForEntity(dmiResourceUrl, httpEntity, Object.class); } catch (final HttpStatusCodeException httpStatusCodeException) { - final String exceptionMessage = "Unable to " + operation.toString() + " resource data."; + final String exceptionMessage = "Unable to " + operationType.toString() + " resource data."; throw new HttpClientRequestException(exceptionMessage, httpStatusCodeException.getResponseBodyAsString(), httpStatusCodeException.getRawStatusCode()); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/EventsPublisher.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/EventsPublisher.java index 4c84629304..b0b091a2f6 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/EventsPublisher.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/EventsPublisher.java @@ -20,13 +20,16 @@ package org.onap.cps.ncmp.api.impl.events; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.header.Headers; +import org.apache.kafka.common.header.internals.RecordHeaders; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.support.SendResult; import org.springframework.stereotype.Service; +import org.springframework.util.SerializationUtils; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureCallback; @@ -70,6 +73,20 @@ public class EventsPublisher<T> { eventFuture.addCallback(handleCallback(topicName)); } + /** + * Generic Event Publisher with headers. + * + * @param topicName valid topic name + * @param eventKey message key + * @param eventHeaders map of event headers + * @param event message payload + */ + public void publishEvent(final String topicName, final String eventKey, final Map<String, Object> eventHeaders, + final T event) { + + publishEvent(topicName, eventKey, convertToKafkaHeaders(eventHeaders), event); + } + private ListenableFutureCallback<SendResult<String, T>> handleCallback(final String topicName) { return new ListenableFutureCallback<>() { @Override @@ -85,4 +102,10 @@ public class EventsPublisher<T> { }; } + private Headers convertToKafkaHeaders(final Map<String, Object> eventMessageHeaders) { + final Headers eventHeaders = new RecordHeaders(); + eventMessageHeaders.forEach((key, value) -> eventHeaders.add(key, SerializationUtils.serialize(value))); + return eventHeaders; + } + } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventHeaderMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventHeaderMapper.java new file mode 100644 index 0000000000..f7707d9f76 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventHeaderMapper.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.events.lcm; + +import org.mapstruct.Mapper; +import org.onap.cps.ncmp.events.lcm.v1.LcmEvent; +import org.onap.cps.ncmp.events.lcm.v1.LcmEventHeader; + +@Mapper(componentModel = "spring") +public interface LcmEventHeaderMapper { + + /** + * Mapper for converting incoming {@link LcmEvent} to outgoing {@link LcmEventHeader}. + */ + + LcmEventHeader toLcmEventHeader(LcmEvent lcmEvent); + +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCmHandleStateHandlerImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCmHandleStateHandlerImpl.java index 9d518432ad..f42cd39d4d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCmHandleStateHandlerImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCmHandleStateHandlerImpl.java @@ -43,7 +43,8 @@ import org.onap.cps.ncmp.api.inventory.CompositeState; import org.onap.cps.ncmp.api.inventory.CompositeStateUtils; import org.onap.cps.ncmp.api.inventory.InventoryPersistence; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; -import org.onap.ncmp.cmhandle.event.lcm.LcmEvent; +import org.onap.cps.ncmp.events.lcm.v1.LcmEvent; +import org.onap.cps.ncmp.events.lcm.v1.LcmEventHeader; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -76,7 +77,7 @@ public class LcmEventsCmHandleStateHandlerImpl implements LcmEventsCmHandleState @Override @Timed(value = "cps.ncmp.cmhandle.state.update.batch", - description = "Time taken to update a batch of cm handle states") + description = "Time taken to update a batch of cm handle states") public void updateCmHandleStateBatch(final Map<YangModelCmHandle, CmHandleState> cmHandleStatePerCmHandle) { final Collection<CmHandleTransitionPair> cmHandleTransitionPairs = prepareCmHandleTransitionBatch(cmHandleStatePerCmHandle); @@ -106,9 +107,12 @@ public class LcmEventsCmHandleStateHandlerImpl implements LcmEventsCmHandleState private void publishLcmEvent(final NcmpServiceCmHandle targetNcmpServiceCmHandle, final NcmpServiceCmHandle existingNcmpServiceCmHandle) { final String cmHandleId = targetNcmpServiceCmHandle.getCmHandleId(); + final LcmEventHeader lcmEventHeader = + lcmEventsCreator.populateLcmEventHeader(cmHandleId, targetNcmpServiceCmHandle, + existingNcmpServiceCmHandle); final LcmEvent lcmEvent = lcmEventsCreator.populateLcmEvent(cmHandleId, targetNcmpServiceCmHandle, existingNcmpServiceCmHandle); - lcmEventsService.publishLcmEvent(cmHandleId, lcmEvent); + lcmEventsService.publishLcmEvent(cmHandleId, lcmEvent, lcmEventHeader); } private Collection<CmHandleTransitionPair> prepareCmHandleTransitionBatch( @@ -221,6 +225,7 @@ public class LcmEventsCmHandleStateHandlerImpl implements LcmEventsCmHandleState @Setter @NoArgsConstructor static class CmHandleTransitionPair { + private YangModelCmHandle currentYangModelCmHandle; private YangModelCmHandle targetYangModelCmHandle; } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreator.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreator.java index a72e664dcf..3c7c92b129 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreator.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreator.java @@ -23,13 +23,15 @@ package org.onap.cps.ncmp.api.impl.events.lcm; import java.util.UUID; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.impl.utils.EventDateTimeFormatter; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; -import org.onap.ncmp.cmhandle.event.lcm.Event; -import org.onap.ncmp.cmhandle.event.lcm.LcmEvent; -import org.onap.ncmp.cmhandle.event.lcm.Values; +import org.onap.cps.ncmp.events.lcm.v1.Event; +import org.onap.cps.ncmp.events.lcm.v1.LcmEvent; +import org.onap.cps.ncmp.events.lcm.v1.LcmEventHeader; +import org.onap.cps.ncmp.events.lcm.v1.Values; import org.springframework.stereotype.Component; @@ -38,8 +40,11 @@ import org.springframework.stereotype.Component; */ @Slf4j @Component +@RequiredArgsConstructor public class LcmEventsCreator { + private final LcmEventHeaderMapper lcmEventHeaderMapper; + /** * Populate Lifecycle Management Event. * @@ -53,6 +58,20 @@ public class LcmEventsCreator { return createLcmEvent(cmHandleId, targetNcmpServiceCmHandle, existingNcmpServiceCmHandle); } + /** + * Populate Lifecycle Management Event Header. + * + * @param cmHandleId cm handle identifier + * @param targetNcmpServiceCmHandle target ncmp service cmhandle + * @param existingNcmpServiceCmHandle existing ncmp service cmhandle + * @return Populated LcmEventHeader + */ + public LcmEventHeader populateLcmEventHeader(final String cmHandleId, + final NcmpServiceCmHandle targetNcmpServiceCmHandle, + final NcmpServiceCmHandle existingNcmpServiceCmHandle) { + return createLcmEventHeader(cmHandleId, targetNcmpServiceCmHandle, existingNcmpServiceCmHandle); + } + private LcmEvent createLcmEvent(final String cmHandleId, final NcmpServiceCmHandle targetNcmpServiceCmHandle, final NcmpServiceCmHandle existingNcmpServiceCmHandle) { final LcmEventType lcmEventType = @@ -63,6 +82,15 @@ public class LcmEventsCreator { return lcmEvent; } + private LcmEventHeader createLcmEventHeader(final String cmHandleId, + final NcmpServiceCmHandle targetNcmpServiceCmHandle, + final NcmpServiceCmHandle existingNcmpServiceCmHandle) { + final LcmEventType lcmEventType = + LcmEventsCreatorHelper.determineEventType(targetNcmpServiceCmHandle, existingNcmpServiceCmHandle); + final LcmEvent lcmEventWithHeaderInformation = lcmEventHeader(cmHandleId, lcmEventType); + return lcmEventHeaderMapper.toLcmEventHeader(lcmEventWithHeaderInformation); + } + private Event lcmEventPayload(final String eventCorrelationId, final NcmpServiceCmHandle targetNcmpServiceCmHandle, final NcmpServiceCmHandle existingNcmpServiceCmHandle, final LcmEventType lcmEventType) { final Event event = new Event(); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreatorHelper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreatorHelper.java index 1322b7277f..d3b45d4a63 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreatorHelper.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreatorHelper.java @@ -34,7 +34,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; -import org.onap.ncmp.cmhandle.event.lcm.Values; +import org.onap.cps.ncmp.events.lcm.v1.Values; /** * LcmEventsCreatorHelper has helper methods to create LcmEvent. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsService.java index f258b45976..2e1b914b1d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsService.java @@ -21,10 +21,13 @@ package org.onap.cps.ncmp.api.impl.events.lcm; import io.micrometer.core.annotation.Timed; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.impl.events.EventsPublisher; -import org.onap.ncmp.cmhandle.event.lcm.LcmEvent; +import org.onap.cps.ncmp.events.lcm.v1.LcmEvent; +import org.onap.cps.ncmp.events.lcm.v1.LcmEventHeader; +import org.onap.cps.utils.JsonObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.kafka.KafkaException; import org.springframework.stereotype.Service; @@ -39,6 +42,7 @@ import org.springframework.stereotype.Service; public class LcmEventsService { private final EventsPublisher<LcmEvent> eventsPublisher; + private final JsonObjectMapper jsonObjectMapper; @Value("${app.lcm.events.topic:ncmp-events}") private String topicName; @@ -47,17 +51,19 @@ public class LcmEventsService { private boolean notificationsEnabled; /** - * Publish the LcmEvent to the public topic. + * Publish the LcmEvent with header to the public topic. * - * @param cmHandleId Cm Handle Id - * @param lcmEvent Lcm Event + * @param cmHandleId Cm Handle Id + * @param lcmEvent Lcm Event + * @param lcmEventHeader Lcm Event Header */ - @Timed(value = "cps.ncmp.lcm.events.publish", - description = "Time taken to publish a LCM event") - public void publishLcmEvent(final String cmHandleId, final LcmEvent lcmEvent) { + @Timed(value = "cps.ncmp.lcm.events.publish", description = "Time taken to publish a LCM event") + public void publishLcmEvent(final String cmHandleId, final LcmEvent lcmEvent, final LcmEventHeader lcmEventHeader) { if (notificationsEnabled) { try { - eventsPublisher.publishEvent(topicName, cmHandleId, lcmEvent); + final Map<String, Object> lcmEventHeadersMap = + jsonObjectMapper.convertToValueType(lcmEventHeader, Map.class); + eventsPublisher.publishEvent(topicName, cmHandleId, lcmEventHeadersMap, lcmEvent); } catch (final KafkaException e) { log.error("Unable to publish message to topic : {} and cause : {}", topicName, e.getMessage()); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidOperationException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidOperationException.java new file mode 100644 index 0000000000..17069098cb --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidOperationException.java @@ -0,0 +1,32 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.exception; + +public class InvalidOperationException extends RuntimeException { + /** + * Instantiates a new invalid operation exception. + * + * @param message the message + */ + public InvalidOperationException(final String message) { + super(message); + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/CmHandle.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/CmHandle.java new file mode 100644 index 0000000000..618da74543 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/CmHandle.java @@ -0,0 +1,42 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.operations; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import lombok.Builder; +import lombok.Getter; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Builder +public class CmHandle { + private String id; + + @JsonProperty("cmHandleProperties") + private Map<String, String> dmiProperties; + + public static CmHandle buildCmHandleWithProperties(final String cmHandleId, + final Map<String, String> dmiProperties) { + return CmHandle.builder().id(cmHandleId).dmiProperties(dmiProperties).build(); + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiBatchOperation.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiBatchOperation.java new file mode 100644 index 0000000000..76ad0f7b2e --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiBatchOperation.java @@ -0,0 +1,64 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.operations; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import org.onap.cps.ncmp.api.models.BatchOperationDefinition; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Builder +@JsonPropertyOrder({"operation", "operationId", "datastore", "options", "resourceIdentifier", "cmHandles"}) +public class DmiBatchOperation { + + @JsonProperty("operation") + private OperationType operationType; + private String operationId; + private String datastore; + private String options; + private String resourceIdentifier; + + private final List<CmHandle> cmHandles = new ArrayList<>(); + + /** + * Create and initialise a (outgoing) DMI batch operation. + * + * @param batchOperationDefinition batchOperationDefinition definition of incoming of batch request + * @return mapped dmi operation details + */ + public static DmiBatchOperation buildDmiBatchRequestBodyWithoutCmHandles( + final BatchOperationDefinition batchOperationDefinition) { + + return DmiBatchOperation.builder() + .operationType(OperationType.fromOperationName(batchOperationDefinition.getOperation())) + .operationId(batchOperationDefinition.getOperationId()) + .datastore(DatastoreType.fromDatastoreName(batchOperationDefinition.getDatastore()).getDatastoreName()) + .options(batchOperationDefinition.getOptions()) + .resourceIdentifier(batchOperationDefinition.getResourceIdentifier()) + .build(); + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java index 1a3952306f..3e8d40a83b 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java @@ -22,25 +22,28 @@ package org.onap.cps.ncmp.api.impl.operations; import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING; -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.READ; +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; import org.onap.cps.ncmp.api.impl.executor.TaskExecutor; -import org.onap.cps.ncmp.api.impl.utils.DmiServiceNameOrganizer; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; +import org.onap.cps.ncmp.api.impl.utils.ResourceDataBatchRequestUtils; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.ncmp.api.inventory.CmHandleState; import org.onap.cps.ncmp.api.inventory.InventoryPersistence; +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest; import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.utils.JsonObjectMapper; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; /** * Operations class for DMI data. @@ -50,7 +53,6 @@ import org.springframework.stereotype.Component; public class DmiDataOperations extends DmiOperations { private static final long DEFAULT_ASYNC_TASK_EXECUTOR_TIMEOUT_IN_MILLISECONDS = 30000L; - private static final String NO_CM_HANDLE_ID = ""; public DmiDataOperations(final InventoryPersistence inventoryPersistence, final JsonObjectMapper jsonObjectMapper, @@ -89,39 +91,12 @@ public class DmiDataOperations extends DmiOperations { } /** - * This method fetches the resource data by data store for given list of cm handles using dmi client. - * - * @param dataStoreName data store name - * @param cmHandleIds list of cm handles - * @param resourceId resource identifier - * @param optionsParamInQuery options query - * @param topicParamInQuery topic name for (triggering) async responses - * @param requestId requestId for async responses - * @return {@code ResponseEntity} response entity - */ - public ResponseEntity<Object> getResourceDataFromDmi(final String dataStoreName, - final List<String> cmHandleIds, - final String resourceId, - final String optionsParamInQuery, - final String topicParamInQuery, - final String requestId) { - final Collection<YangModelCmHandle> yangModelCmHandles - = inventoryPersistence.getYangModelCmHandles(cmHandleIds); - final Map<String, Map<String, Map<String, String>>> dmiServiceNameCmHandlePropertiesMap = - DmiServiceNameOrganizer.getDmiPropertiesPerCmHandleIdPerServiceName(yangModelCmHandles); - - buildBulkResourceDataRequestAndSend(dataStoreName, resourceId, optionsParamInQuery, - topicParamInQuery, requestId, dmiServiceNameCmHandlePropertiesMap); - return new ResponseEntity<>(HttpStatus.ACCEPTED); - } - - /** * This method fetches all the resource data from operational data store for given cm handle * identifier using dmi client. * - * @param dataStoreName data store name - * @param cmHandleId network resource identifier - * @param requestId requestId for async responses + * @param dataStoreName data store name + * @param cmHandleId network resource identifier + * @param requestId requestId for async responses * @return {@code ResponseEntity} response entity */ public ResponseEntity<Object> getResourceDataFromDmi(final String dataStoreName, @@ -130,12 +105,37 @@ public class DmiDataOperations extends DmiOperations { final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId); final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); - final String dmiResourceDataUrl = getDmiRequestUrl(dataStoreName, cmHandleId, "/", null, - null, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); + final String dmiResourceDataUrl = getDmiRequestUrl(dataStoreName, cmHandleId, "/", + null, null, + yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); - return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, - READ); + return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, READ); + } + + /** + * This method requests the resource data by data store for given list of cm handles using dmi client. + * The data wil be returned as message on the topic specified. + * + * @param topicParamInQuery topic name for (triggering) async responses + * @param resourceDataBatchRequest batch request for resource data + * @param requestId requestId for as a response + */ + public void requestResourceDataFromDmi(final String topicParamInQuery, + final ResourceDataBatchRequest resourceDataBatchRequest, + final String requestId) { + + final Set<String> cmHandlesIds + = getDistinctCmHandleIdsFromBatchRequest(resourceDataBatchRequest); + + final Collection<YangModelCmHandle> yangModelCmHandles + = getYangModelCmHandlesInReadyState(cmHandlesIds); + + final Map<String, List<DmiBatchOperation>> operationsOutPerDmiServiceName + = ResourceDataBatchRequestUtils.processPerOperationInBatchRequest(resourceDataBatchRequest, + yangModelCmHandles); + + buildBatchRequestUrlAndSendToDmiService(topicParamInQuery, requestId, operationsOutPerDmiServiceName); } /** @@ -143,36 +143,39 @@ public class DmiDataOperations extends DmiOperations { * identifier on given resource using dmi client. * * @param cmHandleId network resource identifier - * @param resourceId resource identifier - * @param operation operation enum - * @param requestData the request data - * @param dataType data type + * @param resourceId resource identifier + * @param operationType operation enum + * @param requestData the request data + * @param dataType data type * @return {@code ResponseEntity} response entity */ public ResponseEntity<Object> writeResourceDataPassThroughRunningFromDmi(final String cmHandleId, final String resourceId, - final OperationEnum operation, + final OperationType operationType, final String requestData, final String dataType) { final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId); - final String jsonRequestBody = getDmiRequestBody(operation, null, requestData, dataType, + final String jsonRequestBody = getDmiRequestBody(operationType, null, requestData, dataType, yangModelCmHandle); final String dmiUrl = getDmiRequestUrl(PASSTHROUGH_RUNNING.getDatastoreName(), cmHandleId, resourceId, null, null, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); - return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonRequestBody, operation); + return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonRequestBody, operationType); } private YangModelCmHandle getYangModelCmHandle(final String cmHandleId) { return inventoryPersistence.getYangModelCmHandle(cmHandleId); } - private String getDmiRequestBody(final OperationEnum operation, final String requestId, final String requestData, - final String dataType, final YangModelCmHandle yangModelCmHandle) { + private String getDmiRequestBody(final OperationType operationType, + final String requestId, + final String requestData, + final String dataType, + final YangModelCmHandle yangModelCmHandle) { final DmiRequestBody dmiRequestBody = DmiRequestBody.builder() - .operation(operation) + .operationType(operationType) .requestId(requestId) .data(requestData) .dataType(dataType) @@ -181,17 +184,6 @@ public class DmiDataOperations extends DmiOperations { return jsonObjectMapper.asJsonString(dmiRequestBody); } - private String getDmiBulkRequestBody(final OperationEnum operation, - final String requestId, - final String requestData) { - final DmiRequestBody dmiBulkRequestBody = DmiRequestBody.builder() - .operation(operation) - .requestId(requestId) - .data(requestData) - .build(); - return jsonObjectMapper.asJsonString(dmiBulkRequestBody); - } - private String getDmiRequestUrl(final String dataStoreName, final String cmHandleId, final String resourceId, @@ -204,15 +196,13 @@ public class DmiDataOperations extends DmiOperations { cmHandleId)); } - private String getDmiServiceBulkRequestUrl(final String dataStoreName, - final String resourceId, - final String optionsParamInQuery, - final String topicParamInQuery, - final String dmiServiceName) { - return dmiServiceUrlBuilder.getBulkRequestUrl( - dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery, - topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables(dataStoreName, dmiServiceName, - NO_CM_HANDLE_ID)); + private String getDmiServiceBatchRequestUrl(final String dmiServiceName, + final String topicParamInQuery, + final String requestId) { + final MultiValueMap<String, String> batchRequestQueryParams = dmiServiceUrlBuilder + .getBatchRequestQueryParams(topicParamInQuery, requestId); + return dmiServiceUrlBuilder.getBatchRequestUrl(batchRequestQueryParams, + dmiServiceUrlBuilder.populateBatchUriVariables(dmiServiceName)); } private void validateIfCmHandleStateReady(final YangModelCmHandle yangModelCmHandle, @@ -224,31 +214,41 @@ public class DmiDataOperations extends DmiOperations { } } - private void buildBulkResourceDataRequestAndSend(final String dataStoreName, - final String resourceId, - final String optionsParamInQuery, - final String topicParamInQuery, - final String requestId, - final Map<String, Map<String, Map<String, String>>> - dmiServiceNameCmHandlePropertiesMap) { - dmiServiceNameCmHandlePropertiesMap.entrySet().parallelStream().forEach( - dmiServiceNameCmHandlePropertiesEntry -> { - final String dmiBulkResourceDataUrl = getDmiServiceBulkRequestUrl(dataStoreName, resourceId, - optionsParamInQuery, topicParamInQuery, dmiServiceNameCmHandlePropertiesEntry.getKey()); - final String jsonRequestBodyAsJsonString = - jsonObjectMapper.asJsonString(dmiServiceNameCmHandlePropertiesEntry.getValue()); - final String jsonRequestBody - = getDmiBulkRequestBody(READ, requestId, jsonRequestBodyAsJsonString); - sendDmiResourceDataRequestToDmiService(dmiBulkResourceDataUrl, jsonRequestBody); - }); + private static Set<String> getDistinctCmHandleIdsFromBatchRequest(final ResourceDataBatchRequest + resourceDataBatchRequest) { + return resourceDataBatchRequest.getBatchOperationDefinitions().stream() + .flatMap(batchOperationDefinition -> + batchOperationDefinition.getCmHandleIds().stream()).collect(Collectors.toSet()); + } + + private Collection<YangModelCmHandle> getYangModelCmHandlesInReadyState(final Set<String> requestedCmHandleIds) { + // TODO Need to publish an error response to client given topic. + // Code should be implemented into https://jira.onap.org/browse/CPS-1614 ( + // NCMP : Error handling for non-ready cm handle state) + return inventoryPersistence.getYangModelCmHandles(requestedCmHandleIds).stream() + .filter(yangModelCmHandle -> yangModelCmHandle.getCompositeState().getCmHandleState() + == CmHandleState.READY).collect(Collectors.toList()); + } + + private void buildBatchRequestUrlAndSendToDmiService(final String topicParamInQuery, + final String requestId, + final Map<String, List<DmiBatchOperation>> + groupsOutPerDmiServiceName) { + + groupsOutPerDmiServiceName.entrySet().forEach(groupsOutPerDmiServiceNameEntry -> { + final String dmiServiceName = groupsOutPerDmiServiceNameEntry.getKey(); + final List<DmiBatchOperation> dmiBatchRequestBodies = groupsOutPerDmiServiceNameEntry.getValue(); + final String dmiBatchResourceDataUrl = getDmiServiceBatchRequestUrl(dmiServiceName, topicParamInQuery, + requestId); + sendBatchRequestToDmiService(dmiBatchResourceDataUrl, dmiBatchRequestBodies); + }); } - private void sendDmiResourceDataRequestToDmiService(final String dmiBulkResourceDataUrl, - final String dmiResourceDataRequestAsJsonString) { - TaskExecutor.executeTask(() -> - dmiRestClient.postOperationWithJsonData(dmiBulkResourceDataUrl, - dmiResourceDataRequestAsJsonString, READ), - DEFAULT_ASYNC_TASK_EXECUTOR_TIMEOUT_IN_MILLISECONDS) + private void sendBatchRequestToDmiService(final String batchResourceDataUrl, + final List<DmiBatchOperation> dmiBatchRequestBodies) { + final String batchRequestBodiesAsJsonString = jsonObjectMapper.asJsonString(dmiBatchRequestBodies); + TaskExecutor.executeTask(() -> dmiRestClient.postOperationWithJsonData(batchResourceDataUrl, + batchRequestBodiesAsJsonString, READ), DEFAULT_ASYNC_TASK_EXECUTOR_TIMEOUT_IN_MILLISECONDS) .whenCompleteAsync(this::handleTaskCompletion); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java index 392e9c1a24..1bbd725646 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java @@ -66,8 +66,7 @@ public class DmiModelOperations extends DmiOperations { * @return module references */ public List<ModuleReference> getModuleReferences(final YangModelCmHandle yangModelCmHandle) { - final DmiRequestBody dmiRequestBody = DmiRequestBody.builder() - .build(); + final DmiRequestBody dmiRequestBody = DmiRequestBody.builder().build(); dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties()); final ResponseEntity<Object> dmiFetchModulesResponseEntity = getResourceFromDmiWithJsonData( yangModelCmHandle.resolveDmiServiceName(MODEL), @@ -109,7 +108,7 @@ public class DmiModelOperations extends DmiOperations { final String resourceName) { final String dmiResourceDataUrl = getDmiResourceUrl(dmiServiceName, cmHandle, resourceName); return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, - OperationEnum.READ); + OperationType.READ); } private static String getRequestBodyToFetchYangResources(final Collection<ModuleReference> newModuleReferences, diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiRequestBody.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiRequestBody.java index 3aa6366155..6613d3c87c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiRequestBody.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiRequestBody.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2022 Nordix Foundation + * Copyright (C) 2021-2023 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ package org.onap.cps.ncmp.api.impl.operations; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -32,9 +33,11 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; @JsonInclude(JsonInclude.Include.NON_NULL) @Getter @Builder +@JsonPropertyOrder({"operation", "dataType", "data", "cmHandleProperties", "requestId"}) public class DmiRequestBody { - private OperationEnum operation; + @JsonProperty("operation") + private OperationType operationType; private String dataType; private String data; @JsonProperty("cmHandleProperties") diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationType.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationType.java new file mode 100644 index 0000000000..fa00d1a15e --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationType.java @@ -0,0 +1,71 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.operations; + +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; +import org.onap.cps.ncmp.api.impl.exception.InvalidOperationException; + +@Getter +public enum OperationType { + + READ("read"), + CREATE("create"), + UPDATE("update"), + PATCH("patch"), + DELETE("delete"); + + private final String operationName; + + OperationType(final String operationName) { + this.operationName = operationName; + } + + @Override + @JsonValue + public String toString() { + return String.valueOf(operationName); + } + + private static final Map<String, OperationType> operationNameToOperationEnum = new HashMap<>(); + + static { + Arrays.stream(OperationType.values()).forEach( + operationType -> operationNameToOperationEnum.put(operationType.getOperationName(), operationType)); + } + + /** + * From operation name get operation enum type. + * + * @param operationName the operation name + * @return the operation enum type + */ + public static OperationType fromOperationName(final String operationName) { + final OperationType operationType = operationNameToOperationEnum.get(operationName); + if (null == operationType) { + throw new InvalidOperationException(operationName + " is an invalid operation name"); + } + return operationType; + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java index bba8f48fbd..5c6fa9f0b0 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java @@ -53,15 +53,17 @@ public class DmiServiceUrlBuilder { } /** - * This method creates the dmi service url for bulk request. + * This method builds batch request url. * - * @param queryParams query param map as key,value pair - * @param uriVariables uri param map as key (placeholder),value pair - * @return {@code String} dmi service url as string + * @param batchRequestQueryParams query param map as key, value pair + * @param batchRequestUriVariables uri param map as key (placeholder), value pair + * @return {@code String} batch request url as string */ - public String getBulkRequestUrl(final MultiValueMap<String, String> queryParams, - final Map<String, Object> uriVariables) { - return getUriComponentsBuilder(getBulkResourceDataBasePathUriBuilder(), queryParams, uriVariables) + public String getBatchRequestUrl(final MultiValueMap<String, String> batchRequestQueryParams, + final Map<String, Object> batchRequestUriVariables) { + return getBatchResourceDataBasePathUriBuilder() + .queryParams(batchRequestQueryParams) + .uriVariables(batchRequestUriVariables) .buildAndExpand().toUriString(); } @@ -84,12 +86,12 @@ public class DmiServiceUrlBuilder { * * @return {@code UriComponentsBuilder} dmi service url builder object */ - public UriComponentsBuilder getBulkResourceDataBasePathUriBuilder() { + public UriComponentsBuilder getBatchResourceDataBasePathUriBuilder() { return UriComponentsBuilder.newInstance() .path("{dmiServiceName}") .pathSegment("{dmiBasePath}") .pathSegment("v1") - .pathSegment("batch"); + .pathSegment("data"); } /** @@ -114,6 +116,20 @@ public class DmiServiceUrlBuilder { } /** + * This method populates uri variables for batch request. + * + * @param dmiServiceName dmi service name + * @return {@code Map<String, Object>} uri variables as map + */ + public Map<String, Object> populateBatchUriVariables(final String dmiServiceName) { + final Map<String, Object> uriVariables = new HashMap<>(); + final String dmiBasePath = dmiProperties.getDmiBasePath(); + uriVariables.put("dmiServiceName", dmiServiceName); + uriVariables.put("dmiBasePath", dmiBasePath); + return uriVariables; + } + + /** * This method is used to populate map from query params. * * @param resourceId unique id of response for valid topic @@ -134,6 +150,21 @@ public class DmiServiceUrlBuilder { return queryParams; } + /** + * This method is used to populate map from query params for batch request. + * + * @param topicParamInQuery topic into url param + * @param requestId unique id of response for valid topic + * @return all valid query params as map + */ + public MultiValueMap<String, String> getBatchRequestQueryParams(final String topicParamInQuery, + final String requestId) { + final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); + getQueryParamConsumer().accept("topic", topicParamInQuery, queryParams); + getQueryParamConsumer().accept("requestId", requestId, queryParams); + return queryParams; + } + private TriConsumer<String, String, MultiValueMap<String, String>> getQueryParamConsumer() { return (paramName, paramValue, paramMap) -> { if (Strings.isNotEmpty(paramValue)) { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/ResourceDataBatchRequestUtils.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/ResourceDataBatchRequestUtils.java new file mode 100644 index 0000000000..e4c9bfb39b --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/ResourceDataBatchRequestUtils.java @@ -0,0 +1,126 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.api.impl.operations.CmHandle; +import org.onap.cps.ncmp.api.impl.operations.DmiBatchOperation; +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.onap.cps.ncmp.api.models.BatchOperationDefinition; +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ResourceDataBatchRequestUtils { + + private static final String UNKNOWN_SERVICE_NAME = null; + + /** + * Create a list of DMI batch operation per DMI service (name). + * + * @param resourceDataBatchRequestIn incoming batch request details for resource data + * @param yangModelCmHandles involved cm handles represented as YangModelCmHandle (incl. metadata) + * + * @return {@code Map<String, List<DmiBatchOperation>>} Create a list of DMI batch operation per DMI service (name). + */ + public static Map<String, List<DmiBatchOperation>> processPerOperationInBatchRequest( + final ResourceDataBatchRequest resourceDataBatchRequestIn, + final Collection<YangModelCmHandle> yangModelCmHandles) { + + final Map<String, Map<String, Map<String, String>>> dmiPropertiesPerCmHandleIdPerServiceName = + DmiServiceNameOrganizer.getDmiPropertiesPerCmHandleIdPerServiceName(yangModelCmHandles); + + final Map<String, String> dmiServiceNamesPerCmHandleId = + getDmiServiceNamesPerCmHandleId(dmiPropertiesPerCmHandleIdPerServiceName); + + final Map<String, List<DmiBatchOperation>> dmiBatchOperationsOutPerDmiServiceName = new HashMap<>(); + + for (final BatchOperationDefinition batchOperationDefinitionIn : + resourceDataBatchRequestIn.getBatchOperationDefinitions()) { + for (final String cmHandleId : batchOperationDefinitionIn.getCmHandleIds()) { + final String dmiServiceName = dmiServiceNamesPerCmHandleId.get(cmHandleId); + final Map<String, String> cmHandleIdProperties + = dmiPropertiesPerCmHandleIdPerServiceName.get(dmiServiceName).get(cmHandleId); + if (cmHandleIdProperties == null) { + publishErrorMessageToClientTopic(cmHandleId); + } else { + final DmiBatchOperation dmiBatchOperationOut = getOrAddDmiBatchOperation(dmiServiceName, + batchOperationDefinitionIn, dmiBatchOperationsOutPerDmiServiceName); + final CmHandle cmHandle = CmHandle.buildCmHandleWithProperties(cmHandleId, cmHandleIdProperties); + dmiBatchOperationOut.getCmHandles().add(cmHandle); + } + } + } + return dmiBatchOperationsOutPerDmiServiceName; + } + + private static void publishErrorMessageToClientTopic(final String requestedCmHandleId) { + log.warn("cm handle {} not found", requestedCmHandleId); + // TODO Need to publish an error response to client given topic. + // Code should be implemented into https://jira.onap.org/browse/CPS-1583 ( + // NCMP : Handle non-existing cm handles) + } + + private static Map<String, String> getDmiServiceNamesPerCmHandleId( + final Map<String, Map<String, Map<String, String>>> dmiDmiPropertiesPerCmHandleIdPerServiceName) { + final Map<String, String> dmiServiceNamesPerCmHandleId = new HashMap<>(); + for (final Map.Entry<String, Map<String, Map<String, String>>> dmiDmiPropertiesEntry + : dmiDmiPropertiesPerCmHandleIdPerServiceName.entrySet()) { + final String dmiServiceName = dmiDmiPropertiesEntry.getKey(); + final Set<String> cmHandleIds = dmiDmiPropertiesPerCmHandleIdPerServiceName.get(dmiServiceName).keySet(); + for (final String cmHandleId : cmHandleIds) { + dmiServiceNamesPerCmHandleId.put(cmHandleId, dmiServiceName); + } + } + dmiDmiPropertiesPerCmHandleIdPerServiceName.put(UNKNOWN_SERVICE_NAME, Collections.emptyMap()); + return dmiServiceNamesPerCmHandleId; + } + + private static DmiBatchOperation getOrAddDmiBatchOperation(final String dmiServiceName, + final BatchOperationDefinition + batchOperationDefinitionIn, + final Map<String, List<DmiBatchOperation>> + dmiBatchOperationsOutPerDmiServiceName) { + dmiBatchOperationsOutPerDmiServiceName + .computeIfAbsent(dmiServiceName, dmiServiceNameAsKey -> new ArrayList<>()); + final List<DmiBatchOperation> dmiBatchOperationsOut + = dmiBatchOperationsOutPerDmiServiceName.get(dmiServiceName); + final boolean isNewOperation = dmiBatchOperationsOut.isEmpty() + || !dmiBatchOperationsOut.get(dmiBatchOperationsOut.size() - 1).getOperationId() + .equals(batchOperationDefinitionIn.getOperationId()); + if (isNewOperation) { + final DmiBatchOperation newDmiBatchOperationOut = + DmiBatchOperation.buildDmiBatchRequestBodyWithoutCmHandles(batchOperationDefinitionIn); + dmiBatchOperationsOut.add(newDmiBatchOperationOut); + return newDmiBatchOperationOut; + } + return dmiBatchOperationsOut.get(dmiBatchOperationsOut.size() - 1); + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/BatchOperationDefinition.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/BatchOperationDefinition.java new file mode 100644 index 0000000000..04075b3b7c --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/BatchOperationDefinition.java @@ -0,0 +1,49 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.List; +import javax.validation.Valid; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +public class BatchOperationDefinition { + + private String operation; + private String operationId; + private String datastore; + private String options; + private String resourceIdentifier; + + @JsonProperty("targetIds") + @Valid + private List<String> cmHandleIds = new ArrayList(); +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/ResourceDataBatchRequest.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/ResourceDataBatchRequest.java new file mode 100644 index 0000000000..7af107c37a --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/ResourceDataBatchRequest.java @@ -0,0 +1,43 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.List; +import javax.validation.Valid; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResourceDataBatchRequest { + + @JsonProperty("operations") + @Valid + private List<BatchOperationDefinition> batchOperationDefinitions = Collections.emptyList(); +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy index 3d8e9cb2e8..79f7e50e76 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy @@ -33,11 +33,13 @@ import org.onap.cps.ncmp.api.inventory.CompositeState import org.onap.cps.ncmp.api.inventory.InventoryPersistence import org.onap.cps.ncmp.api.inventory.LockReasonCategory import org.onap.cps.ncmp.api.inventory.DataStoreSyncState +import org.onap.cps.ncmp.api.models.BatchOperationDefinition import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters import org.onap.cps.ncmp.api.models.ConditionApiProperties import org.onap.cps.ncmp.api.models.DmiPluginRegistration import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest import org.onap.cps.spi.exceptions.CpsException import org.onap.cps.spi.model.ConditionProperties import spock.lang.Shared @@ -54,8 +56,8 @@ import spock.lang.Specification import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.CREATE -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.UPDATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE class NetworkCmProxyDataServiceImplSpec extends Specification { @@ -133,21 +135,13 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { response == '{dmi-response}' } - def 'Get bulk resource data for #datastoreName from DMI.'() { + def 'Get batch resource data for #datastoreName from DMI.'() { given: 'cpsDataService returns valid data node' - mockDataNode() - and: 'DMI returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(datastoreName, ['testCmHandle'], - 'testResourceId', OPTIONS_PARAM,'some topic','requestId') >> - new ResponseEntity<>('{dmi-bulk-response}', HttpStatus.OK) + def resourceDataBatchRequest = getResourceDataBatchRequest(datastoreName) when: 'get batch resource data is called' - def response = objectUnderTest.getResourceDataForCmHandleBatch(datastoreName, ['testCmHandle'], - 'testResourceId', - OPTIONS_PARAM, - 'some topic', - 'requestId') - then: 'get bulk resource data returns expected response' - response == '{dmi-bulk-response}' + objectUnderTest.requestResourceDataForCmHandleBatch('some topic', resourceDataBatchRequest, 'requestId') + then: 'get batch resource data returns expected response' + 1 * mockDmiDataOperations.requestResourceDataFromDmi('some topic', resourceDataBatchRequest, 'requestId') where: 'the following data stores are used' datastoreName << [PASSTHROUGH_RUNNING.datastoreName, PASSTHROUGH_OPERATIONAL.datastoreName] } @@ -373,4 +367,22 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode } + + def getResourceDataBatchRequest(datastore) { + def resourceDataBatchRequest = new ResourceDataBatchRequest() + def batchOperationDefinitions = new ArrayList() + batchOperationDefinitions.add(getBatchOperationDefinition(datastore)) + resourceDataBatchRequest.setBatchOperationDefinitions(batchOperationDefinitions) + } + + def getBatchOperationDefinition(datastore) { + def batchOperationDefinition = new BatchOperationDefinition() + batchOperationDefinition.setOperation("read") + batchOperationDefinition.setOperationId("operational-12") + batchOperationDefinition.setDatastore(datastore) + def targetIds = new ArrayList() + targetIds.add("some-cm-handle") + batchOperationDefinition.setCmHandleIds(targetIds) + return batchOperationDefinition + } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/async/NcmpAsyncBatchEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/async/NcmpAsyncBatchEventConsumerSpec.groovy new file mode 100644 index 0000000000..65c43a011d --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/async/NcmpAsyncBatchEventConsumerSpec.groovy @@ -0,0 +1,104 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.async + +import com.fasterxml.jackson.databind.ObjectMapper +import org.apache.commons.lang3.SerializationUtils +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.clients.consumer.KafkaConsumer +import org.apache.kafka.common.header.internals.RecordHeader +import org.onap.cps.ncmp.api.impl.events.EventsPublisher +import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec +import org.onap.cps.ncmp.events.async.BatchDataResponseEventV1 +import org.onap.cps.ncmp.utils.TestUtils +import org.onap.cps.utils.JsonObjectMapper +import org.spockframework.spring.SpringBean +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.kafka.listener.adapter.RecordFilterStrategy +import org.springframework.test.annotation.DirtiesContext +import org.testcontainers.spock.Testcontainers + +import java.time.Duration + +@SpringBootTest(classes = [EventsPublisher, NcmpAsyncBatchEventConsumer, BatchRecordFilterStrategy,JsonObjectMapper, + ObjectMapper]) +@Testcontainers +@DirtiesContext +class NcmpAsyncBatchEventConsumerSpec extends MessagingBaseSpec { + + @SpringBean + EventsPublisher asyncBatchEventPublisher = new EventsPublisher<BatchDataResponseEventV1>(kafkaTemplate) + + @SpringBean + NcmpAsyncBatchEventConsumer asyncBatchEventConsumer = new NcmpAsyncBatchEventConsumer(asyncBatchEventPublisher) + + @Autowired + JsonObjectMapper jsonObjectMapper + + @Autowired + RecordFilterStrategy<Object, Object> recordFilterStrategy + + def kafkaConsumer = new KafkaConsumer<>(consumerConfigProperties('test')) + def static clientTopic = 'client-topic' + def static batchEventType = 'org.onap.cps.ncmp.events.async.BatchDataResponseEventV1' + + def 'Consume and publish event to client specified topic'() { + given: 'consumer subscribing to client topic' + kafkaConsumer.subscribe([clientTopic]) + and: 'consumer record for batch event' + def consumerRecordIn = createConsumerRecord(batchEventType) + when: 'the batch event is consumed and published to client specified topic' + asyncBatchEventConsumer.consumeAndPublish(consumerRecordIn) + and: 'the client specified topic is polled' + def consumerRecordOut = kafkaConsumer.poll(Duration.ofMillis(1500))[0] + then: 'verifying consumed event operationID is same as published event operationID' + def operationIdIn = consumerRecordIn.value.event.batchResponses[0].operationId + def operationIdOut = jsonObjectMapper.convertJsonString((String)consumerRecordOut.value(), BatchDataResponseEventV1.class).event.batchResponses[0].operationId + assert operationIdIn == operationIdOut + } + + def 'Filter an event with type #eventType'() { + given: 'consumer record for event with type #eventType' + def consumerRecord = createConsumerRecord(eventType) + when: 'while consuming the topic ncmp-async-m2m it executes the filter strategy' + def result = recordFilterStrategy.filter(consumerRecord) + then: 'the event is #description' + assert result == expectedResult + where: 'filter the event based on the eventType #eventType' + description | eventType || expectedResult + 'not filtered(the consumer will see the event)' | batchEventType || false + 'filtered(the consumer will not see the event)' | 'wrongType' || true + } + + def createConsumerRecord(eventTypeAsString) { + def jsonData = TestUtils.getResourceFileContent('batchDataEvent.json') + def testEventSent = jsonObjectMapper.convertJsonString(jsonData, BatchDataResponseEventV1.class) + def eventTarget = SerializationUtils.serialize(clientTopic) + def eventType = SerializationUtils.serialize(eventTypeAsString) + def eventId = SerializationUtils.serialize('12345') + def consumerRecord = new ConsumerRecord<String, Object>(clientTopic, 1, 1L, '123', testEventSent) + consumerRecord.headers().add(new RecordHeader('eventId', eventId)) + consumerRecord.headers().add(new RecordHeader('eventTarget', eventTarget)) + consumerRecord.headers().add(new RecordHeader('eventType', eventType)) + return consumerRecord + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy index b38ca10f7b..6b0355eee8 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy @@ -34,9 +34,9 @@ import org.springframework.web.client.HttpServerErrorException import org.springframework.web.client.RestTemplate import spock.lang.Specification -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.READ -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.PATCH -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.CREATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ +import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE @SpringBootTest @ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, DmiRestClient]) diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCmHandleStateHandlerImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCmHandleStateHandlerImplSpec.groovy index f660be7103..e449d65ac2 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCmHandleStateHandlerImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCmHandleStateHandlerImplSpec.groovy @@ -54,7 +54,7 @@ class LcmEventsCmHandleStateHandlerImplSpec extends Specification { then: 'state is saved using inventory persistence' expectedCallsToInventoryPersistence * mockInventoryPersistence.saveCmHandleState(cmHandleId, _) and: 'event service is called to publish event' - expectedCallsToEventService * mockLcmEventsService.publishLcmEvent(cmHandleId, _) + expectedCallsToEventService * mockLcmEventsService.publishLcmEvent(cmHandleId, _, _) where: 'state change parameters are provided' stateChange | fromCmHandleState | toCmHandleState || expectedCallsToInventoryPersistence | expectedCallsToEventService 'ADVISED to READY' | ADVISED | READY || 1 | 1 @@ -73,7 +73,7 @@ class LcmEventsCmHandleStateHandlerImplSpec extends Specification { then: 'state is saved using inventory persistence' 1 * mockInventoryPersistence.saveCmHandle(yangModelCmHandle) and: 'event service is called to publish event' - 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _) + 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _, _) } def 'Update and Publish Events on State Change from LOCKED to ADVISED'() { @@ -90,7 +90,7 @@ class LcmEventsCmHandleStateHandlerImplSpec extends Specification { } } and: 'event service is called to publish event' - 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _) + 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _, _) } def 'Update and Publish Events on State Change to READY'() { @@ -111,7 +111,7 @@ class LcmEventsCmHandleStateHandlerImplSpec extends Specification { } } and: 'event service is called to publish event' - 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _) + 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _, _) } def 'Update cmHandle state to "DELETING"' (){ @@ -125,7 +125,7 @@ class LcmEventsCmHandleStateHandlerImplSpec extends Specification { and: 'method to persist cm handle state is called once' 1 * mockInventoryPersistence.saveCmHandleState(yangModelCmHandle.getId(), yangModelCmHandle.getCompositeState()) and: 'the method to publish Lcm event is called once' - 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _) + 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _, _) } def 'Update cmHandle state to "DELETED"' (){ @@ -137,7 +137,7 @@ class LcmEventsCmHandleStateHandlerImplSpec extends Specification { then: 'the cm handle state is as expected' yangModelCmHandle.getCompositeState().getCmHandleState() == DELETED and: 'the method to publish Lcm event is called once' - 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _) + 1 * mockLcmEventsService.publishLcmEvent(cmHandleId, _, _) } def 'No state change and no event to be published'() { @@ -167,7 +167,7 @@ class LcmEventsCmHandleStateHandlerImplSpec extends Specification { } } and: 'event service is called to publish event' - 2 * mockLcmEventsService.publishLcmEvent(_, _) + 2 * mockLcmEventsService.publishLcmEvent(_, _, _) } @@ -183,7 +183,7 @@ class LcmEventsCmHandleStateHandlerImplSpec extends Specification { } } and: 'event service is called to publish event' - 2 * mockLcmEventsService.publishLcmEvent(_, _) + 2 * mockLcmEventsService.publishLcmEvent(_, _, _) } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreatorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreatorSpec.groovy index f4adfc587c..6d7d6250f1 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreatorSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsCreatorSpec.groovy @@ -20,10 +20,11 @@ package org.onap.cps.ncmp.api.impl.events.lcm +import org.mapstruct.factory.Mappers import org.onap.cps.ncmp.api.inventory.CmHandleState import org.onap.cps.ncmp.api.inventory.CompositeState import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle -import org.onap.ncmp.cmhandle.event.lcm.Values +import org.onap.cps.ncmp.events.lcm.v1.Values import spock.lang.Specification import static org.onap.cps.ncmp.api.inventory.CmHandleState.ADVISED @@ -32,7 +33,9 @@ import static org.onap.cps.ncmp.api.inventory.CmHandleState.READY class LcmEventsCreatorSpec extends Specification { - def objectUnderTest = new LcmEventsCreator() + LcmEventHeaderMapper lcmEventsHeaderMapper = Mappers.getMapper(LcmEventHeaderMapper) + + def objectUnderTest = new LcmEventsCreator(lcmEventsHeaderMapper) def cmHandleId = 'test-cm-handle' def 'Map the LcmEvent for #operation'() { @@ -159,4 +162,15 @@ class LcmEventsCreatorSpec extends Specification { 'null to null' | null | null } + + def 'Map the LcmEventHeader'() { + given: 'NCMP cm handle details with current and old details' + def existingNcmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, compositeState: new CompositeState(cmHandleState: ADVISED)) + def targetNcmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, compositeState: new CompositeState(cmHandleState: READY)) + when: 'the event header is populated' + def result = objectUnderTest.populateLcmEventHeader(cmHandleId, targetNcmpServiceCmHandle, existingNcmpServiceCmHandle) + then: 'the header has fields populated' + assert result.eventCorrelationId == cmHandleId + assert result.eventId != null + } }
\ No newline at end of file diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsPublisherSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsPublisherSpec.groovy index 7c9464dccb..93741261f6 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsPublisherSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsPublisherSpec.groovy @@ -24,14 +24,15 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.apache.kafka.clients.consumer.KafkaConsumer import org.onap.cps.ncmp.api.impl.events.EventsPublisher import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec +import org.onap.cps.ncmp.events.lcm.v1.Event +import org.onap.cps.ncmp.events.lcm.v1.LcmEvent import org.onap.cps.ncmp.utils.TestUtils import org.onap.cps.utils.JsonObjectMapper -import org.onap.ncmp.cmhandle.event.lcm.Event -import org.onap.ncmp.cmhandle.event.lcm.LcmEvent import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.annotation.DirtiesContext +import org.springframework.util.SerializationUtils import org.testcontainers.spock.Testcontainers import java.time.Duration @@ -55,19 +56,35 @@ class LcmEventsPublisherSpec extends MessagingBaseSpec { def 'Produce and Consume Lcm Event'() { given: 'event key and event data' def eventKey = 'lcm' + def eventId = 'test-uuid' + def eventCorrelationId = 'cmhandle-test' + def eventSource = 'org.onap.ncmp' + def eventTime = '2022-12-31T20:30:40.000+0000' + def eventType = 'org.onap.ncmp.cmhandle.lcm.event' + def eventSchema = 'org.onap.ncmp.cmhandle.lcm.event' + def eventSchemaVersion = 'v1' def eventData = new LcmEvent( - eventId: 'test-uuid', - eventCorrelationId: 'cmhandle-as-correlationid', - eventSource: 'org.onap.ncmp', - eventTime: '2022-12-31T20:30:40.000+0000', - eventType: 'org.onap.ncmp.cmhandle.lcm.event', - eventSchema: 'org.onap.ncmp.cmhandle.lcm.event', - eventSchemaVersion: 'v1', + eventId: eventId, + eventCorrelationId: eventCorrelationId, + eventSource: eventSource, + eventTime: eventTime, + eventType: eventType, + eventSchema: eventSchema, + eventSchemaVersion: eventSchemaVersion, event: new Event(cmHandleId: 'cmhandle-test')) + and: 'we have a event header' + def eventHeader = [ + eventId : eventId, + eventCorrelationId: eventCorrelationId, + eventSource : eventSource, + eventTime : eventTime, + eventType : eventType, + eventSchema : eventSchema, + eventSchemaVersion: eventSchemaVersion] and: 'consumer has a subscription' kafkaConsumer.subscribe([testTopic] as List<String>) when: 'an event is published' - lcmEventsPublisher.publishEvent(testTopic, eventKey, eventData) + lcmEventsPublisher.publishEvent(testTopic, eventKey, eventHeader, eventData) and: 'topic is polled' def records = kafkaConsumer.poll(Duration.ofMillis(1500)) then: 'poll returns one record' @@ -79,5 +96,8 @@ class LcmEventsPublisherSpec extends MessagingBaseSpec { def expectedJsonString = TestUtils.getResourceFileContent('expectedLcmEvent.json') def expectedLcmEvent = jsonObjectMapper.convertJsonString(expectedJsonString, LcmEvent.class) assert expectedLcmEvent == jsonObjectMapper.convertJsonString(record.value, LcmEvent.class) + and: 'record header matches the expected parameters' + assert SerializationUtils.deserialize(record.headers().lastHeader('eventId').value()) == eventId + assert SerializationUtils.deserialize(record.headers().lastHeader('eventCorrelationId').value()) == eventCorrelationId } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsServiceSpec.groovy index 65f4d50c68..2d3f8ac516 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsServiceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/lcm/LcmEventsServiceSpec.groovy @@ -21,26 +21,42 @@ package org.onap.cps.ncmp.api.impl.events.lcm import org.onap.cps.ncmp.api.impl.events.EventsPublisher -import org.onap.ncmp.cmhandle.event.lcm.LcmEvent +import org.onap.cps.ncmp.events.lcm.v1.LcmEvent +import org.onap.cps.ncmp.events.lcm.v1.LcmEventHeader +import org.onap.cps.utils.JsonObjectMapper import org.springframework.kafka.KafkaException import spock.lang.Specification class LcmEventsServiceSpec extends Specification { def mockLcmEventsPublisher = Mock(EventsPublisher) + def mockJsonObjectMapper = Mock(JsonObjectMapper) - def objectUnderTest = new LcmEventsService(mockLcmEventsPublisher) + def objectUnderTest = new LcmEventsService(mockLcmEventsPublisher, mockJsonObjectMapper) def 'Create and Publish lcm event where events are #scenario'() { given: 'a cm handle id and Lcm Event' def cmHandleId = 'test-cm-handle-id' - def lcmEvent = new LcmEvent(eventId: UUID.randomUUID().toString(), eventCorrelationId: cmHandleId) + def eventId = UUID.randomUUID().toString() + def lcmEvent = new LcmEvent(eventId: eventId, eventCorrelationId: cmHandleId) + and: 'we also have a lcm event header' + def lcmEventHeader = new LcmEventHeader(eventId: eventId, eventCorrelationId: cmHandleId) and: 'notificationsEnabled is #notificationsEnabled and it will be true as default' objectUnderTest.notificationsEnabled = notificationsEnabled + and: 'lcm event header is transformed to headers map' + mockJsonObjectMapper.convertToValueType(lcmEventHeader, Map.class) >> ['eventId': eventId, 'eventCorrelationId': cmHandleId] when: 'service is called to publish lcm event' - objectUnderTest.publishLcmEvent('test-cm-handle-id', lcmEvent) + objectUnderTest.publishLcmEvent('test-cm-handle-id', lcmEvent, lcmEventHeader) then: 'publisher is called #expectedTimesMethodCalled times' - expectedTimesMethodCalled * mockLcmEventsPublisher.publishEvent(_, cmHandleId, lcmEvent) + expectedTimesMethodCalled * mockLcmEventsPublisher.publishEvent(_, cmHandleId, _, lcmEvent) >> { + args -> { + def eventHeaders = (args[2] as Map<String,Object>) + assert eventHeaders.containsKey('eventId') + assert eventHeaders.containsKey('eventCorrelationId') + assert eventHeaders.get('eventId') == eventId + assert eventHeaders.get('eventCorrelationId') == cmHandleId + } + } where: 'the following values are used' scenario | notificationsEnabled || expectedTimesMethodCalled 'enabled' | true || 1 @@ -50,12 +66,14 @@ class LcmEventsServiceSpec extends Specification { def 'Unable to send message'(){ given: 'a cm handle id and Lcm Event and notification enabled' def cmHandleId = 'test-cm-handle-id' - def lcmEvent = new LcmEvent(eventId: UUID.randomUUID().toString(), eventCorrelationId: cmHandleId) + def eventId = UUID.randomUUID().toString() + def lcmEvent = new LcmEvent(eventId: eventId, eventCorrelationId: cmHandleId) + def lcmEventHeader = new LcmEventHeader(eventId: eventId, eventCorrelationId: cmHandleId) objectUnderTest.notificationsEnabled = true when: 'publisher set to throw an exception' - mockLcmEventsPublisher.publishEvent(*_) >> { throw new KafkaException('publishing failed')} + mockLcmEventsPublisher.publishEvent(_, _, _, _) >> { throw new KafkaException('publishing failed')} and: 'an event is publised' - objectUnderTest.publishLcmEvent(cmHandleId, lcmEvent) + objectUnderTest.publishLcmEvent(cmHandleId, lcmEvent, lcmEventHeader) then: 'the exception is just logged and not bubbled up' noExceptionThrown() } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy index 5fd4fbd43f..9343666260 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy @@ -24,6 +24,8 @@ package org.onap.cps.ncmp.api.impl.operations import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest +import org.onap.cps.ncmp.utils.TestUtils import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired @@ -35,9 +37,9 @@ import spock.lang.Shared import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.CREATE -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.READ -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.UPDATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ +import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE @SpringBootTest @ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, DmiDataOperations]) @@ -50,8 +52,6 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { def NO_REQUEST_ID = null @Shared def OPTIONS_PARAM = '(a=1,b=2)' - @Shared - def expectedBulkRequestAsJson = '{"operation": "read","data": {"fe1c1f1a070c4ce5bbfda7198592a0d3": {"neType": "RadioNode"},"b8e42eed0d9541ed8d8839e8eb86b3e0": {"neType": "RadioNode"}},"requestId": "bbb67474-f705-410a-93d1-b2844e7f45fd"}' @SpringBean JsonObjectMapper spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper())) @@ -82,23 +82,26 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { 'datastore running with properties' | [yangModelCmHandleProperty] | PASSTHROUGH_RUNNING | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-running' | '&options=(a=1,b=2)' } - def 'call get bulk resource data for #dataStore from DMI service with topic #scenario.'() { - given: 'collection of yang model cm Handles' + def 'call get batch resource data from DMI service #scenario.'() { + given: 'collection of yang model cm Handles and resource data batch request' mockYangModelCmHandleCollectionRetrieval([yangModelCmHandleProperty]) - and: 'a positive response from DMI service when it is called with the expected parameters' + def resourceDataBatchRequestJsonData = TestUtils.getResourceFileContent('resourceDataBatchRequest.json') + def resourceDataBatchRequest = spiedJsonObjectMapper.convertJsonString(resourceDataBatchRequestJsonData, ResourceDataBatchRequest.class) + resourceDataBatchRequest.batchOperationDefinitions[0].cmHandleIds = [cmHandleId] + def requestBodyAsJsonStringArg = null + and: 'a positive response from DMI service when it is called with valid request parameters' def responseFromDmi = new ResponseEntity<Object>(HttpStatus.ACCEPTED) - def expectedDmiBulkResourceDataUrl = "ncmp/v1/batch/data/ds/${dataStore}?resourceIdentifier=parent/child%26options=(a=1,b=2)&topic=my-topic-name&options=(fields=schemas/schema)" - mockDmiRestClient.postOperationWithJsonData(expectedDmiBulkResourceDataUrl, expectedBulkRequestAsJson, READ) >> responseFromDmi - dmiServiceUrlBuilder.getBulkRequestUrl(_, _) >> expectedDmiBulkResourceDataUrl - when: 'get resource data for bulk cm handle is invoked' - def result = objectUnderTest.getResourceDataFromDmi( dataStore.datastoreName, [cmHandleId], resourceIdentifier, - OPTIONS_PARAM, 'some-topic','requestId') - then: 'the result is the response from the DMI service' - assert result == responseFromDmi - where: 'the following parameters are used' - scenario | dataStore - 'datastore operational' | PASSTHROUGH_OPERATIONAL - 'datastore running' | PASSTHROUGH_RUNNING + def expectedDmiBatchResourceDataUrl = "ncmp/v1/data/topic=my-topic-name" + def expectedBatchRequestAsJson = '[{"operation":"read","operationId":"operational-14","datastore":"ncmp-datastore:passthrough-operational","options":"some option","resourceIdentifier":"some resource identifier","cmHandles":[{"id":"some-cm-handle","cmHandleProperties":{"prop1":"val1"}}]}]' + mockDmiRestClient.postOperationWithJsonData(expectedDmiBatchResourceDataUrl, _, READ.operationName) >> responseFromDmi + dmiServiceUrlBuilder.getBatchRequestUrl(_, _) >> expectedDmiBatchResourceDataUrl + when: 'get resource data for batch of cm handles are invoked' + objectUnderTest.requestResourceDataFromDmi('my-topic-name', resourceDataBatchRequest, 'requestId') + then: 'wait a little to allow execution of service method by task executor (on separate thread)' + Thread.sleep(100) + then: 'validate ncmp generated dmi request body json args' + 1 * mockDmiRestClient.postOperationWithJsonData(expectedDmiBatchResourceDataUrl, _, READ) >> { args -> requestBodyAsJsonStringArg = args[1] } + assert requestBodyAsJsonStringArg == expectedBatchRequestAsJson } def 'call get all resource data.'() { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy index ed74ad3342..d1025f9d65 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy @@ -36,7 +36,7 @@ import org.springframework.http.ResponseEntity import org.springframework.test.context.ContextConfiguration import spock.lang.Shared -import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.READ +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ @SpringBootTest @ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, DmiModelOperations]) @@ -103,9 +103,9 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { then: 'the result is the response from DMI service' assert result == [] where: 'the following DMI properties are used' - scenario | dmiProperties || expectedAdditionalPropertiesInRequest - 'with properties' | [yangModelCmHandleProperty] || '{"prop1":"val1"}' - 'without properties' | [] || '{}' + scenario | dmiProperties || expectedAdditionalPropertiesInRequest + 'with properties' | [yangModelCmHandleProperty] || '{"prop1":"val1"}' + 'without properties' | [] || '{}' } def 'Retrieving yang resources.'() { @@ -154,10 +154,10 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { then: 'the result is the response from DMI service' assert result == [mod1:'some yang source'] where: 'the following DMI properties are used' - scenario | dmiProperties | unknownModuleReferences || expectedAdditionalPropertiesInRequest | expectedModuleReferencesInRequest - 'with module references and properties' | [yangModelCmHandleProperty] | newModuleReferences || '{"prop1":"val1"}' | '{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}' - 'without module references' | [yangModelCmHandleProperty] | [] || '{"prop1":"val1"}' | '' - 'without properties' | [] | newModuleReferences || '{}' | '{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}' + scenario | dmiProperties | unknownModuleReferences || expectedAdditionalPropertiesInRequest | expectedModuleReferencesInRequest + 'with module references and properties' | [yangModelCmHandleProperty] | newModuleReferences || '{"prop1":"val1"}' | '{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}' + 'without module references' | [yangModelCmHandleProperty] | [] || '{"prop1":"val1"}' | '' + 'without properties' | [] | newModuleReferences || '{}' | '{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}' } def 'Retrieving yang resources from DMI with null DMI properties.'() { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/ResourceDataBatchRequestUtilsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/ResourceDataBatchRequestUtilsSpec.groovy new file mode 100644 index 0000000000..e65874930b --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/ResourceDataBatchRequestUtilsSpec.groovy @@ -0,0 +1,80 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * 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.api.impl.utils + +import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.inventory.CmHandleState +import org.onap.cps.ncmp.api.inventory.CompositeStateBuilder +import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest +import org.onap.cps.ncmp.utils.TestUtils +import org.onap.cps.utils.JsonObjectMapper +import org.spockframework.spring.SpringBean +import spock.lang.Specification + +class ResourceDataBatchRequestUtilsSpec extends Specification { + + @SpringBean + JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + + def 'Process per operation in batch request with #serviceName.'() { + given: 'batch request with 3 operations' + def resourceDataBatchRequestJsonData = TestUtils.getResourceFileContent('resourceDataBatchRequest.json') + def resourceDataBatchRequest = jsonObjectMapper.convertJsonString(resourceDataBatchRequestJsonData, ResourceDataBatchRequest.class) + and: '4 known cm handles: ch1-dmi1, ch2-dmi1, ch3-dmi2, ch4-dmi2' + def yangModelCmHandles = getYangModelCmHandles() + when: 'Operation in batch request is processed' + def operationsOutPerDmiServiceName = ResourceDataBatchRequestUtils.processPerOperationInBatchRequest(resourceDataBatchRequest, yangModelCmHandles) + and: 'converted to a json node' + def dmiBatchRequestBody = jsonObjectMapper.asJsonString(operationsOutPerDmiServiceName.get(serviceName)) + def dmiBatchRequestBodyAsJsonNode = jsonObjectMapper.convertToJsonNode(dmiBatchRequestBody).get(operationIndex) + then: 'it contains the correct operation details' + assert dmiBatchRequestBodyAsJsonNode.get('operation').asText() == 'read' + assert dmiBatchRequestBodyAsJsonNode.get('operationId').asText() == expectedOperationId + assert dmiBatchRequestBodyAsJsonNode.get('datastore').asText() == expectedDatastore + and: 'the correct cm handles (just for #serviceName)' + assert dmiBatchRequestBodyAsJsonNode.get('cmHandles').size() == expectedCmHandleIds.size() + expectedCmHandleIds.each { + dmiBatchRequestBodyAsJsonNode.get('cmHandles').toString().contains(it) + } + where: 'the following dmi service and operations are checked' + serviceName | operationIndex || expectedOperationId | expectedDatastore | expectedCmHandleIds + 'dmi1' | 0 || 'operational-14' | 'ncmp-datastore:passthrough-operational' | ['ch6-dmi1'] + 'dmi1' | 1 || 'running-12' | 'ncmp-datastore:passthrough-running' | ['ch1-dmi1', 'ch2-dmi1'] + 'dmi1' | 2 || 'operational-15' | 'ncmp-datastore:passthrough-operational' | ['ch6-dmi1'] + 'dmi2' | 0 || 'operational-14' | 'ncmp-datastore:passthrough-operational' | ['ch3-dmi2'] + 'dmi2' | 1 || 'running-12' | 'ncmp-datastore:passthrough-running' | ['ch7-dmi2'] + 'dmi2' | 2 || 'operational-15' | 'ncmp-datastore:passthrough-operational' | ['ch4-dmi2'] + } + + static def getYangModelCmHandles() { + def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')] + def readyState = new CompositeStateBuilder().withCmHandleState(CmHandleState.READY).withLastUpdatedTimeNow().build() + return [new YangModelCmHandle(id: 'ch1-dmi1', dmiServiceName: 'dmi1', dmiProperties: dmiProperties, compositeState: readyState), + new YangModelCmHandle(id: 'ch2-dmi1', dmiServiceName: 'dmi1', dmiProperties: dmiProperties, compositeState: readyState), + new YangModelCmHandle(id: 'ch6-dmi1', dmiServiceName: 'dmi1', dmiProperties: dmiProperties, compositeState: readyState), + new YangModelCmHandle(id: 'ch8-dmi1', dmiServiceName: 'dmi1', dmiProperties: dmiProperties, compositeState: readyState), + new YangModelCmHandle(id: 'ch3-dmi2', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: readyState), + new YangModelCmHandle(id: 'ch4-dmi2', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: readyState), + new YangModelCmHandle(id: 'ch7-dmi2', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: readyState), + ] + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/RestQueryParametersValidatorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/RestQueryParametersValidatorSpec.groovy index e1055bb217..dc471e64fa 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/RestQueryParametersValidatorSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/RestQueryParametersValidatorSpec.groovy @@ -64,13 +64,13 @@ class RestQueryParametersValidatorSpec extends Specification { and: 'the exception details contain the correct significant term ' thrown.details.contains(expectedWordInDetails) where: - scenario | conditionName | conditionParameters || expectedWordInDetails - 'unknown condition name' | 'unknownCondition' | [['key':'value']] || 'conditionName' - 'no condition name' | '' | [['key':'value']] || 'conditionName' - 'empty properties' | 'validConditionName' | [[ : ]] || 'conditionsParameter' - 'empty conditions' | 'validConditionName' | [[:]] || 'conditionsParameter' - 'too many properties' | 'validConditionName' | [[key1:'value1', key2:'value2']] || 'conditionsParameter' - 'empty key' | 'validConditionName' | [['':'wrong']] || 'conditionsParameter' + scenario | conditionName | conditionParameters || expectedWordInDetails + 'unknown condition name' | 'unknownCondition' | [['key': 'value']] || 'conditionName' + 'no condition name' | '' | [['key': 'value']] || 'conditionName' + 'empty properties' | 'validConditionName' | [[:]] || 'conditionsParameter' + 'empty conditions' | 'validConditionName' | [[:]] || 'conditionsParameter' + 'too many properties' | 'validConditionName' | [[key1: 'value1', key2: 'value2']] || 'conditionsParameter' + 'empty key' | 'validConditionName' | [['': 'wrong']] || 'conditionsParameter' } def 'CM Handle Query validation: validate module name condition properties - valid query.'() { diff --git a/cps-ncmp-service/src/test/resources/batchDataEvent.json b/cps-ncmp-service/src/test/resources/batchDataEvent.json new file mode 100644 index 0000000000..49eb273f58 --- /dev/null +++ b/cps-ncmp-service/src/test/resources/batchDataEvent.json @@ -0,0 +1,46 @@ +{ + "event":{ + "batch-responses":[ + { + "operationId":"1", + "ids":[ + "123", + "124" + ], + "status-code":1, + "status-message":"Batch operation success on the above cmhandle ids ", + "data":{ + "ietf-netconf-monitoring:netconf-state":{ + "schemas":{ + "schema":[ + { + "identifier":"ietf-tls-server", + "version":"2016-11-02", + "format":"ietf-netconf-monitoring:yang", + "namespace":"urn:ietf:params:xml:ns:yang:ietf-tls-server", + "location":[ + "NETCONF" + ] + } + ] + } + } + } + }, + { + "operationId":"101", + "ids":[ + "456", + "457" + ], + "status-code":101, + "status-message":"cmHandle(s) do not exist", + "data":{ + "error":{ + "message":"cmHandle(s) do not exist" + } + } + } + ] + } +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/test/resources/expectedLcmEvent.json b/cps-ncmp-service/src/test/resources/expectedLcmEvent.json index 1db16ee82a..20d557dc4f 100644 --- a/cps-ncmp-service/src/test/resources/expectedLcmEvent.json +++ b/cps-ncmp-service/src/test/resources/expectedLcmEvent.json @@ -1,6 +1,6 @@ { "eventId": "test-uuid", - "eventCorrelationId": "cmhandle-as-correlationid", + "eventCorrelationId": "cmhandle-test", "eventTime": "2022-12-31T20:30:40.000+0000", "eventSource": "org.onap.ncmp", "eventType": "org.onap.ncmp.cmhandle.lcm.event", diff --git a/cps-ncmp-service/src/test/resources/resourceDataBatchRequest.json b/cps-ncmp-service/src/test/resources/resourceDataBatchRequest.json new file mode 100644 index 0000000000..98ed39b9ae --- /dev/null +++ b/cps-ncmp-service/src/test/resources/resourceDataBatchRequest.json @@ -0,0 +1,36 @@ +{ + "operations": [ + { + "operation": "read", + "operationId": "operational-14", + "datastore": "ncmp-datastore:passthrough-operational", + "options": "some option", + "resourceIdentifier": "some resource identifier", + "targetIds": [ + "ch3-dmi2", + "unknown-cm-handle", + "ch6-dmi1" + ] + }, + { + "operation": "read", + "operationId": "running-12", + "datastore": "ncmp-datastore:passthrough-running", + "targetIds": [ + "ch1-dmi1", + "ch7-dmi2", + "ch2-dmi1" + ] + }, + { + "operation": "read", + "operationId": "operational-15", + "datastore": "ncmp-datastore:passthrough-operational", + "options": "some option", + "targetIds": [ + "ch4-dmi2", + "ch6-dmi1" + ] + } + ] +} diff --git a/cps-ncmp-service/src/test/resources/sampleAvcInputEvent.json b/cps-ncmp-service/src/test/resources/sampleAvcInputEvent.json index de8a523c0f..569343fed9 100644 --- a/cps-ncmp-service/src/test/resources/sampleAvcInputEvent.json +++ b/cps-ncmp-service/src/test/resources/sampleAvcInputEvent.json @@ -1,5 +1,42 @@ { - "event": { - "payload": "Hello world!" + "event":{ + "push-change-update":{ + "datastore-changes":{ + "ietf-yang-patch:yang-patch":{ + "patch-id":"34534ffd98", + "edit":[ + { + "edit-id":"ded43434-1", + "operation":"replace", + "target":"ran-network:ran-network/NearRTRIC[@id='22']/GNBCUCPFunction[@id='cucpserver2']/NRCellCU[@id='15549']/NRCellRelation[@id='14427']", + "value":{ + "attributes":[ + { + "isHoAllowed":true + } + ] + } + }, + { + "edit-id":"ded43434-2", + "operation":"create", + "target":"ran-network:ran-network/NearRTRIC[@id='22']/GNBCUCPFunction[@id='cucpserver1']/NRCellCU[@id='15548']/NRCellRelation[@id='14426']", + "value":{ + "attributes":[ + { + "isHoAllowed":false + } + ] + } + }, + { + "edit-id":"ded43434-3", + "operation":"delete", + "target":"ran-network:ran-network/NearRTRIC[@id='22']/GNBCUCPFunction[@id='cucpserver1']/NRCellCU[@id='15548']/NRCellRelation[@id='14426']" + } + ] + } + } + } } }
\ No newline at end of file diff --git a/dmi-plugin-stub/files/batchResponse.json b/dmi-plugin-stub/files/batchResponse.json index b0615df945..9181b64f21 100644 --- a/dmi-plugin-stub/files/batchResponse.json +++ b/dmi-plugin-stub/files/batchResponse.json @@ -2,8 +2,8 @@ "eventId": "4cb32729-85e3-44d1-aa6e-c923b9b059a5", "eventCorrelationId": "68f15800-8ed4-4bae-9e53-27a9e03e1911", "eventTime": "2023-03-28T14:29:23.876+0000", - "eventType": "org.onap.cps.ncmp.event.model.BulkResponseEvent", - "eventSchema": "urn:cps:org.onap.cps.ncmp.event.model.BulkResponseEvent", + "eventType": "org.onap.cps.ncmp.event.model.BatchResponseEvent", + "eventSchema": "urn:cps:org.onap.cps.ncmp.event.model.BatchResponseEvent", "eventSchemaVersion": "v1", "event": { "payload": [ diff --git a/dmi-plugin-stub/mappings/batchCmHandles.json b/dmi-plugin-stub/mappings/batchCmHandles.json index 5f290cd47d..851959bf92 100644 --- a/dmi-plugin-stub/mappings/batchCmHandles.json +++ b/dmi-plugin-stub/mappings/batchCmHandles.json @@ -1,7 +1,7 @@ { "request": { "method": "POST", - "urlPattern": "/dmi/v1/batch/data/ds/.*" + "urlPattern": "/dmi/v1/data?.*" }, "response": { "status": 200, |