Move CPS REST business logic to CPS-Service Layer 19/140419/7
authorToineSiebelink <toine.siebelink@est.tech>
Mon, 10 Mar 2025 16:46:22 +0000 (16:46 +0000)
committerToineSiebelink <toine.siebelink@est.tech>
Thu, 13 Mar 2025 10:06:38 +0000 (10:06 +0000)
(scope limited to methods using PrefixResolver)

- Introduced CPSFacade (for methods invoking multiple CPS Services related to Prefix insertion)
- Introduced DataMapper to combine PrefixResolver & DataMapUtils
- Moved includeDecendants boolean to Enum conversion to Enum class
- Removed redundant tests from DataRestControllerSpec
- Removed redundant tests from QueryRestControllerSpec
- Cleaned up some legacy testware (adding assert etc)

Issue-ID: CPS-2428
Change-Id: Ib3b4dae941ada441be0dc76aaa5cd14e48685cf7
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
19 files changed:
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsFacade.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/api/parameters/FetchDescendantsOption.java
cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java
cps-service/src/main/java/org/onap/cps/utils/DataMapper.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java
cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy
cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy

index 2d33234..1b5dd2f 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2024 Nordix Foundation
+ *  Copyright (C) 2022-2025 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -47,7 +47,8 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle
      */
     public Collection<DataNode> executeRequest(final String cmHandleId, final String resourceIdentifier,
                                                  final boolean includeDescendants) {
-        final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants);
+        final FetchDescendantsOption fetchDescendantsOption
+            = FetchDescendantsOption.getFetchDescendantsOption(includeDescendants);
         return networkCmProxyQueryService.queryResourceDataOperational(cmHandleId, resourceIdentifier,
             fetchDescendantsOption);
     }
@@ -59,7 +60,8 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle
                                                       final String requestId,
                                                       final boolean includeDescendants,
                                                       final String authorization) {
-        final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants);
+        final FetchDescendantsOption fetchDescendantsOption
+            = FetchDescendantsOption.getFetchDescendantsOption(includeDescendants);
 
         final DataNode dataNode = cpsDataService.getDataNodes(cmResourceAddress.getDatastoreName(),
             cmResourceAddress.resolveCmHandleReferenceToId(),
@@ -68,8 +70,4 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle
         return Mono.justOrEmpty(dataNode);
     }
 
-    private static FetchDescendantsOption getFetchDescendantsOption(final boolean includeDescendants) {
-        return includeDescendants ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-            : FetchDescendantsOption.OMIT_DESCENDANTS;
-    }
 }
index be552ec..b6a2e42 100755 (executable)
@@ -2,7 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
- *  Modifications Copyright (C) 2021-2024 Nordix Foundation
+ *  Modifications Copyright (C) 2021-2025 Nordix Foundation
  *  Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
  *  ================================================================================
@@ -30,24 +30,19 @@ import io.micrometer.core.annotation.Timed;
 import jakarta.validation.ValidationException;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import lombok.RequiredArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
-import org.onap.cps.api.CpsAnchorService;
 import org.onap.cps.api.CpsDataService;
-import org.onap.cps.api.model.Anchor;
-import org.onap.cps.api.model.DataNode;
+import org.onap.cps.api.CpsFacade;
 import org.onap.cps.api.model.DeltaReport;
 import org.onap.cps.api.parameters.FetchDescendantsOption;
 import org.onap.cps.rest.api.CpsDataApi;
 import org.onap.cps.utils.ContentType;
-import org.onap.cps.utils.DataMapUtils;
 import org.onap.cps.utils.JsonObjectMapper;
-import org.onap.cps.utils.PrefixResolver;
 import org.onap.cps.utils.XmlFileUtils;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -64,10 +59,9 @@ public class DataRestController implements CpsDataApi {
     private static final String ISO_TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
     private static final DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_FORMAT);
 
+    private final CpsFacade cpsFacade;
     private final CpsDataService cpsDataService;
-    private final CpsAnchorService cpsAnchorService;
     private final JsonObjectMapper jsonObjectMapper;
-    private final PrefixResolver prefixResolver;
 
     @Override
     public ResponseEntity<String> createNode(final String apiVersion,
@@ -116,24 +110,20 @@ public class DataRestController implements CpsDataApi {
     }
 
     @Override
-    @Timed(value = "cps.data.controller.datanode.get.v1",
-            description = "Time taken to get data node")
+    @Timed(value = "cps.data.controller.datanode.get.v1", description = "Time taken to get data node")
     public ResponseEntity<Object> getNodeByDataspaceAndAnchor(final String dataspaceName,
                                                               final String anchorName,
                                                               final String xpath,
                                                               final Boolean includeDescendants) {
-        final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants)
-            ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS;
-        final DataNode dataNode = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath,
-            fetchDescendantsOption).iterator().next();
-        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
-        final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath());
-        return new ResponseEntity<>(DataMapUtils.toDataMapWithIdentifier(dataNode, prefix), HttpStatus.OK);
+        final FetchDescendantsOption fetchDescendantsOption =
+            FetchDescendantsOption.getFetchDescendantsOption(includeDescendants);
+        final Map<String, Object> dataNodeAsMap =
+            cpsFacade.getFirstDataNodeByAnchor(dataspaceName, anchorName, xpath, fetchDescendantsOption);
+        return new ResponseEntity<>(dataNodeAsMap, HttpStatus.OK);
     }
 
     @Override
-    @Timed(value = "cps.data.controller.datanode.get.v2",
-            description = "Time taken to get data node")
+    @Timed(value = "cps.data.controller.datanode.get.v2", description = "Time taken to get data node")
     public ResponseEntity<Object> getNodeByDataspaceAndAnchorV2(final String dataspaceName, final String anchorName,
                                                                 final String xpath,
                                                                 final String fetchDescendantsOptionAsString,
@@ -141,16 +131,9 @@ public class DataRestController implements CpsDataApi {
         final ContentType contentType = ContentType.fromString(contentTypeInHeader);
         final FetchDescendantsOption fetchDescendantsOption =
                 FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString);
-        final Collection<DataNode> dataNodes = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath,
-                fetchDescendantsOption);
-        final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size());
-        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
-        for (final DataNode dataNode: dataNodes) {
-            final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath());
-            final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
-            dataMaps.add(dataMap);
-        }
-        return buildResponseEntity(dataMaps, contentType);
+        final List<Map<String, Object>> dataNodesAsMaps =
+            cpsFacade.getDataNodesByAnchor(dataspaceName, anchorName, xpath, fetchDescendantsOption);
+        return buildResponseEntity(dataNodesAsMaps, contentType);
     }
 
     @Override
index f883309..11713ad 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2024 Nordix Foundation
+ *  Copyright (C) 2021-2025 Nordix Foundation
  *  Modifications Copyright (C) 2022 Bell Canada.
  *  Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
  *  ================================================================================
 package org.onap.cps.rest.controller;
 
 import io.micrometer.core.annotation.Timed;
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
-import org.onap.cps.api.CpsAnchorService;
-import org.onap.cps.api.CpsQueryService;
-import org.onap.cps.api.model.Anchor;
-import org.onap.cps.api.model.DataNode;
+import org.onap.cps.api.CpsFacade;
 import org.onap.cps.api.parameters.FetchDescendantsOption;
 import org.onap.cps.api.parameters.PaginationOption;
 import org.onap.cps.rest.api.CpsQueryApi;
 import org.onap.cps.utils.ContentType;
-import org.onap.cps.utils.DataMapUtils;
 import org.onap.cps.utils.JsonObjectMapper;
-import org.onap.cps.utils.PrefixResolver;
 import org.onap.cps.utils.XmlFileUtils;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -51,27 +43,24 @@ import org.springframework.web.bind.annotation.RestController;
 @RequiredArgsConstructor
 public class QueryRestController implements CpsQueryApi {
 
-    private final CpsQueryService cpsQueryService;
-    private final CpsAnchorService cpsAnchorService;
+    private final CpsFacade cpsFacade;
     private final JsonObjectMapper jsonObjectMapper;
-    private final PrefixResolver prefixResolver;
 
     @Override
-    @Timed(value = "cps.data.controller.datanode.query.v1",
-            description = "Time taken to query data nodes")
+    @Timed(value = "cps.data.controller.datanode.query.v1", description = "Time taken to query data nodes")
     public ResponseEntity<Object> getNodesByDataspaceAndAnchorAndCpsPath(final String dataspaceName,
                                                                          final String anchorName,
                                                                          final String cpsPath,
                                                                          final Boolean includeDescendants) {
-        final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants)
-            ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS;
-        return executeNodesByDataspaceQueryAndCreateResponse(dataspaceName, anchorName, cpsPath,
-                fetchDescendantsOption, ContentType.JSON);
+        final FetchDescendantsOption fetchDescendantsOption =
+            FetchDescendantsOption.getFetchDescendantsOption(includeDescendants);
+        final List<Map<String, Object>> dataNodesAsMaps
+            = cpsFacade.executeAnchorQuery(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
+        return buildResponseEntity(dataNodesAsMaps, ContentType.JSON);
     }
 
     @Override
-    @Timed(value = "cps.data.controller.datanode.query.v2",
-            description = "Time taken to query data nodes")
+    @Timed(value = "cps.data.controller.datanode.query.v2", description = "Time taken to query data nodes")
     public ResponseEntity<Object> getNodesByDataspaceAndAnchorAndCpsPathV2(final String dataspaceName,
                                                                            final String anchorName,
                                                                            final String cpsPath,
@@ -80,8 +69,9 @@ public class QueryRestController implements CpsQueryApi {
         final ContentType contentType = ContentType.fromString(contentTypeInHeader);
         final FetchDescendantsOption fetchDescendantsOption =
             FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString);
-        return executeNodesByDataspaceQueryAndCreateResponse(dataspaceName, anchorName, cpsPath,
-                fetchDescendantsOption, contentType);
+        final List<Map<String, Object>> dataNodesAsMaps
+            = cpsFacade.executeAnchorQuery(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
+        return buildResponseEntity(dataNodesAsMaps, contentType);
     }
 
     @Override
@@ -96,65 +86,21 @@ public class QueryRestController implements CpsQueryApi {
                 FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString);
         final PaginationOption paginationOption = (pageIndex == null || pageSize == null)
                 ? PaginationOption.NO_PAGINATION : new PaginationOption(pageIndex, pageSize);
-        final Collection<DataNode> dataNodes = cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName,
-                cpsPath, fetchDescendantsOption, paginationOption);
-        final List<Map<String, Object>> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size());
-        String prefix = null;
-        final Map<String, List<DataNode>> dataNodesPerAnchor = groupDataNodesPerAnchor(dataNodes);
-        for (final Map.Entry<String, List<DataNode>> dataNodesPerAnchorEntry : dataNodesPerAnchor.entrySet()) {
-            final String anchorName = dataNodesPerAnchorEntry.getKey();
-            if (prefix == null) {
-                final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
-                prefix = prefixResolver.getPrefix(anchor, dataNodesPerAnchorEntry.getValue().get(0).getXpath());
-            }
-            final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifierAndAnchor(
-                    dataNodesPerAnchorEntry.getValue(), anchorName, prefix);
-            dataNodesAsListOfMaps.add(dataMap);
-        }
-        final Integer totalPages = getTotalPages(dataspaceName, cpsPath, paginationOption);
-        return ResponseEntity.ok().header("total-pages",
-                totalPages.toString()).body(jsonObjectMapper.asJsonString(dataNodesAsListOfMaps));
-    }
+        final List<Map<String, Object>> dataNodesAsMaps
+            = cpsFacade.executeDataspaceQuery(dataspaceName, cpsPath, fetchDescendantsOption, paginationOption);
 
