Merge "Device heartbeat listener"
authorLuke Gleeson <luke.gleeson@est.tech>
Fri, 18 Aug 2023 13:17:32 +0000 (13:17 +0000)
committerGerrit Code Review <gerrit@onap.org>
Fri, 18 Aug 2023 13:17:32 +0000 (13:17 +0000)
23 files changed:
cps-ncmp-rest/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/DataOperationEventCreator.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy
cps-ncmp-service/src/test/resources/dataOperationRequest.json
cps-ncmp-service/src/test/resources/dataOperationResponseEvent.json [new file with mode: 0644]
cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/AnchorRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentPrefetchRepositoryImpl.java
cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
docs/release-notes.rst
integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsAdminServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
integration-test/src/test/resources/data/tree/new-test-tree.json [new file with mode: 0644]
integration-test/src/test/resources/data/tree/new-test-tree.yang [new file with mode: 0644]
integration-test/src/test/resources/data/tree/updated-test-tree.json [new file with mode: 0644]
integration-test/src/test/resources/data/tree/updated-test-tree.yang [new file with mode: 0644]

index 63c5f16..e60e174 100644 (file)
@@ -34,7 +34,7 @@
     <artifactId>cps-ncmp-rest</artifactId>
 
     <properties>
-        <minimum-coverage>0.99</minimum-coverage>
+        <minimum-coverage>1.00</minimum-coverage>
     </properties>
 
     <dependencies>
index 02de985..ba6f891 100644 (file)
@@ -261,22 +261,22 @@ public class DmiDataOperations extends DmiOperations {
             final String topicName = dataOperationResourceUrlParameters.get("topic").get(0);
             final String requestId = dataOperationResourceUrlParameters.get("requestId").get(0);
 
-            final MultiValueMap<String, Map<NcmpEventResponseCode, List<String>>>
-                    cmHandleIdsPerResponseCodesPerOperationId = new LinkedMultiValueMap<>();
+            final MultiValueMap<DmiDataOperation, Map<NcmpEventResponseCode, List<String>>>
+                    cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>();
 
             dmiDataOperationRequestBodies.forEach(dmiDataOperationRequestBody -> {
                 final List<String> cmHandleIds = dmiDataOperationRequestBody.getCmHandles().stream()
                         .map(CmHandle::getId).collect(Collectors.toList());
                 if (throwable.getCause() instanceof HttpClientRequestException) {
-                    cmHandleIdsPerResponseCodesPerOperationId.add(dmiDataOperationRequestBody.getOperationId(),
+                    cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody,
                             Map.of(NcmpEventResponseCode.UNABLE_TO_READ_RESOURCE_DATA, cmHandleIds));
                 } else {
-                    cmHandleIdsPerResponseCodesPerOperationId.add(dmiDataOperationRequestBody.getOperationId(),
+                    cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody,
                             Map.of(NcmpEventResponseCode.DMI_SERVICE_NOT_RESPONDING, cmHandleIds));
                 }
             });
             ResourceDataOperationRequestUtils.publishErrorMessageToClientTopic(topicName, requestId,
-                    cmHandleIdsPerResponseCodesPerOperationId);
+                    cmHandleIdsPerResponseCodesPerOperation);
         }
     }
 }
index 2d9a51b..65cda94 100644 (file)
@@ -30,6 +30,7 @@ import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.api.NcmpEventResponseCode;
 import org.onap.cps.ncmp.api.impl.events.NcmpCloudEventBuilder;
+import org.onap.cps.ncmp.api.impl.operations.DmiDataOperation;
 import org.onap.cps.ncmp.events.async1_0_0.Data;
 import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent;
 import org.onap.cps.ncmp.events.async1_0_0.Response;
