Split the data sync request using module and root references 53/141653/4
authormpriyank <priyank.maheshwari@est.tech>
Wed, 6 Aug 2025 12:48:07 +0000 (13:48 +0100)
committermpriyank <priyank.maheshwari@est.tech>
Thu, 7 Aug 2025 14:03:52 +0000 (15:03 +0100)
- Introduced a method to get concatenated module and root node for a
  given cm handle(using already synched modules) to use options field
- The request is splitted as some devices dont support fetching the data
  using root url and the devices that support that can in turn return
  huge amount of data in one go.
- Updated the data sync logic to make multiple calls for the initial
  data sync
- The failing requests are ignored and the algorithm works on best
  effort basis
- Testware to support all the above use cases

Issue-ID: CPS-2758
Change-Id: I3d50ef8448705efdd004c6a1ee039e9691f14815
Signed-off-by: mpriyank <priyank.maheshwari@est.tech>
12 files changed:
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdog.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtils.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DataSyncWatchdogSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleOperationsUtilsSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
cps-service/src/main/java/org/onap/cps/impl/CpsModuleServiceImpl.java
cps-service/src/main/java/org/onap/cps/utils/YangParser.java
cps-service/src/test/groovy/org/onap/cps/impl/CpsModuleServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy

index 4975c35..b64d456 100644 (file)
@@ -114,16 +114,18 @@ public class DmiDataOperations {
      *
      * @param cmHandleId    network resource identifier
      * @param requestId     requestId for async responses
+     * @param options       options field for filtered response
      * @return {@code ResponseEntity} response entity
      */
-    public ResponseEntity<Object> getAllResourceDataFromDmi(final String cmHandleId, final String requestId) {
+    public ResponseEntity<Object> getAllResourceDataFromDmi(final String cmHandleId, final String requestId,
+            final String options) {
         final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId);
         final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState();
         validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState);
 
         final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle);
         final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters(
-                PASSTHROUGH_OPERATIONAL.getDatastoreName(), yangModelCmHandle, "/", null,
+                PASSTHROUGH_OPERATIONAL.getDatastoreName(), yangModelCmHandle, "/", options,
                 null);
         return dmiRestClient.synchronousPostOperationWithJsonData(DATA, urlTemplateParameters, jsonRequestBody, READ,
                 DmiRestClient.NO_AUTHORIZATION);
index 7080779..5e498bb 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2024 Nordix Foundation
+ *  Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -24,14 +24,18 @@ import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_D
 
 import com.hazelcast.map.IMap;
 import java.time.OffsetDateTime;