-    private Integer getTotalPages(final String dataspaceName, final String cpsPath,
-                                  final PaginationOption paginationOption) {
-        if (paginationOption == PaginationOption.NO_PAGINATION) {
-            return 1;
-        }
-        final int totalAnchors =  cpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath);
-        return totalAnchors <= paginationOption.getPageSize() ? 1
-                : (int) Math.ceil((double) totalAnchors / paginationOption.getPageSize());
-    }
-
-    private static Map<String, List<DataNode>> groupDataNodesPerAnchor(final Collection<DataNode> dataNodes) {
-        return dataNodes.stream().collect(Collectors.groupingBy(DataNode::getAnchorName));
-    }
-
-    private ResponseEntity<Object> executeNodesByDataspaceQueryAndCreateResponse(final String dataspaceName,
-             final String anchorName, final String cpsPath, final FetchDescendantsOption fetchDescendantsOption,
-                                                                                 final ContentType contentType) {
-        final Collection<DataNode> dataNodes =
-            cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
-        final List<Map<String, Object>> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size());
-        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
-        String prefix = null;
-        for (final DataNode dataNode : dataNodes) {
-            if (prefix == null) {
-                prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath());
-            }
-            final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
-            dataNodesAsListOfMaps.add(dataMap);
-        }
-        return buildResponseEntity(dataNodesAsListOfMaps, contentType);
+        final int totalPages = cpsFacade.countAnchorsInDataspaceQuery(dataspaceName, cpsPath, paginationOption);
+        return ResponseEntity.ok().header("total-pages", String.valueOf(totalPages))
+                .body(jsonObjectMapper.asJsonString(dataNodesAsMaps));
     }
 
-    private ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataNodesAsListOfMaps,
+    private ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataNodesAsMaps,
                                                final ContentType contentType) {
         final String responseData;
         if (ContentType.XML.equals(contentType)) {
-            responseData = XmlFileUtils.convertDataMapsToXml(dataNodesAsListOfMaps);
+            responseData = XmlFileUtils.convertDataMapsToXml(dataNodesAsMaps);
         } else {
-            responseData = jsonObjectMapper.asJsonString(dataNodesAsListOfMaps);
+            responseData = jsonObjectMapper.asJsonString(dataNodesAsMaps);
         }
         return new ResponseEntity<>(responseData, HttpStatus.OK);
     }
index f2f9624..e4cd8c4 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2024 Nordix Foundation
+ *  Copyright (C) 2021-2025 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
 package org.onap.cps.rest.controller
 
 import com.fasterxml.jackson.databind.ObjectMapper
-import groovy.json.JsonSlurper
-import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
-import org.onap.cps.api.parameters.FetchDescendantsOption
-import org.onap.cps.api.model.DataNode
-import org.onap.cps.impl.DataNodeBuilder
+import org.onap.cps.api.CpsFacade
 import org.onap.cps.impl.DeltaReportBuilder
 import org.onap.cps.utils.ContentType
 import org.onap.cps.utils.DateTimeUtility
 import org.onap.cps.utils.JsonObjectMapper
-import org.onap.cps.utils.PrefixResolver
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.beans.factory.annotation.Value
@@ -44,7 +39,6 @@ import org.springframework.http.HttpStatus
 import org.springframework.http.MediaType
 import org.springframework.mock.web.MockMultipartFile
 import org.springframework.test.web.servlet.MockMvc
-import org.springframework.web.multipart.MultipartFile
 import spock.lang.Shared
 import spock.lang.Specification
 