@@ -44,49 +45,48 @@ public class DataOperationEventCreator {
      *
      * @param clientTopic                              topic the client wants to use for responses
      * @param requestId                                unique identifier per request
-     * @param cmHandleIdsPerResponseCodesPerOperationId map of cm handles per operation response per response code
+     * @param cmHandleIdsPerResponseCodesPerOperation map of cm handles per operation response per response code
      * @return Cloud Event
      */
     public static CloudEvent createDataOperationEvent(final String clientTopic,
                                                       final String requestId,
-                                                      final MultiValueMap<String,
+                                                      final MultiValueMap<DmiDataOperation,
                                                               Map<NcmpEventResponseCode, List<String>>>
-                                                              cmHandleIdsPerResponseCodesPerOperationId) {
+                                                              cmHandleIdsPerResponseCodesPerOperation) {
         final DataOperationEvent dataOperationEvent = new DataOperationEvent();
-        final Data data = createPayloadFromDataOperationResponses(cmHandleIdsPerResponseCodesPerOperationId);
+        final Data data = createPayloadFromDataOperationResponses(cmHandleIdsPerResponseCodesPerOperation);
         dataOperationEvent.setData(data);
         final Map<String, String> extensions = createDataOperationExtensions(requestId, clientTopic);
         return NcmpCloudEventBuilder.builder().type(DataOperationEvent.class.getName())
                 .event(dataOperationEvent).extensions(extensions).setCloudEvent().build();
     }
 
-    private static Data createPayloadFromDataOperationResponses(final MultiValueMap<String, Map<NcmpEventResponseCode,
-            List<String>>> cmHandleIdsPerOperationIdPerResponseCode) {
+    private static Data createPayloadFromDataOperationResponses(final MultiValueMap<DmiDataOperation,
+            Map<NcmpEventResponseCode, List<String>>> cmHandleIdsPerResponseCodesPerOperation) {
         final Data data = new Data();
         final List<org.onap.cps.ncmp.events.async1_0_0.Response> responses = new ArrayList<>();
-        cmHandleIdsPerOperationIdPerResponseCode.entrySet().forEach(cmHandleIdsPerOperationIdPerResponseCodeEntries ->
-                cmHandleIdsPerOperationIdPerResponseCodeEntries.getValue().forEach(cmHandleIdsPerResponseCodeEntries ->
+        cmHandleIdsPerResponseCodesPerOperation.forEach((dmiDataOperation, cmHandleIdsPerResponseCodes) ->
+                cmHandleIdsPerResponseCodes.forEach(cmHandleIdsPerResponseCodeEntries ->
                         responses.addAll(createResponseFromDataOperationResponses(
-                                cmHandleIdsPerOperationIdPerResponseCodeEntries.getKey(),
-                                cmHandleIdsPerResponseCodeEntries)
-                        )));
+                                dmiDataOperation, cmHandleIdsPerResponseCodeEntries))));
         data.setResponses(responses);
         return data;
     }
 
     private static List<Response> createResponseFromDataOperationResponses(
-            final String operationId,
+            final DmiDataOperation dmiDataOperation,
             final Map<NcmpEventResponseCode, List<String>> cmHandleIdsPerResponseCodeEntries) {
         final List<org.onap.cps.ncmp.events.async1_0_0.Response> responses = new ArrayList<>();
-        cmHandleIdsPerResponseCodeEntries.entrySet()
-                .forEach(cmHandleIdsPerResponseCodeEntry -> {
-                    final Response response = new Response();
-                    response.setOperationId(operationId);
-                    response.setStatusCode(cmHandleIdsPerResponseCodeEntry.getKey().getStatusCode());
-                    response.setStatusMessage(cmHandleIdsPerResponseCodeEntry.getKey().getStatusMessage());
-                    response.setIds(cmHandleIdsPerResponseCodeEntry.getValue());
-                    responses.add(response);
-                });
+        cmHandleIdsPerResponseCodeEntries.forEach((ncmpEventResponseCode, cmHandleIds) -> {
+            final Response response = new Response();
+            response.setOperationId(dmiDataOperation.getOperationId());
+            response.setStatusCode(ncmpEventResponseCode.getStatusCode());
+            response.setStatusMessage(ncmpEventResponseCode.getStatusMessage());
+            response.setIds(cmHandleIds);
+            response.setResourceIdentifier(dmiDataOperation.getResourceIdentifier());
+            response.setOptions(dmiDataOperation.getOptions());
+            responses.add(response);
+        });
         return responses;
     }
 
