Merge "Cm Subscription: PENDING logic handling in NCMP"
authorLuke Gleeson <luke.gleeson@est.tech>
Mon, 21 Aug 2023 10:20:09 +0000 (10:20 +0000)
committerGerrit Code Review <gerrit@onap.org>
Mon, 21 Aug 2023 10:20:09 +0000 (10:20 +0000)
31 files changed:
cps-application/src/main/resources/application.yml
cps-ncmp-rest/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/embeddedcache/TrustLevelCacheConfig.java [new file with mode: 0644]
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/trustlevel/DeviceHeartbeatConsumer.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/DeviceTrustLevel.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/TrustLevel.java [new file with mode: 0644]
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/trustlevel/DeviceHeartbeatConsumerSpec.groovy [new file with mode: 0644]
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/cache/HazelcastCacheConfig.java
cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.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 a18de2a..6aefda9 100644 (file)
@@ -109,6 +109,8 @@ app:
     dmi:
         cm-events:
             topic: ${DMI_CM_EVENTS_TOPIC:dmi-cm-events}
+        device-heartbeat:
+            topic: ${DMI_DEVICE_HEARTBEAT_TOPIC:dmi-device-heartbeat}
 
 
 notification:
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>
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/embeddedcache/TrustLevelCacheConfig.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/embeddedcache/TrustLevelCacheConfig.java
new file mode 100644 (file)
index 0000000..816fc50
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * ============LICENSE_START========================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.config.embeddedcache;
+
+import com.hazelcast.collection.ISet;
+import com.hazelcast.config.SetConfig;
+import org.onap.cps.cache.HazelcastCacheConfig;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class TrustLevelCacheConfig extends HazelcastCacheConfig {
+
+    private static final SetConfig untrustworthyCmHandlesSetConfig = createSetConfig("untrustworthyCmHandlesSetConfig");
+
+    /**
+     * Untrustworthy cmhandle set instance.
+     *
+     * @return instance of distributed set of untrustworthy cmhandles.
+     */
+    @Bean
+    public ISet<String> untrustworthyCmHandlesSet() {
+        return createHazelcastInstance("untrustworthyCmHandlesSet", untrustworthyCmHandlesSetConfig).getSet(
+                "untrustworthyCmHandlesSet");
+    }
+
+
+}
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);
         }
     }
 }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/DeviceHeartbeatConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/DeviceHeartbeatConsumer.java