@@ -61,17 +55,14 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
 class DataRestControllerSpec extends Specification {
 
     @SpringBean
-    CpsDataService mockCpsDataService = Mock()
+    CpsFacade mockCpsFacade = Mock()
 
     @SpringBean
-    CpsAnchorService mockCpsAnchorService = Mock()
+    CpsDataService mockCpsDataService = Mock()
 
     @SpringBean
     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
 
-    @SpringBean
-    PrefixResolver prefixResolver = Mock()
-
     @Autowired
     MockMvc mvc
 
@@ -97,20 +88,7 @@ class DataRestControllerSpec extends Specification {
     def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
 
     @Shared
-    static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/parent-1')
-        .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
-
-    @Shared
-    static DataNode dataNodeWithLeavesNoChildren2 = new DataNodeBuilder().withXpath('/parent-2')
-        .withLeaves([leaf: 'value']).build()
-
-    @Shared
-    static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
-        .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
-
-    @Shared
-    static MultipartFile multipartYangFile = new MockMultipartFile("file", 'filename.yang', "text/plain", 'content'.getBytes())
-
+    def multipartYangFile = new MockMultipartFile("file", 'filename.yang', "text/plain", 'content'.getBytes())
 
     def setup() {
         dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName"
@@ -130,7 +108,7 @@ class DataRestControllerSpec extends Specification {
                 ).andReturn().response
         then: 'a created response is returned'
             response.status == HttpStatus.CREATED.value()
-        then: 'the java API was called with the correct parameters'
+        then: 'the cps data service was called with the correct parameters'
             1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, noTimestamp, expectedContentType)
         where: 'following xpath parameters are are used'
             scenario                                   | parentNodeXpath | contentType                | expectedContentType | requestBody     | expectedData
@@ -140,7 +118,7 @@ class DataRestControllerSpec extends Specification {
             'XML content: xpath parameter point root'  | '/'             | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
     }
 
-    def 'Create a node with observed-timestamp'() {
+    def 'Create a node with observed-timestamp.'() {
         given: 'endpoint to create a node'
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         when: 'post is invoked with datanode endpoint and json'
@@ -154,7 +132,7 @@ class DataRestControllerSpec extends Specification {
                 ).andReturn().response
         then: 'a created response is returned'
             response.status == expectedHttpStatus.value()
-        then: 'the java API was called with the correct parameters'
+        then: 'the cps data service was called with the correct parameters'
             expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData,
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
         where:
@@ -164,7 +142,7 @@ class DataRestControllerSpec extends Specification {
             'with invalid observed-timestamp' | 'invalid'                      | MediaType.APPLICATION_JSON | requestBodyJson || 0                | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
     }
 
-    def 'Validate data using create a node API'() {
+    def 'Validate data using create a node API.'() {
         given: 'an endpoint to create a node'
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
             def parentNodeXpath = '/'
@@ -181,11 +159,11 @@ class DataRestControllerSpec extends Specification {
                 ).andReturn().response
         then: 'a 200 OK response is returned'
             response.status == HttpStatus.OK.value()
-        then: 'the service was called with correct parameters'
+        then: 'the cps data service was called with correct parameters'
             1 * mockCpsDataService.validateData(dataspaceName, anchorName, parentNodeXpath, requestBodyJson, ContentType.JSON)
     }
 
-    def 'Create a child node #scenario'() {
+    def 'Create a child node #scenario.'() {
         given: 'endpoint to create a node'
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         and: 'parent node xpath'
@@ -201,7 +179,7 @@ class DataRestControllerSpec extends Specification {
                 mvc.perform(postRequestBuilder).andReturn().response
         then: 'a created response is returned'
             response.status == HttpStatus.CREATED.value()
-        then: 'the java API was called with the correct parameters'
+        then: 'the cps data service was called with the correct parameters'
             1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedData,
                 DateTimeUtility.toOffsetDateTime(observedTimestamp), expectedContentType)
         where:
@@ -251,10 +229,10 @@ class DataRestControllerSpec extends Specification {
             def response = mvc.perform(postRequestBuilder).andReturn().response
         then: 'a created response is returned'
             response.status == expectedHttpStatus.value()
-        then: 'the java API was called with the correct parameters'
+        then: 'the cps data service was called with the correct parameters when needed'
             expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, expectedData,
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
-        where:
+        where: 'the following parameters are used'
             scenario                                            | observedTimestamp              | contentType                | requestBody     || expectedApiCount | expectedHttpStatus     | expectedData     | expectedContentType
             'Content type JSON with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
             'Content type JSON without observed-timestamp'      | null                           | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
@@ -280,34 +258,14 @@ class DataRestControllerSpec extends Specification {
                 ).andReturn().response
         then: 'a 200 OK response is returned'
             response.status == HttpStatus.OK.value()
-        then: 'the service was called with correct parameters'
+        then: 'the cps data service was called with correct parameters'
             1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON)
     }
 
-    def 'Get data node with leaves'() {
-        given: 'the service returns data node leaves'
-            def xpath = 'parent-1'
-            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
-            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren]
-        when: 'get request is performed through REST API'
-            def response =
-                mvc.perform(get(endpoint).param('xpath', xpath))
-                    .andReturn().response
-        then: 'a success response is returned'
-            response.status == HttpStatus.OK.value()
-        then: 'the response contains the the datanode in json format'
-            response.getContentAsString() == '{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}'
-        and: 'response contains expected leaf and value'
-            response.contentAsString.contains('"leaf":"value"')
-        and: 'response contains expected leaf-list and values'
-            response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]')
-    }
-
-    def 'Get data node with #scenario.'() {
+    def 'Get data nodes [V1] with #scenario.'() {
         given: 'the service returns data node with #scenario'
-            def xpath = 'some xPath'
+            def xpath = 'my/path'
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
-            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode]
         when: 'get request is performed through REST API'
             def response =
                 mvc.perform(
@@ -315,121 +273,42 @@ class DataRestControllerSpec extends Specification {
                         .param('xpath', xpath)
                         .param('include-descendants', includeDescendantsOption))
                     .andReturn().response
+        then: 'the cps facade is called with the correct parameters'
+            1 * mockCpsFacade.getFirstDataNodeByAnchor(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [mocked:'result']
         then: 'a success response is returned'
             response.status == HttpStatus.OK.value()
-        and: 'the response contains the root node identifier: #expectedRootidentifier'
-            response.contentAsString.contains(expectedRootidentifier)
-        and: 'the response contains child is #expectChildInResponse'
-            response.contentAsString.contains('"child"') == expectChildInResponse
-        where:
-            scenario                    | dataNode                     | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
-            'no descendants by default' | dataNodeWithLeavesNoChildren | ''                       || OMIT_DESCENDANTS             | false                 | 'parent-1'
-            'no descendant explicitly'  | dataNodeWithLeavesNoChildren | 'false'                  || OMIT_DESCENDANTS             | false                 | 'parent-1'
-            'with descendants'          | dataNodeWithChild            | 'true'                   || INCLUDE_ALL_DESCENDANTS      | true                  | 'parent'
-    }
-
-    def 'Get all the data trees as json array with root node xPath using V2'() {
-        given: 'the service returns all data node leaves'
-            def xpath = '/'
-            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
-            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren, dataNodeWithLeavesNoChildren2]
-        when: 'V2 of get request is performed through REST API'
-            def response =
-                mvc.perform(get(endpoint)
-                    .contentType(MediaType.APPLICATION_JSON)
-                    .param('xpath', xpath))
-                    .andReturn().response
-        then: 'a success response is returned'
-            response.status == HttpStatus.OK.value()
-        and: 'the response contains the datanode in json array format'
-            response.getContentAsString() == '[{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}},' +
-                '{"parent-2":{"leaf":"value"}}]'
-        and: 'the json array contains expected number of data trees'
-            def numberOfDataTrees = new JsonSlurper().parseText(response.getContentAsString()).iterator().size()
-            assert numberOfDataTrees == 2
-    }
-
-    def 'Get all the data trees using V2 without Content-Type defaults to json'() {
-        given: 'the service returns all data node leaves'
-            def xpath = '/'
-            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
-            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren, dataNodeWithLeavesNoChildren2]
-        when: 'V2 of get request is performed through REST API without specifying content-type header'
-            def response =
-                    mvc.perform(get(endpoint)
-                            .param('xpath', xpath))
-                            .andReturn().response
-        then: 'a success response is returned'
-            response.status == HttpStatus.OK.value()
-        and: 'the response contains the datanode in json array format'
-            response.getContentAsString() == '[{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}},' +
-                    '{"parent-2":{"leaf":"value"}}]'
-    }
-
-    def 'Get all the data trees as XML with root node xPath using V2'() {
-        given: 'the service returns all data node leaves'
-            def xpath = '/'
-            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
-            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren]
-        when: 'V2 of get request is performed through REST API with XML content type'
-            def response =
-                mvc.perform(get(endpoint).contentType(MediaType.APPLICATION_XML).param('xpath', xpath))
-                    .andReturn().response
-        then: 'a success response is returned'
-            response.status == HttpStatus.OK.value()
-        and: 'the response contains the datanode in XML format'
-            response.getContentAsString() == '<parent-1><leaf>value</leaf><leafList>leaveListElement1</leafList><leafList>leaveListElement2</leafList></parent-1>'
+        and: 'the response contains the facade result in json format'
+            response.getContentAsString() == '{"mocked":"result"}'
+        where: 'the following parameters are used'
+            scenario                    | includeDescendantsOption || expectedCpsDataServiceOption
+            'no descendants (default) ' | ''                       || OMIT_DESCENDANTS
+            'with descendants'          | 'true'                   || INCLUDE_ALL_DESCENDANTS
     }
 
-    def 'Get data node with #scenario using V2.'() {
+    def 'Get data node with #scenario using V2. output type #scenario.'() {
         given: 'the service returns data nodes with #scenario'
             def xpath = 'some xPath'
             def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
-            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode]
         when: 'V2 of get request is performed through REST API'
             def response =
-                mvc.perform(
-                    get(endpoint)
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .param('xpath', xpath)
-                        .param('descendants', includeDescendantsOption))
-                    .andReturn().response
-        then: 'a success response is returned'
-            response.status == HttpStatus.OK.value()
-        and: 'the response contains the root node identifier: #expectedRootidentifier'
-            response.contentAsString.contains(expectedRootidentifier)
-        and: 'the response contains child is #expectChildInResponse'
-            response.contentAsString.contains('"child"') == expectChildInResponse
-        where:
-            scenario                    | dataNode                     | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
-            'no descendants by default' | dataNodeWithLeavesNoChildren | ''                       || OMIT_DESCENDANTS             | false                 | 'parent-1'
-            'no descendant explicitly'  | dataNodeWithLeavesNoChildren | '0'                      || OMIT_DESCENDANTS             | false                 | 'parent-1'
-            'with descendants'          | dataNodeWithChild            | '-1'                     || INCLUDE_ALL_DESCENDANTS      | true                  | 'parent'
-    }
-
-    def 'Get data node using v2 api'() {
-        given: 'the service returns data node'
-            def xpath = 'some xPath'
-            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
-            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, { descendantsOption -> {
-                assert descendantsOption.depth == 2}} as FetchDescendantsOption) >> [dataNodeWithChild]
-        when: 'get request is performed through REST API'
-            def response =
-                mvc.perform(
-                    get(endpoint)
-                        .contentType(MediaType.APPLICATION_JSON)
+                mvc.perform(get(endpoint)
+                        .contentType(contentType)
                         .param('xpath', xpath)
-                        .param('descendants', '2'))
+                        .param('descendants', 'all'))
                     .andReturn().response
-        then: 'a success response is returned'
+        then: 'the cps service facade is called with the correct parameters and returns some data'
+            1 * mockCpsFacade.getDataNodesByAnchor(dataspaceName, anchorName, xpath, INCLUDE_ALL_DESCENDANTS) >> [[mocked:'result1'], [mocked:'result2']]
+        and: 'a success response is returned'
             assert response.status == HttpStatus.OK.value()
-        and: 'the response contains the root node identifier'
-            assert response.contentAsString.contains('parent')
-        and: 'the response contains child is true'
-            assert response.contentAsString.contains('"child"')
+        and: 'the response is in the expected format'
+            assert response.contentAsString == expectedResult
+        where: 'the following content types are used'
+            scenario | contentType                || expectedResult
+            'XML'    | MediaType.APPLICATION_XML  || '<mocked>result1</mocked><mocked>result2</mocked>'
+            'JSON'   | MediaType.APPLICATION_JSON || '[{"mocked":"result1"},{"mocked":"result2"}]'
     }
 
-    def 'Get delta between two anchors'() {
+    def 'Get delta between two anchors.'() {
         given: 'the service returns a list containing delta reports'
             def deltaReports = new DeltaReportBuilder().actionReplace().withXpath('some xpath').withSourceData('some key': 'some value').withTargetData('some key': 'some value').build()
             def xpath = 'some xpath'
@@ -468,7 +347,7 @@ class DataRestControllerSpec extends Specification {
             assert response.contentAsString.contains("[{\"action\":\"create\",\"xpath\":\"some xpath\"}]")
     }
 
-    def 'Get delta between anchor and JSON payload without multipart file'() {
+    def 'Get delta between anchor and JSON payload without multipart file.'() {
         given: 'sample delta report, xpath, and json payload'
             def deltaReports = new DeltaReportBuilder().actionRemove().withXpath('some xpath').build()
             def xpath = 'some xpath'
@@ -499,7 +378,7 @@ class DataRestControllerSpec extends Specification {
                         .content(requestBody)
                         .param('xpath', inputXpath)
                 ).andReturn().response
-        then: 'the service method is invoked with expected parameters'
+        then: 'the cps data service method is invoked with expected parameters'
             1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, expectedData, null, expectedContentType)
         and: 'response status indicates success'
             response.status == HttpStatus.OK.value()
@@ -513,7 +392,7 @@ class DataRestControllerSpec extends Specification {
             'XML content: some xpath by parent'  | '/some/xpath' | MediaType.APPLICATION_XML  || '/some/xpath'         | requestBodyXml  | expectedXmlData     | ContentType.XML
     }
 
-    def 'Update data node leaves with observedTimestamp'() {
+    def 'Update data node leaves with observedTimestamp.'() {
         given: 'endpoint to update a node leaves '
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         when: 'patch request is performed'
@@ -525,7 +404,7 @@ class DataRestControllerSpec extends Specification {
                         .param('xpath', '/')
                         .param('observed-timestamp', observedTimestamp)
                 ).andReturn().response
-        then: 'the service method is invoked with expected parameters'
+        then: 'the cps data service method is invoked with expected parameters'
             expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', expectedJsonData,
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON)
         and: 'response status indicates success'
@@ -536,7 +415,7 @@ class DataRestControllerSpec extends Specification {
             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
     }
 
-    def 'Validate data using Update a node API'() {
+    def 'Validate data using Update a node API.'() {
         given: 'endpoint to update a node leaves'
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         and: 'dryRunEnabled flag is set to true'
@@ -552,7 +431,7 @@ class DataRestControllerSpec extends Specification {
                 ).andReturn().response
         then: 'a 200 OK response is returned'
             response.status == HttpStatus.OK.value()
-        then: 'the service was called with correct parameters'
+        then: 'the cps data service was called with correct parameters'
             1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON)
     }
 
@@ -567,7 +446,7 @@ class DataRestControllerSpec extends Specification {
                         .content(requestBody)
                         .param('xpath', inputXpath))
                     .andReturn().response
-        then: 'the service method is invoked with expected parameters'
+        then: 'the cps data service method is invoked with expected parameters'
             1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedData, noTimestamp, expectedContentType)
         and: 'response status indicates success'
             response.status == HttpStatus.OK.value()
@@ -581,7 +460,7 @@ class DataRestControllerSpec extends Specification {
             'XML content: some xpath by parent'  | '/some/xpath' | MediaType.APPLICATION_XML  || '/some/xpath'         | requestBodyXml  | expectedXmlData  | ContentType.XML
     }
 
-    def 'Validate data using Replace data node API'() {
+    def 'Validate data using Replace data node API.'() {
         given: 'endpoint to replace node'
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         and: 'dryRunEnabled flag is set to true'
@@ -597,7 +476,7 @@ class DataRestControllerSpec extends Specification {
                 ).andReturn().response
         then: 'a 200 OK response is returned'
             response.status == HttpStatus.OK.value()
-        then: 'the service was called with correct parameters'
+        then: 'the cps data service was called with correct parameters'
             1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON)
     }
 
@@ -613,7 +492,7 @@ class DataRestControllerSpec extends Specification {
                         .param('xpath', '')
                         .param('observed-timestamp', observedTimestamp))
                     .andReturn().response
-        then: 'the service method is invoked with expected parameters'
+        then: 'the cps data service method is invoked with expected parameters'
             expectedApiCount * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData,
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON)
         and: 'response status indicates success'
@@ -635,7 +514,7 @@ class DataRestControllerSpec extends Specification {
             def response = mvc.perform(putRequestBuilder).andReturn().response
         then: 'a success response is returned'
             response.status == expectedHttpStatus.value()
-        and: 'the java API was called with the correct parameters'
+        and: 'the cps data service was called with the correct parameters'
             expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData,
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON)
         where:
@@ -656,7 +535,7 @@ class DataRestControllerSpec extends Specification {
             def response = mvc.perform(putRequestBuilder).andReturn().response
         then: 'a success response is returned'
             response.status == expectedHttpStatus.value()
-        and: 'the java API was called with the correct parameters'
+        and: 'the cps data service was called with the correct parameters'
             expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedXmlData,
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.XML)
         where:
@@ -666,7 +545,7 @@ class DataRestControllerSpec extends Specification {
             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
     }
 
-    def 'Validate data using Replace list content API'() {
+    def 'Validate data using Replace list content API.'() {
         given: 'endpoint to replace list-nodes'
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes"
         and: 'dryRunEnabled flag is set to true'
@@ -682,7 +561,7 @@ class DataRestControllerSpec extends Specification {
                 ).andReturn().response
         then: 'a 200 OK response is returned'
             response.status == HttpStatus.OK.value()
-        then: 'the service was called with correct parameters'
+        then: 'the cps data service was called with correct parameters'
             1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON)
     }
 
@@ -695,7 +574,7 @@ class DataRestControllerSpec extends Specification {
             def response = mvc.perform(deleteRequestBuilder).andReturn().response
         then: 'a success response is returned'
             response.status == expectedHttpStatus.value()
-        and: 'the java API was called with the correct parameters'
+        and: 'the cps data service was called with the correct parameters'
             expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
         where:
@@ -717,7 +596,7 @@ class DataRestControllerSpec extends Specification {
             def response = mvc.perform(deleteDataNodeRequest).andReturn().response
         then: 'a successful response is returned'
             response.status == expectedHttpStatus.value()
-        and: 'the api is called with the correct parameters'
+        and: 'the cps data service is called with the correct parameters'
             expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
         where:
index 2b5c471..5f6de2e 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2024 Nordix Foundation
+ *  Copyright (C) 2021-2025 Nordix Foundation
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
 package org.onap.cps.rest.controller
 
 import com.fasterxml.jackson.databind.ObjectMapper
-import org.onap.cps.api.CpsAnchorService
-import org.onap.cps.api.CpsQueryService
+import org.onap.cps.api.CpsFacade
 import org.onap.cps.api.parameters.PaginationOption
-import org.onap.cps.impl.DataNodeBuilder
 import org.onap.cps.utils.JsonObjectMapper
-import org.onap.cps.utils.PrefixResolver
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.beans.factory.annotation.Value
@@ -39,216 +36,98 @@ import org.springframework.http.MediaType
 import org.springframework.test.web.servlet.MockMvc
 import spock.lang.Specification
 
-import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY
 import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
+import static org.onap.cps.api.parameters.PaginationOption.NO_PAGINATION
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 
 @WebMvcTest(QueryRestController)
 class QueryRestControllerSpec extends Specification {
 
     @SpringBean
-    CpsQueryService mockCpsQueryService = Mock()
-
-    @SpringBean
-    CpsAnchorService mockCpsAnchorService = Mock()
+    CpsFacade mockCpsFacade = Mock()
 
     @SpringBean
     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
 
-    @SpringBean
-    PrefixResolver prefixResolver = Mock()
-
     @Autowired
     MockMvc mvc
 
     @Value('${rest.api.cps-base-path}')
     def basePath
 
-    def dataspaceName = 'my_dataspace'
-    def anchorName = 'my_anchor'
-    def cpsPath = 'some cps-path'
-    def dataNodeEndpointV2
-
-    def setup() {
-         dataNodeEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName/anchors/$anchorName/nodes/query"
-    }
+    def dataNodeAsMap = ['prefixedPath':[path:[leaf:'value']]]
 
-    def 'Query data node by cps path for the given dataspace and anchor with #scenario.'() {
-        given: 'service method returns a list containing a data node'
-            def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
-                    .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
-            mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, expectedCpsDataServiceOption) >> [dataNode1, dataNode1]
-        and: 'the query endpoint'
-            def dataNodeEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors/$anchorName/nodes/query"
+    def 'Query data node (v1) by cps path for the given dataspace and anchor with #scenario.'() {
+        given: 'the query endpoint'
+            def dataNodeEndpoint = "$basePath/v1/dataspaces/my_dataspace/anchors/my_anchor/nodes/query"
         when: 'query data nodes API is invoked'
-            def response =
-                    mvc.perform(
-                            get(dataNodeEndpoint)
-                                    .param('cps-path', cpsPath)
-                                    .param('include-descendants', includeDescendantsOption))
-                            .andReturn().response
+            def response = mvc.perform(get(dataNodeEndpoint).param('cps-path', 'my/path').param('include-descendants', includeDescendantsOption))
+                .andReturn().response
+        then: 'the call is delegated to the cps service facade which returns a list containing one data node as a map'
+            1 * mockCpsFacade.executeAnchorQuery('my_dataspace', 'my_anchor', 'my/path', expectedCpsDataServiceOption) >> [dataNodeAsMap]
         then: 'the response contains the the datanode in json format'
-            response.status == HttpStatus.OK.value()
-            response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
+            assert response.status == HttpStatus.OK.value()
+            assert response.getContentAsString() == '[{"prefixedPath":{"path":{"leaf":"value"}}}]'
         where: 'the following options for include descendants are provided in the request'
             scenario                    | includeDescendantsOption || expectedCpsDataServiceOption
             'no descendants by default' | ''                       || OMIT_DESCENDANTS
-            'no descendant explicitly'  | 'false'                  || OMIT_DESCENDANTS
             'descendants'               | 'true'                   || INCLUDE_ALL_DESCENDANTS
     }
 
-    def 'Query data node v2 API by cps path for the given dataspace and anchor with #scenario and media type JSON'() {
-        given: 'service method returns a list containing a data node'
-            def dataNode = new DataNodeBuilder().withXpath('/xpath')
-                .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
-            mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption ->
-                assert descendantsOption.depth == expectedDepth
-            }) >> [dataNode, dataNode]
+    def 'Query data node (v2) by cps path for given dataspace and anchor with #scenario'() {
+        given: 'the query endpoint'
+            def dataNodeEndpointV2  = "$basePath/v2/dataspaces/my_dataspace/anchors/my_anchor/nodes/query"
         when: 'query data nodes API is invoked'
-            def response =
-                mvc.perform(
-                    get(dataNodeEndpointV2)
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .param('cps-path', cpsPath)
-                        .param('descendants', includeDescendantsOptionString))
+            def response = mvc.perform(get(dataNodeEndpointV2).contentType(contentType).param('cps-path', 'my/path') .param('descendants', includeDescendantsOptionString))
                     .andReturn().response
-        then: 'the response contains the datanode in the expected JSON format'
+        then: 'the call is delegated to the cps service facade which returns a list containing one data node as a map'
+            1 * mockCpsFacade.executeAnchorQuery('my_dataspace', 'my_anchor', 'my/path',
+                { descendantsOption -> assert descendantsOption.depth == expectedDepth }) >> [dataNodeAsMap]
+        and: 'the response contains the datanode in the expected format'
             assert response.status == HttpStatus.OK.value()
-            assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
+            assert response.getContentAsString() == expectedOutput
         where: 'the following options for include descendants are provided in the request'
-            scenario          | includeDescendantsOptionString || expectedDepth
-            'direct children' | 'direct'                       || 1
-            'descendants'     | '2'                            || 2
+            scenario               | includeDescendantsOptionString | contentType                || expectedDepth || expectedOutput
+            'direct children JSON' | 'direct'                       | MediaType.APPLICATION_JSON || 1             || '[{"prefixedPath":{"path":{"leaf":"value"}}}]'
+            'descendants JSON'     | '2'                            | MediaType.APPLICATION_JSON || 2             || '[{"prefixedPath":{"path":{"leaf":"value"}}}]'
+            'descendants XML'      | '2'                            | MediaType.APPLICATION_XML  || 2             || '<prefixedPath><path><leaf>value</leaf></path></prefixedPath>'
     }
 
-    def 'Query data node v2 API by cps path for the given dataspace and anchor with #scenario and media type XML'() {
-        given: 'service method returns a list containing a data node'
-            def dataNode = new DataNodeBuilder().withXpath('/xpath')
-                .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
-            mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption ->
-                assert descendantsOption.depth == expectedDepth
-            }) >> [dataNode, dataNode]
+    def 'Query data node by cps path for given dataspace across all anchors'() {
+        given: 'the query endpoint'
+            def dataNodeEndpoint = "$basePath/v2/dataspaces/my_dataspace/nodes/query"
+        and: 'the  cps service facade will say there are 123 pages '
+            mockCpsFacade.countAnchorsInDataspaceQuery('my_dataspace', 'my/path', new PaginationOption(2,5) ) >> 123
         when: 'query data nodes API is invoked'
-            def response =
-                mvc.perform(
-                    get(dataNodeEndpointV2)
-                        .contentType(MediaType.APPLICATION_XML)
-                        .param('cps-path', cpsPath)
-                        .param('descendants', includeDescendantsOptionString))
-                    .andReturn().response
-        then: 'the response contains the datanode in the expected XML format'
-            assert response.status == HttpStatus.OK.value()
-            assert response.getContentAsString().contains('<xpath><leaf>value</leaf><leafList>leaveListElement1</leafList><leafList>leaveListElement2</leafList></xpath>')
-        where: 'the following options for include descendants are provided in the request'
-            scenario          | includeDescendantsOptionString || expectedDepth
-            'direct children' | 'direct'                       || 1
-            'descendants'     | '2'                            || 2
-    }
-
-    def 'Query data node by cps path for the given dataspace across all anchors 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: 'second data node for the same anchor'
-            def dataNode3 = new DataNodeBuilder().withXpath('/xpath')
-                .withAnchor('my_anchor_2')
-                .withLeaves([leaf: 'value', leafList: ['leaveListElement5', 'leaveListElement6']]).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,
-                expectedCpsDataServiceOption, PaginationOption.NO_PAGINATION) >> [dataNode1, dataNode2, dataNode3]
-            mockCpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath) >> 2
-        when: 'query data nodes API is invoked'
-            def response =
-                mvc.perform(
-                        get(dataNodeEndpoint)
-                                .param('cps-path', cpsPath)
-                                .param('descendants', includeDescendantsOptionString))
+            def response = mvc.perform(
+                        get(dataNodeEndpoint).param('cps-path', 'my/path').param('pageIndex', String.valueOf(2)).param('pageSize', String.valueOf(5)))
                         .andReturn().response
-        then: 'the response contains the the datanode in json format'
-            response.status == HttpStatus.OK.value()
-            response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
-            response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement3","leaveListElement4"]}}')
-            response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement5","leaveListElement6"]}}')
-        where: 'the following options for include descendants are provided in the request'
-            scenario                    | includeDescendantsOptionString || expectedCpsDataServiceOption
-            'no descendants by default' | ''                             || OMIT_DESCENDANTS
-            'no descendant explicitly'  | 'none'                         || OMIT_DESCENDANTS
-            'descendants'               | 'all'                          || INCLUDE_ALL_DESCENDANTS
-            'direct children'           | 'direct'                       || DIRECT_CHILDREN_ONLY
-    }
-
-    def 'Query data node by cps path for the given dataspace across all anchors with pagination #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, new PaginationOption(pageIndex,pageSize)) >> [dataNode1, dataNode2]
-            mockCpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath) >> totalAnchors
-        when: 'query data nodes API is invoked'
-            def response =
-                mvc.perform(
-                        get(dataNodeEndpoint)
-                                .param('cps-path', cpsPath)
-                                .param('descendants', "all")
-                                .param('pageIndex', String.valueOf(pageIndex))
-                                .param('pageSize', String.valueOf(pageSize)))
-                        .andReturn().response
-        then: 'the response contains the the datanode in json format'
+        then: 'the call is delegated to the cps service facade which returns a list containing one data node as a map'
+            1 * mockCpsFacade.executeDataspaceQuery('my_dataspace', 'my/path', OMIT_DESCENDANTS, new PaginationOption(2,5)) >> [dataNodeAsMap]
+        then: 'the response is OK and contains the the datanode in json format'
             assert response.status == HttpStatus.OK.value()
-            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 || expectedTotalPageSize
-            '1st page with all anchors'  | 1         | 3        | 3            || 1
-            '1st page with less anchors' | 1         | 2        | 3            || 2
+            assert response.getContentAsString() == '[{"prefixedPath":{"path":{"leaf":"value"}}}]'
+        and: 'the header indicates the correct number of pages'
+            assert response.getHeaderValue('total-pages') == '123'
     }
 