index d8fb904..c455337 100644 (file)
@@ -68,8 +68,8 @@ public class ResourceDataOperationRequestUtils {
             final Collection<YangModelCmHandle> yangModelCmHandles) {
 
         final Map<String, List<DmiDataOperation>> dmiDataOperationsOutPerDmiServiceName = new HashMap<>();
-        final MultiValueMap<String, Map<NcmpEventResponseCode, List<String>>> cmHandleIdsPerResponseCodesPerOperationId
-                = new LinkedMultiValueMap<>();
+        final MultiValueMap<DmiDataOperation, Map<NcmpEventResponseCode,
+                List<String>>> cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>();
         final Set<String> nonReadyCmHandleIdsLookup = filterAndGetNonReadyCmHandleIds(yangModelCmHandles);
 
         final Map<String, Map<String, Map<String, String>>> dmiPropertiesPerCmHandleIdPerServiceName =
@@ -100,15 +100,15 @@ public class ResourceDataOperationRequestUtils {
                     }
                 }
             }
-            populateCmHandleIdsPerOperationIdPerResponseCode(cmHandleIdsPerResponseCodesPerOperationId,
-                    dataOperationDefinitionIn.getOperationId(), NcmpEventResponseCode.CM_HANDLES_NOT_FOUND,
-                    nonExistingCmHandleIds);
-            populateCmHandleIdsPerOperationIdPerResponseCode(cmHandleIdsPerResponseCodesPerOperationId,
-                    dataOperationDefinitionIn.getOperationId(), NcmpEventResponseCode.CM_HANDLES_NOT_READY,
-                    nonReadyCmHandleIds);
+            populateCmHandleIdsPerOperationIdPerResponseCode(cmHandleIdsPerResponseCodesPerOperation,
+                    DmiDataOperation.buildDmiDataOperationRequestBodyWithoutCmHandles(dataOperationDefinitionIn),
+                    NcmpEventResponseCode.CM_HANDLES_NOT_FOUND, nonExistingCmHandleIds);
+            populateCmHandleIdsPerOperationIdPerResponseCode(cmHandleIdsPerResponseCodesPerOperation,
+                    DmiDataOperation.buildDmiDataOperationRequestBodyWithoutCmHandles(dataOperationDefinitionIn),
+                    NcmpEventResponseCode.CM_HANDLES_NOT_READY, nonReadyCmHandleIds);
         }
-        if (!cmHandleIdsPerResponseCodesPerOperationId.isEmpty()) {
-            publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperationId);
+        if (!cmHandleIdsPerResponseCodesPerOperation.isEmpty()) {
+            publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperation);
         }
         return dmiDataOperationsOutPerDmiServiceName;
     }
@@ -118,16 +118,16 @@ public class ResourceDataOperationRequestUtils {
      *
      * @param clientTopic                              client given topic
      * @param requestId                                unique identifier per request
-     * @param cmHandleIdsPerResponseCodesPerOperationId list of cm handle ids per operation id with response code
+     * @param cmHandleIdsPerResponseCodesPerOperation list of cm handle ids per operation with response code
      */
     @Async
     public static void publishErrorMessageToClientTopic(final String clientTopic,
                                                          final String requestId,
-                                                         final MultiValueMap<String,
+                                                         final MultiValueMap<DmiDataOperation,
                                                                  Map<NcmpEventResponseCode, List<String>>>
-                                                                    cmHandleIdsPerResponseCodesPerOperationId) {
+                                                                    cmHandleIdsPerResponseCodesPerOperation) {
         final CloudEvent dataOperationCloudEvent = DataOperationEventCreator.createDataOperationEvent(clientTopic,
-                requestId, cmHandleIdsPerResponseCodesPerOperationId);
+                requestId, cmHandleIdsPerResponseCodesPerOperation);
         final EventsPublisher<CloudEvent> eventsPublisher = CpsApplicationContext.getCpsBean(EventsPublisher.class);
         eventsPublisher.publishCloudEvent(clientTopic, requestId, dataOperationCloudEvent);
     }
