Add attribute-axis to CPS query nodes rest API 89/140189/3
authordanielhanrahan <daniel.hanrahan@est.tech>
Thu, 13 Mar 2025 13:33:27 +0000 (13:33 +0000)
committerdanielhanrahan <daniel.hanrahan@est.tech>
Thu, 13 Mar 2025 14:52:11 +0000 (14:52 +0000)
Support attribute-axis in query nodes api for both JSON and XML:
  /cps/v2/dataspaces/{dataspace}/anchors/{anchor}/nodes/query

It allows such queries as:
  //books/@title
which returns a JSON response like: [{"title":"Matilda"},{"title":"Dune"}]
and an XML response like: <title>Matilda</title><title>Dune</title>

Issue-ID: CPS-2620
Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech>
Change-Id: Iab51fbe76281740b8dbde373e11864d3509696ef

cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java
cps-service/src/main/java/org/onap/cps/utils/DataMapper.java
cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy

index 5f6de2e..b49afb4 100644 (file)
@@ -94,6 +94,23 @@ class QueryRestControllerSpec extends Specification {
             'descendants XML'      | '2'                            | MediaType.APPLICATION_XML  || 2             || '<prefixedPath><path><leaf>value</leaf></path></prefixedPath>'
     }
 
+    def 'Query data node (v2) by cps path for given dataspace and anchor with attribute-axis and #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(contentType).param('cps-path', '/my/path/@myAttribute').param('descendants', '0'))
+                    .andReturn().response
+        then: 'the call is delegated to the cps service facade which returns a list containing two attributes as maps'
+            1 * mockCpsFacade.executeAnchorQuery('my_dataspace', 'my_anchor', '/my/path/@myAttribute', OMIT_DESCENDANTS) >> [['myAttribute':'value1'], ['myAttribute':'value2']]
+        and: 'the response contains the datanode in the expected format'
+            assert response.status == HttpStatus.OK.value()
+            assert response.getContentAsString() == expectedOutput
+        where: 'the following options for content type are provided in the request'
+            scenario | contentType                || expectedOutput
+            'JSON'   | MediaType.APPLICATION_JSON || '[{"myAttribute":"value1"},{"myAttribute":"value2"}]'
+            'XML'    | MediaType.APPLICATION_XML  || '<myAttribute>value1</myAttribute><myAttribute>value2</myAttribute>'
+    }
+
     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"
index 4ac0d5d..35a0368 100644 (file)
@@ -23,6 +23,7 @@ package org.onap.cps.impl;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsFacade;
@@ -30,6 +31,8 @@ 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.cpspath.parser.CpsPathQuery;
+import org.onap.cps.cpspath.parser.CpsPathUtil;
 import org.onap.cps.utils.DataMapper;
 import org.springframework.stereotype.Service;
 
@@ -66,6 +69,13 @@ public class CpsFacadeImpl implements CpsFacade {
                                                         final String anchorName,
                                                         final String cpsPath,
                                                         final FetchDescendantsOption fetchDescendantsOption) {
+        final CpsPathQuery cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+        if (cpsPathQuery.hasAttributeAxis()) {
+            final String attributeName = cpsPathQuery.getAttributeAxisAttributeName();
+            final Set<Object> attributeValues =
+                    cpsQueryService.queryDataLeaf(dataspaceName, anchorName, cpsPath, Object.class);
+            return dataMapper.toAttributeMaps(attributeName, attributeValues);
+        }
         final Collection<DataNode> dataNodes =
             cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
         return dataMapper.toDataMaps(dataspaceName, anchorName, dataNodes);
index 6e7eff9..29d61ff 100644 (file)
@@ -106,6 +106,17 @@ public class DataMapper {
         return dataNodesAsMaps;
     }
 
+    /**
+     * Converts list of attributes values to a list of data maps.
+     * @param attributeName   attribute name
+     * @param attributeValues attribute values
+     * @return a list of maps representing the attribute values
+     */
+    public List<Map<String, Object>> toAttributeMaps(final String attributeName,
+                                                     final Collection<Object> attributeValues) {
+        return attributeValues.stream().map(attributeValue -> Map.of(attributeName, attributeValue)).toList();
+    }
+
     /**
      * Convert a collection of data nodes to a data map.
      *
index c754970..4351631 100644 (file)
@@ -75,15 +75,26 @@ class CpsFacadeImplSpec extends Specification {
 
     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]
+           mockCpsQueryService.queryDataNodes('my dataspace', 'my anchor', '/my/path', myFetchDescendantsOption) >> [ dataNode1, dataNode2]
         when: 'get data node by dataspace and anchor'
-            def result = objectUnderTest.executeAnchorQuery('my dataspace', 'my anchor', 'my cps path', myFetchDescendantsOption)
+            def result = objectUnderTest.executeAnchorQuery('my dataspace', 'my anchor', '/my/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 anchor query with attribute-axis.'() {
+        given: 'the cps query service returns two attribute values'
+            mockCpsQueryService.queryDataLeaf('my dataspace', 'my anchor', '/my/path/@myAttribute', Object) >> ['value1', 'value2']
+        when: 'get data using attribute axis'
+            def result = objectUnderTest.executeAnchorQuery('my dataspace', 'my anchor', '/my/path/@myAttribute', myFetchDescendantsOption)
+        then: 'attribute values (from the query service result) are returned'
+            assert result.size() == 2
+            assert result[0] == ['myAttribute': 'value1']
+            assert result[1] == ['myAttribute': 'value2']
+    }
+
     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 ]