-    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
+    def 'Query data node across all anchors with pagination option with #scenario i.e. no pagination.'() {
+        given: 'the query endpoint'
+            def dataNodeEndpoint = "$basePath/v2/dataspaces/my_dataspace/nodes/query"
+        and: 'the  cps service facade will say there is 1 page '
+            mockCpsFacade.countAnchorsInDataspaceQuery('my_dataspace', 'my/path', NO_PAGINATION ) >> 1
         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'
+            def response = mvc.perform(get(dataNodeEndpoint).param('cps-path', 'my/path').param(parameterName, '1'))
+                .andReturn().response
+        then: 'the call is delegated to the cps service facade which returns a list containing one data node as a map'
+            1 * mockCpsFacade.executeDataspaceQuery('my_dataspace', 'my/path', OMIT_DESCENDANTS, PaginationOption.NO_PAGINATION) >> [dataNodeAsMap]
+        then: 'the response is OK and contains 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:
+            assert response.getContentAsString() == '[{"prefixedPath":{"path":{"leaf":"value"}}}]'
+        and: 'the header indicates the correct number of pages'
+            assert response.getHeaderValue('total-pages') == '1'
+        where: 'only the following rest parameter is used'
             scenario           | parameterName
             'only page size'   | 'pageSize'
             'only page index'  | 'pageIndex'
index 4e1d27c..0cbdffb 100644 (file)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2020 Pantheon.tech
- *  Modifications Copyright (C) 2021-2023 Nordix Foundation
+ *  Modifications Copyright (C) 2021-2025 Nordix Foundation
  *  Modifications Copyright (C) 2021 Bell Canada.
  *  Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
@@ -26,23 +26,24 @@ package org.onap.cps.rest.exceptions
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import groovy.json.JsonSlurper
-import org.onap.cps.api.CpsDataspaceService
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
+import org.onap.cps.api.CpsDataspaceService
+import org.onap.cps.api.CpsFacade
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.api.CpsNotificationService
 import org.onap.cps.api.CpsQueryService
-import org.onap.cps.rest.controller.CpsRestInputMapper
 import org.onap.cps.api.exceptions.AlreadyDefinedException
 import org.onap.cps.api.exceptions.CpsException
 import org.onap.cps.api.exceptions.CpsPathException
 import org.onap.cps.api.exceptions.DataInUseException
 import org.onap.cps.api.exceptions.DataNodeNotFoundException
 import org.onap.cps.api.exceptions.DataValidationException
+import org.onap.cps.api.exceptions.DataspaceInUseException
 import org.onap.cps.api.exceptions.ModelValidationException
 import org.onap.cps.api.exceptions.NotFoundInDataspaceException
 import org.onap.cps.api.exceptions.SchemaSetInUseException
-import org.onap.cps.api.exceptions.DataspaceInUseException
+import org.onap.cps.rest.controller.CpsRestInputMapper
 import org.onap.cps.utils.JsonObjectMapper
 import org.onap.cps.utils.PrefixResolver
 import org.spockframework.spring.SpringBean
@@ -64,6 +65,9 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
 @WebMvcTest
 class CpsRestExceptionHandlerSpec extends Specification {
 
+    @SpringBean
+    CpsFacade mockCpsFacade = Stub()
+
     @SpringBean
     CpsDataspaceService mockCpsAdminService = Stub()
 
@@ -86,10 +90,10 @@ class CpsRestExceptionHandlerSpec extends Specification {
     CpsRestInputMapper cpsRestInputMapper = Stub()
 
     @SpringBean
-    PrefixResolver prefixResolver = Mock()
+    PrefixResolver prefixResolver = Stub()
 
     @SpringBean
-    CpsNotificationService mockCpsNotificationService = Mock()
+    CpsNotificationService mockCpsNotificationService = Stub()
 
     @Autowired
     MockMvc mvc
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsFacade.java b/cps-service/src/main/java/org/onap/cps/api/CpsFacade.java
new file mode 100644 (file)
index 0000000..8933f02
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 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.api;
+
+import java.util.List;
+import java.util.Map;
+import org.onap.cps.api.parameters.FetchDescendantsOption;
+import org.onap.cps.api.parameters.PaginationOption;
+
+public interface CpsFacade {
+
+    /**
+     * Get the first data node for a given dataspace, anchor and xpath.
+     *
+     * @param dataspaceName          the name of the dataspace
+     * @param anchorName             the name of the anchor
+     * @param xpath                  the xpath
+     * @param fetchDescendantsOption control what level of descendants should be returned
+     * @return                       a map representing the data node and its descendants
+     */
+    Map<String, Object> getFirstDataNodeByAnchor(String dataspaceName,
+                                                 String anchorName,
+                                                 String xpath,
+                                                 FetchDescendantsOption fetchDescendantsOption);
+
+    /**
+     * Get data nodes for a given dataspace, anchor and xpath.
+     *
+     * @param dataspaceName          the name of the dataspace
+     * @param anchorName             the name of the anchor
+     * @param xpath                  the xpath
+     * @param fetchDescendantsOption control what level of descendants should be returned
+     * @return                       a map representing the data nodes and their descendants
+     */
+    List<Map<String, Object>> getDataNodesByAnchor(String dataspaceName,
+                                                   String anchorName,
+                                                   String xpath,
+                                                   FetchDescendantsOption fetchDescendantsOption);
+
+    /**
+     * Query the given anchor using a cps path expression.
+     *
+     * @param dataspaceName          the name of the dataspace
+     * @param anchorName             the name of the anchor
+     * @param cpsPath                the xpath i.e. query
+     * @param fetchDescendantsOption control what level of descendants should be returned
+     * @return                       a map representing the data nodes and their descendants
+     */
+    List<Map<String, Object>> executeAnchorQuery(String dataspaceName,
+                                                 String anchorName,
+                                                 String cpsPath,
+                                                 FetchDescendantsOption fetchDescendantsOption);
+
+    /**
+     * Query the given dataspace (all anchors) using a cps path expression.
+     *
+     * @param dataspaceName          the name of the dataspace
+     * @param cpsPath                the xpath i.e. query
+     * @param fetchDescendantsOption control what level of descendants should be returned
+     * @return                        a map representing the data nodes and their descendants
+     */
+    List<Map<String, Object>> executeDataspaceQuery(String dataspaceName,
+                                                    String cpsPath,
+                                                    FetchDescendantsOption fetchDescendantsOption,
+                                                    PaginationOption paginationOption);
+
+    /**
+     * Query how many anchors wil be returned for the given dataspace and a cps path query.
+     *
+     * @param dataspaceName    the name of the dataspace
+     * @param cpsPath          the xpath i.e. query
+     * @param paginationOption the options for pagination
+     * @return                 the number of anchors involved in the output
+     */
+    int countAnchorsInDataspaceQuery(String dataspaceName,
+                                     String cpsPath,
+                                     PaginationOption paginationOption);
+}
index 46022ba..05fa366 100644 (file)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Pantheon.tech
- *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Copyright (C) 2022-2025 Nordix Foundation
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,6 +24,7 @@ package org.onap.cps.api.parameters;
 import com.google.common.base.Strings;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.api.exceptions.DataValidationException;
 
@@ -44,6 +45,12 @@ public class FetchDescendantsOption {
     private static final Pattern FETCH_DESCENDANTS_OPTION_PATTERN =
         Pattern.compile("^$|^all$|^none$|^direct$|^[0-9]+$|^-1$|^1$");
 
+    /**
+     * Get depth.
+     *
+     * @return depth: -1 for all descendants, 0 for no descendants, or positive value for fixed level of descendants
+     */
+    @Getter
     private final int depth;
 
     private final String optionName;
@@ -76,15 +83,7 @@ public class FetchDescendantsOption {
     }
 
     /**
-     * Get depth.
-     * @return depth: -1 for all descendants, 0 for no descendants, or positive value for fixed level of descendants
-     */
-    public int getDepth() {
-        return depth;
-    }
-
-    /**
-     * get fetch descendants option for given descendant.
+     * Convert fetch descendants option from string to enum with depth.
      *
      * @param fetchDescendantsOptionAsString fetch descendants option string
      * @return fetch descendants option for given descendant
@@ -99,11 +98,22 @@ public class FetchDescendantsOption {
         } else if ("1".equals(fetchDescendantsOptionAsString) || "direct".equals(fetchDescendantsOptionAsString)) {
             return FetchDescendantsOption.DIRECT_CHILDREN_ONLY;
         } else {
-            final Integer depth = Integer.valueOf(fetchDescendantsOptionAsString);
+            final int depth = Integer.parseInt(fetchDescendantsOptionAsString);
             return new FetchDescendantsOption(depth);
         }
     }
 
+    /**
+     * Convert include all-descendants boolean parameter to FetchDescendantsOption enum.
+     *
+     * @param includedDescendantsOptionAsBoolean fetch descendants option as Boolean
+     * @return fetch descendants option for given descendant
+     */
+    public static FetchDescendantsOption getFetchDescendantsOption(final Boolean includedDescendantsOptionAsBoolean) {
+        return Boolean.TRUE.equals(includedDescendantsOptionAsBoolean)
+            ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS;
+    }
+
     @Override
     public String toString() {
         return optionName;
index ab6f7a2..a93bf9a 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2024 Nordix Foundation
+ *  Copyright (C) 2021-2025 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
@@ -26,6 +26,7 @@ package org.onap.cps.impl;
 
 import static org.onap.cps.cpspath.parser.CpsPathUtil.NO_PARENT_PATH;
 import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH;
+import static org.onap.cps.utils.ContentType.JSON;
 
 import io.micrometer.core.annotation.Timed;
 import java.io.Serializable;
@@ -33,7 +34,6 @@ import java.time.OffsetDateTime;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -53,9 +53,8 @@ import org.onap.cps.events.model.Data.Operation;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.utils.ContentType;
 import org.onap.cps.utils.CpsValidator;
-import org.onap.cps.utils.DataMapUtils;
+import org.onap.cps.utils.DataMapper;
 import org.onap.cps.utils.JsonObjectMapper;
-import org.onap.cps.utils.PrefixResolver;
 import org.onap.cps.utils.YangParser;
 import org.springframework.stereotype.Service;
 
@@ -74,18 +73,17 @@ public class CpsDataServiceImpl implements CpsDataService {
     private final CpsValidator cpsValidator;
     private final YangParser yangParser;
     private final CpsDeltaService cpsDeltaService;
+    private final DataMapper dataMapper;
     private final JsonObjectMapper jsonObjectMapper;
-    private final PrefixResolver prefixResolver;
 
     @Override
     public void saveData(final String dataspaceName, final String anchorName, final String nodeData,
         final OffsetDateTime observedTimestamp) {
-        saveData(dataspaceName, anchorName, nodeData, observedTimestamp, ContentType.JSON);
+        saveData(dataspaceName, anchorName, nodeData, observedTimestamp, JSON);
     }
 
     @Override
-    @Timed(value = "cps.data.service.datanode.root.save",
-        description = "Time taken to save a root data node")
+    @Timed(value = "cps.data.service.datanode.root.save", description = "Time taken to save a root data node")
     public void saveData(final String dataspaceName, final String anchorName, final String nodeData,
                          final OffsetDateTime observedTimestamp, final ContentType contentType) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
@@ -99,12 +97,11 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
                          final String nodeData, final OffsetDateTime observedTimestamp) {
-        saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, ContentType.JSON);
+        saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, JSON);
     }
 
     @Override
-    @Timed(value = "cps.data.service.datanode.child.save",
-        description = "Time taken to save a child data node")
+    @Timed(value = "cps.data.service.datanode.child.save", description = "Time taken to save a child data node")
     public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
                          final String nodeData, final OffsetDateTime observedTimestamp,
                          final ContentType contentType) {
@@ -117,8 +114,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.list.element.save",
-        description = "Time taken to save list elements")
+    @Timed(value = "cps.data.service.list.element.save", description = "Time taken to save list elements")
     public void saveListElements(final String dataspaceName, final String anchorName,
                                  final String parentNodeXpath, final String nodeData,
                                  final OffsetDateTime observedTimestamp, final ContentType contentType) {
@@ -136,8 +132,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.datanode.get",
-            description = "Time taken to get data nodes for an xpath")
+    @Timed(value = "cps.data.service.datanode.get", description = "Time taken to get data nodes for an xpath")
     public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
                                              final String xpath,
                                              final FetchDescendantsOption fetchDescendantsOption) {
@@ -146,8 +141,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.datanode.batch.get",
-        description = "Time taken to get a batch of data nodes")
+    @Timed(value = "cps.data.service.datanode.batch.get", description = "Time taken to get a batch of data nodes")
     public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName,
                                                               final Collection<String> xpaths,
                                                               final FetchDescendantsOption fetchDescendantsOption) {
@@ -182,7 +176,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> dataNodeUpdates = dataNodeFactory
                 .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, dataNodeUpdatesAsJson,
-                        ContentType.JSON);
+                        JSON);
         for (final DataNode dataNodeUpdate : dataNodeUpdates) {
             processDataNodeUpdate(anchor, dataNodeUpdate);
         }
