Data Sync Watchdog Process 00/129400/23
authorsourabh_sourabh <sourabh.sourabh@est.tech>
Mon, 20 Jun 2022 14:57:24 +0000 (15:57 +0100)
committersourabh_sourabh <sourabh.sourabh@est.tech>
Wed, 22 Jun 2022 11:20:19 +0000 (12:20 +0100)
- Get all the Cm Handles state in READY and Operational datastores sync state in UNSYNCHRONIZED
- Get a random Cm Handle
- Get the first resource data from the node
- Save the data in Cps Db
- Update the Operational datastores sync state to SYNCHRONIZED

Issue-ID: CPS-1052
Issue-ID: CPS-1053
Issue-ID: CPS-1054
Change-Id: I9a20391ef30e6d56c4d789a92b8bf42cd3756c62
Signed-off-by: Lathish <lathishbabu.ganesan@est.tech>
Signed-off-by: sourabh_sourabh <sourabh.sourabh@est.tech>
22 files changed:
cps-application/src/main/resources/application.yml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/RestOutputCmHandleStateMapper.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/mapper/RestOutputCmHandleStateMapperTest.groovy
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/inventory/CompositeState.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CompositeStateBuilder.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/SyncState.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/DataSyncWatchdog.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncWatchdog.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateBuilderSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/DataSyncSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/models/YangModelCmHandleSpec.groovy
cps-service/src/main/java/org/onap/cps/utils/JsonObjectMapper.java
cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy

index 2b4dc4b..802a18b 100644 (file)
@@ -158,6 +158,7 @@ dmi:
 timers:\r
     advised-modules-sync:\r
         sleep-time-ms: 30000\r
-\r
     locked-modules-sync:\r
-        sleep-time-ms: 300000
\ No newline at end of file
+        sleep-time-ms: 300000\r
+    cm-handle-data-sync:\r
+        sleep-time-ms: 30000\r
index 5f4b311..afad109 100644 (file)
@@ -55,7 +55,7 @@ public interface RestOutputCmHandleStateMapper {
 
         if (compositeStateDataStore.getOperationalDataStore() != null) {
             final SyncState operationalSyncState = new SyncState();
-            operationalSyncState.setState(compositeStateDataStore.getOperationalDataStore().getSyncState());
+            operationalSyncState.setState(compositeStateDataStore.getOperationalDataStore().getSyncState().name());
             operationalSyncState.setLastSyncTime(compositeStateDataStore.getOperationalDataStore().getLastSyncTime());
             dataStores.setOperational(operationalSyncState);
         }
index 036928f..7125810 100644 (file)
@@ -26,6 +26,7 @@ package org.onap.cps.ncmp.rest.controller
 import org.mapstruct.factory.Mappers
 import org.onap.cps.ncmp.api.inventory.CmHandleState
 import org.onap.cps.ncmp.api.inventory.CompositeState
+import org.onap.cps.ncmp.api.inventory.SyncState
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
 import org.onap.cps.ncmp.rest.mapper.RestOutputCmHandleStateMapper
 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
@@ -392,7 +393,7 @@ class NetworkCmProxyControllerSpec extends Specification {
     def dataStores() {
         DataStores.builder()
             .operationalDataStore(Operational.builder()
-                .syncState('NONE_REQUESTED')
+                .syncState(SyncState.NONE_REQUESTED)
                 .lastSyncTime(formattedDateAndTime.toString()).build()).build()
     }
 
index 22c9fe6..695ca5a 100644 (file)
@@ -24,6 +24,7 @@ import org.mapstruct.factory.Mappers
 import org.onap.cps.ncmp.api.inventory.CmHandleState
 import org.onap.cps.ncmp.api.inventory.CompositeStateBuilder
 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
+import org.onap.cps.ncmp.api.inventory.SyncState
 import org.onap.cps.ncmp.rest.model.RestOutputCmHandleState
 import spock.lang.Specification
 
@@ -43,7 +44,7 @@ class RestOutputCmHandleStateMapperTest extends Specification {
                 .withCmHandleState(CmHandleState.ADVISED)
                 .withLastUpdatedTime(formattedDateAndTime.toString())
                 .withLockReason(LockReasonCategory.LOCKED_MISBEHAVING, 'locked other details')
-                .withOperationalDataStores('SYNCHRONIZED', formattedDateAndTime).build()
+                .withOperationalDataStores(SyncState.SYNCHRONIZED, formattedDateAndTime).build()
         compositeState.setDataSyncEnabled(false)
         when: 'mapper is called'
             def result = objectUnderTest.toRestOutputCmHandleState(compositeState)
@@ -54,7 +55,7 @@ class RestOutputCmHandleStateMapperTest extends Specification {
             assert result.lastUpdateTime == formattedDateAndTime
             assert result.lockReason.reason == 'LOCKED_MISBEHAVING'
             assert result.lockReason.details == 'locked other details'
-            assert result.cmHandleState == CmHandleState.ADVISED.name()
+            assert result.cmHandleState == 'ADVISED'
             assert result.dataSyncState.operational.getState() != null
     }
 
index d0e17d4..d46d634 100644 (file)
@@ -75,21 +75,33 @@ public class DmiDataOperations extends DmiOperations {
                                                          final DataStoreEnum dataStore,
                                                          final String requestId,
                                                          final String topicParamInQuery) {
-        CpsValidator.validateNameCharacters(cmHandleId);
-        final YangModelCmHandle yangModelCmHandle =
-                inventoryPersistence.getYangModelCmHandle(cmHandleId);
+        final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId);
+        final String jsonBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle);
+        final String dmiResourceDataUrl = getDmiRequestUrl(cmHandleId, resourceId, optionsParamInQuery, dataStore,
+                topicParamInQuery, yangModelCmHandle);
+        final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState();
+        isCmHandleStateReady(yangModelCmHandle, cmHandleState);
+        return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonBody, READ);
+    }
+
+    /**
+     * This method fetches all the resource data from operational data store for given cm handle
+     * identifier using dmi client.
+     *
+     * @param cmHandleId network resource identifier
+     * @param dataStore  data store enum
+     * @param requestId  requestId for async responses
+     * @return {@code ResponseEntity} response entity
+     */
+    public ResponseEntity<Object> getResourceDataFromDmi(final String cmHandleId,
+                                                         final DataStoreEnum dataStore,
+                                                         final String requestId) {
+        final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId);
+        final String jsonBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle);
+        final String dmiResourceDataUrl = getDmiRequestUrl(cmHandleId, "/", null, dataStore,
+                null, yangModelCmHandle);
         final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState();
         isCmHandleStateReady(yangModelCmHandle, cmHandleState);
