From: danielhanrahan Date: Thu, 13 Mar 2025 13:33:27 +0000 (+0000) Subject: Add attribute-axis to CPS query nodes rest API X-Git-Tag: 3.6.2~38^2 X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F89%2F140189%2F3;p=cps.git Add attribute-axis to CPS query nodes rest API 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: MatildaDune Issue-ID: CPS-2620 Signed-off-by: danielhanrahan Change-Id: Iab51fbe76281740b8dbde373e11864d3509696ef --- diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy index 5f6de2ec4c..b49afb4798 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy @@ -94,6 +94,23 @@ class QueryRestControllerSpec extends Specification { 'descendants XML' | '2' | MediaType.APPLICATION_XML || 2 || 'value' } + 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 || 'value1value2' + } + 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" 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 index 4ac0d5d8e8..35a03685b6 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java @@ -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 attributeValues = + cpsQueryService.queryDataLeaf(dataspaceName, anchorName, cpsPath, Object.class); + return dataMapper.toAttributeMaps(attributeName, attributeValues); + } final Collection dataNodes = cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption); return dataMapper.toDataMaps(dataspaceName, anchorName, dataNodes); 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 index 6e7eff9132..29d61ffcc4 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java +++ b/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java @@ -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> toAttributeMaps(final String attributeName, + final Collection attributeValues) { + return attributeValues.stream().map(attributeValue -> Map.of(attributeName, attributeValue)).toList(); + } + /** * Convert a collection of data nodes to a data map. * 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 index c754970518..4351631ee1 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy @@ -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 ]