new file mode 100644 (file)
index 0000000..458c1b8
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.trustlevel;
+
+import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent;
+
+import com.hazelcast.collection.ISet;
+import io.cloudevents.CloudEvent;
+import io.cloudevents.kafka.impl.KafkaHeaders;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DeviceHeartbeatConsumer {
+
+    private final ISet<String> untrustworthyCmHandlesSet;
+
+    /**
+     * Listening the device heartbeats.
+     *
+     * @param deviceHeartbeatConsumerRecord Device Heartbeat record.
+     */
+    @KafkaListener(topics = "${app.dmi.device-heartbeat.topic}",
+            containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory")
+    public void heartbeatListener(final ConsumerRecord<String, CloudEvent> deviceHeartbeatConsumerRecord) {
+
+        final String cmHandleId = KafkaHeaders.getParsedKafkaHeader(deviceHeartbeatConsumerRecord.headers(), "ce_id");
+
+        final DeviceTrustLevel deviceTrustLevel =
+                toTargetEvent(deviceHeartbeatConsumerRecord.value(), DeviceTrustLevel.class);
+
+        if (deviceTrustLevel == null || deviceTrustLevel.getTrustLevel() == null) {
+            log.warn("No or Invalid trust level defined");
+            return;
+        }
+
+        if (deviceTrustLevel.getTrustLevel().equals(TrustLevel.NONE)) {
+            untrustworthyCmHandlesSet.add(cmHandleId);
+            log.debug("Added cmHandleId to untrustworthy set : {}", cmHandleId);
+        } else if (deviceTrustLevel.getTrustLevel().equals(TrustLevel.COMPLETE) && untrustworthyCmHandlesSet.contains(
+                cmHandleId)) {
+            untrustworthyCmHandlesSet.remove(cmHandleId);
+            log.debug("Removed cmHandleId from untrustworthy set : {}", cmHandleId);
+        }
+    }
+
+}
+
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/DeviceTrustLevel.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/DeviceTrustLevel.java
new file mode 100644 (file)
index 0000000..2ed4e45
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.trustlevel;
+
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor
+@Data
+@NoArgsConstructor
+class DeviceTrustLevel implements Serializable {
+
+    private static final long serialVersionUID = -1705715024067165212L;
+
+    private TrustLevel trustLevel;
+
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/TrustLevel.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/trustlevel/TrustLevel.java
new file mode 100644 (file)
index 0000000..f4254bb
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.trustlevel;
+
+public enum TrustLevel {
+    NONE, COMPLETE;
+}
\ No newline at end of file
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));
         }
     }
 }
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/trustlevel/DeviceHeartbeatConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/trustlevel/DeviceHeartbeatConsumerSpec.groovy
new file mode 100644 (file)
index 0000000..48de23d
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.trustlevel
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.hazelcast.collection.ISet
+import io.cloudevents.CloudEvent
+import io.cloudevents.core.builder.CloudEventBuilder
+import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.onap.cps.utils.JsonObjectMapper
+import org.springframework.boot.test.context.SpringBootTest
+import spock.lang.Specification
+
+@SpringBootTest(classes = [ObjectMapper, JsonObjectMapper])
+class DeviceHeartbeatConsumerSpec extends Specification {
+
+    def mockUntrustworthyCmHandlesSet = Mock(ISet<String>)
+    def objectMapper = new ObjectMapper()
+
+    def objectUnderTest = new DeviceHeartbeatConsumer(mockUntrustworthyCmHandlesSet)
+
+    def 'Operations to be done in an empty untrustworthy set for #scenario'() {
+        given: 'an event with trustlevel as #trustLevel'
+            def incomingEvent = testCloudEvent(trustLevel)
+        and: 'transformed as a kafka record'
+            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'cmhandle1', incomingEvent)
+            consumerRecord.headers().add('ce_id', objectMapper.writeValueAsBytes('cmhandle1'))
+        when: 'the event is consumed'
+            objectUnderTest.heartbeatListener(consumerRecord)
+        then: 'untrustworthy cmhandles are stored'
+            untrustworthyCmHandlesSetInvocationForAdd * mockUntrustworthyCmHandlesSet.add(_)
+        and: 'trustworthy cmHandles will be removed from untrustworthy set'
+            untrustworthyCmHandlesSetInvocationForContains * mockUntrustworthyCmHandlesSet.contains(_)
+
+        where: 'below scenarios are applicable'
+            scenario         | trustLevel          || untrustworthyCmHandlesSetInvocationForAdd | untrustworthyCmHandlesSetInvocationForContains
+            'None trust'     | TrustLevel.NONE     || 1                                         | 0
+            'Complete trust' | TrustLevel.COMPLETE || 0                                         | 1
+    }
+
+    def 'Invalid trust'() {
+        when: 'we provide an invalid trust in the event'
+            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'cmhandle1', testCloudEvent(null))
+            consumerRecord.headers().add('ce_id', objectMapper.writeValueAsBytes('cmhandle1'))
+            objectUnderTest.heartbeatListener(consumerRecord)
+        then: 'no interaction with the untrustworthy cmhandles set'
+            0 * mockUntrustworthyCmHandlesSet.add(_)
+            0 * mockUntrustworthyCmHandlesSet.contains(_)
+            0 * mockUntrustworthyCmHandlesSet.remove(_)
+        and: 'control flow returns without any exception'
+            noExceptionThrown()
+
+    }
+
+    def 'Remove trustworthy cmhandles from untrustworthy cmhandles set'() {
+        given: 'an event with COMPLETE trustlevel'
+            def incomingEvent = testCloudEvent(TrustLevel.COMPLETE)
+        and: 'transformed as a kafka record'
+            def consumerRecord = new ConsumerRecord<String, CloudEvent>('test-device-heartbeat', 0, 0, 'cmhandle1', incomingEvent)
+            consumerRecord.headers().add('ce_id', objectMapper.writeValueAsBytes('cmhandle1'))
+        and: 'untrustworthy cmhandles set contains cmhandle1'
+            1 * mockUntrustworthyCmHandlesSet.contains(_) >> true
+        when: 'the event is consumed'
+            objectUnderTest.heartbeatListener(consumerRecord)
+        then: 'cmhandle removed from untrustworthy cmhandles set'
+            1 * mockUntrustworthyCmHandlesSet.remove(_) >> {
+                args ->
+                    {
+                        args[0].equals('cmhandle1')
+                    }
+            }
+
+    }
+
+    def testCloudEvent(trustLevel) {
+        return CloudEventBuilder.v1().withData(objectMapper.writeValueAsBytes(new DeviceTrustLevel(trustLevel)))
+            .withId("cmhandle1")
+            .withSource(URI.create('DMI'))
+            .withDataSchema(URI.create('test'))
+            .withType('org.onap.cm.events.trustlevel-notification')
+            .build()
+    }
+
+}
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 405e6e2..067191b 100644 (file)
@@ -24,6 +24,7 @@ import com.hazelcast.config.Config;
 import com.hazelcast.config.MapConfig;
 import com.hazelcast.config.NamedConfig;
 import com.hazelcast.config.QueueConfig;