@@ -211,8 +205,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.get.delta",
-            description = "Time taken to get delta between anchors")
+    @Timed(value = "cps.data.service.get.delta", description = "Time taken to get delta between anchors")
     public List<DeltaReport> getDeltaByDataspaceAndAnchors(final String dataspaceName,
                                                            final String sourceAnchorName,
                                                            final String targetAnchorName, final String xpath,
@@ -226,9 +219,9 @@ public class CpsDataServiceImpl implements CpsDataService {
         return cpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes);
     }
 
+    @Override
     @Timed(value = "cps.data.service.get.deltaBetweenAnchorAndPayload",
             description = "Time taken to get delta between anchor and a payload")
-    @Override
     public List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName,
                                                                 final String sourceAnchorName, final String xpath,
                                                                 final Map<String, String> yangResourceContentPerName,
@@ -279,8 +272,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.list.update",
-        description = "Time taken to update a list")
+    @Timed(value = "cps.data.service.list.update", description = "Time taken to update a list")
     public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
             final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
@@ -291,8 +283,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.list.batch.update",
-        description = "Time taken to update a batch of lists")
+    @Timed(value = "cps.data.service.list.batch.update", description = "Time taken to update a batch of lists")
     public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
             final Collection<DataNode> dataNodes, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
@@ -302,8 +293,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.datanode.delete",
-        description = "Time taken to delete a datanode")
+    @Timed(value = "cps.data.service.datanode.delete", description = "Time taken to delete a datanode")
     public void deleteDataNode(final String dataspaceName, final String anchorName, final String dataNodeXpath,
                                final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
@@ -313,8 +303,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.datanode.batch.delete",
-        description = "Time taken to delete a batch of datanodes")
+    @Timed(value = "cps.data.service.datanode.batch.delete", description = "Time taken to delete a batch of datanodes")
     public void deleteDataNodes(final String dataspaceName, final String anchorName,
                                 final Collection<String> dataNodeXpaths, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
@@ -350,8 +339,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     @Override
-    @Timed(value = "cps.data.service.list.delete",
-        description = "Time taken to delete a list or list element")
+    @Timed(value = "cps.data.service.list.delete", description = "Time taken to delete a list or list element")
     public void deleteListOrListElement(final String dataspaceName, final String anchorName, final String listNodeXpath,
         final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
@@ -369,54 +357,29 @@ public class CpsDataServiceImpl implements CpsDataService {
         yangParser.validateData(contentType, nodeData, anchor, xpath);
     }
 
-    private Collection<DataNode> rebuildSourceDataNodes(final String xpath, final Anchor sourceAnchor,
+    private Collection<DataNode> rebuildSourceDataNodes(final String xpath,
+                                                        final Anchor sourceAnchor,
                                                         final Collection<DataNode> sourceDataNodes) {
-
         final Collection<DataNode> sourceDataNodesRebuilt = new ArrayList<>();
         if (sourceDataNodes != null) {
-            final String sourceDataNodesAsJson = getDataNodesAsJson(sourceAnchor, sourceDataNodes);
-            sourceDataNodesRebuilt.addAll(dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData(
-                    sourceAnchor, xpath, sourceDataNodesAsJson, ContentType.JSON));
+            final Map<String, Object> sourceDataNodesAsMap = dataMapper.toFlatDataMap(sourceAnchor, sourceDataNodes);
+            final String sourceDataNodesAsJson = jsonObjectMapper.asJsonString(sourceDataNodesAsMap);
+            final Collection<DataNode> dataNodes = dataNodeFactory
+                    .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, sourceDataNodesAsJson, JSON);
+            sourceDataNodesRebuilt.addAll(dataNodes);
         }
         return sourceDataNodesRebuilt;
     }
 
-    private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor, final String xpath,
+    private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor,
+                                                      final String xpath,
                                                       final Map<String, String> yangResourceContentPerName,
                                                       final String targetData) {
         if (yangResourceContentPerName.isEmpty()) {
-            return dataNodeFactory
-                    .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, targetData, ContentType.JSON);
-        } else {
-            return dataNodeFactory
-                    .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath,
-                            targetData, ContentType.JSON);
-        }
-    }
-
-    private String getDataNodesAsJson(final Anchor anchor, final Collection<DataNode> dataNodes) {
-
-        final List<Map<String, Object>> prefixToDataNodes = prefixResolver(anchor, dataNodes);
-        final Map<String, Object> targetDataAsJsonObject = getNodeDataAsJsonString(prefixToDataNodes);
-        return jsonObjectMapper.asJsonString(targetDataAsJsonObject);
-    }
-
-    private Map<String, Object> getNodeDataAsJsonString(final List<Map<String, Object>> prefixToDataNodes) {
-        final Map<String, Object>  nodeDataAsJson = new HashMap<>();
-        for (final Map<String, Object> prefixToDataNode : prefixToDataNodes) {
-            nodeDataAsJson.putAll(prefixToDataNode);
-        }
-        return nodeDataAsJson;
-    }
-
-    private List<Map<String, Object>> prefixResolver(final Anchor anchor, final Collection<DataNode> dataNodes) {
-        final List<Map<String, Object>> prefixToDataNodes = new ArrayList<>(dataNodes.size());
-        for (final DataNode dataNode: dataNodes) {
-            final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath());
-            final Map<String, Object> prefixToDataNode = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
-            prefixToDataNodes.add(prefixToDataNode);
+            return dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, targetData, JSON);
         }
