Merge "Subscription Creation: NCMP to Client CloudEvent transformation"
authorLuke Gleeson <luke.gleeson@est.tech>
Thu, 27 Jul 2023 10:20:54 +0000 (10:20 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 27 Jul 2023 10:20:54 +0000 (10:20 +0000)
69 files changed:
checkstyle/pom.xml
cps-application/pom.xml
cps-bom/pom.xml
cps-dependencies/pom.xml
cps-events/pom.xml
cps-ncmp-events/pom.xml
cps-ncmp-rest-stub/cps-ncmp-rest-stub-app/pom.xml
cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/pom.xml
cps-ncmp-rest-stub/pom.xml
cps-ncmp-rest/pom.xml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpCachedResourceRequestHandler.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/TaskManagementDefaultHandler.java [deleted file]
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/executor/CpsNcmpTaskExecutor.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandlerSpec.groovy [new file with mode: 0644]
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/executor/CpsNcmpTaskExecutorSpec.groovy [new file with mode: 0644]
cps-ncmp-service/pom.xml
cps-parent/pom.xml
cps-path-parser/pom.xml
cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBooleanOperatorType.java [deleted file]
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathComparativeOperator.java [deleted file]
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy
cps-rest/pom.xml
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-ri/pom.xml
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java
cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java
cps-ri/src/main/java/org/onap/cps/spi/utils/EscapeUtils.java
cps-ri/src/main/resources/changelog/changelog-master.yaml
cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-forward.sql [new file with mode: 0644]
cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-rollback.sql [new file with mode: 0644]
cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath.yaml [new file with mode: 0644]
cps-ri/src/test/groovy/org/onap/cps/spi/utils/EscapeUtilsSpec.groovy
cps-service/pom.xml
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/utils/YangUtils.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/resources/bookstore.json
cps-service/src/test/resources/bookstore.yang
dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-app/pom.xml
dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/pom.xml
dmi-plugin-demo-and-csit-stub/pom.xml
docs/cps-path.rst
docs/release-notes.rst
integration-test/pom.xml
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsAdminServiceLimitsPerfTest.groovy [moved from integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsAdminServiceLimits.groovy with 97% similarity]
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy [moved from integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimits.groovy with 98% similarity]
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/DeletePerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/GetPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy
integration-test/src/test/resources/data/bookstore/bookstore.yang
integration-test/src/test/resources/data/bookstore/bookstoreData.json
jacoco-report/pom.xml
pom.xml
releases/3.3.4-container.yaml [new file with mode: 0644]
releases/3.3.4.yaml [new file with mode: 0644]
releases/3.3.5-container.yaml [new file with mode: 0644]
releases/3.3.5.yaml [new file with mode: 0644]
spotbugs/pom.xml
test-tools/test-deregistration.sh
version.properties

index a6fa570..59dea69 100644 (file)
@@ -26,7 +26,7 @@
     <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>
index 4b28469..c4b3fe6 100755 (executable)
@@ -28,7 +28,7 @@
     <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>
 
@@ -37,7 +37,7 @@
     <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>
index 7374b4c..bc350b2 100644 (file)
@@ -25,7 +25,7 @@
     <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>
index 48e7044..dafc923 100755 (executable)
@@ -27,7 +27,7 @@
     <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>
index 6eb8b50..97c5c47 100644 (file)
@@ -24,7 +24,7 @@
     <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>
 
index cd08e56..975b928 100644 (file)
@@ -23,7 +23,7 @@
     <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>
 
index 9254086..982a288 100644 (file)
@@ -22,7 +22,7 @@
     <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>
index 9f3e904..44c23d6 100644 (file)
@@ -21,7 +21,7 @@
     <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>
 
index 6d1cd5a..ff8582d 100644 (file)
@@ -22,7 +22,7 @@
     <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>
 
index 7444619..b56dbcd 100644 (file)
@@ -27,7 +27,7 @@
     <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>
 
index 76946d3..85a1eae 100644 (file)
@@ -25,6 +25,7 @@ import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
 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
@@ -48,37 +49,53 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle
         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;
+    }
+
+
 }