+import java.util.Collection;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.CpsDataService;
+import org.onap.cps.api.CpsModuleService;
 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState;
 import org.onap.cps.ncmp.api.inventory.models.CompositeState;
 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence;
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
@@ -44,11 +48,9 @@ public class DataSyncWatchdog {
     private static final boolean DATA_SYNC_DONE = true;
 
     private final InventoryPersistence inventoryPersistence;
-
+    private final CpsModuleService cpsModuleService;
     private final CpsDataService cpsDataService;
-
     private final ModuleOperationsUtils moduleOperationsUtils;
-
     private final IMap<String, Boolean> dataSyncSemaphores;
 
     /**
@@ -57,29 +59,57 @@ public class DataSyncWatchdog {
      */
     @Scheduled(initialDelayString = "${ncmp.timers.cm-handle-data-sync.initial-delay-ms:40000}",
             fixedDelayString = "${ncmp.timers.cm-handle-data-sync.sleep-time-ms:30000}")
-    public void executeUnSynchronizedReadyCmHandlePoll() {
-        moduleOperationsUtils.getUnsynchronizedReadyCmHandles().forEach(unSynchronizedReadyCmHandle -> {
-            final String cmHandleId = unSynchronizedReadyCmHandle.getId();
-            if (hasPushedIntoSemaphoreMap(cmHandleId)) {
-                log.info("Executing data sync on {}", cmHandleId);
-                final CompositeState compositeState = inventoryPersistence
-                        .getCmHandleState(cmHandleId);
-                final String resourceData = moduleOperationsUtils.getResourceData(cmHandleId);
-                if (resourceData == null) {
-                    log.error("Error retrieving resource data for Cm-Handle: {}", cmHandleId);
-                } else {
-                    cpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId,
-                            resourceData, OffsetDateTime.now());
-                    setSyncStateToSynchronized().accept(compositeState);
-                    inventoryPersistence.saveCmHandleState(cmHandleId, compositeState);
-                    updateDataSyncSemaphoreMap(cmHandleId);
-                    log.info("Data sync finished for {}", cmHandleId);
-                }
-            } else {
-                log.info("{} already processed by another instance", cmHandleId);
+    public void executeUnsynchronizedReadyCmHandleForInitialDataSync() {
+        final List<YangModelCmHandle> unsynchronizedReadyCmHandles =
+                moduleOperationsUtils.getUnsynchronizedReadyCmHandles();
+        unsynchronizedReadyCmHandles.stream().map(YangModelCmHandle::getId).forEach(this::synchronizeCmHandle);
+    }
+
+    private void synchronizeCmHandle(final String cmHandleId) {
+
+        if (!hasPushedIntoSemaphoreMap(cmHandleId)) {
+            log.debug("{} already processed by another instance", cmHandleId);
+            return;
+        }
+
+        try {
+            performDataSynchronizationForCmHandle(cmHandleId);
+        } catch (final Exception exception) {
+            log.error("Failed to complete data sync for CM handle: {}", cmHandleId, exception);
+        }
+    }
+
+    private void performDataSynchronizationForCmHandle(final String cmHandleId) {
+        log.info("Executing data sync on {}", cmHandleId);
+        final CompositeState compositeState = inventoryPersistence.getCmHandleState(cmHandleId);
+        final Collection<String> rootNodeReferences =
+                cpsModuleService.getRootNodeReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId);
+
+        for (final String rootNodeReference : rootNodeReferences) {
+            synchronizeRootNodeReferences(cmHandleId, rootNodeReference);
+        }
+
+        setSyncStateToSynchronized().accept(compositeState);
+        inventoryPersistence.saveCmHandleState(cmHandleId, compositeState);
+        updateDataSyncSemaphoreMap(cmHandleId);
+        log.info("Data sync finished for {}", cmHandleId);
+    }
+
+    private void synchronizeRootNodeReferences(final String cmHandleId, final String rootNodeReference) {
+        final String options = String.format("(fields=%s)", rootNodeReference);
+
+        try {
+            final String resourceData = moduleOperationsUtils.getResourceData(cmHandleId, options);
+            if (resourceData == null) {
+                log.warn("No resource data found for CM handle: {} with options: {}", cmHandleId, options);
+                return;
             }
-        });
-        log.debug("No Cm-Handles currently found in READY State and Operational Sync State is UNSYNCHRONIZED");
+            cpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId, resourceData,
+                    OffsetDateTime.now());
+        } catch (final Exception exception) {
+            log.error("Failed to sync module and root node for CM handle: {} with options: {}", cmHandleId, options,
+                    exception);
+        }
     }
 
     private Consumer<CompositeState> setSyncStateToSynchronized() {
index 203198d..e7e6540 100644 (file)
@@ -56,6 +56,7 @@ public class ModuleOperationsUtils {
     private final CmHandleQueryService cmHandleQueryService;
     private final DmiDataOperations dmiDataOperations;
     private final JsonObjectMapper jsonObjectMapper;
+
     private static final String RETRY_ATTEMPT_KEY = "attempt";
     private static final String MODULE_SET_TAG_KEY = "moduleSetTag";
     public static final String MODULE_SET_TAG_MESSAGE_FORMAT = "Upgrade to ModuleSetTag: %s";
@@ -167,9 +168,9 @@ public class ModuleOperationsUtils {
      * @param cmHandleId cm handle id
      * @return optional string containing the resource data
      */
-    public String getResourceData(final String cmHandleId) {
+    public String getResourceData(final String cmHandleId, final String options) {
         final ResponseEntity<Object> resourceDataResponseEntity = dmiDataOperations.getAllResourceDataFromDmi(
-                cmHandleId, UUID.randomUUID().toString());
+                cmHandleId, UUID.randomUUID().toString(), options);
         if (resourceDataResponseEntity.getStatusCode().is2xxSuccessful()) {
             return getFirstResource(resourceDataResponseEntity.getBody());
         }
index 1f4da93..fa8a346 100644 (file)
@@ -151,11 +151,11 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
             mockYangModelCmHandleRetrieval([yangModelCmHandleProperty], 'my-module-set-tag')
         and: 'a positive response from DMI service when it is called with the expected parameters'
             def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK)
-            def expectedTemplateWithVariables = new UrlTemplateParameters('myServiceName/dmi/v1/ch/{cmHandleId}/data/ds/{datastore}?resourceIdentifier={resourceIdentifier}', ['resourceIdentifier': '/', 'datastore': 'ncmp-datastore:passthrough-operational', 'cmHandleId': cmHandleId])
+            def expectedTemplateWithVariables = new UrlTemplateParameters('myServiceName/dmi/v1/ch/{cmHandleId}/data/ds/{datastore}?resourceIdentifier={resourceIdentifier}&options={options}', ['resourceIdentifier': '/', 'datastore': 'ncmp-datastore:passthrough-operational', 'cmHandleId': cmHandleId, 'options': OPTIONS_PARAM])
             def expectedJson = '{"operation":"read","cmHandleProperties":{"prop1":"val1"},"moduleSetTag":"my-module-set-tag"}'
             mockDmiRestClient.synchronousPostOperationWithJsonData(DATA, expectedTemplateWithVariables, expectedJson, READ, null) >> responseFromDmi
         when: 'get resource data is invoked'
-            def result = objectUnderTest.getAllResourceDataFromDmi(cmHandleId, NO_REQUEST_ID)
+            def result = objectUnderTest.getAllResourceDataFromDmi(cmHandleId, NO_REQUEST_ID, OPTIONS_PARAM)
         then: 'the result is the response from the DMI service'
             assert result == responseFromDmi
     }
index ae6cb12..03a3029 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ package org.onap.cps.ncmp.impl.inventory.sync
 
 import com.hazelcast.map.IMap
 import org.onap.cps.api.CpsDataService
+import org.onap.cps.api.CpsModuleService
 import org.onap.cps.ncmp.api.inventory.models.CompositeState
 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence
@@ -34,34 +35,33 @@ import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_D
 class DataSyncWatchdogSpec extends Specification {
 
     def mockInventoryPersistence = Mock(InventoryPersistence)
-
+    def mockCpsModuleService = Mock(CpsModuleService)
     def mockCpsDataService = Mock(CpsDataService)
-
-    def mockSyncUtils = Mock(ModuleOperationsUtils)
-
+    def mockModuleOperationUtils = Mock(ModuleOperationsUtils)
     def mockDataSyncSemaphores = Mock(IMap<String,Boolean>)
 
     def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
 
-    def objectUnderTest = new DataSyncWatchdog(mockInventoryPersistence, mockCpsDataService, mockSyncUtils, mockDataSyncSemaphores)
+    def objectUnderTest = new DataSyncWatchdog(mockInventoryPersistence, mockCpsModuleService, mockCpsDataService, mockModuleOperationUtils, mockDataSyncSemaphores)
 
     def compositeState = getCompositeState()
-
     def yangModelCmHandle1 = createSampleYangModelCmHandle('cm-handle-1')
-
     def yangModelCmHandle2 = createSampleYangModelCmHandle('cm-handle-2')
 
     def 'Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED.'() {
         given: 'sample resource data'
             def resourceData = jsonString
         and: 'sync utilities returns a cm handle twice'
-            mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1, yangModelCmHandle2]
+            mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1, yangModelCmHandle2]
+        and: 'we have the module and root nodes references to form the options field'
+            mockCpsModuleService.getRootNodeReferences(_, 'cm-handle-1') >> ['some-module-1:some-root-node']
+            mockCpsModuleService.getRootNodeReferences(_, 'cm-handle-2') >> ['some-module-2:some-root-node']
         when: 'data sync poll is executed'
-            objectUnderTest.executeUnSynchronizedReadyCmHandlePoll()
+            objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync()
         then: 'the inventory persistence cm handle returns a composite state for the first cm handle'
             1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState
         and: 'the sync util returns first resource data'
-            1 * mockSyncUtils.getResourceData('cm-handle-1') >> resourceData
+            1 * mockModuleOperationUtils.getResourceData('cm-handle-1', '(fields=some-module-1:some-root-node)') >> resourceData
         and: 'the cm-handle data is saved'
             1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-1', jsonString, _)
         and: 'the first cm handle operational sync state is updated'
@@ -69,7 +69,7 @@ class DataSyncWatchdogSpec extends Specification {
         then: 'the inventory persistence cm handle returns a composite state for the second cm handle'
             1 * mockInventoryPersistence.getCmHandleState('cm-handle-2') >> compositeState
         and: 'the sync util returns first resource data'
-            1 * mockSyncUtils.getResourceData('cm-handle-2') >> resourceData
+            1 * mockModuleOperationUtils.getResourceData('cm-handle-2', '(fields=some-module-2:some-root-node)') >> resourceData
         and: 'the cm-handle data is saved'
             1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-2', jsonString, _)
         and: 'the second cm handle operational sync state is updated from "UNSYNCHRONIZED" to "SYNCHRONIZED"'
@@ -78,28 +78,68 @@ class DataSyncWatchdogSpec extends Specification {
 
     def 'Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED without resource data.'() {
         given: 'sync utilities returns a cm handle'
-            mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1]
+            mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1]
+        and: 'the module service returns the module and root nodes references to form the options field'
+            mockCpsModuleService.getRootNodeReferences(_,'cm-handle-1') >> ['some-module-1:some-root-node']
         when: 'data sync poll is executed'
-            objectUnderTest.executeUnSynchronizedReadyCmHandlePoll()
+            objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync()
         then: 'the inventory persistence cm handle returns a composite state for the first cm handle'
             1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState
         and: 'the sync util returns no resource data'
-            1 * mockSyncUtils.getResourceData('cm-handle-1') >> null
+            1 * mockModuleOperationUtils.getResourceData('cm-handle-1', '(fields=some-module-1:some-root-node)') >> null
         and: 'the cm-handle data is not saved'
             0 * mockCpsDataService.saveData(*_)
     }
 
     def 'Data Sync for Cm Handle that is already being processed.'() {
         given: 'sync utilities returns a cm handle'
-            mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1]
+            mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1]
+        and: 'the module service returns the module and root nodes references to form the options field'
+            mockCpsModuleService.getRootNodeReferences(_,'cm-handle-1') >> ['some-module-1:some-root-node']
         and: 'the shared data sync semaphore indicate it is already being processed'
             mockDataSyncSemaphores.putIfAbsent('cm-handle-1', _, _, _) >> 'something (not null)'
         when: 'data sync poll is executed'
-            objectUnderTest.executeUnSynchronizedReadyCmHandlePoll()
+            objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync()
         then: 'it is NOT processed e.g. state is not requested'
             0 * mockInventoryPersistence.getCmHandleState(*_)
     }
 