-        return prefixToDataNodes;
+        return dataNodeFactory
+            .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, targetData, JSON);
     }
 
     private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) {
@@ -428,8 +391,10 @@ public class CpsDataServiceImpl implements CpsDataService {
         }
     }
 
-    private void sendDataUpdatedEvent(final Anchor anchor, final String xpath,
-                                      final Operation operation, final OffsetDateTime observedTimestamp) {
+    private void sendDataUpdatedEvent(final Anchor anchor,
+                                      final String xpath,
+                                      final Operation operation,
+                                      final OffsetDateTime observedTimestamp) {
         try {
             cpsDataUpdateEventsService.publishCpsDataUpdateEvent(anchor, xpath, operation, observedTimestamp);
         } catch (final Exception exception) {
diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java
new file mode 100644 (file)
index 0000000..4ac0d5d
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 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.impl;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.api.CpsDataService;
+import org.onap.cps.api.CpsFacade;
+import org.onap.cps.api.CpsQueryService;
+import org.onap.cps.api.model.DataNode;
+import org.onap.cps.api.parameters.FetchDescendantsOption;
+import org.onap.cps.api.parameters.PaginationOption;
+import org.onap.cps.utils.DataMapper;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+public class CpsFacadeImpl implements CpsFacade {
+
+    private final CpsDataService cpsDataService;
+    private final CpsQueryService cpsQueryService;
+    private final DataMapper dataMapper;
+
+    @Override
+    public Map<String, Object> getFirstDataNodeByAnchor(final String dataspaceName,
+                                                        final String anchorName,
+                                                        final String xpath,
+                                                        final FetchDescendantsOption fetchDescendantsOption) {
+        final DataNode dataNode = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath,
+            fetchDescendantsOption).iterator().next();
+        return dataMapper.toDataMap(dataspaceName, anchorName, dataNode);
+    }
+
+    @Override
+    public List<Map<String, Object>> getDataNodesByAnchor(final String dataspaceName,
+                                                          final String anchorName,
+                                                          final String xpath,
+                                                          final FetchDescendantsOption fetchDescendantsOption) {
+        final Collection<DataNode> dataNodes = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath,
+            fetchDescendantsOption);
+        return dataMapper.toDataMaps(dataspaceName, anchorName, dataNodes);
+    }
+
+    @Override
+    public List<Map<String, Object>> executeAnchorQuery(final String dataspaceName,
+                                                        final String anchorName,
+                                                        final String cpsPath,
+                                                        final FetchDescendantsOption fetchDescendantsOption) {
+        final Collection<DataNode> dataNodes =
+            cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
+        return dataMapper.toDataMaps(dataspaceName, anchorName, dataNodes);
+    }
+
+    @Override
+    public List<Map<String, Object>> executeDataspaceQuery(final String dataspaceName,
+                                                           final String cpsPath,
+                                                           final FetchDescendantsOption fetchDescendantsOption,
+                                                           final PaginationOption paginationOption) {
+        final Collection<DataNode> dataNodes = cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName,
+            cpsPath, fetchDescendantsOption, paginationOption);
+        return dataMapper.toDataMaps(dataspaceName, dataNodes);
+    }
+
+    @Override
+    public int countAnchorsInDataspaceQuery(final String dataspaceName,
+                                            final String cpsPath,
+                                            final PaginationOption paginationOption) {
+        if (paginationOption == PaginationOption.NO_PAGINATION) {
+            return 1;
+        }
+        final int totalAnchors =  cpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath);
+        return totalAnchors <= paginationOption.getPageSize() ? 1
+            : (int) Math.ceil((double) totalAnchors / paginationOption.getPageSize());
+    }
+
+}
+
index 09ef637..dc293b2 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2025 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2025 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -22,7 +23,6 @@ package org.onap.cps.impl;
 
 import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -36,8 +36,7 @@ import org.onap.cps.api.model.DataNode;
 import org.onap.cps.cpspath.parser.CpsPathUtil;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.utils.ContentType;
-import org.onap.cps.utils.DataMapUtils;
-import org.onap.cps.utils.PrefixResolver;
+import org.onap.cps.utils.DataMapper;
 import org.onap.cps.utils.YangParser;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.springframework.stereotype.Service;
@@ -53,10 +52,10 @@ public class CpsNotificationServiceImpl implements CpsNotificationService {
 
     private final YangParser yangParser;
 
-    private final PrefixResolver prefixResolver;
+    private final DataMapper dataMapper;
 
     private static final String ADMIN_DATASPACE = "CPS-Admin";
-    private static final String ANCHOR_NAME = "cps-notification-subscriptions";
+    private static final String CPS_SUBSCRIPTION_ANCHOR_NAME = "cps-notification-subscriptions";
     private static final String DATASPACE_SUBSCRIPTION_XPATH_FORMAT = "/dataspaces/dataspace[@name='%s']";
     private static final String ANCHORS_SUBSCRIPTION_XPATH_FORMAT = "/dataspaces/dataspace[@name='%s']/anchors";
     private static final String ANCHOR_SUBSCRIPTION_XPATH_FORMAT =
@@ -65,29 +64,22 @@ public class CpsNotificationServiceImpl implements CpsNotificationService {
     @Override
     public void createNotificationSubscription(final String notificationSubscriptionAsJson, final String xpath) {
 
-        final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, ANCHOR_NAME);
+        final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME);
         final Collection<DataNode> dataNodes =
             buildDataNodesWithParentNodeXpath(anchor, xpath, notificationSubscriptionAsJson);
-        cpsDataPersistenceService.addListElements(ADMIN_DATASPACE, ANCHOR_NAME, xpath, dataNodes);
+        cpsDataPersistenceService.addListElements(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, dataNodes);
     }
 
     @Override
     public void deleteNotificationSubscription(final String xpath) {
-        cpsDataPersistenceService.deleteDataNode(ADMIN_DATASPACE, ANCHOR_NAME, xpath);
+        cpsDataPersistenceService.deleteDataNode(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath);
     }
 
     @Override
     public List<Map<String, Object>> getNotificationSubscription(final String xpath) {
-        final Collection<DataNode> dataNodes =
-                cpsDataPersistenceService.getDataNodes(ADMIN_DATASPACE, ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS);
-        final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size());
-        final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, ANCHOR_NAME);
-        for (final DataNode dataNode: dataNodes) {
-            final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath());
-            final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
-            dataMaps.add(dataMap);
-        }
-        return dataMaps;
+        final Collection<DataNode> dataNodes = cpsDataPersistenceService
+            .getDataNodes(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS);
+        return dataMapper.toDataMaps(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, dataNodes);
     }
 
     @Override
