<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>checkstyle</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<profiles>
<profile>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<properties>
<app>org.onap.cps.Application</app>
<maven.build.timestamp.format>yyyyMMdd'T'HHmmss'Z'</maven.build.timestamp.format>
- <minimum-coverage>0.82</minimum-coverage>
+ <minimum-coverage>0.86</minimum-coverage>
<base.image>${docker.pull.registry}/onap/integration-java17:12.0.0</base.image>
<image.tag>${project.version}-${maven.build.timestamp}</image.tag>
</properties>
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>cps-bom</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<packaging>pom</packaging>
<description>This artifact contains dependencyManagement declarations of all published CPS components.</description>
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>cps-dependencies</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<packaging>pom</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-ncmp-rest-stub</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
</parent>
<artifactId>cps-ncmp-rest-stub-app</artifactId>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-ncmp-rest-stub</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
</parent>
<artifactId>cps-ncmp-rest-stub-service</artifactId>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
import org.onap.cps.ncmp.api.NetworkCmProxyQueryService;
import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor;
import org.onap.cps.spi.FetchDescendantsOption;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
@Component
this.networkCmProxyQueryService = networkCmProxyQueryService;
}
+ /**
+ * Executes a synchronous query request for given cm handle.
+ * Note. Currently only ncmp-datastore:operational supports query operations.
+ *
+ * @param cmHandleId the cm handle
+ * @param resourceIdentifier the resource identifier
+ * @param includeDescendants whether include descendants
+ * @return the response entity
+ */
+ public ResponseEntity<Object> executeRequest(final String cmHandleId,
+ final String resourceIdentifier,
+ final boolean includeDescendants) {
+
+ final Supplier<Object> taskSupplier = getTaskSupplierForQueryRequest(cmHandleId, resourceIdentifier,
+ includeDescendants);
+ return executeTaskSync(taskSupplier);
+ }
+
@Override
- public Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
- final String cmHandleId,
- final String resourceIdentifier,
- final String optionsParamInQuery,
- final String topicParamInQuery,
- final String requestId,
- final boolean includeDescendants) {
+ protected Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
+ final String cmHandleId,
+ final String resourceIdentifier,
+ final String optionsParamInQuery,
+ final String topicParamInQuery,
+ final String requestId,
+ final boolean includeDescendants) {
- final FetchDescendantsOption fetchDescendantsOption =
- TaskManagementDefaultHandler.getFetchDescendantsOption(includeDescendants);
+ final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants);
return () -> networkCmProxyDataService.getResourceDataForCmHandle(datastoreName, cmHandleId, resourceIdentifier,
- fetchDescendantsOption);
+ fetchDescendantsOption);
}
- /**
- * Gets ncmp datastore query handler.
- * Note. Currently only ncmp-datastore:operational supports query operations
- * @return a ncmp datastore query handler.
- */
- @Override
- public Supplier<Object> getTaskSupplierForQueryRequest(final String cmHandleId,
- final String resourceIdentifier,
- final boolean includeDescendants) {
+ private Supplier<Object> getTaskSupplierForQueryRequest(final String cmHandleId,
+ final String resourceIdentifier,
+ final boolean includeDescendants) {
- final FetchDescendantsOption fetchDescendantsOption =
- TaskManagementDefaultHandler.getFetchDescendantsOption(includeDescendants);
+ final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants);
return () -> networkCmProxyQueryService.queryResourceDataOperational(cmHandleId, resourceIdentifier,
- fetchDescendantsOption);
+ fetchDescendantsOption);
}
+ private static FetchDescendantsOption getFetchDescendantsOption(final boolean includeDescendants) {
+ return includeDescendants ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+ : FetchDescendantsOption.OMIT_DESCENDANTS;
+ }
+
+
}
package org.onap.cps.ncmp.rest.controller.handlers;
-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.DataOperationRequest;
-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;
-import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
-public class NcmpDatastoreRequestHandler implements TaskManagementDefaultHandler {
+public abstract class NcmpDatastoreRequestHandler {
+
+ private static final String NO_REQUEST_ID = null;
+ private static final String NO_TOPIC = null;
@Value("${notification.async.executor.time-out-value-in-ms:2000}")
protected int timeOutInMilliSeconds;
@Value("${notification.enabled:true}")
protected boolean notificationFeatureEnabled;
- private final CpsNcmpTaskExecutor cpsNcmpTaskExecutor;
+ protected final CpsNcmpTaskExecutor cpsNcmpTaskExecutor;
/**
- * Executes synchronous/asynchronous request for given cm handle.
+ * Executes synchronous/asynchronous get request for given cm handle.
*
* @param datastoreName the name of the datastore
* @param cmHandleId the cm handle
return executeTaskSync(taskSupplier);
}
- /**
- * Executes a synchronous request for given cm handle.
- * Note. Currently only ncmp-datastore:operational supports query operations.
- *
- * @param cmHandleId the cm handle
- * @param resourceIdentifier the resource identifier
- * @param includeDescendants whether include descendants
- * @return the response entity
- */
- public ResponseEntity<Object> executeRequest(final String cmHandleId,
- final String resourceIdentifier,
- final boolean includeDescendants) {
-
- final Supplier<Object> taskSupplier = getTaskSupplierForQueryRequest(cmHandleId, resourceIdentifier,
- includeDescendants);
- return executeTaskSync(taskSupplier);
- }
- /**
- * Executes asynchronous request for group of cm handles to resource data.
- *
- * @param topicParamInQuery the topic param in query
- * @param dataOperationRequest data operation request details for resource data
- * @return the response entity
- */
- public ResponseEntity<Object> executeRequest(final String topicParamInQuery,
- final DataOperationRequest
- dataOperationRequest) {
- validateDataOperationRequest(topicParamInQuery, dataOperationRequest);
- if (!notificationFeatureEnabled) {
- return ResponseEntity.ok(Map.of("status",
- "Asynchronous request is unavailable as notification feature is currently disabled."));
- }
- return getRequestIdAndSendDataOperationRequestToDmiService(topicParamInQuery, dataOperationRequest);
- }
-
- protected ResponseEntity<Object> executeTaskAsync(final String topicParamInQuery,
+ private ResponseEntity<Object> executeTaskAsync(final String topicParamInQuery,
final String requestId,
final Supplier<Object> taskSupplier) {
-
TopicValidator.validateTopicName(topicParamInQuery);
log.debug("Received Async request with id {}", requestId);
cpsNcmpTaskExecutor.executeTask(taskSupplier, timeOutInMilliSeconds);
final String requestId = UUID.randomUUID().toString();
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> getRequestIdAndSendDataOperationRequestToDmiService(final String topicParamInQuery,
- final DataOperationRequest
- dataOperationRequest) {
- final String requestId = UUID.randomUUID().toString();
- sendDataOperationRequestAsynchronously(topicParamInQuery, dataOperationRequest, requestId);
- return ResponseEntity.ok(Map.of("requestId", requestId));
- }
+ protected abstract Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
+ final String cmHandleId,
+ final String resourceIdentifier,
+ final String optionsParamInQuery,
+ final String topicParamInQuery,
+ final String requestId,
+ final boolean includeDescendant);
- private void validateDataOperationRequest(final String topicParamInQuery,
- final DataOperationRequest
- dataOperationRequest) {
- TopicValidator.validateTopicName(topicParamInQuery);
- dataOperationRequest.getDataOperationDefinitions().forEach(dataOperationDetail -> {
- if (OperationType.fromOperationName(dataOperationDetail.getOperation()) != READ) {
- throw new OperationNotSupportedException(
- dataOperationDetail.getOperation() + " operation not yet supported");
- } else if (DatastoreType.fromDatastoreName(dataOperationDetail.getDatastore()) == OPERATIONAL) {
- throw new InvalidDatastoreException(dataOperationDetail.getDatastore()
- + " datastore is not supported");
- }
- });
- }
}
package org.onap.cps.ncmp.rest.controller.handlers;
+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 org.onap.cps.ncmp.api.NetworkCmProxyDataService;
+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.DataOperationRequest;
+import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException;
import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor;
-import org.springframework.scheduling.annotation.Async;
+import org.onap.cps.ncmp.rest.util.TopicValidator;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
@Component
private final NetworkCmProxyDataService networkCmProxyDataService;
+ private static final Object noReturn = null;
+
/**
* Constructor.
*
this.networkCmProxyDataService = networkCmProxyDataService;
}
+ /**
+ * Executes asynchronous request for group of cm handles to resource data.
+ *
+ * @param topicParamInQuery the topic param in query
+ * @param dataOperationRequest data operation request details for resource data
+ * @return the response entity
+ */
+ public ResponseEntity<Object> executeRequest(final String topicParamInQuery,
+ final DataOperationRequest
+ dataOperationRequest) {
+ validateDataOperationRequest(topicParamInQuery, dataOperationRequest);
+ if (!notificationFeatureEnabled) {
+ return ResponseEntity.ok(Map.of("status",
+ "Asynchronous request is unavailable as notification feature is currently disabled."));
+ }
+ return getRequestIdAndSendDataOperationRequestToDmiService(topicParamInQuery, dataOperationRequest);
+ }
+
@Override
- public Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
- final String cmHandleId,
- final String resourceIdentifier,
- final String optionsParamInQuery,
- final String topicParamInQuery,
- final String requestId,
- final boolean includeDescendants) {
+ protected Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
+ final String cmHandleId,
+ final String resourceIdentifier,
+ final String optionsParamInQuery,
+ final String topicParamInQuery,
+ final String requestId,
+ final boolean includeDescendants) {
return () -> networkCmProxyDataService.getResourceDataForCmHandle(
- datastoreName, cmHandleId, resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId);
+ datastoreName, cmHandleId, resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId);
}
- @Async
- @Override
- public void sendDataOperationRequestAsynchronously(final String topicParamInQuery,
- final DataOperationRequest
- dataOperationRequest,
- final String requestId) {
- networkCmProxyDataService.executeDataOperationForCmHandles(topicParamInQuery, dataOperationRequest,
- requestId);
+ private ResponseEntity<Object> getRequestIdAndSendDataOperationRequestToDmiService(final String topicParamInQuery,
+ final DataOperationRequest
+ dataOperationRequest) {
+ final String requestId = UUID.randomUUID().toString();
+ cpsNcmpTaskExecutor.executeTask(
+ getTaskSupplierForDataOperationRequest(topicParamInQuery, dataOperationRequest, requestId),
+ timeOutInMilliSeconds);
+ return ResponseEntity.ok(Map.of("requestId", requestId));
+ }
+ private void validateDataOperationRequest(final String topicParamInQuery,
+ final DataOperationRequest
+ dataOperationRequest) {
+ TopicValidator.validateTopicName(topicParamInQuery);
+ dataOperationRequest.getDataOperationDefinitions().forEach(dataOperationDetail -> {
+ if (OperationType.fromOperationName(dataOperationDetail.getOperation()) != READ) {
+ throw new OperationNotSupportedException(
+ dataOperationDetail.getOperation() + " operation not yet supported");
+ } else if (DatastoreType.fromDatastoreName(dataOperationDetail.getDatastore()) == OPERATIONAL) {
+ throw new InvalidDatastoreException(dataOperationDetail.getDatastore()
+ + " datastore is not supported");
+ }
+ });
}
+
+ private Supplier<Object> getTaskSupplierForDataOperationRequest(final String topicParamInQuery,
+ final DataOperationRequest dataOperationRequest,
+ final String requestId) {
+ return () -> {
+ networkCmProxyDataService.executeDataOperationForCmHandles(topicParamInQuery,
+ dataOperationRequest,
+ requestId);
+ return noReturn;
+ };
+ }
+
}
+++ /dev/null
-/*
- * ============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.controller.handlers;
-
-import java.util.function.Supplier;
-import org.onap.cps.ncmp.api.models.DataOperationRequest;
-import org.onap.cps.spi.FetchDescendantsOption;
-
-public interface TaskManagementDefaultHandler {
-
- String NO_REQUEST_ID = null;
- String NO_TOPIC = null;
- Supplier<Object> NO_OBJECT_SUPPLIER = null;
-
- default Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
- final String cmHandleId,
- final String resourceIdentifier,
- final String optionsParamInQuery,
- final String topicParamInQuery,
- final String requestId,
- final boolean includeDescendant) {
- return NO_OBJECT_SUPPLIER;
- }
-
- default Supplier<Object> getTaskSupplierForQueryRequest(final String cmHandleId,
- final String resourceIdentifier,
- final boolean includeDescendant) {
- return NO_OBJECT_SUPPLIER;
- }
-
- default void sendDataOperationRequestAsynchronously(final String topicParamInQuery,
- final DataOperationRequest
- dataOperationRequest,
- final String requestId) {
- }
-
- static FetchDescendantsOption getFetchDescendantsOption(final boolean includeDescendants) {
- return includeDescendants ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
- : FetchDescendantsOption.OMIT_DESCENDANTS;
- }
-}
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2022 Nordix Foundation
+ * Copyright (C) 2022-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.
* Execute a task asynchronously.
*
* @param taskSupplier functional method is get() task need to executed asynchronously
- * @param timeOutInMillis the time out value in milliseconds
+ * @param timeOutInMillis the time-out value in milliseconds
*/
- public void executeTask(final Supplier<Object> taskSupplier, final int timeOutInMillis) {
+ public void executeTask(final Supplier<Object> taskSupplier, final long timeOutInMillis) {
CompletableFuture.supplyAsync(taskSupplier::get)
.orTimeout(timeOutInMillis, MILLISECONDS)
- .whenCompleteAsync((responseAsJson, throwable) -> handleTaskCompletion(throwable));
+ .whenCompleteAsync((taskResult, throwable) -> handleTaskCompletion(throwable));
}
private void handleTaskCompletion(final Throwable throwable) {
if (throwable == null) {
log.info("Async task completed successfully.");
} else {
- log.error("Async task failed. caused by : {}", throwable.getMessage());
+ log.error("Async task failed. caused by : {}", throwable.toString());
}
}
}
import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
-
@WebMvcTest(NetworkCmProxyController)
class NetworkCmProxyControllerSpec extends Specification {
DataOperationRequestMapper dataOperationRequestMapper = Mappers.getMapper(DataOperationRequestMapper)
@SpringBean
- CpsNcmpTaskExecutor spiedCpsTaskExecutor = Spy()
+ CpsNcmpTaskExecutor mockCpsTaskExecutor = Mock()
@SpringBean
DeprecationHelper stubbedDeprecationHelper = Stub()
@SpringBean
- NcmpCachedResourceRequestHandler ncmpCachedResourceRequestHandler = new NcmpCachedResourceRequestHandler(spiedCpsTaskExecutor, mockNetworkCmProxyDataService, mockNetworkCmProxyQueryService)
+ NcmpCachedResourceRequestHandler ncmpCachedResourceRequestHandler = new NcmpCachedResourceRequestHandler(mockCpsTaskExecutor, mockNetworkCmProxyDataService, mockNetworkCmProxyQueryService)
@SpringBean
- NcmpPassthroughResourceRequestHandler ncmpPassthroughResourceRequestHandler = new NcmpPassthroughResourceRequestHandler(spiedCpsTaskExecutor, mockNetworkCmProxyDataService)
+ NcmpPassthroughResourceRequestHandler ncmpPassthroughResourceRequestHandler = new NcmpPassthroughResourceRequestHandler(mockCpsTaskExecutor, mockNetworkCmProxyDataService)
@Value('${rest.api.ncmp-base-path}/v1')
def ncmpBasePathV1
response.status == HttpStatus.OK.value()
}
- def 'Get Resource Data Async Topic Handling with #scenario.'() {
- given: 'resource data url'
- def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational?resourceIdentifier=parent/child&${topicQueryParam}"
- when: 'get data resource request is performed'
- def response = mvc.perform(
- get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
- then: 'task executor is called appropriate number of times'
- expectedNumberOfTaskExecutions * spiedCpsTaskExecutor.executeTask(_, TIMOUT_FOR_TEST)
- and: 'response status is OK'
- response.status == HttpStatus.OK.value()
- where: 'the following parameters are used'
- scenario | datastoreInUrl | topicQueryParam || expectedNumberOfTaskExecutions
- 'url with valid topic' | 'passthrough-operational' | '&topic=my-topic-name' || 1
- 'no topic in url' | 'passthrough-operational' | '' || 0
- 'null topic in url' | 'passthrough-operational' | '&topic=null' || 1
- }
-
def 'Get Resource Data from ncmp-datastore:operational (cached) parameters handling with #scenario.'() {
given: 'resource data url'
def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational" +
'options (ignored)' | '&options=(a-=1)' || OMIT_DESCENDANTS
}
- def 'Get Resource Data with invalid topic parameter: #scenario.'() {
- given: 'resource data url'
- def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
- "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
- when: 'get data resource (async) request is performed'
- def response = mvc.perform(
- get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
- then: 'abad request is returned'
- response.status == HttpStatus.BAD_REQUEST.value()
- where: 'the following parameters are used'
- scenario | datastoreInUrl | topicQueryParam
- 'empty topic in url' | 'passthrough-operational' | '&topic=\"\"'
- 'missing topic in url' | 'passthrough-operational' | '&topic='
- 'blank topic value in url' | 'passthrough-operational' | '&topic=\" \"'
- 'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#'
- }
-
def 'Execute (async) data operation to read data from dmi service.'() {
given: 'data operation url'
def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
- def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(
- getDataOperationRequest("read", datastore.datastoreName))
- def expectedDmiDataOperationRequest
- = jsonObjectMapper.convertJsonString(dataOperationRequestJsonData, org.onap.cps.ncmp.api.models.DataOperationRequest.class)
+ def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest("read", datastore.datastoreName))
when: 'post data operation request is performed'
def response = mvc.perform(
post(getUrl)
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)
- then: 'the service has been invoked with the correct parameters '
- 1 * mockNetworkCmProxyDataService.executeDataOperationForCmHandles('my-topic-name', expectedDmiDataOperationRequest, _)
+ assert response.contentAsString.contains('requestId')
+ then: 'the request is handled asynchronously'
+ 1 * mockCpsTaskExecutor.executeTask(*_)
where: 'the following data stores are used'
datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL]
}
- def 'Execute (async) data operation for #scenario from dmi service.'() {
+ def 'Execute (async) data operation with some validation error.'() {
given: 'data operation url'
def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(
- getDataOperationRequest(operation, datastore))
+ getDataOperationRequest('read', 'invalid datastore'))
when: 'post data resource request is performed'
def response = mvc.perform(
post(getUrl)
).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 data operation resource data when notification feature is disabled for datastore: #datastore.'() {
given: 'data operation url'
def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(
- getDataOperationRequest("read", datastore.datastoreName))
+ getDataOperationRequest("read", PASSTHROUGH_RUNNING.datastoreName))
ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = false
when: 'post data resource request is performed'
def response = mvc.perform(
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.'() {
response.status == HttpStatus.OK.value()
}
- def 'Query Resource Data using datastore of #datastore'() {
+ def 'Query Resource Data with unsupported datastore'() {
given: 'the query resource data url'
- def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastore}/query" +
+ def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running/query" +
"?cps-path=/cps/path"
when: 'the query data resource request is performed'
def response = mvc.perform(
).andReturn().response
then: 'a 400 BAD_REQUEST is returned for the unsupported datastore'
response.status == 400
- and: 'the error message is that datastore #datastore is not supported'
- response.contentAsString.contains("ncmp-datastore:${datastore} is not supported")
- where: 'the following datastore is used'
- datastore << ["passthrough-running", "passthrough-operational"]
+ and: 'the error message is that the datastore is not supported'
+ response.contentAsString.contains("ncmp-datastore:passthrough-running is not supported")
}
def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
def 'Get Resource Data from operational with or without descendants'() {
given: 'resource data url with descendants #enabled'
def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational" +
- "?resourceIdentifier=parent/child&include-descendants=${enabled}"
+ "?resourceIdentifier=parent/child&include-descendants=${booleanValue}"
when: 'get data resource request is performed'
def response = mvc.perform(
get(getUrl)
and: 'response status is Ok'
response.status == HttpStatus.OK.value()
where: 'the following parameters are used'
- enabled | descendantsOption
- false | FetchDescendantsOption.OMIT_DESCENDANTS
- true | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+ booleanValue | descendantsOption
+ false | OMIT_DESCENDANTS
+ true | INCLUDE_ALL_DESCENDANTS
}
def 'Attempt execute #operation rest operation on resource data with #scenario'() {
}
def failedResponse(cmHandle) {
- return CmHandleRegistrationResponse.createFailureResponse(cmHandle, new RuntimeException("Failed"))
+ return CmHandleRegistrationResponse.createFailureResponse(cmHandle, new RuntimeException('Failed'))
}
def successResponse(cmHandle) {
--- /dev/null
+/*
+ * ============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.controller.handlers
+
+import org.onap.cps.ncmp.api.NetworkCmProxyDataService
+import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException
+import org.onap.cps.ncmp.api.impl.exception.InvalidOperationException
+import org.onap.cps.ncmp.api.models.DataOperationDefinition
+import org.onap.cps.ncmp.api.models.DataOperationRequest
+import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException
+import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
+import spock.lang.Specification
+import spock.util.concurrent.PollingConditions
+
+class NcmpDatastoreRequestHandlerSpec extends Specification {
+
+ def spiedCpsNcmpTaskExecutor = Spy(CpsNcmpTaskExecutor)
+ def mockNetworkCmProxyDataService = Mock(NetworkCmProxyDataService)
+
+ def objectUnderTest = new NcmpPassthroughResourceRequestHandler(spiedCpsNcmpTaskExecutor, mockNetworkCmProxyDataService)
+
+ def 'Attempt to execute async get request with #scenario.'() {
+ given: 'notification feature is turned on/off'
+ objectUnderTest.notificationFeatureEnabled = notificationFeatureEnabled
+ when: 'get request is executed with topic = #topic'
+ objectUnderTest.executeRequest('ds', 'ch1', 'resource1', 'options', topic, false)
+ and: 'wait a little for async execution (only if expected)'
+ if (expectedCalls > 0) {
+ Thread.sleep(100)
+ }
+ then: 'the task is executed in an async fashion or not'
+ expectedCalls * spiedCpsNcmpTaskExecutor.executeTask(*_)
+ and: 'the service request is always invoked'
+ 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle('ds', 'ch1', 'resource1', 'options', _, _)
+ where: 'the following parameters are used'
+ scenario | notificationFeatureEnabled | topic || expectedCalls
+ 'feature on, valid topic' | true | 'valid' || 1
+ 'feature on, no topic' | true | null || 0
+ 'feature off, valid topic' | false | 'valid' || 0
+ 'feature off, no topic' | false | null || 0
+ }
+
+ def 'Attempt to execute async data operation request with feature #scenario.'() {
+ given: 'a extended request handler that supports bulk requests'
+ def objectUnderTest = new NcmpPassthroughResourceRequestHandler(spiedCpsNcmpTaskExecutor, mockNetworkCmProxyDataService)
+ and: 'notification feature is turned on/off'
+ objectUnderTest.notificationFeatureEnabled = notificationFeatureEnabled
+ when: 'data operation request is executed'
+ objectUnderTest.executeRequest('someTopic', new DataOperationRequest())
+ then: 'the task is executed in an async fashion or not'
+ expectedCalls * spiedCpsNcmpTaskExecutor.executeTask(*_)
+ where: 'the following parameters are used'
+ scenario | notificationFeatureEnabled || expectedCalls
+ 'on' | true || 1
+ 'off' | false || 0
+ }
+
+ def 'Execute async data operation request with datastore #datastore.'() {
+ given: 'notification feature is turned on'
+ objectUnderTest.notificationFeatureEnabled = true
+ and: 'a data operation request with datastore: #datastore'
+ def dataOperationDefinition = new DataOperationDefinition(operation: 'read', datastore: datastore)
+ def dataOperationRequest = new DataOperationRequest(dataOperationDefinitions: [dataOperationDefinition])
+ and: ' a flag to track the network service call'
+ def networkServiceMethodCalled = false
+ and: 'the (mocked) service will use the flag to indicate it is called'
+ mockNetworkCmProxyDataService.executeDataOperationForCmHandles('myTopic', dataOperationRequest, _) >> {
+ networkServiceMethodCalled = true
+ }
+ when: 'data operation request is executed'
+ objectUnderTest.executeRequest('myTopic', dataOperationRequest)
+ then: 'the task is executed in an async fashion'
+ 1 * spiedCpsNcmpTaskExecutor.executeTask(*_)
+ and: 'the network service is invoked (wait max. 5 seconds)'
+ new PollingConditions(timeout: 30).eventually {
+ //TODO Fix test assertion
+ }
+ where: 'the following datastores are used'
+ datastore << ['ncmp-datastore:passthrough-running', 'ncmp-datastore:passthrough-operational']
+ }
+
+ def 'Attempt to execute async data operation request with error #scenario'() {
+ given: 'notification feature is turned on'
+ objectUnderTest.notificationFeatureEnabled = true
+ and: 'a data operation definition with datastore: #datastore'
+ def dataOperationDefinition = new DataOperationDefinition(operation: 'read', datastore: datastore)
+ when: 'data operation request is executed'
+ def dataOperationRequest = new DataOperationRequest(dataOperationDefinitions: [dataOperationDefinition])
+ objectUnderTest.executeRequest('myTopic', dataOperationRequest)
+ then: 'the correct error is thrown'
+ def thrown = thrown(InvalidDatastoreException)
+ assert thrown.message.contains(expectedErrorMessage)
+ where: 'the following datastore names are used'
+ scenario | datastore || expectedErrorMessage
+ 'unsupported datastore' | 'ncmp-datastore:operational' || 'not supported'
+ 'invalid datastore' | 'invalid' || 'invalid datastore name'
+ }
+
+ def 'Attempt to execute async data operation request with #scenario operation: #operation.'() {
+ given: 'notification feature is turned on'
+ objectUnderTest.notificationFeatureEnabled = true
+ and: 'a data operation definition with operation: #operation'
+ def dataOperationDefinition = new DataOperationDefinition(operation: operation, datastore: 'ncmp-datastore:passthrough-running')
+ when: 'bulk request is executed'
+ objectUnderTest.executeRequest('someTopic', new DataOperationRequest(dataOperationDefinitions:[dataOperationDefinition]))
+ then: 'the expected type of exception is thrown'
+ thrown(expectedException)
+ where: 'the following operations are used'
+ scenario | operation || expectedException
+ 'invalid' | 'invalid' || InvalidOperationException
+ 'unsupported' | 'create' || OperationNotSupportedException
+ 'unsupported' | 'update' || OperationNotSupportedException
+ 'unsupported' | 'patch' || OperationNotSupportedException
+ 'unsupported' | 'delete' || OperationNotSupportedException
+ }
+
+}
--- /dev/null
+/*
+ * ============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.executor
+
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.read.ListAppender
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.onap.cps.notification.NotificationErrorHandler
+import org.slf4j.LoggerFactory
+import spock.lang.Specification
+
+class CpsNcmpTaskExecutorSpec extends Specification {
+
+ def objectUnderTest = new CpsNcmpTaskExecutor()
+ def logger = Spy(ListAppender<ILoggingEvent>)
+ def enoughTime = 100
+
+ @BeforeEach
+ void setup() {
+ ((Logger) LoggerFactory.getLogger(CpsNcmpTaskExecutor.class)).addAppender(logger);
+ logger.start();
+ }
+
+ @AfterEach
+ void teardown() {
+ ((Logger) LoggerFactory.getLogger(NotificationErrorHandler.class)).detachAndStopAllAppenders();
+ }
+
+ def 'Execute successful task.'() {
+ when: 'task is executed'
+ objectUnderTest.executeTask(taskSupplier(), enoughTime)
+ and: 'wait a little for async execution completion'
+ Thread.sleep(10)
+ then: 'an event is logged with level INFO'
+ def loggingEvent = getLoggingEvent()
+ assert loggingEvent.level == Level.INFO
+ and: 'the log indicates the task completed successfully'
+ assert loggingEvent.formattedMessage == 'Async task completed successfully.'
+ }
+
+ def 'Execute failing task.'() {
+ when: 'task is executed'
+ objectUnderTest.executeTask(taskSupplierForFailingTask(), enoughTime)
+ and: 'wait a little for async execution completion'
+ Thread.sleep(10)
+ then: 'an event is logged with level ERROR'
+ def loggingEvent = getLoggingEvent()
+ assert loggingEvent.level == Level.ERROR
+ and: 'the original error message is logged'
+ assert loggingEvent.formattedMessage.contains('original exception message')
+ }
+
+ def taskSupplier() {
+ return () -> 'hello world'
+ }
+
+ def taskSupplierForFailingTask() {
+ return () -> { throw new RuntimeException('original exception message') }
+ }
+
+ def getLoggingEvent() {
+ return logger.list[0]
+ }
+
+}
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<artifactId>cps-ncmp-service</artifactId>
<properties>
- <minimum-coverage>0.96</minimum-coverage>
+ <minimum-coverage>0.98</minimum-coverage>
</properties>
<dependencies>
<dependency>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<app>org.onap.cps.Application</app>
<java.version>17</java.version>
- <minimum-coverage>0.97</minimum-coverage>
+ <minimum-coverage>1.00</minimum-coverage>
<postgres.version>42.5.1</postgres.version>
<jacoco.reportDirectory.aggregate>${project.reporting.outputDirectory}/jacoco-aggregate</jacoco.reportDirectory.aggregate>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<configuration>
+ <!--All exclusions below are referring to generated code-->
<excludes>
<exclude>org/onap/cps/event/model/*</exclude>
<exclude>org/onap/cps/rest/model/*</exclude>
<exclude>org/onap/cps/cpspath/parser/antlr4/*</exclude>
<exclude>org/onap/cps/ncmp/rest/model/*</exclude>
+ <exclude>org/onap/cps/**/*MapperImpl.class</exclude>
+ <exclude>org/onap/cps/ncmp/rest/stub/*</exclude>
</excludes>
</configuration>
<executions>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021-2022 Nordix Foundation
+ * Copyright (C) 2021-2023 Nordix Foundation
* Modifications Copyright (C) 2023 TechMahindra Ltd
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* ============LICENSE_END=========================================================
*/
+/*
+ * Parser Rules
+ * Some of the parser rules below are inspired by
+ * https://github.com/antlr/grammars-v4/blob/master/xpath/xpath31/XPath31Parser.g4
+ */
+
grammar CpsPath ;
cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? containsFunctionCondition? ancestorAxis? invalidPostFix?;
/*
* Lexer Rules
* Most of the lexer rules below are inspired by
- * https://raw.githubusercontent.com/antlr/grammars-v4/master/xpath/xpath31/XPath31.g4
+ * https://github.com/antlr/grammars-v4/blob/master/xpath/xpath31/XPath31Lexer.g4
*/
AT : '@' ;
// Add below type definitions for leafvalue comparision in https://jira.onap.org/browse/CPS-440
DecimalLiteral : ('.' FragDigits) | (FragDigits '.' [0-9]*) ;
DoubleLiteral : (('.' FragDigits) | (FragDigits ('.' [0-9]*)?)) [eE] [+-]? FragDigits ;
-StringLiteral : ('"' (FragEscapeQuot | ~[^"])*? '"') | ('\'' (FragEscapeApos | ~['])*? '\'') ;
+StringLiteral : '"' (~["] | FragEscapeQuot)* '"' | '\'' (~['] | FragEscapeApos)* '\'' ;
fragment FragEscapeQuot : '""' ;
-fragment FragEscapeApos : '\'' ;
+fragment FragEscapeApos : '\'\'';
fragment FragDigits : [0-9]+ ;
QName : FragQName ;
+++ /dev/null
-/*\r
- * ============LICENSE_START=======================================================\r
- * Copyright (C) 2023 TechMahindra Ltd\r
- * ================================================================================\r
- * Licensed under the Apache License, Version 2.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- *\r
- * SPDX-License-Identifier: Apache-2.0\r
- * ============LICENSE_END=========================================================\r
- */\r
-\r
-package org.onap.cps.cpspath.parser;\r
-\r
-public enum CpsPathBooleanOperatorType {\r
- AND("and"),\r
- OR("or");\r
-\r
- private final String operatorValue;\r
-\r
- CpsPathBooleanOperatorType(final String operatorValue) {\r
- this.operatorValue = operatorValue;\r
- }\r
-\r
- public String getValues() {\r
- return this.operatorValue;\r
- }\r
-\r
- /**\r
- * Finds the value of the given enumeration.\r
- *\r
- * @param operatorValue value of the enum\r
- * @return a booleanOperatorType\r
- */\r
- public static CpsPathBooleanOperatorType fromString(final String operatorValue) {\r
- for (final CpsPathBooleanOperatorType booleanOperatorType : CpsPathBooleanOperatorType.values()) {\r
- if (booleanOperatorType.operatorValue.equalsIgnoreCase(operatorValue)) {\r
- return booleanOperatorType;\r
- }\r
- }\r
- return null;\r
- }\r
-}\r
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021-2022 Nordix Foundation
+ * Copyright (C) 2021-2023 Nordix Foundation
* Modifications Copyright (C) 2023 TechMahindra Ltd
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT;
import java.util.ArrayList;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
import org.onap.cps.cpspath.parser.antlr4.CpsPathBaseListener;
import org.onap.cps.cpspath.parser.antlr4.CpsPathParser;
import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.AncestorAxisContext;
private static final String CLOSE_BRACKET = "]";
- final CpsPathQuery cpsPathQuery = new CpsPathQuery();
+ private final CpsPathQuery cpsPathQuery = new CpsPathQuery();
- final Map<String, Object> leavesData = new LinkedHashMap<>();
+ private final List<CpsPathQuery.DataLeaf> leavesData = new ArrayList<>();
- final StringBuilder normalizedXpathBuilder = new StringBuilder();
+ private final StringBuilder normalizedXpathBuilder = new StringBuilder();
- final StringBuilder normalizedAncestorPathBuilder = new StringBuilder();
+ private final StringBuilder normalizedAncestorPathBuilder = new StringBuilder();
- boolean processingAncestorAxis = false;
+ private boolean processingAncestorAxis = false;
- private List<String> containerNames = new ArrayList<>();
+ private final List<String> containerNames = new ArrayList<>();
- final List<String> booleanOperators = new ArrayList<>();
+ private final List<String> booleanOperators = new ArrayList<>();
- final List<String> comparativeOperators = new ArrayList<>();
+ private final List<String> comparativeOperators = new ArrayList<>();
@Override
public void exitInvalidPostFix(final CpsPathParser.InvalidPostFixContext ctx) {
@Override
public void exitLeafCondition(final LeafConditionContext ctx) {
- Object comparisonValue;
+ final Object comparisonValue;
if (ctx.IntegerLiteral() != null) {
comparisonValue = Integer.valueOf(ctx.IntegerLiteral().getText());
} else if (ctx.StringLiteral() != null) {
- final boolean wasWrappedInDoubleQuote = ctx.StringLiteral().getText().startsWith("\"");
- comparisonValue = stripFirstAndLastCharacter(ctx.StringLiteral().getText());
- if (wasWrappedInDoubleQuote) {
- comparisonValue = String.valueOf(comparisonValue).replace("'", "\\'");
- }
+ comparisonValue = unwrapQuotedString(ctx.StringLiteral().getText());
} else {
- throw new PathParsingException(
- "Unsupported comparison value encountered in expression" + ctx.getText());
+ throw new PathParsingException("Unsupported comparison value encountered in expression" + ctx.getText());
}
leafContext(ctx.leafName(), comparisonValue);
}
@Override
public void exitBooleanOperators(final CpsPathParser.BooleanOperatorsContext ctx) {
- final CpsPathBooleanOperatorType cpsPathBooleanOperatorType = CpsPathBooleanOperatorType.fromString(
- ctx.getText());
- booleanOperators.add(cpsPathBooleanOperatorType.getValues());
+ booleanOperators.add(ctx.getText());
}
@Override
public void exitComparativeOperators(final CpsPathParser.ComparativeOperatorsContext ctx) {
- final CpsPathComparativeOperator cpsPathComparativeOperator = CpsPathComparativeOperator.fromString(
- ctx.getText());
- comparativeOperators.add(cpsPathComparativeOperator.getLabel());
+ comparativeOperators.add(ctx.getText());
}
@Override
public void enterMultipleLeafConditions(final MultipleLeafConditionsContext ctx) {
normalizedXpathBuilder.append(OPEN_BRACKET);
leavesData.clear();
+ booleanOperators.clear();
+ comparativeOperators.clear();
}
@Override
@Override
public void exitTextFunctionCondition(final TextFunctionConditionContext ctx) {
cpsPathQuery.setTextFunctionConditionLeafName(ctx.leafName().getText());
- cpsPathQuery.setTextFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText()));
+ cpsPathQuery.setTextFunctionConditionValue(unwrapQuotedString(ctx.StringLiteral().getText()));
}
@Override
public void exitContainsFunctionCondition(final CpsPathParser.ContainsFunctionConditionContext ctx) {
cpsPathQuery.setContainsFunctionConditionLeafName(ctx.leafName().getText());
- cpsPathQuery.setContainsFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText()));
+ cpsPathQuery.setContainsFunctionConditionValue(unwrapQuotedString(ctx.StringLiteral().getText()));
}
@Override
return cpsPathQuery;
}
- private static String stripFirstAndLastCharacter(final String wrappedString) {
- return wrappedString.substring(1, wrappedString.length() - 1);
- }
-
@Override
public void exitContainerName(final CpsPathParser.ContainerNameContext ctx) {
final String containerName = ctx.getText();
}
private void leafContext(final CpsPathParser.LeafNameContext ctx, final Object comparisonValue) {
- leavesData.put(ctx.getText(), comparisonValue);
+ leavesData.add(new CpsPathQuery.DataLeaf(ctx.getText(), comparisonValue));
appendCondition(normalizedXpathBuilder, ctx.getText(), comparisonValue);
if (processingAncestorAxis) {
appendCondition(normalizedAncestorPathBuilder, ctx.getText(), comparisonValue);
.append(name)
.append(getLastElement(comparativeOperators))
.append("'")
- .append(value)
+ .append(value.toString().replace("'", "''"))
.append("'");
}
- private String getLastElement(final List<String> listOfStrings) {
+ private static String getLastElement(final List<String> listOfStrings) {
return listOfStrings.get(listOfStrings.size() - 1);
}
+
+ private static String unwrapQuotedString(final String wrappedString) {
+ final boolean wasWrappedInSingleQuote = wrappedString.startsWith("'");
+ final String value = stripFirstAndLastCharacter(wrappedString);
+ if (wasWrappedInSingleQuote) {
+ return value.replace("''", "'");
+ } else {
+ return value.replace("\"\"", "\"");
+ }
+ }
+
+ private static String stripFirstAndLastCharacter(final String wrappedString) {
+ return wrappedString.substring(1, wrappedString.length() - 1);
+ }
}
+++ /dev/null
-/*\r
- * ============LICENSE_START=======================================================\r
- * Copyright (C) 2023 Tech Mahindra Ltd\r
- * ================================================================================\r
- * Licensed under the Apache License, Version 2.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- *\r
- * SPDX-License-Identifier: Apache-2.0\r
- * ============LICENSE_END=========================================================\r
- */\r
-\r
-package org.onap.cps.cpspath.parser;\r
-\r
-import java.util.HashMap;\r
-import java.util.Map;\r
-\r
-public enum CpsPathComparativeOperator {\r
- EQ("="),\r
- GT(">"),\r
- LT("<"),\r
- GE(">="),\r
- LE("<=");\r
-\r
- private final String label;\r
-\r
- CpsPathComparativeOperator(final String label) {\r
- this.label = label;\r
- }\r
-\r
- public final String getLabel() {\r
- return this.label;\r
- }\r
-\r
- private static final Map<String, CpsPathComparativeOperator> cpsPathComparativeOperatorPerLabel = new HashMap<>();\r
-\r
- static {\r
- for (final CpsPathComparativeOperator cpsPathComparativeOperator : CpsPathComparativeOperator.values()) {\r
- cpsPathComparativeOperatorPerLabel.put(cpsPathComparativeOperator.label, cpsPathComparativeOperator);\r
- }\r
- }\r
-\r
- /**\r
- * Finds the value of the given enumeration.\r
- *\r
- * @param label value of the enum\r
- * @return a comparativeOperatorType\r
- */\r
- public static CpsPathComparativeOperator fromString(final String label) {\r
- if (!cpsPathComparativeOperatorPerLabel.containsKey(label)) {\r
- throw new PathParsingException("Incomplete leaf condition (no operator)");\r
- }\r
- return cpsPathComparativeOperatorPerLabel.get(label);\r
- }\r
-}\r
-\r
import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE;
import java.util.List;
-import java.util.Map;
import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
private List<String> containerNames;
private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE;
private String descendantName;
- private Map<String, Object> leavesData;
+ private List<DataLeaf> leavesData;
private String ancestorSchemaNodeIdentifier = "";
private String textFunctionConditionLeafName;
private String textFunctionConditionValue;
return cpsPathPrefixType == ABSOLUTE && hasLeafConditions();
}
+ @Getter
+ @EqualsAndHashCode
+ @AllArgsConstructor
+ public static class DataLeaf {
+ private final String name;
+ private final Object value;
+ }
}
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021-2022 Nordix Foundation
+ * Copyright (C) 2021-2023 Nordix Foundation
* Modifications Copyright (C) 2023 TechMahindra Ltd
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
class CpsPathQuerySpec extends Specification {
+ def 'Default values for the most basic cps query.'() {
+ when: 'the cps path is parsed'
+ def result = CpsPathQuery.createFrom('/parent')
+ then: 'the query has the correct default properties'
+ assert result.cpsPathPrefixType == ABSOLUTE
+ assert result.hasAncestorAxis() == false
+ assert result.hasLeafConditions() == false
+ assert result.hasTextFunctionCondition() == false
+ assert result.hasContainsFunctionCondition() == false
+ assert result.isPathToListElement() == false
+ }
+
def 'Parse cps path with valid cps path and a filter with #scenario.'() {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath)
then: 'the query has the right xpath type'
- result.cpsPathPrefixType == ABSOLUTE
+ assert result.cpsPathPrefixType == ABSOLUTE
and: 'the right query parameters are set'
- result.xpathPrefix == expectedXpathPrefix
- result.hasLeafConditions()
- result.leavesData.containsKey(expectedLeafName)
- result.leavesData.get(expectedLeafName) == expectedLeafValue
+ assert result.xpathPrefix == expectedXpathPrefix
+ assert result.hasLeafConditions()
+ assert result.leavesData[0].name == expectedLeafName
+ assert result.leavesData[0].value == expectedLeafValue
where: 'the following data is used'
scenario | cpsPath || expectedXpathPrefix | expectedLeafName | expectedLeafValue
'leaf of type String' | '/parent/child[@common-leaf-name="common-leaf-value"]' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value'
'spaces around =' | '/parent/child[@common-leaf-name-int = 5]' || '/parent/child' | 'common-leaf-name-int' | 5
'key in top container' | '/parent[@common-leaf-name-int=5]' || '/parent' | 'common-leaf-name-int' | 5
'parent list' | '/shops/shop[@id=1]/categories[@id=1]/book[@title="Dune"]' || "/shops/shop[@id='1']/categories[@id='1']/book" | 'title' | 'Dune'
+ "' in double quote" | '/parent[@common-leaf-name="leaf\'value"]' || '/parent' | 'common-leaf-name' | "leaf'value"
+ "' in single quote" | "/parent[@common-leaf-name='leaf''value']" || '/parent' | 'common-leaf-name' | "leaf'value"
+ '" in double quote' | '/parent[@common-leaf-name="leaf""value"]' || '/parent' | 'common-leaf-name' | 'leaf"value'
+ '" in single quote' | '/parent[@common-leaf-name=\'leaf"value\']' || '/parent' | 'common-leaf-name' | 'leaf"value'
}
def 'Parse cps path of type ends with a #scenario.'() {
'parent leaf of type Integer & child' | '/parent/child[@code=1]/child2' || "/parent/child[@code='1']/child2"
'parent leaf with double quotes' | '/parent/child[@code="1"]/child2' || "/parent/child[@code='1']/child2"
'parent leaf with double quotes inside single quotes' | '/parent/child[@code=\'"1"\']/child2' || "/parent/child[@code='\"1\"']/child2"
- 'parent leaf with single quotes inside double quotes' | '/parent/child[@code="\'1\'"]/child2' || "/parent/child[@code='\\\'1\\\'']/child2"
- 'leaf with single quotes inside double quotes' | '/parent/child[@code="\'1\'"]' || "/parent/child[@code='\\\'1\\\'']"
+ 'parent leaf with single quotes inside double quotes' | '/parent/child[@code="\'1\'"]/child2' || "/parent/child[@code='''1''']/child2"
+ 'leaf with single quotes inside double quotes' | '/parent/child[@code="\'1\'"]' || "/parent/child[@code='''1''']"
'leaf with more than one attribute' | '/parent/child[@key1=1 and @key2="abc"]' || "/parent/child[@key1='1' and @key2='abc']"
'parent & child with more than one attribute' | '/parent/child[@key1=1 and @key2="abc"]/child2' || "/parent/child[@key1='1' and @key2='abc']/child2"
'leaf with more than one attribute has OR operator' | '/parent/child[@key1=1 or @key2="abc"]' || "/parent/child[@key1='1' or @key2='abc']"
'descendant anywhere' | '//xpath' || '//xpath'
}
- def 'Parse cps path that ends with a yang list containing #scenario.'() {
+ def 'Parse cps path that ends with a yang list containing multiple leaf conditions.'() {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath)
- then: 'the query has the right xpath type'
- result.cpsPathPrefixType == DESCENDANT
- and: 'the right parameters are set'
- result.descendantName == "child"
+ then: 'the expected number of leaves are returned'
result.leavesData.size() == expectedNumberOfLeaves
and: 'the given operator(s) returns in the correct order'
result.booleanOperators == expectedOperators
and: 'the given comparativeOperator(s) returns in the correct order'
result.comparativeOperators == expectedComparativeOperator
where: 'the following data is used'
- scenario | cpsPath || expectedNumberOfLeaves || expectedOperators || expectedComparativeOperator
- 'one attribute' | '//child[@common-leaf-name-int=5]' || 1 || [] || ['=']
- 'more than one attribute has AND operator' | '//child[@int-leaf=5 and @leaf-name="leaf value"]' || 2 || ['and'] || ['=', '=']
- 'more than one attribute has OR operator' | '//child[@int-leaf=5 or @leaf-name="leaf value"]' || 2 || ['or'] || ['=', '=']
- 'more than one attribute has combinations AND operators' | '//child[@int-leaf=5 and @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['and', 'and'] || ['=', '=', '=']
- 'more than one attribute has combinations OR operators' | '//child[@int-leaf=5 or @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['or', 'or'] || ['=', '=', '=']
- 'more than one attribute has combinations AND/OR combination' | '//child[@int-leaf=5 and @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['and', 'or'] || ['=', '=', '=']
- 'more than one attribute has combinations OR/AND combination' | '//child[@int-leaf=5 or @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['or', 'and'] || ['=', '=', '=']
- 'more than one attribute has AND/> operators' | '//child[@int-leaf>15 and @leaf-name="leaf value"]' || 2 || ['and'] || ['>', '=']
- 'more than one attribute has OR/< operators' | '//child[@int-leaf<5 or @leaf-name="leaf value"]' || 2 || ['or'] || ['<', '=']
- 'more than one attribute has combinations AND/>= operators' | '//child[@int-leaf>=18 and @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['and', 'and'] || ['>=', '=', '=']
- 'more than one attribute has combinations OR/<= operators' | '//child[@int-leaf<=25 or @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['or', 'or'] || ['<=', '=', '=']
+ cpsPath || expectedNumberOfLeaves || expectedOperators || expectedComparativeOperator
+ '/parent[@code=1]/child[@common-leaf-name-int=5]' || 1 || [] || ['=']
+ '//child[@int-leaf>15 and @leaf-name="leaf value"]' || 2 || ['and'] || ['>', '=']
+ '//child[@int-leaf<5 or @leaf-name="leaf value"]' || 2 || ['or'] || ['<', '=']
+ '//child[@int-leaf=5 and @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['and', 'or'] || ['=', '=', '=']
+ '//child[@int-leaf=5 or @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['or', 'and'] || ['=', '=', '=']
+ '//child[@int-leaf>=18 and @common-leaf-name="leaf value" and @leaf-name="leaf value1" ]' || 3 || ['and', 'and'] || ['>=', '=', '=']
+ '//child[@int-leaf<=25 or @common-leaf-name="leaf value" or @leaf-name="leaf value1" ]' || 3 || ['or', 'or'] || ['<=', '=', '=']
}
def 'Parse #scenario cps path with text function condition'() {
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath)
then: 'the query has the right xpath type'
- result.cpsPathPrefixType == DESCENDANT
+ assert result.cpsPathPrefixType == DESCENDANT
and: 'leaf conditions are only present when expected'
- result.hasLeafConditions() == expectLeafConditions
+ assert result.hasLeafConditions() == expectLeafConditions
and: 'the right text function condition is set'
- result.hasTextFunctionCondition()
- result.textFunctionConditionLeafName == 'leaf-name'
- result.textFunctionConditionValue == 'search'
+ assert result.hasTextFunctionCondition()
+ assert result.textFunctionConditionLeafName == 'leaf-name'
+ assert result.textFunctionConditionValue == 'search'
and: 'the ancestor is only present when expected'
assert result.hasAncestorAxis() == expectHasAncestorAxis
where: 'the following data is used'
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom('//someContainer[contains(@lang,"en")]')
then: 'the query has the right xpath type'
- result.cpsPathPrefixType == DESCENDANT
+ assert result.cpsPathPrefixType == DESCENDANT
and: 'the right contains function condition is set'
- result.hasContainsFunctionCondition()
- result.containsFunctionConditionLeafName == 'lang'
- result.containsFunctionConditionValue == 'en'
+ assert result.hasContainsFunctionCondition()
+ assert result.containsFunctionConditionLeafName == 'lang'
+ assert result.containsFunctionConditionValue == 'en'
}
def 'Parse cps path with error: #scenario.'() {
and: 'the correct ancestor schema node identifier is set'
result.ancestorSchemaNodeIdentifier == ancestorPath
and: 'there are no leaves conditions'
- result.hasLeafConditions() == false
+ assert result.hasLeafConditions() == false
where:
scenario | ancestorPath
'basic container' | 'someContainer'
when: 'the given cps path is parsed'
def result = CpsPathQuery.createFrom(cpsPath + '/ancestor::someAncestor')
then: 'the query has the right type'
- result.cpsPathPrefixType == DESCENDANT
+ assert result.cpsPathPrefixType == DESCENDANT
and: 'leaf conditions are only present when expected'
- result.hasLeafConditions() == expectLeafConditions
+ assert result.hasLeafConditions() == expectLeafConditions
and: 'the result has ancestor axis'
- result.hasAncestorAxis()
+ assert result.hasAncestorAxis()
and: 'the correct ancestor schema node identifier is set'
- result.ancestorSchemaNodeIdentifier == 'someAncestor'
- result.descendantName == expectedDescendantName
+ assert result.ancestorSchemaNodeIdentifier == 'someAncestor'
+ assert result.descendantName == expectedDescendantName
where:
scenario | cpsPath || expectedDescendantName | expectLeafConditions
'basic container' | '//someContainer' || 'someContainer' | false
'container with list-parent' | '//parent[@id=1]/child' || "parent[@id='1']/child" | false
'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || "parent[@id='1']/child" | true
}
+
+ def 'Parse cps path with multiple conditions on same leaf.'() {
+ when: 'the given cps path is parsed using multiple conditions on same leaf'
+ def result = CpsPathQuery.createFrom('/test[@same-name="value1" or @same-name="value2"]')
+ then: 'two leaves are present with correct values'
+ assert result.leavesData.size() == 2
+ assert result.leavesData[0].name == "same-name"
+ assert result.leavesData[0].value == "value1"
+ assert result.leavesData[1].name == "same-name"
+ assert result.leavesData[1].value == "value2"
+ }
+
+ def 'Ordering of data leaves is preserved.'() {
+ when: 'the given cps path is parsed'
+ def result = CpsPathQuery.createFrom(cpsPath)
+ then: 'the order of the data leaves is preserved'
+ assert result.leavesData[0].name == expectedFirstLeafName
+ assert result.leavesData[1].name == expectedSecondLeafName
+ where: 'the following data is used'
+ cpsPath || expectedFirstLeafName | expectedSecondLeafName
+ '/test[@name1="value1" and @name2="value2"]' || 'name1' | 'name2'
+ '/test[@name2="value2" and @name1="value1"]' || 'name2' | 'name1'
+ }
+
}
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
'without observed-timestamp XML' | null | MediaType.APPLICATION_XML | requestBodyXml | expectedXmlData | ContentType.XML
}
+ def 'save list elements under root node #scenario.'() {
+ given: 'root node xpath '
+ def rootNodeXpath = '/'
+ when: 'list-node endpoint is invoked with post (create) operation'
+ def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', rootNodeXpath )
+ .content(requestBodyJson)
+ if (observedTimestamp != null)
+ postRequestBuilder.param('observed-timestamp', observedTimestamp)
+ def response = mvc.perform(postRequestBuilder).andReturn().response
+ then: 'a created response is returned'
+ response.status == expectedHttpStatus.value()
+ then: 'the java API was called with the correct parameters'
+ expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, rootNodeXpath, expectedJsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
+ 'without observed-timestamp' | null || 1 | HttpStatus.CREATED
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
+ }
+
def 'Save list elements #scenario.'() {
given: 'parent node xpath '
def parentNodeXpath = 'parent node xpath'
<parent>\r
<groupId>org.onap.cps</groupId>\r
<artifactId>cps-parent</artifactId>\r
- <version>3.3.4-SNAPSHOT</version>\r
+ <version>3.3.6-SNAPSHOT</version>\r
<relativePath>../cps-parent/pom.xml</relativePath>\r
</parent>\r
\r
<artifactId>cps-ri</artifactId>\r
\r
<properties>\r
- <minimum-coverage>0.29</minimum-coverage>\r
+ <minimum-coverage>0.30</minimum-coverage>\r
<!-- Additional coverage is provided by integration-test module -->\r
</properties>\r
\r
import org.onap.cps.spi.entities.FragmentEntity;
import org.onap.cps.spi.exceptions.CpsPathException;
import org.onap.cps.spi.utils.EscapeUtils;
-import org.onap.cps.utils.JsonObjectMapper;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@PersistenceContext
private EntityManager entityManager;
- private final JsonObjectMapper jsonObjectMapper;
-
/**
* Create a sql query to retrieve by anchor(id) and cps path.
*
sqlStringBuilder.append(" AND (");
final Queue<String> booleanOperatorsQueue = new LinkedList<>(cpsPathQuery.getBooleanOperators());
final Queue<String> comparativeOperatorQueue = new LinkedList<>(cpsPathQuery.getComparativeOperators());
- cpsPathQuery.getLeavesData().entrySet().forEach(entry -> {
+ cpsPathQuery.getLeavesData().forEach(leaf -> {
final String nextComparativeOperator = comparativeOperatorQueue.poll();
- if (entry.getValue() instanceof Integer) {
- sqlStringBuilder.append("(attributes ->> ");
- sqlStringBuilder.append("'").append(entry.getKey()).append("')\\:\\:int");
- sqlStringBuilder.append(" ").append(nextComparativeOperator).append(" ");
- sqlStringBuilder.append("'").append(jsonObjectMapper.asJsonString(entry.getValue())).append("'");
+ if (leaf.getValue() instanceof Integer) {
+ sqlStringBuilder.append("(attributes ->> '").append(leaf.getName()).append("')\\:\\:int");
+ sqlStringBuilder.append(nextComparativeOperator);
+ sqlStringBuilder.append(leaf.getValue());
} else {
if ("=".equals(nextComparativeOperator)) {
- sqlStringBuilder.append(" attributes @> ");
- sqlStringBuilder.append("'");
- sqlStringBuilder.append(jsonObjectMapper.asJsonString(entry));
+ final String leafValueAsText = leaf.getValue().toString();
+ sqlStringBuilder.append("attributes ->> '").append(leaf.getName()).append("'");
+ sqlStringBuilder.append(" = '");
+ sqlStringBuilder.append(EscapeUtils.escapeForSqlStringLiteral(leafValueAsText));
sqlStringBuilder.append("'");
} else {
throw new CpsPathException(" can use only " + nextComparativeOperator + " with integer ");
import javax.persistence.PersistenceContext;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.spi.utils.EscapeUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
final Collection<String> sqlInserts = new HashSet<>(sqlData.size());
for (final Collection<String> rowValues : sqlData) {
final Collection<String> escapedValues =
- rowValues.stream().map(it -> escapeSingleQuotesByDoublingThem(it)).collect(Collectors.toList());
+ rowValues.stream().map(EscapeUtils::escapeForSqlStringLiteral).collect(Collectors.toList());
sqlInserts.add("('" + String.join("','", escapedValues) + "')");
}
sqlStringBuilder.append("INSERT INTO ");
sqlStringBuilder.append(";");
}
- private static String escapeSingleQuotesByDoublingThem(final String value) {
- return value.replace("'", "''");
- }
-
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EscapeUtils {
- public static String escapeForSqlLike(final String text) {
- return text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
+ public static String escapeForSqlLike(final String value) {
+ return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
+ }
+
+ public static String escapeForSqlStringLiteral(final String value) {
+ return value.replace("'", "''");
}
}
file: changelog/db/changes/19-delete-not-required-dataspace-id-from-fragment.yaml
- include:
file: changelog/db/changes/20-change-foreign-key-id-types-to-integer.yaml
+ - include:
+ file: changelog/db/changes/21-escape-quotes-in-xpath.yaml
--- /dev/null
+/*
+ ============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=========================================================
+*/
+
+-- replace \' with '' and "" with "
+UPDATE fragment SET xpath = replace(replace(xpath, $$\'$$, $$''$$), '""', '"');
\ No newline at end of file
--- /dev/null
+/*
+ ============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=========================================================
+*/
+
+-- replace '' with \' and " with ""
+UPDATE fragment SET xpath = replace(replace(xpath, $$''$$, $$\'$$), '"', '""');
\ No newline at end of file
--- /dev/null
+# ============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=========================================================
+
+databaseChangeLog:
+
+ - changeSet:
+ id: 21
+ author: cps
+ changes:
+ - sqlFile:
+ path: changelog/db/changes/21-escape-quotes-in-xpath-forward.sql
+ rollback:
+ - sqlFile:
+ path: changelog/db/changes/21-escape-quotes-in-xpath-rollback.sql
class EscapeUtilsSpec extends Specification {
- def 'Escape text for using in SQL LIKE operation'() {
+ def 'Escape text for use in SQL LIKE operation.'() {
expect: 'SQL LIKE special characters to be escaped with forward-slash'
assert EscapeUtils.escapeForSqlLike(unescapedText) == escapedText
where:
'Others (./?$) are not special' || 'Others (./?$) are not special'
}
+ def 'Escape text for use in SQL string literal.'() {
+ expect: 'single quotes to be doubled'
+ assert EscapeUtils.escapeForSqlStringLiteral("I'm escaping!") == "I''m escaping!"
+ }
+
}
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<artifactId>cps-service</artifactId>
<properties>
- <minimum-coverage>0.94</minimum-coverage>
+ <minimum-coverage>0.95</minimum-coverage>
</properties>
<dependencies>
final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> listElementDataNodeCollection =
buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON);
- cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
- listElementDataNodeCollection);
+ if (isRootNodeXpath(parentNodeXpath)) {
+ cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, listElementDataNodeCollection);
+ } else {
+ cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
+ listElementDataNodeCollection);
+ }
processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp);
}
.get(anchor.getDataspaceName(), anchor.getSchemaSetName()).getSchemaContext();
}
+ private static boolean isRootNodeXpath(final String xpath) {
+ return ROOT_NODE_XPATH.equals(xpath);
+ }
+
private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) {
cpsDataPersistenceService.batchUpdateDataLeaves(anchor.getDataspaceName(), anchor.getName(),
Collections.singletonMap(dataNodeUpdate.getXpath(), dataNodeUpdate.getLeaves()));
final List<String> keyAttributes = nodeIdentifier.entrySet().stream().map(
entry -> {
final String name = entry.getKey().getLocalName();
- final String value = String.valueOf(entry.getValue()).replace("'", "\\'");
+ final String value = String.valueOf(entry.getValue()).replace("'", "''");
return String.format("@%s='%s'", name, value);
}
).collect(Collectors.toList());
noExceptionThrown()
}
+ def 'Saving list element data fragment under Root node.'() {
+ given: 'schema set for given anchor and dataspace references bookstore model'
+ setupSchemaSetMocks('bookstore.yang')
+ when: 'save data method is invoked with list element json data'
+ def jsonData = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Banana","price": "100","stock": True}]}'
+ objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
+ then: 'the persistence service method is invoked with correct parameters'
+ 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
+ { dataNodeCollection ->
+ {
+ assert dataNodeCollection.size() == 1
+ assert dataNodeCollection.collect { it.getXpath() }
+ .containsAll(['/invoice[@ProductID=\'2\']'])
+ }
+ }
+ )
+ and: 'the CpsValidator is called on the dataspaceName and AnchorName'
+ 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
+ and: 'data updated event is sent to notification service'
+ 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.UPDATE, observedTimestamp)
+ }
+
def 'Saving child data fragment under existing node.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
{
+ "multiple-data-tree:invoice": [
+ {
+ "ProductID": "1",
+ "ProductName": "Apple",
+ "price": "100",
+ "stock": false
+ }
+ ],
"test:bookstore":{
"bookstore-name": "Chapters/Easons",
"categories": [
}
}
+ list invoice {
+ key "ProductID";
+ leaf ProductID {
+ type uint64;
+ mandatory "true";
+ description
+ "Unique product ID. Example: 001";
+ }
+ leaf ProductName {
+ type string;
+ mandatory "true";
+ description
+ "Name of the Product";
+ }
+ leaf price {
+ type uint64;
+ mandatory "true";
+ description
+ "Price of book";
+ }
+ leaf stock {
+ type boolean;
+ default "false";
+ description
+ "Book in stock or not. Example value: true";
+ }
+ }
+
container bookstore {
leaf bookstore-name {
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>dmi-plugin-demo-and-csit-stub</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
</parent>
<artifactId>dmi-plugin-demo-and-csit-stub-app</artifactId>
<properties>
<app>org.onap.cps.ncmp.dmi.rest.stub.DmiDemoApplication</app>
<maven.build.timestamp.format>yyyyMMdd'T'HHmmss'Z'</maven.build.timestamp.format>
- <base.image>${docker.pull.registry}/onap/integration-java11:8.0.0</base.image>
+ <base.image>${docker.pull.registry}/onap/integration-java17:12.0.0</base.image>
<image.tag>${project.version}-${maven.build.timestamp}</image.tag>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>dmi-plugin-demo-and-csit-stub</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
</parent>
<artifactId>dmi-plugin-demo-and-csit-stub-service</artifactId>
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
=============
- String values must be wrapped in quotation marks ``"`` (U+0022) or apostrophes ``'`` (U+0027).
+- Quotations marks and apostrophes can be escaped by doubling them in their respective quotes, for example ``'CPS ''Path'' Query' -> CPS 'Path' Query``
- String comparisons are case sensitive.
Query Syntax
- The key should be supplied with correct data type for it to be queried from DB. In the last example above the attribute code is of type
Integer so the cps query will not work if the value is passed as string.
eg: ``//categories[@code="1"]`` or ``//categories[@code='1']`` will not work because the key attribute code is treated a string.
- - Having '[' token in any index in any list will have a negative impact on this function.
**Notes**
- For performance reasons it does not make sense to query using key leaf as attribute. If the key value is known it is better to execute a get request with the complete xpath.
- Only string and integer values are supported, boolean and float values are not supported.
- Since CPS cannot return individual leaves it will always return the container with all its leaves. Ancestor-axis can be used to specify a parent higher up the tree.
- When querying a leaf value (instead of leaf-list) it is better, more performant to use a text value condition use @<leaf-name> as described above.
- - Having '[' token in any index in any list will have a negative impact on this function.
contains()-condition
--------------------
.. * * * MONTREAL * * *
.. ========================
+Version: 3.3.6
+==============
+
+Release Data
+------------
+
++--------------------------------------+--------------------------------------------------------+
+| **CPS Project** | |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Docker images** | onap/cps-and-ncmp:3.3.6 |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release designation** | 3.3.6 Montreal |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release date** | Not yet released |
+| | |
++--------------------------------------+--------------------------------------------------------+
+
+Bug Fixes
+---------
+3.3.6
+
+Features
+--------
+3.3.6
+
+
+Version: 3.3.5
+==============
+
+Release Data
+------------
+
++--------------------------------------+--------------------------------------------------------+
+| **CPS Project** | |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Docker images** | onap/cps-and-ncmp:3.3.5 |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release designation** | 3.3.5 Montreal |
+| | |
++--------------------------------------+--------------------------------------------------------+
+| **Release date** | 2023 July 21 |
+| | |
++--------------------------------------+--------------------------------------------------------+
+
+Bug Fixes
+---------
+3.3.5
+
+Features
+--------
+ - `CPS-1760 <https://jira.onap.org/browse/CPS-1760>`_ Improve handling of special characters in Cps Paths
+
Version: 3.3.4
==============
| **Release designation** | 3.3.4 Montreal |
| | |
+--------------------------------------+--------------------------------------------------------+
-| **Release date** | Not yet released |
+| **Release date** | 2023 July 19 |
| | |
+--------------------------------------+--------------------------------------------------------+
Features
--------
+ - `CPS-1767 <https://jira.onap.org/browse/CPS-1767>`_ Upgrade CPS to java 17
Version: 3.3.3
==============
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
CpsDataService objectUnderTest
def originalCountBookstoreChildNodes
+ def originalCountBookstoreTopLevelListNodes
def now = OffsetDateTime.now()
def setup() {
objectUnderTest = cpsDataService
originalCountBookstoreChildNodes = countDataNodesInBookstore()
+ originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
}
def 'Read bookstore top-level container(s) using #fetchDescendantsOption.'() {
where: 'the following option is used'
fetchDescendantsOption || expectNumberOfDataNodes
OMIT_DESCENDANTS || 1
- DIRECT_CHILDREN_ONLY || 6
- INCLUDE_ALL_DESCENDANTS || 17
- new FetchDescendantsOption(2) || 17
+ DIRECT_CHILDREN_ONLY || 7
+ INCLUDE_ALL_DESCENDANTS || 28
+ new FetchDescendantsOption(2) || 28
}
def 'Read bookstore top-level container(s) using "root" path variations.'() {
when: 'get data nodes for bookstore container'
def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, root, OMIT_DESCENDANTS)
then: 'the tree consist ouf of one data node'
- assert countDataNodesInTree(result) == 1
+ assert countDataNodesInTree(result) == 2
and: 'the top level data node has the expected attribute and value'
- assert result.leaves['bookstore-name'] == ['Easons']
+ assert result.leaves.size() == 2
where: 'the following variations of "root" are used'
root << [ '/', '' ]
}
thrown(DataNodeNotFoundExceptionBatch)
}
+ def 'Add and Delete top-level list (element) data nodes with root node.'() {
+ given: 'a new (multiple-data-tree:invoice) datanodes'
+ def json = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Mango","price": "150","stock": true}]}'
+ when: 'the new list elements are saved'
+ objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/', json, now)
+ then: 'they can be retrieved by their xpaths'
+ objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', INCLUDE_ALL_DESCENDANTS)
+ and: 'there is one extra datanode'
+ assert originalCountBookstoreTopLevelListNodes + 1 == countTopLevelListDataNodesInBookstore()
+ when: 'the new elements are deleted'
+ objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', now)
+ then: 'the original number of datanodes is restored'
+ assert originalCountBookstoreTopLevelListNodes == countTopLevelListDataNodesInBookstore()
+ }
+
def 'Add and Delete list (element) data nodes.'() {
given: 'two new (categories) data nodes'
def json = '{"categories": [ {"code":"new1"}, {"code":"new2" } ] }'
def countDataNodesInBookstore() {
return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
}
+
+ def countTopLevelListDataNodesInBookstore() {
+ return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
+ }
}
'the AND is used where result does not exist' | '//books[@lang="English" and @price=1000]' || 0 | []
}
- def 'Cps Path query using combinations of OR operator #scenario.'() {
+ def 'Cps Path query using comparative and boolean operators.'() {
+ given: 'a cps path query in the discount category'
+ def cpsPath = "/bookstore/categories[@code='5']/books" + leafCondition
when: 'a query is executed to get response by the given cps path'
- def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, OMIT_DESCENDANTS)
- then: 'the result contains expected number of nodes'
- assert result.size() == expectedResultSize
- and: 'the cps-path of queryDataNodes has the expectedLeaves'
- assert result.leaves.sort() == expectedLeaves.sort()
- where: 'the following data is used'
- scenario | cpsPath || expectedResultSize | expectedLeaves
- 'the "OR" condition' | '//books[@lang="English" or @price=15]' || 6 | [[lang: "English", price: 15, title: "Annihilation", authors: ["Jeff VanderMeer"], editions: [2014]],
- [lang: "English", price: 15, title: "The Gruffalo", authors: ["Julia Donaldson"], editions: [1999]],
- [lang: "English", price: 14, title: "The Light Fantastic", authors: ["Terry Pratchett"], editions: [1986]],
- [lang: "English", price: 13, title: "Good Omens", authors: ["Terry Pratchett", "Neil Gaiman"], editions: [2006]],
- [lang: "English", price: 12, title: "The Colour of Magic", authors: ["Terry Pratchett"], editions: [1983]],
- [lang: "English", price: 10, title: "Matilda", authors: ["Roald Dahl"], editions: [1988, 2000]]]
- 'the "OR" condition with non-json data' | '//books[@title="xyz" or @price=15]' || 2 | [[lang: "English", price: 15, title: "Annihilation", authors: ["Jeff VanderMeer"], editions: [2014]],
- [lang: "English", price: 15, title: "The Gruffalo", authors: ["Julia Donaldson"], editions: [1999]]]
- 'combination of multiple AND' | '//books[@lang="English" and @price=15 and @edition=1983]' || 0 | []
- 'combination of multiple OR' | '//books[ @title="Matilda" or @price=15 or @edition=1983]' || 3 | [[lang: "English", price: 15, title: "Annihilation", authors: ["Jeff VanderMeer"], editions: [2014]],
- [lang: "English", price: 10, title: "Matilda", authors: ["Roald Dahl"], editions: [1988, 2000]],
- [lang: "English", price: 15, title: "The Gruffalo", authors: ["Julia Donaldson"], editions: [1999]]]
- 'combination of AND/OR' | '//books[@edition=1983 and @price=15 or @title="Good Omens"]' || 1 | [[lang: "English", price: 13, title: "Good Omens", authors: ["Terry Pratchett", "Neil Gaiman"], editions: [2006]]]
- 'combination of OR/AND' | '//books[@title="Annihilation" or @price=39 and @lang="arabic"]' || 1 | [[lang: "English", price: 15, title: "Annihilation", authors: ["Jeff VanderMeer"], editions: [2014]]]
- }
-
- def 'cps-path query using combinations of Comparative Operators #scenario.'() {
- when: 'a query is executed to get response by the given cpsPath'
- def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, OMIT_DESCENDANTS)
- then: 'the result contains expected number of nodes'
- assert result.size() == expectedResultSize
- and: 'xpaths of the retrieved data nodes are as expected'
- def bookTitles = result.collect { it.getLeaves().get('title') }
- assert bookTitles.sort() == expectedBookTitles.sort()
+ def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1,
+ cpsPath, OMIT_DESCENDANTS)
+ then: 'the cps-path of queryDataNodes has the expectedLeaves'
+ def bookPrices = result.collect { it.getLeaves().get('price') }
+ assert bookPrices.sort() == expectedBookPrices.sort()
where: 'the following data is used'
- scenario | cpsPath || expectedResultSize | expectedBookTitles
- 'the ">" condition' | '//books[@price>13 ]' || 5 | ['A Book with No Language', 'Annihilation', 'Debian GNU/Linux', 'The Gruffalo', 'The Light Fantastic']
- 'the "<" condition ' | '//books[@price<15]' || 5 | ['Good Omens', 'Logarithm tables', 'Matilda', 'The Colour of Magic', 'The Light Fantastic']
- 'the "<=" condition' | '//books[@price<=15]' || 7 | ['Annihilation', 'Good Omens', 'Logarithm tables', 'Matilda', 'The Colour of Magic', 'The Gruffalo', 'The Light Fantastic']
- 'the ">=" condition' | '//books[@price>=20]' || 2 | ['A Book with No Language', 'Debian GNU/Linux']
- 'the "<" condition where result does not exist' | '//books[@price<5]' || 0 | []
- 'the ">" condition where result does not exist' | '//books[@price>1000]' || 0 | []
- 'the ">" condition with AND condition' | '//books[@price>13 and @title="A Book with No Language"]' || 1 | ['A Book with No Language']
- 'the "<" condition with OR condition' | '//books[@price<10 or @lang="German"]' || 1 | ['Debian GNU/Linux']
- 'the "<=" condition with AND/OR condition' | '//books[@price<=15 and @title="Annihilation" or @lang="Spanish"]' || 1 | ['Annihilation']
- 'the ">=" condition with OR/AND condition' | '//books[@price>=13 or @lang="Spanish" and @title="Good Omens"]' || 6 | ['A Book with No Language', 'Annihilation', 'Good Omens', 'Debian GNU/Linux', 'The Gruffalo', 'The Light Fantastic']
- 'Mix of integer and string condition ' | '//books[@lang="German" and @price>38]' || 1 | ['Debian GNU/Linux']
+ leafCondition || expectedBookPrices
+ '[@price = 5]' || [5]
+ '[@price < 5]' || [1, 2, 3, 4]
+ '[@price > 5]' || [6, 7, 8, 9, 10]
+ '[@price <= 5]' || [1, 2, 3, 4, 5]
+ '[@price >= 5]' || [5, 6, 7, 8, 9, 10]
+ '[@price > 10]' || []
+ '[@price = 3 or @price = 7]' || [3, 7]
+ '[@price = 3 and @price = 7]' || []
+ '[@price > 3 and @price <= 6]' || [4, 5, 6]
+ '[@price < 3 or @price > 8]' || [1, 2, 9, 10]
+ '[@price = 1 or @price = 3 or @price = 5]' || [1, 3, 5]
+ '[@price = 1 or @price >= 8 and @price < 10]' || [1, 8, 9]
+ '[@price >= 3 and @price <= 5 or @price > 9]' || [3, 4, 5, 10]
}
def 'Cps Path query for leaf value(s) with #scenario.'() {
scenario | cpsPath | fetchDescendantsOption || expectedNumberOfParentNodes | expectedTotalNumberOfNodes
'string and no descendants' | '/bookstore/categories[@code="1"]/books[@title="Matilda"]' | OMIT_DESCENDANTS || 1 | 1
'integer and descendants' | '/bookstore/categories[@code="1"]/books[@price=15]' | INCLUDE_ALL_DESCENDANTS || 1 | 1
- 'no condition and no descendants' | '/bookstore/categories' | OMIT_DESCENDANTS || 4 | 4
- 'no condition and level 1 descendants' | '/bookstore' | new FetchDescendantsOption(1) || 1 | 6
- 'no condition and level 2 descendants' | '/bookstore' | new FetchDescendantsOption(2) || 1 | 17
+ 'no condition and no descendants' | '/bookstore/categories' | OMIT_DESCENDANTS || 5 | 5
+ 'no condition and level 1 descendants' | '/bookstore' | new FetchDescendantsOption(1) || 1 | 7
+ 'no condition and level 2 descendants' | '/bookstore' | new FetchDescendantsOption(2) || 1 | 28
}
def 'Query for attribute by cps path with cps paths that return no data because of #scenario.'() {
when: 'a query is executed to get all books'
def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '//books', OMIT_DESCENDANTS)
then: 'the expected number of books are returned'
- assert result.size() == 9
+ assert result.size() == 19
}
def 'Cps Path query using descendant anywhere with #scenario.'() {
'string leaf condition' | '//books[@title="Matilda"]' || ["Matilda"]
'text condition on leaf' | '//books/title[text()="Matilda"]' || ["Matilda"]
'text condition case mismatch' | '//books/title[text()="matilda"]' || []
- 'text condition on int leaf' | '//books/price[text()="10"]' || ["Matilda"]
+ 'text condition on int leaf' | '//books/price[text()="20"]' || ["A Book with No Language", "Matilda"]
'text condition on leaf-list' | '//books/authors[text()="Terry Pratchett"]' || ["Good Omens", "The Colour of Magic", "The Light Fantastic"]
'text condition partial match' | '//books/authors[text()="Terry"]' || []
'text condition (existing) empty string' | '//books/lang[text()=""]' || ["A Book with No Language"]
'contains condition with leaf' | '//books[contains(@title,"Mat")]' || ["Matilda"]
'contains condition with case-sensitive' | '//books[contains(@title,"Ti")]' || []
'contains condition with Integer Value' | '//books[contains(@price,"15")]' || ["Annihilation", "The Gruffalo"]
- 'contains condition with No-value' | '//books[contains(@title,"")]' || ["A Book with No Language", "Annihilation", "Debian GNU/Linux", "Good Omens", "Logarithm tables", "Matilda", "The Colour of Magic", "The Gruffalo", "The Light Fantastic"]
+ }
+
+ def 'Query for attribute by cps path using contains condition with no value.'() {
+ when: 'a query is executed to get response by the given cps path'
+ def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '//books[contains(@title,"")]', OMIT_DESCENDANTS)
+ then: 'all books are returned'
+ assert result.size() == 19
}
def 'Cps Path query using descendant anywhere with #scenario condition for a container element.'() {
where: 'the following data is used'
scenario | cpsPath || expectedBookTitles
'one leaf' | '//books[@price=14]' || ['The Light Fantastic']
- 'one leaf with ">" condition' | '//books[@price>14]' || ['A Book with No Language', 'Annihilation', 'Debian GNU/Linux', 'The Gruffalo']
+ 'one leaf with ">" condition' | '//books[@price>14]' || ['A Book with No Language', 'Annihilation', 'Debian GNU/Linux', 'Matilda', 'The Gruffalo']
'one text' | '//books/authors[text()="Terry Pratchett"]' || ['Good Omens', 'The Colour of Magic', 'The Light Fantastic']
'more than one leaf' | '//books[@price=12 and @lang="English"]' || ['The Colour of Magic']
'more than one leaf has "OR" condition' | '//books[@lang="English" or @price=15]' || ['Annihilation', 'Good Omens', 'Matilda', 'The Colour of Magic', 'The Gruffalo', 'The Light Fantastic']
assert result.xpath.sort() == expectedXPaths.sort()
where: 'the following data is used'
scenario | cpsPath || expectedXPaths
- 'multiple list-ancestors' | '//books/ancestor::categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"]
+ 'multiple list-ancestors' | '//books/ancestor::categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"]
'one ancestor with list value' | '//books/ancestor::categories[@code="1"]' || ["/bookstore/categories[@code='1']"]
'top ancestor' | '//books/ancestor::bookstore' || ["/bookstore"]
'list with index value in the xpath prefix' | '//categories[@code="1"]/books/ancestor::bookstore' || ["/bookstore"]
- 'ancestor with parent list' | '//books/ancestor::bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"]
+ 'ancestor with parent list' | '//books/ancestor::bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"]
'ancestor with parent' | '//books/ancestor::bookstore/categories[@code="2"]' || ["/bookstore/categories[@code='2']"]
'ancestor combined with text condition' | '//books/title[text()="Matilda"]/ancestor::bookstore' || ["/bookstore"]
'ancestor with parent that does not exist' | '//books/ancestor::parentDoesNoExist/categories' || []
where: 'the following data is used'
scenario | fetchDescendantsOption || expectedNumberOfNodes
'no' | OMIT_DESCENDANTS || 1
- 'direct' | DIRECT_CHILDREN_ONLY || 6
- 'all' | INCLUDE_ALL_DESCENDANTS || 17
+ 'direct' | DIRECT_CHILDREN_ONLY || 7
+ 'all' | INCLUDE_ALL_DESCENDANTS || 28
}
def 'Cps Path query with #scenario throws a CPS Path Exception.'() {
where: 'the following data is used'
scenario | cpsPath || expectedXpathsPerAnchor
'container node' | '/bookstore' || ["/bookstore"]
- 'list node' | '/bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"]
+ 'list node' | '/bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"]
'string leaf-condition' | '/bookstore[@bookstore-name="Easons"]' || ["/bookstore"]
'integer leaf-condition' | '/bookstore/categories[@code="1"]/books[@price=15]' || ["/bookstore/categories[@code='1']/books[@title='The Gruffalo']"]
- 'multiple list-ancestors' | '//books/ancestor::categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"]
+ 'multiple list-ancestors' | '//books/ancestor::categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"]
'one ancestor with list value' | '//books/ancestor::categories[@code="1"]' || ["/bookstore/categories[@code='1']"]
'list with index value in the xpath prefix' | '//categories[@code="1"]/books/ancestor::bookstore' || ["/bookstore"]
- 'ancestor with parent list' | '//books/ancestor::bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']"]
+ 'ancestor with parent list' | '//books/ancestor::bookstore/categories' || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"]
'ancestor with parent list element' | '//books/ancestor::bookstore/categories[@code="2"]' || ["/bookstore/categories[@code='2']"]
'ancestor combined with text condition' | '//books/title[text()="Matilda"]/ancestor::bookstore' || ["/bookstore"]
}
where: 'the following data is used'
scenario | fetchDescendantsOption || expectedNumberOfNodesPerAnchor
'no' | OMIT_DESCENDANTS || 1
- 'direct' | DIRECT_CHILDREN_ONLY || 6
- 'all' | INCLUDE_ALL_DESCENDANTS || 17
+ 'direct' | DIRECT_CHILDREN_ONLY || 7
+ 'all' | INCLUDE_ALL_DESCENDANTS || 28
}
def 'Cps Path query across anchors with ancestors and #scenario descendants.'() {
where: 'the following data is used'
scenario | fetchDescendantsOption || expectedNumberOfNodesPerAnchor
'no' | OMIT_DESCENDANTS || 1
- 'direct' | DIRECT_CHILDREN_ONLY || 6
- 'all' | INCLUDE_ALL_DESCENDANTS || 17
+ 'direct' | DIRECT_CHILDREN_ONLY || 7
+ 'all' | INCLUDE_ALL_DESCENDANTS || 28
}
def 'Cps Path query across anchors with syntax error throws a CPS Path Exception.'() {
assert countDataNodesInTree(result) == expectedNumberOfDataNodes
where:
scenario | cpsPath || expectedNumberOfDataNodes
- 'absolute path all list entries' | '/bookstore/categories' || 13
+ 'absolute path all list entries' | '/bookstore/categories' || 24
'absolute path 1 list entry by key' | '/bookstore/categories[@code="3"]' || 5
'absolute path 1 list entry by name' | '/bookstore/categories[@name="Comedy"]' || 5
- 'relative path all list entries' | '//categories' || 13
+ 'relative path all list entries' | '//categories' || 24
'relative path 1 list entry by key' | '//categories[@code="3"]' || 5
'relative path 1 list entry by leaf' | '//categories[@name="Comedy"]' || 5
'incomplete absolute path' | '/categories' || 0
'text-condition' || "/bookstore/categories[@code='1']/books/title[text()='[@hello=world]']"
'contains-condition' || "/bookstore/categories[@code='1']/books[contains(@title, '[@hello=world]')]"
}
+
+ def 'Cps Path get and query can handle apostrophe inside #quotes.'() {
+ given: 'a book with special characters in title'
+ cpsDataService.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, "/bookstore/categories[@code='1']",
+ '{"books": [ {"title":"I\'m escaping"} ] }', OffsetDateTime.now())
+ when: 'a query is executed'
+ def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, OMIT_DESCENDANTS)
+ then: 'the node is returned'
+ assert result.size() == 1
+ assert result[0].xpath == "/bookstore/categories[@code='1']/books[@title='I''m escaping']"
+ cleanup: 'the new datanode'
+ cpsDataService.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, "/bookstore/categories[@code='1']/books[@title='I''m escaping']", OffsetDateTime.now())
+ where:
+ quotes || cpsPath
+ 'single quotes' || "/bookstore/categories[@code='1']/books[@title='I''m escaping']"
+ 'double quotes' || '/bookstore/categories[@code="1"]/books[@title="I\'m escaping"]'
+ 'text-condition' || "/bookstore/categories[@code='1']/books/title[text()='I''m escaping']"
+ 'contains-condition' || "/bookstore/categories[@code='1']/books[contains(@title, 'I''m escaping')]"
+ }
}
import org.onap.cps.api.CpsAdminService
import org.onap.cps.integration.performance.base.CpsPerfTestBase
-class CpsAdminServiceLimits extends CpsPerfTestBase {
+class CpsAdminServiceLimitsPerfTest extends CpsPerfTestBase {
CpsAdminService objectUnderTest
import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-class CpsDataServiceLimits extends CpsPerfTestBase {
+class CpsDataServiceLimitsPerfTest extends CpsPerfTestBase {
CpsDataService objectUnderTest
package org.onap.cps.integration.performance.cps
+import org.onap.cps.spi.exceptions.DataNodeNotFoundException
+
import java.time.OffsetDateTime
import org.onap.cps.api.CpsDataService
import org.onap.cps.integration.performance.base.CpsPerfTestBase
when: 'multiple anchors with a node with a large number of descendants is created'
stopWatch.start()
def data = generateOpenRoadData(50)
- addAnchorsWithData(9, CPS_PERFORMANCE_TEST_DATASPACE, LARGE_SCHEMA_SET, 'delete', data)
+ addAnchorsWithData(10, CPS_PERFORMANCE_TEST_DATASPACE, LARGE_SCHEMA_SET, 'delete', data)
stopWatch.stop()
def setupDurationInMillis = stopWatch.getTotalTimeMillis()
then: 'setup duration is under 40 seconds'
recordAndAssertPerformance('Delete data nodes for anchor', 300, deleteDurationInMillis)
}
+ def 'Batch delete 100 non-existing nodes'() {
+ given: 'a list of xpaths to delete'
+ def xpathsToDelete = (1..100).collect { "/path/to/non-existing/node[@id='" + it + "']" }
+ when: 'child nodes are deleted'
+ stopWatch.start()
+ try {
+ objectUnderTest.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'delete10', xpathsToDelete, OffsetDateTime.now())
+ } catch (DataNodeNotFoundException ignored) {}
+ stopWatch.stop()
+ def deleteDurationInMillis = stopWatch.getTotalTimeMillis()
+ then: 'delete duration is under 300 milliseconds'
+ recordAndAssertPerformance('Batch delete 100 non-existing', 300, deleteDurationInMillis)
+ }
+
def 'Clean up test data'() {
given: 'a list of anchors to delete'
- def anchorNames = (1..9).collect {'delete' + it}
+ def anchorNames = (1..10).collect {'delete' + it}
when: 'data nodes are deleted'
stopWatch.start()
cpsAdminService.deleteAnchors(CPS_PERFORMANCE_TEST_DATASPACE, anchorNames)
recordAndAssertPerformance("Read datatrees with ${scenario}", durationLimit, durationInMillis)
where: 'the following parameters are used'
scenario | fetchDescendantsOption | anchor || durationLimit | expectedNumberOfDataNodes
- 'no descendants' | OMIT_DESCENDANTS | 'openroadm1' || 50 | 1
+ 'no descendants' | OMIT_DESCENDANTS | 'openroadm1' || 20 | 1
'direct descendants' | DIRECT_CHILDREN_ONLY | 'openroadm2' || 100 | 1 + 50
'all descendants' | INCLUDE_ALL_DESCENDANTS | 'openroadm3' || 200 | 1 + 50 * 86
}
stopWatch.start()
def result = objectUnderTest.getDataNodesForMultipleXpaths(CPS_PERFORMANCE_TEST_DATASPACE, 'openroadm4', xpaths, INCLUDE_ALL_DESCENDANTS)
stopWatch.stop()
- assert countDataNodesInTree(result) == 50 * 86
def durationInMillis = stopWatch.getTotalTimeMillis()
- then: 'all data is read within 500 ms'
+ then: 'requested nodes and their descendants are returned'
+ assert countDataNodesInTree(result) == 50 * 86
+ and: 'all data is read within 200 ms'
recordAndAssertPerformance("Read datatrees for multiple xpaths", 200, durationInMillis)
}
+ def 'Read for multiple xpaths to non-existing datanodes'() {
+ given: 'a collection of xpaths to get'
+ def xpaths = (1..50).collect { "/path/to/non-existing/node[@id='" + it + "']" }
+ when: 'get data nodes from 1 anchor'
+ stopWatch.start()
+ def result = objectUnderTest.getDataNodesForMultipleXpaths(CPS_PERFORMANCE_TEST_DATASPACE, 'openroadm4', xpaths, INCLUDE_ALL_DESCENDANTS)
+ stopWatch.stop()
+ def durationInMillis = stopWatch.getTotalTimeMillis()
+ then: 'no data is returned'
+ assert result.isEmpty()
+ and: 'the operation completes within within 20 ms'
+ recordAndAssertPerformance("Read non-existing xpaths", 20, durationInMillis)
+ }
+
def 'Read complete data trees using #scenario.'() {
when: 'get data nodes for 5 anchors'
stopWatch.start()
'leaf condition' | 'openroadm2' | '//openroadm-device[@ne-state="inservice"]' || 200 | 50 * 86
'ancestors' | 'openroadm3' | '//openroadm-device/ancestor::openroadm-devices' || 120 | 50 * 86 + 1
'leaf condition + ancestors' | 'openroadm4' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 120 | 50 * 86 + 1
+ 'non-existing data' | 'openroadm1' | '/path/to/non-existing/node[@id="1"]' || 10 | 0
}
def 'Query complete data trees across all anchors with #scenario.'() {
}
}
+ list invoice {
+ key "ProductID";
+ leaf ProductID {
+ type uint64;
+ mandatory "true";
+ description
+ "Unique product ID. Example: 001";
+ }
+ leaf ProductName {
+ type string;
+ mandatory "true";
+ description
+ "Name of the Product";
+ }
+ leaf price {
+ type uint64;
+ mandatory "true";
+ description
+ "Price of book";
+ }
+ leaf stock {
+ type boolean;
+ default "false";
+ description
+ "Book in stock or not. Example value: true";
+ }
+ }
+
container bookstore {
leaf bookstore-name {
{
+ "multiple-data-tree:invoice": [
+ {
+ "ProductID": "1",
+ "ProductName": "Apple",
+ "price": "100",
+ "stock": false
+ }
+ ],
"bookstore": {
"bookstore-name": "Easons",
"premises": {
"lang": "English",
"authors": ["Roald Dahl"],
"editions": [1988, 2000],
- "price": 10
+ "price": 20
},
{
"title": "The Gruffalo",
"price": 11
}
]
+ },
+ {
+ "code": 5,
+ "name": "Discount books",
+ "books" : [
+ {
+ "title": "Book 1",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 1
+ },
+ {
+ "title": "Book 2",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 2
+ },
+ {
+ "title": "Book 3",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 3
+ },
+ {
+ "title": "Book 4",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 4
+ },
+ {
+ "title": "Book 5",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 5
+ },
+ {
+ "title": "Book 6",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 6
+ },
+ {
+ "title": "Book 7",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 7
+ },
+ {
+ "title": "Book 8",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 8
+ },
+ {
+ "title": "Book 9",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 9
+ },
+ {
+ "title": "Book 10",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 10
+ }
+ ]
}
]
}
<parent>
<groupId>org.onap.cps</groupId>
<artifactId>cps-parent</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<relativePath>../cps-parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
- <configuration>
- <!--All exclusions below are referring to generated code-->
- <excludes>
- <exclude>org/onap/cps/event/model/*</exclude>
- <exclude>org/onap/cps/rest/model/*</exclude>
- <exclude>org/onap/cps/cpspath/parser/antlr4/*</exclude>
- <exclude>org/onap/cps/ncmp/rest/model/*</exclude>
- <exclude>org/onap/cps/**/*MapperImpl.class</exclude>
- <exclude>org/onap/cps/ncmp/rest/stub/*</exclude>
- </excludes>
- </configuration>
<executions>
- <execution>
- <id>default-prepare-agent</id>
- <goals>
- <goal>prepare-agent</goal>
- </goals>
- </execution>
<execution>
<id>report</id>
<goals>
\r
<groupId>org.onap.cps</groupId>\r
<artifactId>cps-aggregator</artifactId>\r
- <version>3.3.4-SNAPSHOT</version>\r
+ <version>3.3.5-SNAPSHOT</version>\r
<packaging>pom</packaging>\r
\r
<name>cps</name>\r
--- /dev/null
+distribution_type: container
+container_release_tag: 3.3.4
+project: cps
+log_dir: cps-maven-docker-stage-master/923/
+ref: 6b31279b2122ff9add6696b5eacfbeea8bb31cef
+containers:
+ - name: 'cps-and-ncmp'
+ version: '3.3.4-20230718T101218Z'
--- /dev/null
+distribution_type: maven
+log_dir: cps-maven-stage-master/931/
+project: cps
+version: 3.3.4
\ No newline at end of file
--- /dev/null
+distribution_type: container
+container_release_tag: 3.3.5
+project: cps
+log_dir: cps-maven-docker-stage-master/926/
+ref: 9a1f60c5515961eb4f1b6460be0235a73f833bef
+containers:
+ - name: 'cps-and-ncmp'
+ version: '3.3.5-20230721T120633Z'
--- /dev/null
+distribution_type: maven
+log_dir: cps-maven-stage-master/934/
+project: cps
+version: 3.3.5
\ No newline at end of file
<modelVersion>4.0.0</modelVersion>
<groupId>org.onap.cps</groupId>
<artifactId>spotbugs</artifactId>
- <version>3.3.4-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<properties>
<nexusproxy>https://nexus.onap.org</nexusproxy>
create_request_bodies() {
local CREATE_SIZE=$1
local REMOVE_SIZE=$2
- echo -n '{"dmiPlugin": "http://ncmp-dmi-plugin-stub:8080","createdCmHandles":[' > $CREATE_REQUEST
- echo -n '{"dmiPlugin": "http://ncmp-dmi-plugin-stub:8080","removedCmHandles":[' > $REMOVE_REQUEST
+ echo -n '{"dmiPlugin": "http://ncmp-dmi-plugin-demo-and-csit-stub:8092","createdCmHandles":[' > $CREATE_REQUEST
+ echo -n '{"dmiPlugin": "http://ncmp-dmi-plugin-demo-and-csit-stub:8092","removedCmHandles":[' > $REMOVE_REQUEST
for i in $(seq 1 "$CREATE_SIZE"); do
local CMHANDLE
CMHANDLE=$(uuidgen | tr -d '-')
major=3
minor=3
-patch=4
+patch=6
base_version=${major}.${minor}.${patch}