@@ -174,14 +174,14 @@ public class ResourceDataOperationRequestUtils {
                         != CmHandleState.READY).map(YangModelCmHandle::getId).collect(Collectors.toSet());
     }
 
-    private static void populateCmHandleIdsPerOperationIdPerResponseCode(final MultiValueMap<String,
-            Map<NcmpEventResponseCode, List<String>>> cmHandleIdsPerResponseCodesPerOperationId,
-                                                                        final String operationId,
+    private static void populateCmHandleIdsPerOperationIdPerResponseCode(final MultiValueMap<DmiDataOperation,
+            Map<NcmpEventResponseCode, List<String>>> cmHandleIdsPerResponseCodesPerOperation,
+                                                                        final DmiDataOperation dmiDataOperation,
                                                                         final NcmpEventResponseCode
                                                                                 ncmpEventResponseCode,
                                                                         final List<String> cmHandleIds) {
         if (!cmHandleIds.isEmpty()) {
-            cmHandleIdsPerResponseCodesPerOperationId.add(operationId, Map.of(ncmpEventResponseCode, cmHandleIds));
+            cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperation, Map.of(ncmpEventResponseCode, cmHandleIds));
         }
     }
 }
index c866824..38b2056 100644 (file)
@@ -110,9 +110,9 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec {
                 toTargetEvent(consumerRecordOut.value(), DataOperationEvent.class)
         and: 'data operation response event response size is 3'
             dataOperationResponseEvent.data.responses.size() == 3
-        and: 'verify published response data as json string'
-            jsonObjectMapper.asJsonString(dataOperationResponseEvent.data.responses)
-                    == '[{"operationId":"operational-14","ids":["unknown-cm-handle"],"statusCode":"100","statusMessage":"cm handle id(s) not found"},{"operationId":"operational-14","ids":["non-ready-cm handle"],"statusCode":"101","statusMessage":"cm handle(s) not ready"},{"operationId":"running-12","ids":["non-ready-cm handle"],"statusCode":"101","statusMessage":"cm handle(s) not ready"}]'
+        and: 'verify published data operation response as json string'
+        def dataOperationResponseEventJson = TestUtils.getResourceFileContent('dataOperationResponseEvent.json')
+            jsonObjectMapper.asJsonString(dataOperationResponseEvent.data.responses) == dataOperationResponseEventJson
     }
 
     static def getYangModelCmHandles() {
@@ -126,7 +126,7 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec {
                 new YangModelCmHandle(id: 'ch3-dmi2', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: readyState),
                 new YangModelCmHandle(id: 'ch4-dmi2', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: readyState),
                 new YangModelCmHandle(id: 'ch7-dmi2', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: readyState),
-                new YangModelCmHandle(id: 'non-ready-cm handle', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: advisedState)
+                new YangModelCmHandle(id: 'non-ready-cm-handle', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: advisedState)
         ]
     }
 }
index d2e0d64..f69b876 100644 (file)
         "ch3-dmi2",
         "unknown-cm-handle",
         "ch6-dmi1",