index d7aeab6..d40ab9b 100644 (file)
 
 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;
@@ -51,10 +45,10 @@ public class NcmpDatastoreRequestHandler implements TaskManagementDefaultHandler
     @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
@@ -86,46 +80,10 @@ public class NcmpDatastoreRequestHandler implements TaskManagementDefaultHandler
         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);
@@ -145,33 +103,15 @@ public class NcmpDatastoreRequestHandler implements TaskManagementDefaultHandler
         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");
-            }
-        });
-    }
 }
index 0e49c6d..8a32575 100644 (file)
 
 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
@@ -32,6 +42,8 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH
 
     private final NetworkCmProxyDataService networkCmProxyDataService;
 
+    private static final Object noReturn = null;
+
     /**
      * Constructor.
      *
@@ -44,27 +56,71 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH
         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;
+        };
+    }
+
 }
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/TaskManagementDefaultHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/TaskManagementDefaultHandler.java
deleted file mode 100644 (file)
index b2520b1..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- *  ============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;
-    }
-}
index 0543c4f..ba68d5b 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============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.
@@ -35,19 +35,19 @@ public class CpsNcmpTaskExecutor {
      * 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());
         }
     }
 }
index 4ee31e1..7964e32 100644 (file)
@@ -75,7 +75,6 @@ import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL
 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 {
 
@@ -104,16 +103,16 @@ 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
@@ -150,23 +149,6 @@ class NetworkCmProxyControllerSpec extends Specification {
             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" +
@@ -188,30 +170,10 @@ class NetworkCmProxyControllerSpec extends Specification {
             '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)
@@ -221,20 +183,18 @@ class NetworkCmProxyControllerSpec extends Specification {
         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)
@@ -243,18 +203,13 @@ class NetworkCmProxyControllerSpec extends Specification {
             ).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(
@@ -266,8 +221,6 @@ class NetworkCmProxyControllerSpec extends Specification {
             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.'() {
@@ -287,9 +240,9 @@ class NetworkCmProxyControllerSpec extends Specification {
             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(
@@ -298,10 +251,8 @@ class NetworkCmProxyControllerSpec extends Specification {
             ).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.'() {
@@ -586,7 +537,7 @@ class NetworkCmProxyControllerSpec extends Specification {
     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)
@@ -597,9 +548,9 @@ class NetworkCmProxyControllerSpec extends Specification {
         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'() {
index 300026d..e755094 100644 (file)
@@ -251,7 +251,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
     }
 
     def failedResponse(cmHandle) {
-        return CmHandleRegistrationResponse.createFailureResponse(cmHandle, new RuntimeException("Failed"))
+        return CmHandleRegistrationResponse.createFailureResponse(cmHandle, new RuntimeException('Failed'))
     }
 
     def successResponse(cmHandle) {
diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandlerSpec.groovy
new file mode 100644 (file)
index 0000000..b11787a
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ *  ============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
+    }
+
+}
diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/executor/CpsNcmpTaskExecutorSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/executor/CpsNcmpTaskExecutorSpec.groovy
new file mode 100644 (file)
index 0000000..870c36c
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ *  ============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]
+    }
+
+}
index 1573034..2aa1410 100644 (file)
     <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>
index 913120d..470c3a0 100755 (executable)
 
     <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>
index 15a2719..fa15ee1 100644 (file)
@@ -23,7 +23,7 @@
     <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>
 
index c88a822..3aef120 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============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?;
@@ -60,7 +66,7 @@ invalidPostFix : (AT | CB | COLONCOLON | comparativeOperators ).+ ;
 /*
  * 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 : '@' ;
@@ -89,9 +95,9 @@ IntegerLiteral : FragDigits ;
 // 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 ;
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBooleanOperatorType.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBooleanOperatorType.java
deleted file mode 100644 (file)
index b2f1ddd..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*\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
index 5c47127..de261e6 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============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");
@@ -24,9 +24,7 @@ package org.onap.cps.cpspath.parser;
 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;
@@ -43,21 +41,21 @@ public class CpsPathBuilder extends CpsPathBaseListener {
 
     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) {
@@ -81,34 +79,25 @@ public class CpsPathBuilder extends CpsPathBaseListener {
 
     @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
@@ -122,6 +111,8 @@ public class CpsPathBuilder extends CpsPathBaseListener {
     public void enterMultipleLeafConditions(final MultipleLeafConditionsContext ctx)  {
         normalizedXpathBuilder.append(OPEN_BRACKET);
         leavesData.clear();
+        booleanOperators.clear();
+        comparativeOperators.clear();
     }
 
     @Override
@@ -144,13 +135,13 @@ public class CpsPathBuilder extends CpsPathBaseListener {
     @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
@@ -177,10 +168,6 @@ public class CpsPathBuilder extends CpsPathBaseListener {
         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();
@@ -193,7 +180,7 @@ public class CpsPathBuilder extends CpsPathBaseListener {
     }
 
     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);
@@ -211,11 +198,25 @@ public class CpsPathBuilder extends CpsPathBaseListener {
                                     .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);
+    }
 }
diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathComparativeOperator.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathComparativeOperator.java
deleted file mode 100644 (file)
index c7ffd0d..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*\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
index 3c3cbcc..f98df05 100644 (file)
@@ -24,8 +24,9 @@ package org.onap.cps.cpspath.parser;
 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;
 
@@ -39,7 +40,7 @@ public class CpsPathQuery {
     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;
@@ -103,4 +104,11 @@ public class CpsPathQuery {
         return cpsPathPrefixType == ABSOLUTE && hasLeafConditions();
     }
 
+    @Getter
+    @EqualsAndHashCode
+    @AllArgsConstructor
+    public static class DataLeaf {
+        private final String name;
+        private final Object value;
+    }
 }
index 9ab5491..ae7ee59 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============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");
@@ -28,16 +28,28 @@ import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT
 
 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'
@@ -46,6 +58,10 @@ class CpsPathQuerySpec extends Specification {
             '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.'() {
@@ -80,8 +96,8 @@ class CpsPathQuerySpec extends Specification {
             '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']"
@@ -103,44 +119,37 @@ class CpsPathQuerySpec extends Specification {
             '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'
@@ -155,11 +164,11 @@ class CpsPathQuerySpec extends Specification {
         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.'() {
@@ -191,7 +200,7 @@ class CpsPathQuerySpec extends Specification {
         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'
@@ -205,14 +214,14 @@ class CpsPathQuerySpec extends Specification {
         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
@@ -220,4 +229,28 @@ class CpsPathQuerySpec extends Specification {
             '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'
+    }
+
 }
index e40aa91..60ad200 100755 (executable)
@@ -28,7 +28,7 @@
     <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>
 
index d88a9cd..81262c8 100755 (executable)
@@ -179,6 +179,29 @@ class DataRestControllerSpec extends Specification {
             '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'
index 1adca2f..6207393 100644 (file)
     <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
index 7b5c0c6..e371035 100644 (file)
@@ -37,7 +37,6 @@ import org.onap.cps.spi.entities.DataspaceEntity;
 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
@@ -49,8 +48,6 @@ public class FragmentQueryBuilder {
     @PersistenceContext
     private EntityManager entityManager;
 
-    private final JsonObjectMapper jsonObjectMapper;
-
     /**
      * Create a sql query to retrieve by anchor(id) and cps path.
      *
@@ -128,18 +125,18 @@ public class FragmentQueryBuilder {
         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 ");
index 139a8b3..4c7971e 100644 (file)
@@ -31,6 +31,7 @@ import javax.persistence.EntityManager;
 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;
 
@@ -86,7 +87,7 @@ public class TempTableCreator {
         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 ");
@@ -98,8 +99,4 @@ public class TempTableCreator {
         sqlStringBuilder.append(";");
     }
 
-    private static String escapeSingleQuotesByDoublingThem(final String value) {
-        return value.replace("'", "''");
-    }
-
 }
index 3092b79..2b61d39 100644 (file)
@@ -26,8 +26,12 @@ import lombok.NoArgsConstructor;
 @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("'", "''");
     }
 
 }
index 4e6986e..f76c5ba 100644 (file)
@@ -56,3 +56,5 @@ databaseChangeLog:
       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
diff --git a/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-forward.sql b/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-forward.sql
new file mode 100644 (file)
index 0000000..9bf7f9a
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+   ============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
diff --git a/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-rollback.sql b/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath-rollback.sql
new file mode 100644 (file)
index 0000000..0fd1633
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+   ============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
diff --git a/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath.yaml b/cps-ri/src/main/resources/changelog/db/changes/21-escape-quotes-in-xpath.yaml
new file mode 100644 (file)
index 0000000..7b5b1db
--- /dev/null
@@ -0,0 +1,29 @@
+# ============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
index 7de9b97..52330e6 100644 (file)
@@ -24,7 +24,7 @@ import spock.lang.Specification
 
 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:
@@ -33,4 +33,9 @@ class EscapeUtilsSpec extends Specification {
             '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!"
+    }
+
 }
index dffc38d..c97623f 100644 (file)
   <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>
index 6e7c164..7db87e8 100755 (executable)
@@ -116,8 +116,12 @@ public class CpsDataServiceImpl implements CpsDataService {
         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);
     }
 
@@ -391,6 +395,10 @@ public class CpsDataServiceImpl implements CpsDataService {
             .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()));
index 7da4024..f00f944 100644 (file)
@@ -253,7 +253,7 @@ public class YangUtils {
         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());
index db86640..ba43849 100644 (file)
@@ -110,6 +110,28 @@ class CpsDataServiceImplSpec extends Specification {
             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')
index 459908b..4b8ed3d 100644 (file)
@@ -1,4 +1,12 @@
 {
+   "multiple-data-tree:invoice": [
+      {
+         "ProductID": "1",
+         "ProductName": "Apple",
+         "price": "100",
+         "stock": false
+      }
+   ],
    "test:bookstore":{
       "bookstore-name": "Chapters/Easons",
       "categories": [
index 2179fb9..b7a52e2 100644 (file)
@@ -15,6 +15,34 @@ module stores {
         }
     }
 
+    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 {
index 71dcec8..1833cf4 100644 (file)
@@ -22,7 +22,7 @@
     <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>
@@ -30,7 +30,7 @@
     <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>
index a9e3827..a148c3d 100644 (file)
@@ -21,7 +21,7 @@
     <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>
 
index e8dd4c0..f6a6578 100644 (file)
@@ -22,7 +22,7 @@
     <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>
 
index 796eb7f..6611789 100644 (file)
@@ -177,6 +177,7 @@ General Notes
 =============
 
 - 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
@@ -247,7 +248,6 @@ leaf-conditions
   - 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.
@@ -272,7 +272,6 @@ The text()-condition  can be added to any CPS path query.
   - 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
 --------------------
index d9033a0..6b35461 100755 (executable)
@@ -16,6 +16,63 @@ CPS Release Notes
 ..      * * *   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
 ==============
 
@@ -32,7 +89,7 @@ Release Data
 | **Release designation**              | 3.3.4 Montreal                                         |
 |                                      |                                                        |
 +--------------------------------------+--------------------------------------------------------+
-| **Release date**                     | Not yet released                                       |
+| **Release date**                     | 2023 July 19                                           |
 |                                      |                                                        |
 +--------------------------------------+--------------------------------------------------------+
 
@@ -42,6 +99,7 @@ Bug Fixes
 
 Features
 --------
+    - `CPS-1767 <https://jira.onap.org/browse/CPS-1767>`_ Upgrade CPS to java 17
 
 Version: 3.3.3
 ==============
index 04ce7cc..16c1664 100644 (file)
@@ -23,7 +23,7 @@
     <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>
 
index 351f310..a3f1439 100644 (file)
@@ -43,11 +43,13 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
 
     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.'() {
@@ -64,18 +66,18 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
         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 << [ '/', '' ]
     }
@@ -179,6 +181,21 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             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" } ] }'
@@ -368,4 +385,8 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
     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))
+    }
 }
index a736ab0..74496d3 100644 (file)
@@ -54,52 +54,30 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             '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.'() {
@@ -113,9 +91,9 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             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.'() {
@@ -146,7 +124,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
         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.'() {
@@ -160,7 +138,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             '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"]
@@ -182,7 +160,13 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             '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.'() {
@@ -194,7 +178,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
         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']
@@ -228,11 +212,11 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             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'      || []
@@ -248,8 +232,8 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
         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.'() {
@@ -277,13 +261,13 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
         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"]
     }
@@ -298,8 +282,8 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
         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.'() {
@@ -312,8 +296,8 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
         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.'() {
@@ -330,10 +314,10 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             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
@@ -372,4 +356,23 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             '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')]"
+    }
 }
@@ -23,7 +23,7 @@ package org.onap.cps.integration.performance.cps
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.integration.performance.base.CpsPerfTestBase
 
-class CpsAdminServiceLimits extends CpsPerfTestBase {
+class CpsAdminServiceLimitsPerfTest extends CpsPerfTestBase {
 
     CpsAdminService objectUnderTest
 
index db36b88..e80a87d 100644 (file)
@@ -20,6 +20,8 @@
 
 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
@@ -34,7 +36,7 @@ class DeletePerfTest extends 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'
@@ -155,9 +157,23 @@ class DeletePerfTest extends CpsPerfTestBase {
             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)
index eee87dd..e096c60 100644 (file)
@@ -44,7 +44,7 @@ class GetPerfTest extends CpsPerfTestBase {
             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
     }
@@ -56,12 +56,27 @@ class GetPerfTest extends CpsPerfTestBase {
             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()
index bad3f8a..78e0d01 100644 (file)
@@ -49,6 +49,7 @@ class QueryPerfTest extends CpsPerfTestBase {
             '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.'() {
index e592a9c..ab384de 100644 (file)
@@ -15,6 +15,34 @@ module stores {
         }
     }
 
+    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 {
index 12df20e..5f66a1d 100644 (file)
@@ -1,4 +1,12 @@
 {
+  "multiple-data-tree:invoice": [
+    {
+      "ProductID": "1",
+      "ProductName": "Apple",
+      "price": "100",
+      "stock": false
+    }
+  ],
   "bookstore": {
     "bookstore-name": "Easons",
     "premises": {
@@ -27,7 +35,7 @@
             "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
+          }
+        ]
       }
     ]
   }
index 623f2a0..9dbd896 100644 (file)
@@ -5,7 +5,7 @@
     <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>
diff --git a/pom.xml b/pom.xml
index 910afa2..119b14b 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
 \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
diff --git a/releases/3.3.4-container.yaml b/releases/3.3.4-container.yaml
new file mode 100644 (file)
index 0000000..ee2a0d4
--- /dev/null
@@ -0,0 +1,8 @@
+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'
diff --git a/releases/3.3.4.yaml b/releases/3.3.4.yaml
new file mode 100644 (file)
index 0000000..073bd42
--- /dev/null
@@ -0,0 +1,4 @@
+distribution_type: maven
+log_dir: cps-maven-stage-master/931/
+project: cps
+version: 3.3.4
\ No newline at end of file
diff --git a/releases/3.3.5-container.yaml b/releases/3.3.5-container.yaml
new file mode 100644 (file)
index 0000000..b9d9b10
--- /dev/null
@@ -0,0 +1,8 @@
+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'
diff --git a/releases/3.3.5.yaml b/releases/3.3.5.yaml
new file mode 100644 (file)
index 0000000..06d3360
--- /dev/null
@@ -0,0 +1,4 @@
+distribution_type: maven
+log_dir: cps-maven-stage-master/934/
+project: cps
+version: 3.3.5
\ No newline at end of file
index 20a10d2..546076f 100644 (file)
@@ -25,7 +25,7 @@
     <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>
index 6608b02..571644d 100755 (executable)
@@ -77,8 +77,8 @@ remove_handles_and_record_time() {
 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 '-')
index 9456209..d567eec 100755 (executable)
@@ -22,7 +22,7 @@
 
 major=3
 minor=3
-patch=4
+patch=6
 
 base_version=${major}.${minor}.${patch}