-        final DmiRequestBody dmiRequestBody = DmiRequestBody.builder()
-            .operation(READ)
-            .requestId(requestId)
-            .build();
-        dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties());
-        final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody);
-        final String dmiResourceDataUrl = dmiServiceUrlBuilder.getDmiDatastoreUrl(
-                dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery,
-                topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables(
-                        yangModelCmHandle, cmHandleId, dataStore));
         return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonBody, READ);
     }
 
@@ -109,23 +121,42 @@ public class DmiDataOperations extends DmiOperations {
                                                                              final OperationEnum operation,
                                                                              final String requestData,
                                                                              final String dataType) {
-        CpsValidator.validateNameCharacters(cmHandleId);
-        final YangModelCmHandle yangModelCmHandle =
-            inventoryPersistence.getYangModelCmHandle(cmHandleId);
+        final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId);
+        final String jsonBody = getDmiRequestBody(operation, null, requestData, dataType, yangModelCmHandle);
+        final String dmiUrl = getDmiRequestUrl(cmHandleId, resourceId, null, PASSTHROUGH_RUNNING,
+                null, yangModelCmHandle);
         final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState();
         isCmHandleStateReady(yangModelCmHandle, cmHandleState);
+        return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonBody, operation);
+    }
+
+    private YangModelCmHandle getYangModelCmHandle(final String cmHandleId) {
+        CpsValidator.validateNameCharacters(cmHandleId);
+        return inventoryPersistence.getYangModelCmHandle(cmHandleId);
+    }
+
+    private String getDmiRequestBody(final OperationEnum operation, final String requestId, final String requestData,
+                                     final String dataType, final YangModelCmHandle yangModelCmHandle) {
         final DmiRequestBody dmiRequestBody = DmiRequestBody.builder()
-            .operation(operation)
-            .data(requestData)
-            .dataType(dataType)
-            .build();
+                .operation(operation)
+                .requestId(requestId)
+                .data(requestData)
+                .dataType(dataType)
+                .build();
         dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties());