-        "non-ready-cm handle"
+        "non-ready-cm-handle"
       ]
     },
     {
       "operation": "read",
       "operationId": "running-12",
       "datastore": "ncmp-datastore:passthrough-running",
+      "options": "some option",
+      "resourceIdentifier": "some resource identifier",
       "targetIds": [
         "ch1-dmi1",
         "ch7-dmi2",
         "ch2-dmi1",
-        "non-ready-cm handle"
+        "non-ready-cm-handle"
       ]
     },
     {
@@ -29,6 +31,7 @@
       "operationId": "operational-15",
       "datastore": "ncmp-datastore:passthrough-operational",
       "options": "some option",
+      "resourceIdentifier": "some resource identifier",
       "targetIds": [
         "ch4-dmi2",
         "ch6-dmi1"
diff --git a/cps-ncmp-service/src/test/resources/dataOperationResponseEvent.json b/cps-ncmp-service/src/test/resources/dataOperationResponseEvent.json
new file mode 100644 (file)
index 0000000..611d47d
--- /dev/null
@@ -0,0 +1 @@
+[{"operationId":"operational-14","ids":["unknown-cm-handle"],"resourceIdentifier":"some resource identifier","options":"some option","statusCode":"100","statusMessage":"cm handle id(s) not found"},{"operationId":"operational-14","ids":["non-ready-cm-handle"],"resourceIdentifier":"some resource identifier","options":"some option","statusCode":"101","statusMessage":"cm handle(s) not ready"},{"operationId":"running-12","ids":["non-ready-cm-handle"],"resourceIdentifier":"some resource identifier","options":"some option","statusCode":"101","statusMessage":"cm handle(s) not ready"}]
\ No newline at end of file
index fd669b7..c30a63f 100644 (file)
@@ -181,12 +181,46 @@ class QueryRestControllerSpec extends Specification {
                         .andReturn().response
         then: 'the response contains the the datanode in json format'
             assert response.status == HttpStatus.OK.value()
-            assert Integer.valueOf(response.getHeaderValue("total-pages")) == expectedPageSize
+            assert Integer.valueOf(response.getHeaderValue("total-pages")) == expectedTotalPageSize
             assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
             assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement3","leaveListElement4"]}}')
         where: 'the following options for include descendants are provided in the request'
-            scenario                     | pageIndex | pageSize | totalAnchors || expectedPageSize
+            scenario                     | pageIndex | pageSize | totalAnchors || expectedTotalPageSize
             '1st page with all anchors'  | 1         | 3        | 3            || 1
             '1st page with less anchors' | 1         | 2        | 3            || 2
     }
+
+    def 'Query data node across all anchors with pagination option with #scenario.'() {
+        given: 'service method returns a list containing a data node from different anchors'
+        def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
+                .withAnchor('my_anchor')
+                .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
+        def dataNode2 = new DataNodeBuilder().withXpath('/xpath')
+                .withAnchor('my_anchor_2')
+                .withLeaves([leaf: 'value', leafList: ['leaveListElement3', 'leaveListElement4']]).build()
+        and: 'the query endpoint'
+            def dataspaceName = 'my_dataspace'
+            def cpsPath = 'some/cps/path'
+            def dataNodeEndpoint = "$basePath/v2/dataspaces/$dataspaceName/nodes/query"
+            mockCpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath,
+                INCLUDE_ALL_DESCENDANTS, PaginationOption.NO_PAGINATION) >> [dataNode1, dataNode2]
+            mockCpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath) >> 2
+        when: 'query data nodes API is invoked'
+            def response =
+                mvc.perform(
+                        get(dataNodeEndpoint)
+                                .param('cps-path', cpsPath)
+                                .param('descendants', "all")
+                                .param(parameterName, "1"))
+                        .andReturn().response
+        then: 'the response contains the the datanode in json format'
+            assert response.status == HttpStatus.OK.value()
+            assert Integer.valueOf(response.getHeaderValue("total-pages")) == 1
+            assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
+            assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement3","leaveListElement4"]}}')
+        where:
+            scenario           | parameterName
+            'only page size'   | 'pageSize'
+            'only page index'  | 'pageIndex'
+    }
 }
index 6f9f5a4..847a4a3 100755 (executable)
@@ -171,6 +171,18 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic
         anchorRepository.deleteAllByDataspaceAndNameIn(dataspaceEntity, anchorNames);
     }
 