+    def 'Data sync handles exception during overall cm handle processing.'() {
+        given: 'sync utilities returns a cm handle'
+            mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1]
+        and: 'semaphore map allows processing'
+            mockDataSyncSemaphores.putIfAbsent('cm-handle-1', false, _, _) >> null
+        and: 'getting cm handle state throws exception'
+            mockInventoryPersistence.getCmHandleState('cm-handle-1') >> { throw new RuntimeException('some exception') }
+        when: 'data sync poll is executed'
+            objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync()
+        then: 'no exception is thrown'
+            noExceptionThrown()
+    }
+
+    def 'Data sync handles exception during resource data retrieval.'() {
+        given: 'sync utilities returns a cm handle'
+            mockModuleOperationUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1]
+        and: 'semaphore map allows processing'
+            mockDataSyncSemaphores.putIfAbsent('cm-handle-1', false, _, _) >> null
+        and: 'module operations returns module and root nodes references'
+            mockCpsModuleService.getRootNodeReferences(_,'cm-handle-1') >> ['some-module-1:some-root-node', 'some-module-2:some-root-node']
+        when: 'data sync poll is executed'
+            objectUnderTest.executeUnsynchronizedReadyCmHandleForInitialDataSync()
+        then: 'cm handle state is retrieved'
+            1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState
+        and: 'first module sync succeeds'
+            1 * mockModuleOperationUtils.getResourceData('cm-handle-1', '(fields=some-module-1:some-root-node)') >> jsonString
+            1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-1', jsonString, _)
+        and: 'second module sync throws exception'
+            1 * mockModuleOperationUtils.getResourceData('cm-handle-1', '(fields=some-module-2:some-root-node)') >> { throw new RuntimeException('Some network error') }
+        and: 'no data is saved for failed module'
+            0 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-1', _, _)
+        and: 'cm handle state is still updated (processing continues after module failure)'
+            1 * mockInventoryPersistence.saveCmHandleState('cm-handle-1', compositeState)
+            1 * mockDataSyncSemaphores.replace('cm-handle-1', true)
+    }
+
     def createSampleYangModelCmHandle(cmHandleId) {
         def compositeState = getCompositeState()
         return new YangModelCmHandle(id: cmHandleId, compositeState: compositeState)
index fdd7e47..5fa8961 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2024 Nordix Foundation
+ *  Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2022 Bell Canada
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,6 +26,7 @@ import ch.qos.logback.classic.Logger
 import ch.qos.logback.core.read.ListAppender
 import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.api.CpsModuleService
 import org.onap.cps.ncmp.api.inventory.models.CompositeState
 import org.onap.cps.ncmp.api.inventory.models.CompositeStateBuilder
 import org.onap.cps.ncmp.impl.data.DmiDataOperations
@@ -49,9 +50,7 @@ import static org.onap.cps.ncmp.api.inventory.models.LockReasonCategory.MODULE_U
 class ModuleOperationsUtilsSpec extends Specification{
 
     def mockCmHandleQueries = Mock(CmHandleQueryService)
-
     def mockDmiDataOperations = Mock(DmiDataOperations)
-
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
 
     def objectUnderTest = new ModuleOperationsUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper)
@@ -154,16 +153,16 @@ class ModuleOperationsUtilsSpec extends Specification{
             JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString)
         and: 'DMI operations are mocked to return a response based on the scenario'
             def responseEntity = new ResponseEntity<>(statusCode == HttpStatus.OK ? jsonNode : null, statusCode)
-            mockDmiDataOperations.getAllResourceDataFromDmi('cm-handle-123', _) >> responseEntity
+            mockDmiDataOperations.getAllResourceDataFromDmi('cm-handle-123', _, 'some options') >> responseEntity
         when: 'get resource data is called'
-            def result = objectUnderTest.getResourceData('cm-handle-123')
+            def actualResult = objectUnderTest.getResourceData('cm-handle-123', 'some options')
         then: 'the returned data matches the expected result'
-            assert result == expectedResult
+            assert actualResult == expectedResult
         where:
-            scenario                              | statusCode                       | expectedResult
-            'successful response'                 | HttpStatus.OK                    | '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
-            'response with not found status'      | HttpStatus.NOT_FOUND             | null
-            'response with internal server error' | HttpStatus.INTERNAL_SERVER_ERROR | null
+            scenario                              | statusCode                       || expectedResult
+            'successful response'                 | HttpStatus.OK                    || '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
+            'response with not found status'      | HttpStatus.NOT_FOUND             || null
+            'response with internal server error' | HttpStatus.INTERNAL_SERVER_ERROR || null
     }
 
         def 'Extract module set tag and number of attempt when lock reason contains #scenario'() {
index 2494be4..460f450 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020-2025 Nordix Foundation
+ *  Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2020-2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
@@ -171,4 +171,13 @@ public interface CpsModuleService {
      */
     void deleteAllUnusedYangModuleData(String dataspaceName);
 
+    /**
+     * Get the collection of concatenated module-name:root-node of the provided anchor in the given dataspace.
+     *
+     * @param dataspaceName dataspace name
+     * @param anchorName    anchor name
+     * @return collection of module and root nodes concatenated with : as separator
+     */
+    Collection<String> getRootNodeReferences(String dataspaceName, String anchorName);
+
 }
index e50325c..cf3e6bb 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020-2025 Nordix Foundation
+ *  Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2020-2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 Bell Canada
  *  Modifications Copyright (C) 2022 TechMahindra Ltd
@@ -38,6 +38,7 @@ import org.onap.cps.api.model.SchemaSet;
 import org.onap.cps.api.parameters.CascadeDeleteAllowed;
 import org.onap.cps.spi.CpsModulePersistenceService;
 import org.onap.cps.utils.CpsValidator;
+import org.onap.cps.utils.YangParser;
 import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder;
 import org.onap.cps.yang.YangTextSchemaSourceSet;
 import org.springframework.stereotype.Service;
@@ -52,6 +53,7 @@ public class CpsModuleServiceImpl implements CpsModuleService {
     private final CpsAnchorService cpsAnchorService;
     private final CpsValidator cpsValidator;
     private final TimedYangTextSchemaSourceSetBuilder timedYangTextSchemaSourceSetBuilder;
+    private final YangParser yangParser;
 
     @Override
     @Timed(value = "cps.module.service.schemaset.create",
@@ -179,6 +181,12 @@ public class CpsModuleServiceImpl implements CpsModuleService {
         cpsModulePersistenceService.deleteAllUnusedYangModuleData(dataspaceName);
     }
 
+    @Override
+    public Collection<String> getRootNodeReferences(final String dataspaceName, final String anchorName) {
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+        return yangParser.getRootNodeReferences(anchor);
+    }
+
     private boolean isCascadeDeleteProhibited(final CascadeDeleteAllowed cascadeDeleteAllowed) {
         return CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED == cascadeDeleteAllowed;
     }
index d998f09..dfce4bf 100644 (file)
@@ -27,6 +27,8 @@ import static org.onap.cps.utils.YangParserHelper.VALIDATE_ONLY;
 
 import io.micrometer.core.annotation.Timed;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.exceptions.DataValidationException;
@@ -116,6 +118,23 @@ public class YangParser {
         return convertToCpsPath(restConfStylePath, schemaContext);
     }
 
+    /**
+     * Get the collection of concatenated module-name:root-node of the provided anchor.
+     *
+     * @param anchor Anchor
+     * @return Concatentated module and root node
+     */
+    public Set<String> getRootNodeReferences(final Anchor anchor) {
+        final SchemaContext schemaContext = getSchemaContext(anchor);
+        return schemaContext.getModules()
+                       .stream()
+                       .flatMap(module ->
+                                module.getChildNodes()
+                                .stream().map(rootNode ->
+                                     module.getName() + ":" + rootNode.getQName().getLocalName()))
+                       .collect(Collectors.toSet());
+    }
+
     private SchemaContext getSchemaContext(final Anchor anchor) {
         return yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(), anchor.getSchemaSetName())
                        .getSchemaContext();
index 48db53c..21ec912 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020-2025 Nordix Foundation
+ *  Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2020-2021 Pantheon.tech
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
@@ -34,6 +34,7 @@ import org.onap.cps.api.model.Anchor
 import org.onap.cps.api.model.ModuleDefinition
 import org.onap.cps.api.model.ModuleReference
 import org.onap.cps.api.model.SchemaSet
+import org.onap.cps.utils.YangParser
 import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
 import org.onap.cps.yang.YangTextSchemaSourceSet
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
@@ -49,8 +50,9 @@ class CpsModuleServiceImplSpec extends Specification {
     def mockCpsAnchorService = Mock(CpsAnchorService)
     def mockCpsValidator = Mock(CpsValidator)
     def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder()
+    def mockYangParser = Mock(YangParser)
 
-    def objectUnderTest = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
+    def objectUnderTest = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder,mockYangParser)
 
     def 'Create schema set.'() {
         when: 'Create schema set method is invoked'
@@ -266,6 +268,16 @@ class CpsModuleServiceImplSpec extends Specification {
             1 * mockCpsModulePersistenceService.schemaSetExists('some-dataspace-name', 'some-schema-set-name')
     }
 
+    def 'Get module and root nodes'() {
+        given: 'an anchor'
+            def myAnchor = createAnchors(1)[0]
+            mockCpsAnchorService.getAnchor('my-dataspace', 'my-anchor-1') >> myAnchor
+        when: 'module and root nodes are fetched for my anchor'
+            objectUnderTest.getRootNodeReferences('my-dataspace', 'my-anchor-1')
+        then: 'the call is delegated to the yang parser with correct anchor'
+            1 * mockYangParser.getRootNodeReferences(myAnchor)
+    }
+
     def getModuleReferences() {
         return [new ModuleReference('some module name','some revision name')]
     }
index 4713283..5d1c937 100755 (executable)
@@ -45,7 +45,7 @@ class E2ENetworkSliceSpec extends Specification {
     def mockCpsValidator = Mock(CpsValidator)
     def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder()
     def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, timedYangTextSchemaSourceSetBuilder)
-    def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
+    def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder,yangParser)
     def mockCpsDataUpdateEventsProducer = Mock(CpsDataUpdateEventsProducer)
     def dataNodeFactory = new DataNodeFactoryImpl(yangParser)
     def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsDataUpdateEventsProducer, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser)
index 93ee182..ad661f6 100644 (file)
@@ -29,7 +29,9 @@ import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
 import org.onap.cps.yang.YangTextSchemaSourceSet
 import org.opendaylight.yangtools.yang.common.QName
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode
 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode
+import org.opendaylight.yangtools.yang.model.api.Module
 import org.opendaylight.yangtools.yang.model.api.SchemaContext
 import spock.lang.Specification
 
@@ -158,4 +160,20 @@ class YangParserSpec extends Specification {
             1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema')
     }
 
+    def 'Get module and root node references using an anchor'() {
+        given: 'a schema context with module and root node'
+            def mockModule = Mock(Module) {
+                getName() >> 'bookstore'
+            }
+            def mockRootNode = Mock(DataSchemaNode) {
+                getQName() >> QName.create('bookstore', 'book')
+            }
+            mockModule.getChildNodes() >> [mockRootNode]
+            mockSchemaContext.getModules() >> [mockModule]
+        when: 'we get module and root nodes references for the anchor'
+            def result = objectUnderTest.getRootNodeReferences(anchor)
+        then: 'the result contains expected module:rootnode combination'
+            assert result == ['bookstore:book'] as Set
+    }
+
 }