-        final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody);
-        final String dmiUrl =
-            dmiServiceUrlBuilder.getDmiDatastoreUrl(dmiServiceUrlBuilder.populateQueryParams(resourceId,
-                    null, null),
-                dmiServiceUrlBuilder.populateUriVariables(yangModelCmHandle, cmHandleId, PASSTHROUGH_RUNNING));
-        return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonBody, operation);
+        return jsonObjectMapper.asJsonString(dmiRequestBody);
+    }
+
+    private String getDmiRequestUrl(final String cmHandleId,
+                                      final String resourceId,
+                                      final String optionsParamInQuery,
+                                      final DataStoreEnum dataStore,
+                                      final String topicParamInQuery,
+                                      final YangModelCmHandle yangModelCmHandle) {
+        return dmiServiceUrlBuilder.getDmiDatastoreUrl(
+                dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery,
+                        topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables(
+                        yangModelCmHandle, cmHandleId, dataStore));
     }
 
     private void isCmHandleStateReady(final YangModelCmHandle yangModelCmHandle, final CmHandleState cmHandleState) {
index eeaa4cd..df303b5 100644 (file)
@@ -57,7 +57,8 @@ public class CompositeState {
     /**
      * Date and Time in the format of yyyy-MM-dd'T'HH:mm:ss.SSSZ
      */
-    public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+    private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter
+            .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
 
 
     /**
@@ -93,7 +94,7 @@ public class CompositeState {
     public static class Operational {
 
         @JsonProperty("sync-state")
-        private String syncState;
+        private SyncState syncState;
 
         @JsonProperty("last-sync-time")
         private String lastSyncTime;
index 4ab0cec..f4d9638 100644 (file)
@@ -98,7 +98,7 @@ public class CompositeStateBuilder {
      * @param lastSyncTime for the locked state
      * @return CompositeStateBuilder
      */
-    public CompositeStateBuilder withOperationalDataStores(final String syncState, final String lastSyncTime) {
+    public CompositeStateBuilder withOperationalDataStores(final SyncState syncState, final String lastSyncTime) {
         this.datastores = DataStores.builder().operationalDataStore(
             Operational.builder().syncState(syncState).lastSyncTime(lastSyncTime).build()).build();
         return this;
@@ -111,20 +111,20 @@ public class CompositeStateBuilder {
      * @return CompositeState
      */
     public CompositeStateBuilder fromDataNode(final DataNode dataNode) {
-        this.cmHandleState = CmHandleState.valueOf((String) dataNode.getLeaves()
-            .get("cm-handle-state"));
+        this.cmHandleState =  CmHandleState.valueOf((String) dataNode.getLeaves()
+                .get("cm-handle-state"));
         for (final DataNode stateChildNode : dataNode.getChildDataNodes()) {
             if (stateChildNode.getXpath().endsWith("/lock-reason")) {
                 this.lockReason = new LockReason(LockReasonCategory.valueOf(
-                    (String) stateChildNode.getLeaves().get("reason")),
-                    (String) stateChildNode.getLeaves().get("details"));
+                        (String) stateChildNode.getLeaves().get("reason")),
+                        (String) stateChildNode.getLeaves().get("details"));
             }
             if (stateChildNode.getXpath().endsWith("/datastores")) {
                 for (final DataNode dataStoreNodes : stateChildNode.getChildDataNodes()) {
                     Operational operationalDataStore = null;
                     if (dataStoreNodes.getXpath().contains("/operational")) {
                         operationalDataStore = Operational.builder()
-                            .syncState((String) dataStoreNodes.getLeaves().get("sync-state"))
+                            .syncState(SyncState.valueOf((String) dataStoreNodes.getLeaves().get("sync-state")))
                             .lastSyncTime((String) dataStoreNodes.getLeaves().get("last-sync-time"))
                             .build();
                     }
index ce34154..1985bd9 100644 (file)
@@ -93,14 +93,39 @@ public class InventoryPersistence {
     }
 
     /**
-     * Method to return cm handles from the cps path.
+     * Method to return data nodes representing the cm handles.
      *
      * @param cpsPath cps path for which the cmHandle is requested
-     * @return a list of cm handles
+     * @return a list of data nodes representing the cm handles.
      */
-    public List<DataNode> getCmHandlesByCpsPath(final String cpsPath) {
+    public List<DataNode> getCmHandleDataNodesByCpsPath(final String cpsPath) {
         return cpsDataPersistenceService.queryDataNodes(
-            NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cpsPath, FetchDescendantsOption.OMIT_DESCENDANTS);
+                NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cpsPath, FetchDescendantsOption.OMIT_DESCENDANTS);
+    }
+
+    /**
+     * Method which returns cm handles by the cm handle id and state.
+     * @param cmHandleId cm handle id
+     * @param cmHandleState cm handle state
+     * @return a list of cm handles
+     */
+    public List<DataNode> getCmHandlesByIdAndState(final String cmHandleId, final CmHandleState cmHandleState) {
+        return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME,
+                NCMP_DMI_REGISTRY_ANCHOR, "//cm-handles[@id='" + cmHandleId + "']/state[@cm-handle-state=\""
+                        + cmHandleState + "\"]/ancestor::cm-handles",
+                FetchDescendantsOption.OMIT_DESCENDANTS);
+    }
+
+    /**
+     * Method which returns cm handles by the operational sync state of cm handle.
+     * @param syncState sync state
+     * @return a list of cm handles
+     */
+    public List<DataNode> getCmHandlesByOperationalSyncState(final SyncState syncState) {
+        return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME,
+                NCMP_DMI_REGISTRY_ANCHOR, "//state/datastores"
+                        + "/operational[@sync-state=\"" + syncState + "\"]/ancestor::cm-handles",
+                FetchDescendantsOption.OMIT_DESCENDANTS);
     }
 
     /**
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/SyncState.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/SyncState.java
new file mode 100644 (file)
index 0000000..9c7a476
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 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.inventory;
+
+public enum SyncState {
+    SYNCHRONIZED, UNSYNCHRONIZED, NONE_REQUESTED
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/DataSyncWatchdog.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/DataSyncWatchdog.java
new file mode 100644 (file)
index 0000000..553db65
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 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.inventory.sync;
+
+import java.time.OffsetDateTime;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.api.CpsDataService;
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.onap.cps.ncmp.api.inventory.CompositeState;
+import org.onap.cps.ncmp.api.inventory.InventoryPersistence;
+import org.onap.cps.ncmp.api.inventory.SyncState;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class DataSyncWatchdog {
+
+    private final InventoryPersistence inventoryPersistence;
+
+    private final CpsDataService cpsDataService;
+
+    private final SyncUtils syncUtils;
+
+    /**
+     * Execute Cm Handle poll which queries the cm handle state in 'READY' and Operational Datastore Sync State in
+     * 'UNSYNCHRONIZED'.
+     */
+    @Scheduled(fixedDelayString = "${timers.cm-handle-data-sync.sleep-time-ms:30000}")
+    public void executeUnSynchronizedReadyCmHandlePoll() {
+        YangModelCmHandle unSynchronizedReadyCmHandle = syncUtils.getAnUnSynchronizedReadyCmHandle();
+        while (unSynchronizedReadyCmHandle != null) {
+            final String cmHandleId = unSynchronizedReadyCmHandle.getId();
+            log.debug("Cm-Handles found in READY and UNSYNCHRONIZED state: {}", cmHandleId);
+            final CompositeState compositeState = inventoryPersistence
+                    .getCmHandleState(cmHandleId);
+            final String resourceData = syncUtils.getResourceData(cmHandleId);
+            if (resourceData == null) {
+                log.debug("Error accessing the node for Cm-Handle: {}", cmHandleId);
+            } else {
+                cpsDataService.saveData("NFP-Operational", cmHandleId,
+                        resourceData, OffsetDateTime.now());
+            }
+            compositeState.setLastUpdateTimeNow();
+            compositeState.getDataStores()
+                    .setOperationalDataStore(CompositeState.Operational.builder()
+                            .syncState(SyncState.SYNCHRONIZED)
+                            .lastSyncTime(CompositeState.nowInSyncTimeFormat()).build());
+            inventoryPersistence.saveCmHandleState(cmHandleId, compositeState);
+            unSynchronizedReadyCmHandle = syncUtils.getAnUnSynchronizedReadyCmHandle();
+        }
+        log.debug("No Cm-Handles currently found in an READY State and Operational Sync State is UNSYNCHRONIZED");
+    }
+}
index d6aaa32..9cfa0a0 100644 (file)
@@ -86,7 +86,7 @@ public class ModuleSyncWatchdog {
      */
     @Scheduled(fixedDelayString = "${timers.locked-modules-sync.sleep-time-ms:300000}")
     public void executeLockedMisbehavingCmHandlePoll() {
-        final List<YangModelCmHandle> lockedMisbehavingCmHandles = syncUtils.getLockedMisbehavingCmHandles();
+        final List<YangModelCmHandle> lockedMisbehavingCmHandles = syncUtils.getLockedMisbehavingYangModelCmHandles();
         for (final YangModelCmHandle lockedMisbehavingModelCmHandle: lockedMisbehavingCmHandles) {
             final CompositeState updatedCompositeState = lockedMisbehavingModelCmHandle.getCompositeState();
             updatedCompositeState.setCmHandleState(CmHandleState.ADVISED);
index 22eeabb..b5456ab 100644 (file)
 
 package org.onap.cps.ncmp.api.inventory.sync;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.ImmutableMap;
 import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.UUID;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations;
+import org.onap.cps.ncmp.api.impl.operations.DmiOperations;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 import org.onap.cps.ncmp.api.inventory.CmHandleState;
 import org.onap.cps.ncmp.api.inventory.CompositeState;
 import org.onap.cps.ncmp.api.inventory.InventoryPersistence;
 import org.onap.cps.ncmp.api.inventory.LockReasonCategory;
+import org.onap.cps.ncmp.api.inventory.SyncState;
 import org.onap.cps.spi.model.DataNode;
-import org.springframework.stereotype.Component;
+import org.onap.cps.utils.JsonObjectMapper;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
 
 @Slf4j
-@Component
+@Service
 @RequiredArgsConstructor
 public class SyncUtils {
 
@@ -46,6 +57,10 @@ public class SyncUtils {
 
     private final InventoryPersistence inventoryPersistence;
 
+    private final DmiDataOperations dmiDataOperations;
+
+    private final JsonObjectMapper jsonObjectMapper;
+
     private static final Pattern retryAttemptPattern = Pattern.compile("^Attempt #(\\d+) failed:");
 
     /**
@@ -64,14 +79,38 @@ public class SyncUtils {
         return inventoryPersistence.getYangModelCmHandle(cmHandleId);
     }
 
+    /**
+     * First query data nodes for cm handles with CM Handle Operational Sync State in "UNSYNCHRONIZED" and
+     * randomly select a CM Handle and query the data nodes for CM Handle State in "READY".
+     *
+     * @return a random yang model cm handle with State in READY and Operation Sync State in "UNSYNCHRONIZED",
+     *         return null if not found
+     */
+    public YangModelCmHandle getAnUnSynchronizedReadyCmHandle() {
+        final List<DataNode> unSynchronizedCmHandles = inventoryPersistence
+                .getCmHandlesByOperationalSyncState(SyncState.UNSYNCHRONIZED);
+        if (unSynchronizedCmHandles.isEmpty()) {
+            return null;
+        }
+        Collections.shuffle(unSynchronizedCmHandles);
+        for (final DataNode cmHandle : unSynchronizedCmHandles) {
+            final String cmHandleId = cmHandle.getLeaves().get("id").toString();
+            final List<DataNode> readyCmHandles = inventoryPersistence
+                    .getCmHandlesByIdAndState(cmHandleId, CmHandleState.READY);
+            if (!readyCmHandles.isEmpty()) {
+                return inventoryPersistence.getYangModelCmHandle(cmHandleId);
+            }
+        }
+        return null;
+    }
 
     /**
      * Query data nodes for cm handles with an "LOCKED" cm handle state with reason LOCKED_MISBEHAVING".
      *
      * @return a random yang model cm handle with an ADVISED state, return null if not found
      */
-    public List<YangModelCmHandle> getLockedMisbehavingCmHandles() {
-        final List<DataNode> lockedCmHandleAsDataNodeList = inventoryPersistence.getCmHandlesByCpsPath(
+    public List<YangModelCmHandle> getLockedMisbehavingYangModelCmHandles() {
+        final List<DataNode> lockedCmHandleAsDataNodeList = inventoryPersistence.getCmHandleDataNodesByCpsPath(
             "//lock-reason[@reason=\"LOCKED_MISBEHAVING\"]/ancestor::cm-handles");
         return lockedCmHandleAsDataNodeList.stream()
             .map(cmHandle -> YangDataConverter.convertCmHandleToYangModel(cmHandle,
@@ -99,4 +138,27 @@ public class SyncUtils {
             .lockReasonCategory(lockReasonCategory).build());
     }
 
+    /**
+     * Get the Resourece Data from Node through DMI Passthrough service.
+     *
+     * @param cmHandleId cm handle id
+     * @return optional string containing the resource data
+     */
+    public String getResourceData(final String cmHandleId) {
+        final ResponseEntity<Object> resourceDataResponseEntity = dmiDataOperations.getResourceDataFromDmi(
+                cmHandleId, DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL,
+                UUID.randomUUID().toString());
+        if (resourceDataResponseEntity.getStatusCode().is2xxSuccessful()) {
+            return getFirstResource(resourceDataResponseEntity.getBody());
+        }
+        return null;
+    }
+
+    private String getFirstResource(final Object responseBody) {
+        final String jsonObjectAsString = jsonObjectMapper.asJsonString(responseBody);
+        final JsonNode overallJsonNode = jsonObjectMapper.convertToJsonNode(jsonObjectAsString);
+        final Iterator<Map.Entry<String, JsonNode>> overallJsonTreeMap = overallJsonNode.fields();
+        final Map.Entry<String, JsonNode> firstElement = overallJsonTreeMap.next();
+        return jsonObjectMapper.asJsonString(ImmutableMap.of(firstElement.getKey(), firstElement.getValue()));
+    }
 }
index b7ebf29..03825c2 100644 (file)
@@ -80,6 +80,20 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
             'datastore running with properties'    | [yangModelCmHandleProperty] | PASSTHROUGH_RUNNING     | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-running'     | '&options=(a=1,b=2)'
     }
 
+    def 'call get all resource data.'() {
+        given: 'the system returns a cm handle with a sample property'
+            mockYangModelCmHandleRetrieval([yangModelCmHandleProperty])
+        and: 'a positive response from DMI service when it is called with the expected parameters'
+            def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK)
+            def expectedUrl = dmiServiceBaseUrl + "passthrough-operational?resourceIdentifier=/"
+            mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}', READ) >> responseFromDmi
+            dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl
+        when: 'get resource data is invoked'
+            def result = objectUnderTest.getResourceDataFromDmi(cmHandleId, PASSTHROUGH_OPERATIONAL, NO_REQUEST_ID)
+        then: 'the result is the response from the DMI service'
+            assert result == responseFromDmi
+    }
+
     def 'Write data for pass-through:running datastore in DMI.'() {
         given: 'a cm handle for #cmHandleId'
             mockYangModelCmHandleRetrieval([yangModelCmHandleProperty])
index d6f4ba6..60fec6f 100644 (file)
@@ -47,11 +47,11 @@ class CompositeStateBuilderSpec extends Specification {
     def "Composite State Specification"() {
         when: 'using composite state builder '
             def compositeState = new CompositeStateBuilder().withCmHandleState(CmHandleState.ADVISED)
-                    .withLockReason(LockReasonCategory.LOCKED_MISBEHAVING,"").withOperationalDataStores("UNSYNCHRONIZED",
+                    .withLockReason(LockReasonCategory.LOCKED_MISBEHAVING,"").withOperationalDataStores(SyncState.UNSYNCHRONIZED,
                     formattedDateAndTime.toString()).withLastUpdatedTime(formattedDateAndTime).build()
         then: 'it matches expected cm handle state and data store sync state'
             assert compositeState.cmHandleState == CmHandleState.ADVISED
-            assert compositeState.dataStores.operationalDataStore.syncState == 'UNSYNCHRONIZED'
+            assert compositeState.dataStores.operationalDataStore.syncState == SyncState.UNSYNCHRONIZED
     }
 
     def "Build composite state from DataNode "() {
index 5387fc6..0a6f8c3 100644 (file)
@@ -54,7 +54,7 @@ class CompositeStateSpec extends Specification {
 
     def dataStores() {
         DataStores.builder().operationalDataStore(Operational.builder()
-            .syncState('NONE_REQUESTED')
+            .syncState(SyncState.NONE_REQUESTED)
             .lastSyncTime(formattedDateAndTime.toString()).build())
             .build()
     }
index e6346cb..578225e 100644 (file)
@@ -28,7 +28,6 @@ import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.FetchDescendantsOption
 import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.spi.model.DataNode
-import org.onap.cps.spi.model.DataNodeBuilder
 import org.onap.cps.utils.JsonObjectMapper
 import spock.lang.Shared
 import spock.lang.Specification
@@ -71,6 +70,9 @@ class InventoryPersistenceSpec extends Specification {
     @Shared
     def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
 
+    @Shared
+    def static sampleDataNodes = [new DataNode()]
+
     def "Retrieve CmHandle using datanode with #scenario."() {
         given: 'the cps data service returns a data node from the DMI registry'
             def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
@@ -148,13 +150,36 @@ class InventoryPersistenceSpec extends Specification {
         given: 'a cm handle state to query'
             def cmHandleState = CmHandleState.ADVISED
         and: 'cps data service returns a list of data nodes'
-            def dataNodes = [new DataNode()]
             mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
-                    '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> dataNodes
+                '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
         when: 'get cm handles by state is invoked'
             def result = objectUnderTest.getCmHandlesByState(cmHandleState)
         then: 'the returned result is a list of data nodes returned by cps data service'
-            assert result == dataNodes
+            assert result == sampleDataNodes
+    }
+
+    def 'Get Cm Handles By State and Cm-Handle Id'() {
+        given: 'a cm handle state to query'
+            def cmHandleState = CmHandleState.READY
+        and: 'cps data service returns a list of data nodes'
+            mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                '//cm-handles[@id=\'some-cm-handle\']/state[@cm-handle-state="'+ 'READY'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
+        when: 'get cm handles by state and id is invoked'
+            def result = objectUnderTest.getCmHandlesByIdAndState(cmHandleId, cmHandleState)
+        then: 'the returned result is a list of data nodes returned by cps data service'
+            assert result == sampleDataNodes
+    }
+
+    def 'Get Cm Handles By Operational Sync State : UNSYNCHRONIZED'() {
+        given: 'a cm handle state to query'
+            def cmHandleState = CmHandleState.READY
+        and: 'cps data service returns a list of data nodes'
+            mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
+        when: 'get cm handles by operational sync state as UNSYNCHRONIZED is invoked'
+            def result = objectUnderTest.getCmHandlesByOperationalSyncState(SyncState.UNSYNCHRONIZED)
+        then: 'the returned result is a list of data nodes returned by cps data service'
+            assert result == sampleDataNodes
     }
 
     def 'Retrieve cm handle by cps path '() {
@@ -166,7 +191,7 @@ class InventoryPersistenceSpec extends Specification {
                     cpsPath, OMIT_DESCENDANTS)
                     >> Arrays.asList(cmHandleDataNode)
         when: 'get cm handles by cps path is invoked'
-            def result = objectUnderTest.getCmHandlesByCpsPath(cpsPath)
+            def result = objectUnderTest.getCmHandleDataNodesByCpsPath(cpsPath)
         then: 'the returned result is a list of data nodes returned by cps data service'
             assert result.contains(cmHandleDataNode)
     }
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/DataSyncSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/DataSyncSpec.groovy
new file mode 100644 (file)
index 0000000..b062635
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 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.inventory.sync
+
+import org.onap.cps.api.CpsDataService
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
+import org.onap.cps.ncmp.api.inventory.CmHandleState
+import org.onap.cps.ncmp.api.inventory.CompositeState
+import org.onap.cps.ncmp.api.inventory.InventoryPersistence
+import org.onap.cps.ncmp.api.inventory.SyncState
+import spock.lang.Specification
+
+class DataSyncSpec extends Specification {
+
+    def mockInventoryPersistence = Mock(InventoryPersistence)
+
+    def mockCpsDataService = Mock(CpsDataService)
+
+    def mockSyncUtils = Mock(SyncUtils)
+
+    def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
+
+    def objectUnderTest = new DataSyncWatchdog(mockInventoryPersistence, mockCpsDataService, mockSyncUtils)
+
+    def compositeState = getCompositeState()
+
+    def yangModelCmHandle1 = createSampleYangModelCmHandle('some-cm-handle-1')
+
+    def yangModelCmHandle2 = createSampleYangModelCmHandle('some-cm-handle-2')
+
+    def 'Schedule Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED'() {
+        given: 'sample resource data'
+            def resourceData = jsonString;
+        and: 'sync utilities return a cm handle twice'
+            mockSyncUtils.getAnUnSynchronizedReadyCmHandle() >>> [yangModelCmHandle1, yangModelCmHandle2, null]
+        when: 'data sync poll is executed'
+            objectUnderTest.executeUnSynchronizedReadyCmHandlePoll()
+        then: 'the inventory persistence cm handle returns a composite state for the first cm handle'
+            1 * mockInventoryPersistence.getCmHandleState('some-cm-handle-1') >> compositeState
+        and: 'the sync util returns first resource data'
+            1 * mockSyncUtils.getResourceData('some-cm-handle-1') >> resourceData
+        and: 'the cm-handle data is saved'
+            1 * mockCpsDataService.saveData('NFP-Operational', 'some-cm-handle-1', jsonString, _)
+        and: 'the first cm handle operational sync state is updated'
+            1 * mockInventoryPersistence.saveCmHandleState('some-cm-handle-1', compositeState)
+        then: 'the inventory persistence cm handle returns a composite state for the second cm handle'
+            1 * mockInventoryPersistence.getCmHandleState('some-cm-handle-2') >> compositeState
+        and: 'the sync util returns first resource data'
+            1 * mockSyncUtils.getResourceData('some-cm-handle-2') >> resourceData
+        and: 'the cm-handle data is saved'
+            1 * mockCpsDataService.saveData('NFP-Operational', 'some-cm-handle-2', jsonString, _)
+        and: 'the second cm handle operational sync state is updated from "UNSYNCHRONIZED" to "SYNCHRONIZED"'
+            1 * mockInventoryPersistence.saveCmHandleState('some-cm-handle-2', compositeState)
+    }
+
+    def 'Schedule Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED which return empty data from Node'() {
+        given: 'cm handles in an ready state and operational sync state in unsynchronized'
+        and: 'sync utilities return a cm handle twice'
+            mockSyncUtils.getAnUnSynchronizedReadyCmHandle() >>> [yangModelCmHandle1, null]
+        when: 'data sync poll is executed'
+            objectUnderTest.executeUnSynchronizedReadyCmHandlePoll()
+        then: 'the inventory persistence cm handle returns a composite state for the first cm handle'
+            1 * mockInventoryPersistence.getCmHandleState('some-cm-handle-1') >> compositeState
+        and: 'the sync util returns first resource data'
+            1 * mockSyncUtils.getResourceData('some-cm-handle-1') >> null
+        and: 'the cm-handle data is not saved'
+            0 * mockCpsDataService.saveData('NFP-Operational', 'some-cm-handle-1', jsonString, _)
+    }
+
+    def createSampleYangModelCmHandle(cmHandleId) {
+        def compositeState = getCompositeState()
+        return new YangModelCmHandle(id: cmHandleId, compositeState: compositeState)
+    }
+
+    def getCompositeState() {
+        def cmHandleState = CmHandleState.READY
+        def compositeState = new CompositeState(cmHandleState: cmHandleState)
+        compositeState.setDataStores(CompositeState.DataStores.builder()
+            .operationalDataStore(CompositeState.Operational.builder().syncState(SyncState.SYNCHRONIZED)
+                .build()).build())
+        return compositeState
+    }
+}
index 802035c..8752bff 100644 (file)
@@ -106,7 +106,7 @@ class ModuleSyncSpec extends Specification {
                     .withLockReason(LockReasonCategory.LOCKED_MISBEHAVING, '').build()
             def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', compositeState: compositeState)
         and: 'sync utilities return a cm handle twice'
-            mockSyncUtils.getLockedMisbehavingCmHandles() >> [yangModelCmHandle, yangModelCmHandle]
+            mockSyncUtils.getLockedMisbehavingYangModelCmHandles() >> [yangModelCmHandle, yangModelCmHandle]
         when: 'module sync poll is executed'
             objectUnderTest.executeLockedMisbehavingCmHandlePoll()
         then: 'the first cm handle is updated to state "ADVISED" from "READY"'
index 15d1efe..14f2015 100644 (file)
 
 package org.onap.cps.ncmp.api.inventory.sync
 
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
+import org.onap.cps.ncmp.api.impl.operations.DmiOperations
 import org.onap.cps.ncmp.api.inventory.CmHandleState
 import org.onap.cps.ncmp.api.inventory.CompositeState
 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
+import org.onap.cps.ncmp.api.inventory.SyncState
 import org.onap.cps.spi.model.DataNode
+import org.onap.cps.utils.JsonObjectMapper
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
 import spock.lang.Shared
 import spock.lang.Specification
 
@@ -33,7 +41,11 @@ class SyncUtilsSpec extends Specification{
 
     def mockInventoryPersistence = Mock(InventoryPersistence)
 
-    def objectUnderTest = new SyncUtils(mockInventoryPersistence)
+    def mockDmiDataOperations = Mock(DmiDataOperations)
+
+    def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+
+    def objectUnderTest = new SyncUtils(mockInventoryPersistence, mockDmiDataOperations, jsonObjectMapper)
 
     @Shared
     def dataNode = new DataNode(leaves: ['id': 'cm-handle-123'])
@@ -67,15 +79,45 @@ class SyncUtilsSpec extends Specification{
             'does not exist' | null                                                                                         || 'Attempt #1 failed: new error message'
             'exists'         | CompositeState.LockReason.builder().details("Attempt #2 failed: some error message").build() || 'Attempt #3 failed: new error message'
     }
+
     def 'Get all locked Cm-Handle where Lock Reason is LOCKED_MISBEHAVING cm handle #scenario'() {
         given: 'the cps (persistence service) returns a collection of data nodes'
-            mockInventoryPersistence.getCmHandlesByCpsPath(
+            mockInventoryPersistence.getCmHandleDataNodesByCpsPath(
                     '//lock-reason[@reason="LOCKED_MISBEHAVING"]/ancestor::cm-handles') >> [dataNode ]
         when: 'get locked Misbehaving cm handle is called'
-            def result = objectUnderTest.getLockedMisbehavingCmHandles()
+            def result = objectUnderTest.getLockedMisbehavingYangModelCmHandles()
         then: 'the returned cm handle collection is the correct size'
             result.size() == 1
         and: 'the correct cm handle is returned'
             result[0].id == 'cm-handle-123'
     }
+
+    def 'Get a Cm-Handle where Operational Sync state is UnSynchronized and Cm-handle state is READY and #scenario'() {
+        given: 'the inventory persistence service returns a collection of data nodes'
+            mockInventoryPersistence.getCmHandlesByOperationalSyncState(SyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes
+            mockInventoryPersistence.getCmHandlesByIdAndState("cm-handle-123", CmHandleState.READY) >> readyDataNodes
+        when: 'get advised cm handle is called'
+            objectUnderTest.getAnUnSynchronizedReadyCmHandle()
+        then: 'the returned data node collection is the correct size'
+            readyDataNodes.size() == expectedDataNodeSize
+        and: 'get yang model cm handles is invoked the correct number of times'
+            expectedCallsToGetYangModelCmHandle * mockInventoryPersistence.getYangModelCmHandle('cm-handle-123')
+        where: 'the following scenarios are used'
+            scenario                             | unSynchronizedDataNodes | readyDataNodes || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize
+            'exists'                             | [dataNode]              | [dataNode]     || 1                                   | 1
+            'unsynchronized exist but not ready' | [dataNode]              | []             || 0                                   | 0
+            'does not exist'                     | []                      | []             || 0                                   | 0
+    }
+
+    def 'Get resource data through DMI Operations #scenario'() {
+        given: 'the inventory persistence service returns a collection of data nodes'
+            def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
+            JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString);
+            def responseEntity = new ResponseEntity<>(jsonNode, HttpStatus.OK)
+            mockDmiDataOperations.getResourceDataFromDmi('cm-handle-123', DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, _) >> responseEntity
+        when: 'get resource data is called'
+            def result = objectUnderTest.getResourceData('cm-handle-123')
+        then: 'the returned data is correct'
+            result == jsonString
+    }
 }
index cdfcf59..3376691 100644 (file)
@@ -22,7 +22,6 @@ package org.onap.cps.ncmp.api.models
 
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
 import org.onap.cps.ncmp.api.inventory.CmHandleState
-import org.onap.ncmp.cmhandle.lcm.event.Event
 import spock.lang.Specification
 
 import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.DATA
index 2459b51..338a841 100644 (file)
@@ -21,6 +21,7 @@
 package org.onap.cps.utils;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -87,4 +88,20 @@ public class JsonObjectMapper {
                     + "JSON content to specific class type.", e.getMessage());
         }
     }
+
+    /**
+     * Deserialize JSON content from given JSON content String to JsonNode.
+     *
+     * @param jsonContent   JSON content
+     * @return a json node
+     */
+    public JsonNode convertToJsonNode(final String jsonContent) {
+        try {
+            return objectMapper.readTree(jsonContent);
+        } catch (final JsonProcessingException e) {
+            log.error("Parsing error occurred while converting JSON content to Json Node.");
+            throw new DataValidationException("Parsing error occurred while converting "
+                    + "JSON content to Json Node.", e.getMessage());
+        }
+    }
 }
index f9b8feb..acb5241 100644 (file)
@@ -84,4 +84,22 @@ class JsonObjectMapperSpec extends Specification {
         then: 'an exception is thrown'
             thrown(DataValidationException)
     }
+
+    def 'Map a structurally compatible json String to JsonNode.'() {
+        given: 'Unstructured object'
+            def content = '{ "nest": { "birds": "bird" } }'
+        when: 'the object is mapped to string'
+            def result = jsonObjectMapper.convertToJsonNode(content);
+        then: 'the result is a valid JsonNode'
+            result.fieldNames().next() == "nest"
+    }
+
+    def 'Map a unstructured json String to JsonNode.'() {
+        given: 'Unstructured object'
+            def content = '{ "nest": { "birds": "bird" }] }'
+        when: 'the object is mapped to string'
+            jsonObjectMapper.convertToJsonNode(content);
+        then: 'a data validation exception is thrown'
+            thrown(DataValidationException)
+    }
 }