+    @Transactional
+    @Override
+    public void updateAnchorSchemaSet(final String dataspaceName,
+                                         final String anchorName,
+                                         final String schemaSetName) {
+        final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+        final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+        final SchemaSetEntity schemaSetEntity = schemaSetRepository
+                .getByDataspaceAndName(dataspaceEntity, schemaSetName);
+        anchorRepository.updateAnchorSchemaSetId(schemaSetEntity.getId(), anchorEntity.getId());
+    }
+
     private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) {
         final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
         return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
index 5bb5857..b8503a7 100755 (executable)
@@ -99,4 +99,8 @@ public interface AnchorRepository extends JpaRepository<AnchorEntity, Long> {
         deleteAllByDataspaceIdAndNameIn(dataspaceEntity.getId(), anchorNames.toArray(new String[0]));
     }
 
+    @Modifying
+    @Query(value = "UPDATE anchor SET schema_set_id =:schemaSetId WHERE id = :anchorId ", nativeQuery = true)
+    void updateAnchorSchemaSetId(@Param("schemaSetId") int schemaSetId, @Param("anchorId") long anchorId);
+
 }
index 4f056c8..c187f20 100644 (file)
@@ -94,7 +94,7 @@ public class FragmentPrefetchRepositoryImpl implements FragmentPrefetchRepositor
             final FragmentEntity fragmentEntity = new FragmentEntity();
             fragmentEntity.setId(resultSet.getLong("id"));
             fragmentEntity.setXpath(resultSet.getString("xpath"));
-            fragmentEntity.setParentId(resultSet.getLong("parentId"));
+            fragmentEntity.setParentId(resultSet.getObject("parentId", Long.class));
             fragmentEntity.setAttributes(resultSet.getString("attributes"));
             fragmentEntity.setAnchor(anchorEntityPerId.get(resultSet.getLong("anchorId")));
             fragmentEntity.setChildFragments(new HashSet<>());
index fcf3f54..edd052a 100755 (executable)
@@ -135,4 +135,13 @@ public interface CpsAdminService {
      *         given module names
      */
     Collection<String> queryAnchorNames(String dataspaceName, Collection<String> moduleNames);
+
+    /**
+     * Update schema set of an anchor.
+     *
+     * @param dataspaceName dataspace name
+     * @param anchorName    anchor name
+     * @param schemaSetName schema set name
+     */
+    void updateAnchorSchemaSet(String dataspaceName, String anchorName, String schemaSetName);
 }
index e286eea..d83ee43 100755 (executable)
@@ -120,4 +120,11 @@ public class CpsAdminServiceImpl implements CpsAdminService {
         final Collection<Anchor> anchors = cpsAdminPersistenceService.queryAnchors(dataspaceName, moduleNames);
         return anchors.stream().map(Anchor::getName).collect(Collectors.toList());
     }
+
+    @Override
+    public void updateAnchorSchemaSet(final String dataspaceName,
+                                         final String anchorName,
+                                         final String schemaSetName) {
+        cpsAdminPersistenceService.updateAnchorSchemaSet(dataspaceName, anchorName, schemaSetName);
+    }
 }
index 1c1e80a..5a1810f 100755 (executable)
@@ -133,4 +133,13 @@ public interface CpsAdminPersistenceService {
      * @param anchorNames   anchor names
      */
     void deleteAnchors(String dataspaceName, Collection<String> anchorNames);
+
+    /**
+     * Delete anchors by name in given dataspace.
+     *
+     * @param dataspaceName dataspace name
+     * @param anchorName    anchor name
+     * @param schemaSetName schema set name
+     */
+    void updateAnchorSchemaSet(String dataspaceName, String anchorName, String schemaSetName);
 }
index eb41e20..12564fb 100755 (executable)
@@ -178,4 +178,11 @@ class CpsAdminServiceImplSpec extends Specification {
         and: 'the CpsValidator is called on the dataspaceName'
             1 * mockCpsValidator.validateNameCharacters('someDataspace')
     }
+
+    def 'Update anchor schema set.'() {
+        when: 'update anchor is invoked'
+            objectUnderTest.updateAnchorSchemaSet('someDataspace', 'someAnchor', 'someSchemaSetName')
+        then: 'associated persistence service method is invoked with correct parameter'
+            1 * mockCpsAdminPersistenceService.updateAnchorSchemaSet('someDataspace', 'someAnchor', 'someSchemaSetName')
+    }
 }
