diff options
27 files changed, 576 insertions, 160 deletions
diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 247fabd023..a7955c19f9 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -211,6 +211,16 @@ components: type: string example: my-module-revision + CmHandleQueryRestParameters: + type: object + title: Cm Handle query parameters for executing cm handle search + properties: + publicCmHandleProperties: + type: object + additionalProperties: + type: string + example: Book Type + RestOutputCmHandle: type: object title: CM handle Details diff --git a/cps-ncmp-rest/docs/openapi/ncmp.yml b/cps-ncmp-rest/docs/openapi/ncmp.yml index 03ed98a0e9..05e4b84853 100755 --- a/cps-ncmp-rest/docs/openapi/ncmp.yml +++ b/cps-ncmp-rest/docs/openapi/ncmp.yml @@ -291,6 +291,33 @@ retrieveCmHandleDetailsById: application/json: schema: $ref: 'components.yaml#/components/schemas/RestOutputCmHandle' + 404: + $ref: 'components.yaml#/components/responses/NotFound' + 500: + $ref: 'components.yaml#/components/responses/InternalServerError' + +queryCmHandles: + post: + description: Execute cm handle query search + tags: + - network-cm-proxy + summary: Execute cm handle query upon a given set of query parameters + operationId: queryCmHandles + requestBody: + required: true + content: + application/json: + schema: + $ref: 'components.yaml#/components/schemas/CmHandleQueryRestParameters' + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + type: string 400: $ref: 'components.yaml#/components/responses/BadRequest' 401: diff --git a/cps-ncmp-rest/docs/openapi/openapi.yml b/cps-ncmp-rest/docs/openapi/openapi.yml index 12a8318efb..935b657e1f 100755 --- a/cps-ncmp-rest/docs/openapi/openapi.yml +++ b/cps-ncmp-rest/docs/openapi/openapi.yml @@ -39,4 +39,7 @@ paths: $ref: 'ncmp.yml#/executeCmHandleSearch' /v1/ch/{cm-handle}: - $ref: 'ncmp.yml#/retrieveCmHandleDetailsById'
\ No newline at end of file + $ref: 'ncmp.yml#/retrieveCmHandleDetailsById' + + /v1/data/ch/searches: + $ref: 'ncmp.yml#/queryCmHandles' 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 de6c3c4f3d..84fcd88a96 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 @@ -31,18 +31,25 @@ import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum 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 java.util.UUID; import java.util.stream.Collectors; import javax.validation.Valid; import javax.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.NetworkCmProxyDataService; +import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException; +import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; import org.onap.cps.ncmp.rest.api.NetworkCmProxyApi; import org.onap.cps.ncmp.rest.model.CmHandleProperties; import org.onap.cps.ncmp.rest.model.CmHandleProperty; import org.onap.cps.ncmp.rest.model.CmHandlePublicProperties; +import org.onap.cps.ncmp.rest.model.CmHandleQueryRestParameters; import org.onap.cps.ncmp.rest.model.CmHandles; import org.onap.cps.ncmp.rest.model.ConditionProperties; import org.onap.cps.ncmp.rest.model.Conditions; @@ -50,6 +57,7 @@ import org.onap.cps.ncmp.rest.model.ModuleNameAsJsonObject; import org.onap.cps.ncmp.rest.model.ModuleNamesAsJsonArray; import org.onap.cps.ncmp.rest.model.RestModuleReference; import org.onap.cps.ncmp.rest.model.RestOutputCmHandle; +import org.onap.cps.utils.CpsValidator; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -63,6 +71,9 @@ import org.springframework.web.bind.annotation.RestController; public class NetworkCmProxyController implements NetworkCmProxyApi { private static final String NO_BODY = null; + private static final String NO_REQUEST_ID = null; + private static final String NO_TOPIC = null; + public static final String ASYNC_REQUEST_ID = "requestId"; private final NetworkCmProxyDataService networkCmProxyDataService; private final JsonObjectMapper jsonObjectMapper; @@ -82,11 +93,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { final @NotNull @Valid String resourceIdentifier, final @Valid String optionsParamInQuery, final @Valid String topicParamInQuery) { + final ResponseEntity<Map<String, Object>> asyncResponse = populateAsyncResponse(topicParamInQuery); + final Map<String, Object> asyncResponseData = asyncResponse.getBody(); + final Object responseObject = networkCmProxyDataService.getResourceDataOperationalForCmHandle(cmHandle, resourceIdentifier, optionsParamInQuery, - topicParamInQuery); - return ResponseEntity.ok(responseObject); + asyncResponseData == null ? NO_TOPIC : topicParamInQuery, + asyncResponseData == null ? NO_REQUEST_ID : asyncResponseData.get(ASYNC_REQUEST_ID).toString()); + + if (asyncResponseData == null) { + return ResponseEntity.ok(responseObject); + } + return ResponseEntity.ok(asyncResponse); } /** @@ -103,11 +122,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { final @NotNull @Valid String resourceIdentifier, final @Valid String optionsParamInQuery, final @Valid String topicParamInQuery) { + final ResponseEntity<Map<String, Object>> asyncResponse = populateAsyncResponse(topicParamInQuery); + final Map<String, Object> asyncResponseData = asyncResponse.getBody(); + final Object responseObject = networkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle(cmHandle, resourceIdentifier, optionsParamInQuery, - topicParamInQuery); - return ResponseEntity.ok(responseObject); + asyncResponseData == null ? NO_TOPIC : topicParamInQuery, + asyncResponseData == null ? NO_REQUEST_ID : asyncResponseData.get(ASYNC_REQUEST_ID).toString()); + + if (asyncResponseData == null) { + return ResponseEntity.ok(responseObject); + } + return ResponseEntity.ok(asyncResponse); } @Override @@ -189,6 +216,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { } /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryRestParameters the cm handle query parameters + * @return collection of cm handle ids + */ + public ResponseEntity<List<String>> queryCmHandles( + final CmHandleQueryRestParameters cmHandleQueryRestParameters) { + final Set<String> cmHandleIds = networkCmProxyDataService.queryCmHandles( + jsonObjectMapper.convertToValueType(cmHandleQueryRestParameters, CmHandleQueryApiParameters.class)); + return ResponseEntity.ok(List.copyOf(cmHandleIds)); + } + + /** * Search for Cm Handle and Properties by Name. * @param cmHandleId cm-handle identifier * @return cm handle and its properties @@ -257,4 +297,33 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { restOutputCmHandle.setPublicCmHandleProperties(cmHandlePublicProperties); return restOutputCmHandle; } + + private ResponseEntity<Map<String, Object>> populateAsyncResponse(final String topicParamInQuery) { + final boolean processAsynchronously = hasTopicParameter(topicParamInQuery); + final Map<String, Object> responseData; + if (processAsynchronously) { + responseData = getAsyncResponseData(); + } else { + responseData = null; + } + return ResponseEntity.ok().body(responseData); + } + + private static boolean hasTopicParameter(final String topicName) { + if (topicName == null) { + return false; + } + if (CpsValidator.validateTopicName(topicName)) { + return true; + } + throw new InvalidTopicException("Topic name " + topicName + " is invalid", "invalid topic"); + } + + private Map<String, Object> getAsyncResponseData() { + final Map<String, Object> asyncResponseData = new HashMap<>(1); + final String resourceDataRequestId = UUID.randomUUID().toString(); + asyncResponseData.put(ASYNC_REQUEST_ID, resourceDataRequestId); + return asyncResponseData; + } + } 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 88633450b3..efe0f3ae6c 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 @@ -60,7 +60,10 @@ class NetworkCmProxyControllerSpec extends Specification { NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock() @SpringBean - JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + ObjectMapper objectMapper = new ObjectMapper() + + @SpringBean + JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper) @SpringBean NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper) @@ -72,6 +75,7 @@ class NetworkCmProxyControllerSpec extends Specification { @Shared def NO_TOPIC = null + def NO_REQUEST_ID = null def 'Get Resource Data from pass-through operational.'() { given: 'resource data url' @@ -86,14 +90,15 @@ class NetworkCmProxyControllerSpec extends Specification { 1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle', 'parent/child', '(a=1,b=2)', - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) and: 'response status is Ok' response.status == HttpStatus.OK.value() } - def 'Get Resource Data from pass-through operational with #scenario.'() { + def 'Get Resource Data from #datastoreInUrl with #scenario.'() { given: 'resource data url' - def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" + + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" + "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}" when: 'get data resource request is performed' def response = mvc.perform( @@ -101,19 +106,30 @@ class NetworkCmProxyControllerSpec extends Specification { .contentType(MediaType.APPLICATION_JSON) ).andReturn().response then: 'the NCMP data service is called with operational data for cm handle' - 1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle', + expectedNumberOfMethodExecutions + * mockNetworkCmProxyDataService."${expectedMethodName}"('testCmHandle', 'parent/child', '(a=1,b=2)', - expectedTopicName) - and: 'response status is Ok' - response.status == HttpStatus.OK.value() + expectedTopicName, + _) + then: 'response status is expected' + response.status == expectedHttpStatus where: 'the following parameters are used' - scenario | topicQueryParam || expectedTopicName - 'Url with valid topic' | "&topic=my-topic-name" || "my-topic-name" - 'No topic in url' | '' || NO_TOPIC - 'Null topic in url' | "&topic=null" || "null" - 'Empty topic in url' | "&topic=\"\"" || "\"\"" - 'Missing topic in url' | "&topic=" || "" + scenario | datastoreInUrl | topicQueryParam || expectedTopicName | expectedMethodName | expectedNumberOfMethodExecutions | expectedHttpStatus + 'url with valid topic' | 'passthrough-operational' | '&topic=my-topic-name' || 'my-topic-name' | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value() + 'no topic in url' | 'passthrough-operational' | '' || NO_TOPIC | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value() + 'null topic in url' | 'passthrough-operational' | '&topic=null' || 'null' | 'getResourceDataOperationalForCmHandle' | 1 | HttpStatus.OK.value() + 'empty topic in url' | 'passthrough-operational' | '&topic=\"\"' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'missing topic in url' | 'passthrough-operational' | '&topic=' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'blank topic value in url' | 'passthrough-operational' | '&topic=\" \"' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#' || null | 'getResourceDataOperationalForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'url with valid topic' | 'passthrough-running' | '&topic=my-topic-name' || 'my-topic-name' | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value() + 'no topic in url' | 'passthrough-running' | '' || NO_TOPIC | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value() + 'null topic in url' | 'passthrough-running' | '&topic=null' || 'null' | 'getResourceDataPassThroughRunningForCmHandle' | 1 | HttpStatus.OK.value() + 'empty topic in url' | 'passthrough-running' | '&topic=\"\"' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'missing topic in url' | 'passthrough-running' | '&topic=' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'blank topic value in url' | 'passthrough-running' | '&topic=\" \"' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() + 'invalid non-empty topic value in url' | 'passthrough-running' | '&topic=1_5_*_#' || null | 'getResourceDataPassThroughRunningForCmHandle' | 0 | HttpStatus.BAD_REQUEST.value() } def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() { @@ -124,7 +140,8 @@ class NetworkCmProxyControllerSpec extends Specification { mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle('testCmHandle', resourceIdentifier, '(a=1,b=2)', - NO_TOPIC) >> '{valid-json}' + NO_TOPIC, + NO_REQUEST_ID) >> '{valid-json}' when: 'get data resource request is performed' def response = mvc.perform( get(getUrl) @@ -241,6 +258,31 @@ class NetworkCmProxyControllerSpec extends Specification { response.contentAsString == '{"cmHandles":[]}' } + def 'Query for cm handles matching query parameters'() { + given: 'an endpoint and json data' + def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches" + String jsonString = '{"publicCmHandleProperties": {"name": "Contact", "value": "newemailforstore@bookstore.com"}}' + and: 'the service method is invoked with module names and returns cm handle ids' + 1 * mockNetworkCmProxyDataService.queryCmHandles(_) >> ['some-cmhandle-id1', 'some-cmhandle-id2'] + when: 'the searches api is invoked' + def response = mvc.perform(post(searchesEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString)).andReturn().response + then: 'cm handle ids are returned' + response.contentAsString == '["some-cmhandle-id1","some-cmhandle-id2"]' + } + + def 'Query for cm handles with invalid request payload'() { + when: 'the searches api is invoked' + def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches" + def invalidInputData = '{invalidJson}' + def response = mvc.perform(post(searchesEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(invalidInputData)).andReturn().response + then: 'BAD_REQUEST is returned' + response.getStatus() == 400 + } + def 'Patch resource data in pass-through running datastore.' () { given: 'patch resource data url' def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" + @@ -271,5 +313,24 @@ class NetworkCmProxyControllerSpec extends Specification { and: 'the response is No Content' response.status == HttpStatus.NO_CONTENT.value() } + + def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() { + given: 'resource data url' + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" + + "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name" + when: 'get data resource request is performed' + def response = mvc.perform( + get(getUrl) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON_VALUE) + ).andReturn().response + then: 'async request id is generated' + assert response.contentAsString.contains("requestId") + where: 'the following parameters are used' + scenario | datastoreInUrl + ':passthrough-operational' | 'passthrough-operational' + ':passthrough-running' | 'passthrough-running' + } + } 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 d50b8c5ea8..058c42b7b9 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 @@ -26,6 +26,8 @@ package org.onap.cps.ncmp.api; import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum; import java.util.Collection; +import java.util.Set; +import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; import org.onap.cps.ncmp.api.models.DmiPluginRegistration; import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; @@ -52,12 +54,14 @@ public interface NetworkCmProxyDataService { * @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 */ Object getResourceDataOperationalForCmHandle(String cmHandleId, String resourceIdentifier, String optionsParamInQuery, - String topicParamInQuery); + String topicParamInQuery, + String requestId); /** * Get resource data for data store pass-through running @@ -66,13 +70,15 @@ public interface NetworkCmProxyDataService { * @param cmHandleId cm handle identifier * @param resourceIdentifier resource identifier * @param optionsParamInQuery options query - * @param topicParamInQuery topic query + * @param topicParamInQuery topic name for (triggering) async responses + * @param requestId unique requestId for async request * @return {@code Object} resource data */ Object getResourceDataPassThroughRunningForCmHandle(String cmHandleId, String resourceIdentifier, String optionsParamInQuery, - String topicParamInQuery); + String topicParamInQuery, + String requestId); /** * Write resource data for data store pass-through running @@ -115,4 +121,11 @@ public interface NetworkCmProxyDataService { */ NcmpServiceCmHandle getNcmpServiceCmHandle(String cmHandleId); + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryApiParameters the cm handle query parameters + * @return collection of cm handle ids + */ + Set<String> queryCmHandles(CmHandleQueryApiParameters cmHandleQueryApiParameters); } 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 81c060eb3b..9c3d9448ef 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 @@ -31,14 +31,14 @@ import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMES import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum; import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED; +import com.google.common.base.Strings; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.UUID; -import java.util.regex.Pattern; +import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -47,12 +47,12 @@ import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsModuleService; import org.onap.cps.ncmp.api.NetworkCmProxyDataService; import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; -import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException; import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations; import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations; import org.onap.cps.ncmp.api.impl.operations.DmiOperations; import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse; import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError; import org.onap.cps.ncmp.api.models.DmiPluginRegistration; @@ -65,7 +65,6 @@ import org.onap.cps.spi.exceptions.SchemaSetNotFoundException; import org.onap.cps.spi.model.ModuleReference; import org.onap.cps.utils.CpsValidator; import org.onap.cps.utils.JsonObjectMapper; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -90,12 +89,6 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService private final YangModelCmHandleRetriever yangModelCmHandleRetriever; - // valid kafka topic name regex - private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|" - + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$"); - private static final String NO_REQUEST_ID = null; - private static final String NO_TOPIC = null; - @Override public DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule( final DmiPluginRegistration dmiPluginRegistration) { @@ -119,20 +112,22 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService public Object getResourceDataOperationalForCmHandle(final String cmHandleId, final String resourceIdentifier, final String optionsParamInQuery, - final String topicParamInQuery) { + final String topicParamInQuery, + final String requestId) { CpsValidator.validateNameCharacters(cmHandleId); - return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, - DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery); + return getResourceDataResponse(cmHandleId, resourceIdentifier, + DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery, requestId); } @Override public Object getResourceDataPassThroughRunningForCmHandle(final String cmHandleId, final String resourceIdentifier, final String optionsParamInQuery, - final String topicParamInQuery) { + final String topicParamInQuery, + final String requestId) { CpsValidator.validateNameCharacters(cmHandleId); - return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, - DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery); + return getResourceDataResponse(cmHandleId, resourceIdentifier, + DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery, requestId); } @Override @@ -165,6 +160,20 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService return cpsAdminService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, moduleNames); } + @Override + public Set<String> queryCmHandles(final CmHandleQueryApiParameters cmHandleQueryApiParameters) { + + cmHandleQueryApiParameters.getPublicProperties().forEach((key, value) -> { + if (Strings.isNullOrEmpty(key)) { + throw new DataValidationException("Invalid Query Parameter.", + "Missing property name - please supply a valid name."); + } + }); + + return cpsAdminService.queryCmHandles(jsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, + org.onap.cps.spi.model.CmHandleQueryParameters.class)); + } + /** * Retrieve cm handle details for a given cm handle. * @@ -329,35 +338,14 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService yangModelCmHandle.getId()); } - private static boolean hasTopicParameter(final String topicName) { - if (topicName == null) { - return false; - } - if (TOPIC_NAME_PATTERN.matcher(topicName).matches()) { - return true; - } - throw new InvalidTopicException("Topic name " + topicName + " is invalid", "invalid topic"); - } - - private Map<String, Object> buildDmiResponse(final String requestId) { - final Map<String, Object> dmiResponseMap = new HashMap<>(); - dmiResponseMap.put("requestId", requestId); - return dmiResponseMap; - } - - private Object validateTopicNameAndGetResourceData(final String cmHandleId, - final String resourceIdentifier, - final DmiOperations.DataStoreEnum dataStore, - final String optionsParamInQuery, - final String topicParamInQuery) { - final boolean processAsynchronously = hasTopicParameter(topicParamInQuery); - if (processAsynchronously) { - final String resourceDataRequestId = UUID.randomUUID().toString(); - return ResponseEntity.status(HttpStatus.OK) - .body(buildDmiResponse(resourceDataRequestId)); - } + private Object getResourceDataResponse(final String cmHandleId, + final String resourceIdentifier, + final DmiOperations.DataStoreEnum dataStore, + final String optionsParamInQuery, + final String topicParamInQuery, + final String requestId) { final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi( - cmHandleId, resourceIdentifier, optionsParamInQuery, dataStore, NO_REQUEST_ID, NO_TOPIC); + cmHandleId, resourceIdentifier, optionsParamInQuery, dataStore, requestId, topicParamInQuery); return handleResponse(responseEntity, OperationEnum.READ); } }
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java new file mode 100644 index 0000000000..3f584ed153 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 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.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.Map; +import javax.validation.Valid; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@JsonInclude(Include.NON_NULL) +public class CmHandleQueryApiParameters { + + @JsonProperty("publicCmHandleProperties") + @Valid + private Map<String, String> publicProperties = Collections.emptyMap(); + +} 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 489c71c0e9..2d01dba522 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 @@ -23,7 +23,6 @@ package org.onap.cps.ncmp.api.impl import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException -import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle import spock.lang.Shared @@ -119,7 +118,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', 'testResourceId', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'DMI returns a json response' response == 'dmi-response' } @@ -137,7 +137,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', 'testResourceId', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'exception is thrown with the expected response code and details' def exceptionThrown = thrown(HttpClientRequestException.class) exceptionThrown.details.contains('NOK-json') @@ -160,7 +161,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', 'testResourceId', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'exception is thrown' def exceptionThrown = thrown(HttpClientRequestException.class) and: 'details contain the original response' @@ -183,7 +185,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle', 'testResourceId', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'get resource data returns expected response' response == '{dmi-response}' } @@ -204,7 +207,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle', 'testResourceId', OPTIONS_PARAM, - NO_TOPIC) + NO_TOPIC, + NO_REQUEST_ID) then: 'exception is thrown' def exceptionThrown = thrown(HttpClientRequestException.class) and: 'details contain the original response' @@ -212,72 +216,6 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value() } - def 'DMI Operational data request with #scenario'() { - given: 'cps data service returns valid data node' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode - and: 'dmi data operation returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC) - >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called data operational with blank topic' - def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '', - '', emptyTopic) - then: 'a invalid topic exception is thrown' - thrown(InvalidTopicException) - where: 'the following parameters are used' - scenario | emptyTopic - 'no topic value in url' | '' - 'empty topic value in url' | '\"\"' - 'blank topic value in url' | ' ' - 'invalid non-empty topic value in url' | '1_5_*_#' - } - - def 'Get resource data for data operational from DMI with valid topic i.e. async request.'() { - given: 'cps data service returns valid data node' - mockCpsDataService.getDataNode(*_) >> dataNode - and: 'dmi data operation returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, 'my-topic-name') - >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called for data operational with valid topic' - def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '', '', 'my-topic-name') - then: 'non empty request id is generated' - assert responseData.body.requestId.length() > 0 - } - - def 'Get resource data for pass through running from DMI with valid topic async request.'() { - given: 'cps data service returns valid data node' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode - and: 'dmi data operation returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, 'my-topic-name') - >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called for data operational with valid topic' - def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('', - '', OPTIONS_PARAM, 'my-topic-name') - then: 'non empty request id is generated' - assert responseData.body.requestId.length() > 0 - } - - def 'DMI pass through running data request with #scenario'() { - given: 'cps data service returns valid data node' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode - and: 'dmi data operation returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, NO_REQUEST_ID, NO_TOPIC) - >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called for data operational with valid topic' - def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('', - '', '', emptyTopic) - then: 'a invalid topic exception is thrown' - thrown(InvalidTopicException) - where: 'the following parameters are used' - scenario | emptyTopic - 'no topic value in url' | '' - 'empty topic value in url' | '\"\"' - 'blank topic value in url' | ' ' - 'invalid non-empty topic value in url' | '1_5_*_#' - } - def 'Getting Yang Resources.'() { when: 'yang resources is called' objectUnderTest.getYangResourcesModuleReferences('some-cm-handle') diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java index 50b27207ee..2e7bb7e969 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java @@ -24,6 +24,7 @@ package org.onap.cps.spi.impl; import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import javax.transaction.Transactional; import lombok.AllArgsConstructor; @@ -36,8 +37,10 @@ import org.onap.cps.spi.exceptions.AlreadyDefinedException; import org.onap.cps.spi.exceptions.DataspaceInUseException; import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException; import org.onap.cps.spi.model.Anchor; +import org.onap.cps.spi.model.CmHandleQueryParameters; import org.onap.cps.spi.repository.AnchorRepository; import org.onap.cps.spi.repository.DataspaceRepository; +import org.onap.cps.spi.repository.ModuleReferenceRepository; import org.onap.cps.spi.repository.SchemaSetRepository; import org.onap.cps.spi.repository.YangResourceRepository; import org.springframework.dao.DataIntegrityViolationException; @@ -51,6 +54,7 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic private final AnchorRepository anchorRepository; private final SchemaSetRepository schemaSetRepository; private final YangResourceRepository yangResourceRepository; + private final ModuleReferenceRepository moduleReferenceRepository; @Override public void createDataspace(final String dataspaceName) { @@ -132,6 +136,11 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic anchorRepository.delete(anchorEntity); } + @Override + public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) { + return moduleReferenceRepository.queryCmHandles(cmHandleQueryParameters); + } + private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) { final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName); return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java index 6551937e10..4bc9dd9603 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java @@ -21,6 +21,8 @@ package org.onap.cps.spi.repository; import java.util.Collection; +import java.util.Set; +import org.onap.cps.spi.model.CmHandleQueryParameters; import org.onap.cps.spi.model.ModuleReference; /** @@ -31,4 +33,12 @@ public interface ModuleReferenceQuery { Collection<ModuleReference> identifyNewModuleReferences( final Collection<ModuleReference> moduleReferencesToCheck); + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryParameters the cm handle query parameters + * @return collection of cm handle ids + */ + Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters); + } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java index ce2bfe7847..f70e218373 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java @@ -27,8 +27,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface ModuleReferenceRepository extends - JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery { +public interface ModuleReferenceRepository extends JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery { Collection<ModuleReference> identifyNewModuleReferences( final Collection<ModuleReference> moduleReferencesToCheck); diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java index 0e79deb8e8..40a93da71c 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java @@ -24,21 +24,32 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import lombok.AllArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.spi.CpsDataPersistenceService; +import org.onap.cps.spi.FetchDescendantsOption; +import org.onap.cps.spi.model.CmHandleQueryParameters; +import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.ModuleReference; import org.springframework.transaction.annotation.Transactional; @Slf4j @Transactional +@AllArgsConstructor public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { @PersistenceContext private EntityManager entityManager; + private final CpsDataPersistenceService cpsDataPersistenceService; + @Override @SneakyThrows public Collection<ModuleReference> identifyNewModuleReferences( @@ -57,6 +68,56 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { return identifyNewModuleReferencesForCmHandle(tempTableName); } + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryParameters the cm handle query parameters + * @return collection of cm handle ids + */ + @Override + public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) { + + if (cmHandleQueryParameters.getPublicProperties().entrySet().isEmpty()) { + return getAllCmHandles(); + } + + final Collection<DataNode> amalgamatedQueryResult = new ArrayList<>(); + int queryConditionCounter = 0; + for (final Map.Entry<String, String> entry : cmHandleQueryParameters.getPublicProperties().entrySet()) { + final StringBuilder cmHandlePath = new StringBuilder(); + cmHandlePath.append("//public-properties[@name='").append(entry.getKey()).append("' "); + cmHandlePath.append("and @value='").append(entry.getValue()).append("']"); + cmHandlePath.append("/ancestor::cm-handles"); + + final Collection<DataNode> singleConditionQueryResult = + cpsDataPersistenceService.queryDataNodes("NCMP-Admin", + "ncmp-dmi-registry", String.valueOf(cmHandlePath), FetchDescendantsOption.OMIT_DESCENDANTS); + if (++queryConditionCounter == 1) { + amalgamatedQueryResult.addAll(singleConditionQueryResult); + } else { + amalgamatedQueryResult.retainAll(singleConditionQueryResult); + } + + if (amalgamatedQueryResult.isEmpty()) { + break; + } + } + + return extractCmHandleIds(amalgamatedQueryResult); + } + + private Set<String> getAllCmHandles() { + final Collection<DataNode> cmHandles = cpsDataPersistenceService.queryDataNodes("NCMP-Admin", + "ncmp-dmi-registry", "//public-properties/ancestor::cm-handles", + FetchDescendantsOption.OMIT_DESCENDANTS); + return extractCmHandleIds(cmHandles); + } + + private Set<String> extractCmHandleIds(final Collection<DataNode> cmHandles) { + return cmHandles.stream().map(cmHandle -> cmHandle.getLeaves().get("id").toString()) + .collect(Collectors.toSet()); + } + private void createTemporaryTable(final String tempTableName) { final StringBuilder sqlStringBuilder = new StringBuilder("CREATE TEMPORARY TABLE " + tempTableName + "("); sqlStringBuilder.append(" id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,"); @@ -95,7 +156,7 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { + " WHERE yang_resource.module_name IS NULL;", tempTableName); final List<Object[]> resultsAsObjects = - entityManager.createNativeQuery(sql).getResultList(); + (List<Object[]>) entityManager.createNativeQuery(sql).getResultList(); final List<ModuleReference> resultsAsModuleReferences = new ArrayList<>(resultsAsObjects.size()); for (final Object[] row : resultsAsObjects) { diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy index 063bd5b5ae..f486cb76d0 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ @@ -22,6 +22,7 @@ package org.onap.cps.spi.impl +import org.mockito.Mock import org.onap.cps.spi.CpsAdminPersistenceService import org.onap.cps.spi.exceptions.AlreadyDefinedException import org.onap.cps.spi.exceptions.AnchorNotFoundException @@ -30,15 +31,21 @@ import org.onap.cps.spi.exceptions.DataspaceNotFoundException import org.onap.cps.spi.exceptions.SchemaSetNotFoundException import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException import org.onap.cps.spi.model.Anchor +import org.onap.cps.spi.model.CmHandleQueryParameters import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.jdbc.Sql +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { @Autowired CpsAdminPersistenceService objectUnderTest + @Mock + ObjectMapper objectMapper + static final String SET_DATA = '/data/anchor.sql' + static final String SET_FRAGMENT_DATA = '/data/fragment.sql' static final String SAMPLE_DATA_FOR_ANCHORS_WITH_MODULES = '/data/anchors-schemaset-modules.sql' static final String DATASPACE_WITH_NO_DATA = 'DATASPACE-002-NO-DATA' static final Integer DELETED_ANCHOR_ID = 3002 @@ -219,4 +226,20 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase { 'dataspace contains schemasets' | 'DATASPACE-003' || DataspaceInUseException | 'contains 1 schemaset(s)' } + @Sql([CLEAR_DATA, SET_FRAGMENT_DATA]) + def 'Retrieve cm handle ids when #scenario.'() { + when: 'the service is invoked' + def cmHandleQueryParameters = new CmHandleQueryParameters() + cmHandleQueryParameters.setPublicProperties(publicProperties) + def returnedCmHandles = objectUnderTest.queryCmHandles(cmHandleQueryParameters) + then: 'the correct expected cm handles are returned' + returnedCmHandles == expectedCmHandleIds + where: 'the following data is used' + scenario | publicProperties || expectedCmHandleIds + 'single matching property' | ['Contact' : 'newemailforstore@bookstore.com'] || ['PNFDemo2', 'PNFDemo', 'PNFDemo4'] as Set + 'public property dont match' | ['wont_match' : 'wont_match'] || [] as Set + '2 properties, only one match (and)' | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': 'newemailforstore2@bookstore.com'] || ['PNFDemo4'] as Set + '2 properties, no match (and)' | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': ''] || [] as Set + 'No public properties - return all cm handles' | [ : ] || ['PNFDemo3', 'PNFDemo', 'PNFDemo2', 'PNFDemo4'] as Set + } } diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index a27bb5fdea..fb4a5f77cb 100755 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -1,6 +1,6 @@ /* ============LICENSE_START======================================================= - Copyright (C) 2021 Nordix Foundation. + Copyright (C) 2021-2022 Nordix Foundation. Modifications Copyright (C) 2021 Pantheon.tech Modifications Copyright (C) 2021-2022 Bell Canada. ================================================================================ @@ -21,14 +21,16 @@ */ INSERT INTO DATASPACE (ID, NAME) VALUES - (1001, 'DATASPACE-001'); + (1001, 'DATASPACE-001'), + (1002, 'NCMP-Admin'); INSERT INTO SCHEMA_SET (ID, NAME, DATASPACE_ID) VALUES (2001, 'SCHEMA-SET-001', 1001); INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES (3001, 'ANCHOR-001', 1001, 2001), - (3003, 'ANCHOR-003', 1001, 2001); + (3003, 'ANCHOR-003', 1001, 2001), + (3004, 'ncmp-dmi-registry', 1002, 2001); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH) VALUES (4001, 1001, 3001, null, '/parent-1'), @@ -67,4 +69,15 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="X"]', '{"key": "X"}'), (4231, 1001, 3003, null, '/parent-206[@key="A"]', '{"key": "A"}'), (4232, 1001, 3003, 4231, '/parent-206[@key="A"]/child-206', '{}'), - (4233, 1001, 3003, null, '/parent-206[@key="B"]', '{"key": "B"}');
\ No newline at end of file + (4233, 1001, 3003, null, '/parent-206[@key="B"]', '{"key": "B"}'); + +INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES + (5000, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo"]', '{"id": "PNFDemo", "dmi-service-name": "http://172.21.235.14:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'), + (5001, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo2"]', '{"id": "PNFDemo2", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'), + (5002, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo3"]', '{"id": "PNFDemo3", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'), + (5003, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo4"]', '{"id": "PNFDemo4", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'), + (5004, 1002, 3004, 5000, '/dmi-registry/cm-handles[@id="PNFDemo"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'), + (5005, 1002, 3004, 5001, '/dmi-registry/cm-handles[@id="PNFDemo2"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'), + (5006, 1002, 3004, 5002, '/dmi-registry/cm-handles[@id="PNFDemo3"]/public-properties[@name="Contact"]', '{"name": "Contact3", "value": "PNF3@bookstore.com"}'), + (5007, 1002, 3004, 5003, '/dmi-registry/cm-handles[@id="PNFDemo4"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'), + (5008, 1002, 3004, 5004, '/dmi-registry/cm-handles[@id="PNFDemo4"]/public-properties[@name="Contact2"]', '{"name": "Contact2", "value": "newemailforstore2@bookstore.com"}');
\ No newline at end of file diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java b/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java index 2cb01ac1ef..2106f1584e 100755 --- a/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2021 Nordix Foundation + * Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * ================================================================================ @@ -23,9 +23,11 @@ package org.onap.cps.api; import java.util.Collection; +import java.util.Set; import org.onap.cps.spi.exceptions.AlreadyDefinedException; import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.spi.model.Anchor; +import org.onap.cps.spi.model.CmHandleQueryParameters; /** * CPS Admin Service. @@ -100,4 +102,12 @@ public interface CpsAdminService { * given module names */ Collection<String> queryAnchorNames(String dataspaceName, Collection<String> moduleNames); + + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryParameters the cm handle query parameters + * @return collection of cm handle ids + */ + Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters); } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java index 7bec1e39f0..762754f9a8 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java @@ -24,12 +24,14 @@ package org.onap.cps.api.impl; import java.time.OffsetDateTime; import java.util.Collection; +import java.util.Set; import java.util.stream.Collectors; import lombok.AllArgsConstructor; import org.onap.cps.api.CpsAdminService; import org.onap.cps.api.CpsDataService; import org.onap.cps.spi.CpsAdminPersistenceService; import org.onap.cps.spi.model.Anchor; +import org.onap.cps.spi.model.CmHandleQueryParameters; import org.onap.cps.utils.CpsValidator; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -91,4 +93,9 @@ public class CpsAdminServiceImpl implements CpsAdminService { final Collection<Anchor> anchors = cpsAdminPersistenceService.queryAnchors(dataspaceName, moduleNames); return anchors.stream().map(Anchor::getName).collect(Collectors.toList()); } + + @Override + public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) { + return cpsAdminPersistenceService.queryCmHandles(cmHandleQueryParameters); + } } diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java index dd4059d88c..25167e844a 100755 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020 Nordix Foundation. + * Copyright (C) 2020-2022 Nordix Foundation. * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * ================================================================================ @@ -23,8 +23,10 @@ package org.onap.cps.spi; import java.util.Collection; +import java.util.Set; import org.onap.cps.spi.exceptions.AlreadyDefinedException; import org.onap.cps.spi.model.Anchor; +import org.onap.cps.spi.model.CmHandleQueryParameters; /* Service for handling CPS admin data. @@ -99,4 +101,12 @@ public interface CpsAdminPersistenceService { * @param anchorName anchor name */ void deleteAnchor(String dataspaceName, String anchorName); + + /** + * Query and return cm handles that match the given query parameters. + * + * @param cmHandleQueryParameters the cm handle query parameters + * @return collection of cm handle ids + */ + Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters); } diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java b/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java new file mode 100644 index 0000000000..ff4e627636 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 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.spi.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.Map; +import javax.validation.Valid; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@JsonInclude(Include.NON_NULL) +public class CmHandleQueryParameters { + + @JsonProperty("publicCmHandleProperties") + @Valid + private Map<String, String> publicProperties = Collections.emptyMap(); + +} diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java index 55e7b9970b..43aa06b81b 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java +++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java @@ -26,11 +26,13 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @Setter(AccessLevel.PROTECTED) @Getter +@EqualsAndHashCode public class DataNode { DataNode() { } diff --git a/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java b/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java index dd16495638..28b49c9666 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java +++ b/cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java @@ -22,6 +22,7 @@ package org.onap.cps.utils; import com.google.common.collect.Lists; import java.util.Collection; +import java.util.regex.Pattern; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -32,6 +33,8 @@ import org.onap.cps.spi.exceptions.DataValidationException; public final class CpsValidator { private static final char[] UNSUPPORTED_NAME_CHARACTERS = "!\" #$%&'()*+,./\\:;<=>?@[]^`{|}~".toCharArray(); + private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|" + + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$"); /** * Validate characters in names within cps. @@ -48,4 +51,12 @@ public final class CpsValidator { } } } + + /** + * Validate kafka topic name pattern. + * @param topicName name of the topic to be validated + */ + public static boolean validateTopicName(final String topicName) { + return topicName != null && TOPIC_NAME_PATTERN.matcher(topicName).matches(); + } } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy index bb122d1ae2..cbe1ebbbdf 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020 Nordix Foundation + * Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * ================================================================================ @@ -25,6 +25,7 @@ package org.onap.cps.api.impl import org.onap.cps.api.CpsDataService import org.onap.cps.spi.CpsAdminPersistenceService import org.onap.cps.spi.model.Anchor +import org.onap.cps.spi.model.CmHandleQueryParameters import spock.lang.Specification import java.time.OffsetDateTime @@ -95,4 +96,13 @@ class CpsAdminServiceImplSpec extends Specification { 1 * mockCpsAdminPersistenceService.deleteDataspace('someDataspace') } + def 'Query CM Handles.'() { + given: 'a cm handle query' + def cmHandleQueryParameters = new CmHandleQueryParameters() + when: 'query cm handles is invoked' + objectUnderTest.queryCmHandles(cmHandleQueryParameters) + then: 'associated persistence service method is invoked with correct parameter' + 1 * mockCpsAdminPersistenceService.queryCmHandles(cmHandleQueryParameters) + } + } diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy index 191472ceea..ce728ef1c1 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy @@ -45,4 +45,18 @@ class CpsValidatorSpec extends Specification { 'position 5' | 'name with spaces' || 'name with spaces invalid token encountered at position 5' 'position 9' | 'nameWith Space' || 'nameWith Space invalid token encountered at position 9' } + + def 'Validating topic names.'() { + when: 'the topic name is validated' + def isValidTopicName = CpsValidator.validateTopicName(topicName) + then: 'boolean response will be returned for #scenario' + assert isValidTopicName == booleanResponse + where: 'the following names are used' + scenario | topicName || booleanResponse + 'valid topic' | 'my-topic-name' || true + 'empty topic' | '' || false + 'blank topic' | ' ' || false + 'null topic' | null || false + 'invalid non empty topic' | '1_5_*_#' || false + } } diff --git a/csit/data/cmHandleRegistration.json b/csit/data/cmHandleRegistration.json deleted file mode 100644 index 0133148fda..0000000000 --- a/csit/data/cmHandleRegistration.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "cmHandles": [ - "PNFDemo" - ] -}
\ No newline at end of file diff --git a/csit/plans/cps/testplan.txt b/csit/plans/cps/testplan.txt index 8069bb72e7..d4615e7010 100644 --- a/csit/plans/cps/testplan.txt +++ b/csit/plans/cps/testplan.txt @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (C) 2021 Nordix Foundation +# Copyright (C) 2021-2022 Nordix Foundation # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ # Test suites are relative paths under csit/tests/. # Place the suites in run order. actuator -cps-model-sync -ncmp-passthrough cps-admin cps-data - +cps-model-sync +ncmp-passthrough +public-properties-query
\ No newline at end of file diff --git a/csit/tests/cps-model-sync/cps-model-sync.robot b/csit/tests/cps-model-sync/cps-model-sync.robot index 7de1f3a1be..ea082b5a89 100644 --- a/csit/tests/cps-model-sync/cps-model-sync.robot +++ b/csit/tests/cps-model-sync/cps-model-sync.robot @@ -34,7 +34,7 @@ ${auth} Basic Y3BzdXNlcjpjcHNyMGNrcyE= ${ncmpInventoryBasePath} /ncmpInventory ${ncmpBasePath} /ncmp ${dmiUrl} http://${DMI_HOST}:${DMI_PORT} -${jsonDataCreate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","createdCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Sci-Fi Book"},"publicCmHandleProperties":{"Contact":"storeemail@bookstore.com"}}]} +${jsonDataCreate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","createdCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Sci-Fi Book"},"publicCmHandleProperties":{"Contact":"storeemail@bookstore.com", "Contact2":"storeemail2@bookstore.com"}}]} ${jsonDataUpdate} {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","updatedCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Romance Book"},"publicCmHandleProperties":{"Contact":"newemailforstore@bookstore.com"}}]} *** Test Cases *** diff --git a/csit/tests/public-properties-query/public-properties-query.robot b/csit/tests/public-properties-query/public-properties-query.robot new file mode 100644 index 0000000000..3a640871b9 --- /dev/null +++ b/csit/tests/public-properties-query/public-properties-query.robot @@ -0,0 +1,51 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 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========================================================= + */ + +*** Settings *** +Documentation Public Properties Query Test + +Library Collections +Library OperatingSystem +Library RequestsLibrary +Library BuiltIn + +Suite Setup Create Session CPS_URL http://${CPS_CORE_HOST}:${CPS_CORE_PORT} + +*** Variables *** + +${auth} Basic Y3BzdXNlcjpjcHNyMGNrcyE= +${ncmpBasePath} /ncmp/v1 +${jsonMatchingQueryParameters} {"publicCmHandleProperties": {"Contact" : "newemailforstore@bookstore.com", "Contact2" : "storeemail2@bookstore.com"}} +${jsonMissingPropertyQueryParameters} {"publicCmHandleProperties": { "" : "doesnt matter"}} + +*** Test Cases *** +Retrieve CM Handles where query parameters Match + ${uri}= Set Variable ${ncmpBasePath}/data/ch/searches + ${headers}= Create Dictionary Content-Type=application/json Authorization=${auth} + ${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonMatchingQueryParameters} + ${responseJson}= Set Variable ${response.json()} + Should Be Equal As Strings ${response.status_code} 200 + Should Contain ${responseJson} PNFDemo + +Throw 400 when Structure of Request is Incorrect + ${uri}= Set Variable ${ncmpBasePath}/data/ch/searches + ${headers}= Create Dictionary Content-Type=application/json Authorization=${auth} + ${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonMissingPropertyQueryParameters} expected_status=400 + Should Be Equal As Strings ${response} <Response [400]> |