@@ -103,7 +95,8 @@ public class CpsNotificationServiceImpl implements CpsNotificationService {
 
     private boolean isNotificationEnabledForXpath(final String xpath) {
         try {
-            cpsDataPersistenceService.getDataNodes(ADMIN_DATASPACE, ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS);
+            cpsDataPersistenceService
+                .getDataNodes(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS);
         } catch (final DataNodeNotFoundException e) {
             return false;
         }
diff --git a/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java b/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java
new file mode 100644 (file)
index 0000000..6e7eff9
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 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.utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.api.CpsAnchorService;
+import org.onap.cps.api.model.Anchor;
+import org.onap.cps.api.model.DataNode;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class DataMapper {
+
+    private final CpsAnchorService cpsAnchorService;
+    private final PrefixResolver prefixResolver;
+
+    /**
+     * Convert a data node to a data map.
+     *
+     * @param dataspaceName the name of the dataspace
+     * @param anchorName    the name of the anchor
+     * @param dataNode      the data node to convert
+     * @return the data node represented as a map of key value pairs
+     */
+    public Map<String, Object> toDataMap(final String dataspaceName, final String anchorName, final DataNode dataNode) {
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+        final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath());
+        return DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
+    }
+
+    /**
+     * Convert a collection of data nodes to a list of data maps.
+     *
+     * @param dataspaceName the name dataspace name
+     * @param anchorName    the name of the anchor
+     * @param dataNodes     the data nodes to convert
+     * @return a list of maps representing the data nodes
+     */
+    public List<Map<String, Object>> toDataMaps(final String dataspaceName, final String anchorName,
+                                                final Collection<DataNode> dataNodes) {
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+        return toDataMaps(anchor, dataNodes);
+    }
+
+    /**
+     * Convert a collection of data nodes to a list of data maps.
+     *
+     * @param anchor        the anchor
+     * @param dataNodes     the data nodes to convert
+     * @return a list of maps representing the data nodes
+     */
+    public List<Map<String, Object>> toDataMaps(final Anchor anchor, final Collection<DataNode> dataNodes) {
+        final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size());
+        for (final DataNode dataNode : dataNodes) {
+            final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath());
+            final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
+            dataMaps.add(dataMap);
+        }
+        return dataMaps;
+    }
+
+    /**
+     * Convert a collection of data nodes (belonging to multiple anchors) to a list of data maps.
+     *
+     * @param dataspaceName the name dataspace name
+     * @param dataNodes     the data nodes to convert
+     * @return a list of maps representing the data nodes
+     */
+    public List<Map<String, Object>> toDataMaps(final String dataspaceName, final Collection<DataNode> dataNodes) {
+        final List<Map<String, Object>> dataNodesAsMaps = new ArrayList<>(dataNodes.size());
+        final Map<String, List<DataNode>> dataNodesPerAnchor = groupDataNodesPerAnchor(dataNodes);
+        for (final Map.Entry<String, List<DataNode>> dataNodesPerAnchorEntry : dataNodesPerAnchor.entrySet()) {
+            final String anchorName = dataNodesPerAnchorEntry.getKey();
+            final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+            final DataNode dataNode = dataNodesPerAnchorEntry.getValue().get(0);
+            final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath());
+            final Map<String, Object> dataNodeAsMap = DataMapUtils.toDataMapWithIdentifierAndAnchor(
+                dataNodesPerAnchorEntry.getValue(), anchorName, prefix);
+            dataNodesAsMaps.add(dataNodeAsMap);
+        }
+        return dataNodesAsMaps;
+    }
+
+    /**
+     * Convert a collection of data nodes to a data map.
+     *
+     * @param anchor        the anchor
+     * @param dataNodes     the data nodes to convert
+     * @return a map representing the data nodes
+     */
+    public Map<String, Object> toFlatDataMap(final Anchor anchor, final Collection<DataNode> dataNodes) {
+        final List<Map<String, Object>> dataNodesAsMaps = toDataMaps(anchor, dataNodes);
+        return flattenDataNodesMaps(dataNodesAsMaps);
+    }
+
+    private Map<String, Object> flattenDataNodesMaps(final List<Map<String, Object>> dataNodesAsMaps) {
+        final Map<String, Object> dataNodesAsFlatMap = new HashMap<>();
+        for (final Map<String, Object> dataNodeAsMap : dataNodesAsMaps) {
+            dataNodesAsFlatMap.putAll(dataNodeAsMap);
+        }
+        return dataNodesAsFlatMap;
+    }
+
+    private static Map<String, List<DataNode>> groupDataNodesPerAnchor(final Collection<DataNode> dataNodes) {
+        return dataNodes.stream().collect(Collectors.groupingBy(DataNode::getAnchorName));
+    }
+
+}
index bd348a2..e59029f 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2024 Nordix Foundation.
+ *  Copyright (C) 2022-2025 Nordix Foundation.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -47,25 +47,29 @@ public class PrefixResolver {
      * @return the prefix of the module the top level element of given xpath
      */
     public String getPrefix(final Anchor anchor, final String xpath) {
+        return getPrefix(anchor.getDataspaceName(), anchor.getSchemaSetName(), xpath);
+    }
+
+    private String getPrefix(final String dataspaceName, final String schemaSetName, final String xpath) {
         final CpsPathQuery cpsPathQuery = CpsPathUtil.getCpsPathQuery(xpath);
         if (cpsPathQuery.getCpsPathPrefixType() != CpsPathPrefixType.ABSOLUTE) {
             return "";
         }
-        final String topLevelContainerName = cpsPathQuery.getContainerNames().get(0);
 
+        final String topLevelContainerName = cpsPathQuery.getContainerNames().get(0);
         final YangTextSchemaSourceSet yangTextSchemaSourceSet =
-                yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(), anchor.getSchemaSetName());
+            yangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName);
         final SchemaContext schemaContext = yangTextSchemaSourceSet.getSchemaContext();
 
         return schemaContext.getChildNodes().stream()
-                .filter(DataNodeContainer.class::isInstance)
-                .map(SchemaNode::getQName)
-                .filter(qname -> qname.getLocalName().equals(topLevelContainerName))
-                .findFirst()
-                .map(QName::getModule)
-                .flatMap(schemaContext::findModule)
-                .map(Module::getPrefix)
-                .orElse("");
+            .filter(DataNodeContainer.class::isInstance)
+            .map(SchemaNode::getQName)
+            .filter(qname -> qname.getLocalName().equals(topLevelContainerName))
+            .findFirst()
+            .map(QName::getModule)
+            .flatMap(schemaContext::findModule)
+            .map(Module::getPrefix)
+            .orElse("");
     }
 
 }
index 126e5b1..508178b 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Copyright (C) 2022-2025 Nordix Foundation
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,6 +25,10 @@ package org.onap.cps.api.parameters
 import org.onap.cps.api.exceptions.DataValidationException
 import spock.lang.Specification
 
+import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY
+import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
+
 class FetchDescendantsOptionSpec extends Specification {
 
     def 'Has next descendant for fetch descendant option: #scenario'() {
@@ -105,11 +109,22 @@ class FetchDescendantsOptionSpec extends Specification {
         expect: 'each fetch descendant option has the correct String value'
             assert fetchDescendantsOption.toString() == expectedStringValue
         where: 'the following option is used'
-            fetchDescendantsOption                         || expectedStringValue
-            FetchDescendantsOption.OMIT_DESCENDANTS        || 'OmitDescendants'
-            FetchDescendantsOption.DIRECT_CHILDREN_ONLY    || 'DirectChildrenOnly'
-            FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS || 'IncludeAllDescendants'
-            new FetchDescendantsOption(2)                  || 'Depth=2'
+            fetchDescendantsOption        || expectedStringValue
+            OMIT_DESCENDANTS              || 'OmitDescendants'
+            DIRECT_CHILDREN_ONLY          || 'DirectChildrenOnly'
+            INCLUDE_ALL_DESCENDANTS       || 'IncludeAllDescendants'
+            new FetchDescendantsOption(2) || 'Depth=2'
+    }
+
+    def 'Convert include-descendants boolean to fetch descendants option with : #includeDescendants'() {
+        when: 'convert boolean #includeDescendants'
+            def result = FetchDescendantsOption.getFetchDescendantsOption(includeDescendants)
+        then: 'result is the expected option'
+            assert result == expectedFetchDescendantsOption
+        where: 'following parameters are used'
+            includeDescendants || expectedFetchDescendantsOption
+            true               || INCLUDE_ALL_DESCENDANTS
+            false              || OMIT_DESCENDANTS
     }
 
 }
index d78c8bb..a21a17f 100644 (file)
@@ -20,7 +20,6 @@
 
 package org.onap.cps.impl
 
-
 import org.onap.cps.utils.CpsValidator
 import org.onap.cps.spi.CpsAdminPersistenceService
 import org.onap.cps.spi.CpsDataPersistenceService
index 6b90e55..967bcc0 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021-2024 Nordix Foundation
+ *  Copyright (C) 2021-2025 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
  *  Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
@@ -30,17 +30,18 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDeltaService
-import org.onap.cps.events.CpsDataUpdateEventsService
-import org.onap.cps.utils.CpsValidator
-import org.onap.cps.spi.CpsDataPersistenceService
-import org.onap.cps.api.parameters.FetchDescendantsOption
 import org.onap.cps.api.exceptions.ConcurrencyException
 import org.onap.cps.api.exceptions.DataNodeNotFoundExceptionBatch
 import org.onap.cps.api.exceptions.DataValidationException
 import org.onap.cps.api.exceptions.SessionManagerException
 import org.onap.cps.api.exceptions.SessionTimeoutException
 import org.onap.cps.api.model.Anchor
+import org.onap.cps.api.parameters.FetchDescendantsOption
+import org.onap.cps.events.CpsDataUpdateEventsService
+import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.utils.ContentType
+import org.onap.cps.utils.CpsValidator
+import org.onap.cps.utils.DataMapper
 import org.onap.cps.utils.JsonObjectMapper
 import org.onap.cps.utils.PrefixResolver
 import org.onap.cps.utils.YangParser
@@ -68,10 +69,11 @@ class CpsDataServiceImplSpec extends Specification {
     def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
     def mockPrefixResolver = Mock(PrefixResolver)
+    def dataMapper = new DataMapper(mockCpsAnchorService, mockPrefixResolver)
     def dataNodeFactory = new DataNodeFactoryImpl(yangParser)
 
     def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService,
-            dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver)
+            dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, dataMapper, jsonObjectMapper)
 
     def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
     def loggingListAppender
diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy
new file mode 100644 (file)
index 0000000..c754970
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 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.impl
+
+import org.onap.cps.api.CpsAnchorService
+import org.onap.cps.api.CpsDataService
+import org.onap.cps.api.CpsQueryService
+import org.onap.cps.api.model.DataNode
+import org.onap.cps.api.parameters.PaginationOption
+import org.onap.cps.utils.DataMapper
+import org.onap.cps.utils.PrefixResolver
+import spock.lang.Specification
+
+import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
+import static org.onap.cps.api.parameters.PaginationOption.NO_PAGINATION
+
+class CpsFacadeImplSpec extends Specification {
+
+    def mockCpsDataService = Mock(CpsDataService)
+    def mockCpsQueryService = Mock(CpsQueryService)
+    def mockCpsAnchorService = Mock(CpsAnchorService)
+    def mockPrefixResolver = Mock(PrefixResolver)
+    def dataMapper = new DataMapper(mockCpsAnchorService, mockPrefixResolver)
+
+    def myFetchDescendantsOption = OMIT_DESCENDANTS
+    def myPaginationOption = NO_PAGINATION
+
+    def objectUnderTest = new CpsFacadeImpl(mockCpsDataService, mockCpsQueryService , dataMapper)
+
+    def dataNode1 = new DataNode(xpath:'/path1', anchorName: 'my anchor')
+    def dataNode2 = new DataNode(xpath:'/path2', anchorName: 'my anchor')
+    def dataNode3 = new DataNode(xpath:'/path3', anchorName: 'other anchor')
+
+    def setup() {
+        mockCpsDataService.getDataNodes('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption) >> [ dataNode1, dataNode2]
+        mockPrefixResolver.getPrefix(_, '/path1') >> 'prefix1'
+        mockPrefixResolver.getPrefix(_, '/path2') >> 'prefix2'
+        mockPrefixResolver.getPrefix(_, '/path3') >> 'prefix3'
+    }
+
+    def 'Get one data node.'() {
+        when: 'get data node by dataspace and anchor'
+            def result = objectUnderTest.getFirstDataNodeByAnchor('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption)
+        then: 'only the first node (from the data service result) is returned'
+            assert result.size() == 1
+            assert result.keySet()[0] == 'prefix1:path1'
+    }
+
+    def 'Get multiple data nodes.'() {
+        when: 'get data node by dataspace and anchor'
+            def result = objectUnderTest.getDataNodesByAnchor('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption)
+        then: 'all nodes (from the data service result) are returned'
+            assert result.size() == 2
+            assert result[0].keySet()[0] == 'prefix1:path1'
+            assert result[1].keySet()[0] == 'prefix2:path2'
+    }
+
+    def 'Execute anchor query.'() {
+        given: 'the cps query service returns two data nodes'
+           mockCpsQueryService.queryDataNodes('my dataspace', 'my anchor', 'my cps path', myFetchDescendantsOption) >> [ dataNode1, dataNode2]
+        when: 'get data node by dataspace and anchor'
+            def result = objectUnderTest.executeAnchorQuery('my dataspace', 'my anchor', 'my cps path', myFetchDescendantsOption)
+        then: 'all nodes (from the query service result) are returned'
+            assert result.size() == 2
+            assert result[0].keySet()[0] == 'prefix1:path1'
+            assert result[1].keySet()[0] == 'prefix2:path2'
+    }
+
+    def 'Execute dataspace query.'() {
+        given: 'the cps query service returns two data nodes (on two different anchors)'
+            mockCpsQueryService.queryDataNodesAcrossAnchors('my dataspace', 'my cps path', myFetchDescendantsOption, myPaginationOption) >> [ dataNode1, dataNode2, dataNode3 ]
+        when: 'get data node by dataspace and anchor'
+            def result = objectUnderTest.executeDataspaceQuery('my dataspace', 'my cps path', myFetchDescendantsOption, myPaginationOption)
+        then: 'all nodes (from the query service result) are returned, grouped by anchor'
+            assert result.size() == 2
+            assert result[0].toString() == '{anchorName=my anchor, dataNodes=[{prefix1:path1={}}, {prefix1:path2={}}]}'
+            assert result[1].toString() == '{anchorName=other anchor, dataNodes=[{prefix3:path3={}}]}'
+    }
+
+    def 'How many pages (anchors) could be in the output with #scenario.'() {
+        given: 'the query service says there are 10 anchors for the given query'
+            mockCpsQueryService.countAnchorsForDataspaceAndCpsPath('my dataspace', 'my cps path') >> 10
+        expect: 'the correct number of pages is returned'
+            assert objectUnderTest.countAnchorsInDataspaceQuery('my dataspace', 'my cps path', paginationOption) == expectedNumberOfPages
+        where: 'the following pagination options are used'
+            scenario                        | paginationOption            || expectedNumberOfPages
+            'no pagination'                 | NO_PAGINATION               || 1
+            '1 anchor per page'             | new PaginationOption(1,1)   || 10
+            '1 anchor per page, start at 2' | new PaginationOption(2,1)   || 10
+            '2 anchors per page'            | new PaginationOption(1,2)   || 5
+            '3 anchors per page'            | new PaginationOption(1,3)   || 4
+            '10 anchors per page'           | new PaginationOption(1,10)  || 1
+            '100 anchors per page'          | new PaginationOption(1,100) || 1
+    }
+
+}
index b7f0645..ab7853c 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * ============LICENSE_START=======================================================
  * Copyright (C) 2025 TechMahindra Ltd.
+ * Modifications Copyright (C) 2025 Nordix Foundation
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
 package org.onap.cps.impl
 
 import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.exceptions.DataNodeNotFoundException
 import org.onap.cps.api.exceptions.DataValidationException
 import org.onap.cps.api.model.Anchor
-import org.onap.cps.api.parameters.FetchDescendantsOption;
+import org.onap.cps.api.parameters.FetchDescendantsOption
 import org.onap.cps.spi.CpsDataPersistenceService
+import org.onap.cps.utils.DataMapper
 import org.onap.cps.utils.JsonObjectMapper
 import org.onap.cps.utils.PrefixResolver
 import org.onap.cps.utils.YangParser
-import org.onap.cps.TestUtils
 import org.onap.cps.utils.YangParserHelper
 import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
 import org.onap.cps.yang.YangTextSchemaSourceSet
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import org.springframework.test.context.ContextConfiguration
-
 import spock.lang.Specification
 
 @ContextConfiguration(classes = [ObjectMapper, JsonObjectMapper])
@@ -53,9 +54,9 @@ class CpsNotificationServiceImplSpec extends Specification {
     def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
     def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder)
     def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder)
-    def mockPrefixResolver = Mock(PrefixResolver)
+    def dataMapper = new DataMapper(mockCpsAnchorService, Mock(PrefixResolver))
 
-    def objectUnderTest = new CpsNotificationServiceImpl(mockCpsAnchorService, mockCpsDataPersistenceService, yangParser, mockPrefixResolver)
+    def objectUnderTest = new CpsNotificationServiceImpl(mockCpsAnchorService, mockCpsDataPersistenceService, yangParser, dataMapper)
 
     def 'add notification subscription for list of dataspaces'() {
         given: 'details for notification subscription and subscription root node xpath'
index f915701..893cce6 100755 (executable)
@@ -27,12 +27,13 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDeltaService
+import org.onap.cps.api.model.Anchor
 import org.onap.cps.events.CpsDataUpdateEventsService
-import org.onap.cps.utils.CpsValidator
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.CpsModulePersistenceService
-import org.onap.cps.api.model.Anchor
 import org.onap.cps.utils.ContentType
+import org.onap.cps.utils.CpsValidator
+import org.onap.cps.utils.DataMapper
 import org.onap.cps.utils.JsonObjectMapper
 import org.onap.cps.utils.PrefixResolver
 import org.onap.cps.utils.YangParser
@@ -42,24 +43,22 @@ import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import spock.lang.Specification
 
 class E2ENetworkSliceSpec extends Specification {
-    def mockModuleStoreService = Mock(CpsModulePersistenceService)
-    def mockDataStoreService = Mock(CpsDataPersistenceService)
+    def mockCpsModulePersistenceService = Mock(CpsModulePersistenceService)
+    def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
     def mockCpsAnchorService = Mock(CpsAnchorService)
     def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
     def mockCpsValidator = Mock(CpsValidator)
     def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder()
     def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, timedYangTextSchemaSourceSetBuilder)
     def mockCpsDeltaService = Mock(CpsDeltaService)
+    def dataMapper = new DataMapper(mockCpsAnchorService, Mock(PrefixResolver))
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
-    def mockPrefixResolver = Mock(PrefixResolver)
 
-    def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService,
-            mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
+    def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
 
     def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)
     def dataNodeFactory = new DataNodeFactoryImpl(yangParser)
-    def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, dataNodeFactory, mockCpsValidator,
-            yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver)
+    def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, dataMapper, jsonObjectMapper)
     def dataspaceName = 'someDataspace'
     def anchorName = 'someAnchor'
     def schemaSetName = 'someSchemaSet'
@@ -75,7 +74,7 @@ class E2ENetworkSliceSpec extends Specification {
         when: 'Create schema set method is invoked'
             cpsModuleServiceImpl.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName)
         then: 'Parameters are validated and processing is delegated to persistence service'
-            1 * mockModuleStoreService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName)
+            1 * mockCpsModulePersistenceService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName)
     }
 
     def 'E2E Coverage Area-Tracking Area & TA-Cell mapping model can be parsed by CPS.'() {
@@ -85,7 +84,7 @@ class E2ENetworkSliceSpec extends Specification {
         when: 'Create schema set method is invoked'
             cpsModuleServiceImpl.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName)
         then: 'Parameters are validated and processing is delegated to persistence service'
-            1 * mockModuleStoreService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName)
+            1 * mockCpsModulePersistenceService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName)
     }
 
     def 'E2E Coverage Area-Tracking Area & TA-Cell mapping data can be parsed by CPS.'() {
@@ -101,31 +100,28 @@ class E2ENetworkSliceSpec extends Specification {
                     new Anchor().builder().name(anchorName).schemaSetName(schemaSetName).dataspaceName(dataspaceName).build()
             mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >>
                     YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName)
-            mockModuleStoreService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext
+            mockCpsModulePersistenceService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext
         when: 'saveData method is invoked'
             cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp)
         then: 'Parameters are validated and processing is delegated to persistence service'
-            1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >>
+            1 * mockCpsDataPersistenceService.storeDataNodes('someDataspace', 'someAnchor', _) >>
                     { args -> dataNodeStored = args[2]}
             def child = dataNodeStored[0].childDataNodes[0]
             assert child.childDataNodes.size() == 1
         and: 'list of Tracking Area for a Coverage Area are stored with correct xpath and child nodes '
             def listOfTAForCoverageArea = child.childDataNodes[0]
-            listOfTAForCoverageArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/' +
-                    'coverage-area[@coverageArea=\'Washington\']'
-            listOfTAForCoverageArea.childDataNodes[0].leaves.get('nRTAC') == 234
+            listOfTAForCoverageArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/coverage-area[@coverageArea=\'Washington\']'
+            assert  listOfTAForCoverageArea.childDataNodes[0].leaves.get('nRTAC') == 234
         and: 'list of cells in a tracking area are stored with correct xpath and child nodes '
             def listOfCellsInTrackingArea = listOfTAForCoverageArea.childDataNodes[0]
-            listOfCellsInTrackingArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/' +
-                    'coverage-area[@coverageArea=\'Washington\']/coverageAreaTAList[@nRTAC=\'234\']'
+            listOfCellsInTrackingArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/coverage-area[@coverageArea=\'Washington\']/coverageAreaTAList[@nRTAC=\'234\']'
             listOfCellsInTrackingArea.childDataNodes[0].leaves.get('cellLocalId') == 15709
     }
 
     def 'E2E Coverage Area-Tracking Area & TA-Cell mapping data can be parsed for RAN inventory.'() {
         def dataNodeStored
         given: 'valid yang resource as name-to-content map'
-            def yangResourceContentPerName = TestUtils.getYangResourcesAsMap(
-                    'e2e/basic/cps-ran-inventory@2021-01-28.yang')
+            def yangResourceContentPerName = TestUtils.getYangResourcesAsMap('e2e/basic/cps-ran-inventory@2021-01-28.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName).getSchemaContext()
         and : 'a valid json is provided for the model'
             def jsonData = TestUtils.getResourceFileContent('e2e/basic/cps-ran-inventory-data.json')
@@ -133,12 +129,11 @@ class E2ENetworkSliceSpec extends Specification {
             mockCpsAnchorService.getAnchor('someDataspace', 'someAnchor') >>
                     new Anchor().builder().name('someAnchor').schemaSetName('someSchemaSet').dataspaceName(dataspaceName).build()
             mockYangTextSchemaSourceSetCache.get('someDataspace', 'someSchemaSet') >> YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName)
-            mockModuleStoreService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext
+            mockCpsModulePersistenceService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext
         when: 'saveData method is invoked'
             cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp)
         then: 'parameters are validated and processing is delegated to persistence service'
-            1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >>
-                    { args -> dataNodeStored = args[2]}
+            1 * mockCpsDataPersistenceService.storeDataNodes('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]}
         and: 'the size of the tree is correct'
             def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored[0])
             assert  cpsRanInventory.size() == 4
@@ -147,17 +142,16 @@ class E2ENetworkSliceSpec extends Specification {
             def ranSlices = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']')
             def sliceProfilesList = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']/sliceProfilesList[@sliceProfileId=\'f33a9dd8-ae51-4acf-8073-c9390c25f6f1\']')
             def pLMNIdList = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']/sliceProfilesList[@sliceProfileId=\'f33a9dd8-ae51-4acf-8073-c9390c25f6f1\']/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']')
-            ranInventory.getChildDataNodes().size() == 1
-            ranInventory.getChildDataNodes().find( {it.xpath == ranSlices.xpath})
+            assert ranInventory.getChildDataNodes().size() == 1
+            assert ranInventory.getChildDataNodes().find( {it.xpath == ranSlices.xpath})
         and: 'ranSlices contains the correct child node'
-            ranSlices.getChildDataNodes().size() == 1
-            ranSlices.getChildDataNodes().find( {it.xpath == sliceProfilesList.xpath})
+            assert ranSlices.getChildDataNodes().size() == 1
+            assert ranSlices.getChildDataNodes().find( {it.xpath == sliceProfilesList.xpath})
         and: 'sliceProfilesList contains the correct child node'
-            sliceProfilesList.getChildDataNodes().size() == 1
-            sliceProfilesList.getChildDataNodes().find( {it.xpath == pLMNIdList.xpath})
+            assert sliceProfilesList.getChildDataNodes().size() == 1
+            assert sliceProfilesList.getChildDataNodes().find( {it.xpath == pLMNIdList.xpath})
         and: 'pLMNIdList contains no children'
-            pLMNIdList.getChildDataNodes().size() == 0
-
+            assert pLMNIdList.getChildDataNodes().size() == 0
     }
 
     def 'E2E RAN Schema Model.'(){