+import com.hazelcast.config.SetConfig;
 import com.hazelcast.core.Hazelcast;
 import com.hazelcast.core.HazelcastInstance;
 import lombok.extern.slf4j.Slf4j;
@@ -57,6 +58,10 @@ public class HazelcastCacheConfig {
         if (namedConfig instanceof QueueConfig) {
             config.addQueueConfig((QueueConfig) namedConfig);
         }
+        if (namedConfig instanceof SetConfig) {
+            config.addSetConfig((SetConfig) namedConfig);
+        }
+
         config.setClusterName(clusterName);
         updateDiscoveryMode(config);
         return config;
@@ -76,6 +81,13 @@ public class HazelcastCacheConfig {
         return commonQueueConfig;
     }
 
+    protected static SetConfig createSetConfig(final String configName) {
+        final SetConfig commonSetConfig = new SetConfig(configName);
+        commonSetConfig.setBackupCount(1);
+        commonSetConfig.setAsyncBackupCount(1);
+        return commonSetConfig;
+    }
+
     protected void updateDiscoveryMode(final Config config) {
         if (cacheKubernetesEnabled) {
             log.info("Enabling kubernetes mode with service-name : {}", cacheKubernetesServiceName);
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 8efd485..415e9fd 100644 (file)
@@ -45,10 +45,17 @@ class HazelcastCacheConfigSpec extends Specification {
             } else {
                 assert result.config.queueConfigs.isEmpty()
             }
+        and: 'if applicable it has a set config with the expected name'
+            if (expectSetConfig) {
+                assert result.config.setConfigs.values()[0].name == 'my set config'
+            } else {
+                assert result.config.setConfigs.isEmpty()
+            }
         where: 'the following configs are used'
-            scenario       | config                                                    || expectMapConfig | expectQueueConfig
-            'Map Config'   | HazelcastCacheConfig.createMapConfig('my map config')     || true            | false
-            'Queue Config' | HazelcastCacheConfig.createQueueConfig('my queue config') || false           | true
+            scenario       | config                                                    || expectMapConfig | expectQueueConfig | expectSetConfig
+            'Map Config'   | HazelcastCacheConfig.createMapConfig('my map config')     || true            | false             | false
+            'Queue Config' | HazelcastCacheConfig.createQueueConfig('my queue config') || false           | true              | false
+            'Set Config'   | HazelcastCacheConfig.createSetConfig('my set config')     || false           | false             | true
     }
 
 }
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