index cd70cf8..3f672ad 100755 (executable)
@@ -39,6 +39,7 @@ Release Data
 Bug Fixes
 ---------
 3.3.6
+    - `CPS-1841 <https://jira.onap.org/browse/CPS-1841>`_ Update of top-level data node fails with exception
 
 Features
 --------
index 4780e36..03ef9c2 100644 (file)
@@ -108,7 +108,7 @@ class CpsIntegrationSpecBase extends Specification {
     def dataspaceExists(dataspaceName) {
         try {
             cpsAdminService.getDataspace(dataspaceName)
-        } catch (DataspaceNotFoundException e) {
+        } catch (DataspaceNotFoundException dataspaceNotFoundException) {
             return false
         }
         return true
index 92fbdaa..bdd894c 100644 (file)
@@ -22,10 +22,12 @@ package org.onap.cps.integration.functional
 
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.integration.base.CpsIntegrationSpecBase
+import org.onap.cps.spi.FetchDescendantsOption
 import org.onap.cps.spi.exceptions.AlreadyDefinedException
 import org.onap.cps.spi.exceptions.AnchorNotFoundException
 import org.onap.cps.spi.exceptions.DataspaceInUseException
 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
+import java.time.OffsetDateTime
 
 class CpsAdminServiceIntegrationSpec extends CpsIntegrationSpecBase {
 
@@ -44,8 +46,8 @@ class CpsAdminServiceIntegrationSpec extends CpsIntegrationSpecBase {
             def thrown = null
             try {
                 objectUnderTest.getDataspace('newDataspace')
-            } catch(Exception e) {
-                thrown = e
+            } catch(Exception exception) {
+                thrown = exception
             }
            assert thrown instanceof DataspaceNotFoundException
     }
@@ -100,8 +102,8 @@ class CpsAdminServiceIntegrationSpec extends CpsIntegrationSpecBase {
             def thrown = null
             try {
                 objectUnderTest.getAnchor(GENERAL_TEST_DATASPACE, 'newAnchor')
-            } catch(Exception e) {
-                thrown = e
+            } catch(Exception exception) {
+                thrown = exception
             }
             assert thrown instanceof AnchorNotFoundException
     }
@@ -151,4 +153,28 @@ class CpsAdminServiceIntegrationSpec extends CpsIntegrationSpecBase {
            'just unknown module(s)' | GENERAL_TEST_DATASPACE
     }
 
+    def 'Update anchor schema set.'() {
+        when: 'a new schema set with tree yang model is created'
+            def newTreeYangModelAsString = readResourceDataFile('tree/new-test-tree.yang')
+            cpsModuleService.createSchemaSet(GENERAL_TEST_DATASPACE, 'newTreeSchemaSet', [tree: newTreeYangModelAsString])
+        then: 'an anchor with new schema set is created'
+            objectUnderTest.createAnchor(GENERAL_TEST_DATASPACE, 'newTreeSchemaSet', 'anchor4')
+        and: 'the new tree datanode is saved'
+            def treeJsonData = readResourceDataFile('tree/new-test-tree.json')
+            cpsDataService.saveData(GENERAL_TEST_DATASPACE, 'anchor4', treeJsonData, OffsetDateTime.now())
+        and: 'saved tree data node can be retrieved by its normalized xpath'
+            def branchName = cpsDataService.getDataNodes(GENERAL_TEST_DATASPACE, 'anchor4', "/test-tree/branch", FetchDescendantsOption.DIRECT_CHILDREN_ONLY)[0].leaves['name']
+            assert branchName == 'left'
+        and: 'a another schema set with updated tree yang model is created'
+            def updatedTreeYangModelAsString = readResourceDataFile('tree/updated-test-tree.yang')
+            cpsModuleService.createSchemaSet(GENERAL_TEST_DATASPACE, 'anotherTreeSchemaSet', [tree: updatedTreeYangModelAsString])
+        and: 'anchor4 schema set is updated with another schema set successfully'
+            objectUnderTest.updateAnchorSchemaSet(GENERAL_TEST_DATASPACE, 'anchor4', 'anotherTreeSchemaSet')
+        when: 'updated tree data node with new leaves'
+            def updatedTreeJsonData = readResourceDataFile('tree/updated-test-tree.json')
+            cpsDataService.updateNodeLeaves(GENERAL_TEST_DATASPACE, "anchor4", "/test-tree/branch[@name='left']", updatedTreeJsonData, OffsetDateTime.now())
+        then: 'updated tree data node can be retrieved by its normalized xpath'
+            def birdsName = cpsDataService.getDataNodes(GENERAL_TEST_DATASPACE, 'anchor4',"/test-tree/branch[@name='left']/nest", FetchDescendantsOption.DIRECT_CHILDREN_ONLY)[0].leaves['birds']
+            assert birdsName as String == '[Raven, Night Owl, Crow]'
+    }
 }
index 9716cb5..82a415e 100644 (file)
@@ -394,6 +394,17 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             restoreBookstoreDataAnchor(1)
     }
 
+    def 'Update bookstore top-level container data node.'() {
+        when: 'the bookstore top-level container is updated'
+            def json = '{ "bookstore": { "bookstore-name": "new bookstore" }}'
+            objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', json, now)
+        then: 'bookstore name has been updated'
+            def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', DIRECT_CHILDREN_ONLY)
+            result.leaves.'bookstore-name'[0] == 'new bookstore'
+        cleanup:
+            restoreBookstoreDataAnchor(1)
+    }
+
     def 'Update multiple data node leaves.'() {
         given: 'Updated json for bookstore data'
             def jsonData =  "{'book-store:books':{'lang':'English/French','price':100,'title':'Matilda'}}"
diff --git a/integration-test/src/test/resources/data/tree/new-test-tree.json b/integration-test/src/test/resources/data/tree/new-test-tree.json
new file mode 100644 (file)
index 0000000..f7aefc4
--- /dev/null
@@ -0,0 +1,12 @@
+{
+  "test-tree": {
+    "branch": [
+      {
+        "name": "left",
+        "nest": {
+          "name": "small"
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/integration-test/src/test/resources/data/tree/new-test-tree.yang b/integration-test/src/test/resources/data/tree/new-test-tree.yang
new file mode 100644 (file)
index 0000000..1a08b92
--- /dev/null
@@ -0,0 +1,21 @@
+module test-tree {
+    yang-version 1.1;
+
+    namespace "org:onap:cps:test:test-tree";
+    prefix tree;
+    revision "2020-02-02";
+
+    container test-tree {
+        list branch {
+            key "name";
+            leaf name {
+                type string;
+            }
+            container nest {
+                leaf name {
+                    type string;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/integration-test/src/test/resources/data/tree/updated-test-tree.json b/integration-test/src/test/resources/data/tree/updated-test-tree.json
new file mode 100644 (file)
index 0000000..2c2eea4
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "nest": {
+    "name": "small",
+    "birds": [
+      "Night Owl",
+      "Raven",
+      "Crow"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/integration-test/src/test/resources/data/tree/updated-test-tree.yang b/integration-test/src/test/resources/data/tree/updated-test-tree.yang
new file mode 100644 (file)
index 0000000..bd883e8
--- /dev/null
@@ -0,0 +1,33 @@
+module test-tree {
+    yang-version 1.1;
+
+    namespace "org:onap:cps:test:test-tree";
+    prefix tree;
+
+    revision "2023-08-17" {
+        description
+            "added list of birds to nest";
+    }
+
+    revision "2020-09-15" {
+        description
+            "Sample Model";
+    }
+
+    container test-tree {
+        list branch {
+            key "name";
+            leaf name {
+                type string;
+            }
+            container nest {
+                leaf name {
+                    type string;
+                }
+                leaf-list birds {
+                    type string;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file