parameters:
- $ref: 'components.yaml#/components/parameters/cmHandleInPath'
- $ref: 'components.yaml#/components/parameters/cpsPathInQuery'
+ - $ref: 'components.yaml#/components/parameters/includeDescendantsOptionInQuery'
responses:
200:
$ref: 'components.yaml#/components/responses/Ok'
}
@Override
- public ResponseEntity<Object> queryNodesByCmHandleAndCpsPath(final String cmHandle, @Valid final String cpsPath) {
- final Collection<DataNode> dataNodes = nfProxyDataService.queryDataNodes(cmHandle, cpsPath);
+ public ResponseEntity<Object> queryNodesByCmHandleAndCpsPath(final String cmHandle, @Valid final String cpsPath,
+ @Valid final Boolean includeDescendants) {
+ final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants)
+ ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS;
+ final Collection<DataNode> dataNodes =
+ nfProxyDataService.queryDataNodes(cmHandle, cpsPath, fetchDescendantsOption);
return new ResponseEntity<>(GSON.toJson(dataNodes), HttpStatus.OK);
}
package org.onap.cps.nfproxy.rest.controller
-import com.google.common.collect.ImmutableMap
+
import com.google.gson.Gson
import org.onap.cps.nfproxy.api.NfProxyDataService
-import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
import org.spockframework.spring.SpringBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import spock.lang.Specification
+import spock.lang.Unroll
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
def cmHandle = 'some handle'
def xpath = 'some xpath'
- def 'Query data node by cps path for the given cm handle.'() {
+ @Unroll
+ def 'Query data node by cps path for the given cm handle with #scenario.'() {
given: 'service method returns a list containing a data node'
- def cpsPath = '/xpath/leaves[@leaf=\'value\']'
- def dataNode = new DataNodeBuilder().withXpath("/xpath")
- .withLeaves(ImmutableMap.of("leaf", "value")).build()
- ArrayList<DataNode> dataNodeList = new ArrayList();
- dataNodeList.add(dataNode)
- mockNfProxyDataService.queryDataNodes(cmHandle, cpsPath) >> dataNodeList
+ def dataNode = new DataNodeBuilder().withXpath('/xpath').build()
+ def cpsPath = 'some cps-path'
+ mockNfProxyDataService.queryDataNodes(cmHandle, cpsPath, expectedCpsDataServiceOption) >> [dataNode]
and: 'the query endpoint'
def dataNodeEndpoint = "$dataNodeBaseEndpoint/cm-handles/$cmHandle/nodes/query"
when: 'query data nodes API is invoked'
- def response = mvc.perform(get(dataNodeEndpoint).param('cps-path', cpsPath)).andReturn().response
+ def response = mvc.perform(get(dataNodeEndpoint)
+ .param('cps-path', cpsPath)
+ .param('include-descendants', includeDescendantsOption))
+ .andReturn().response
then: 'the response contains the the datanode in json format'
response.status == HttpStatus.OK.value()
def expectedJsonContent = new Gson().toJson(dataNode)
response.getContentAsString().contains(expectedJsonContent)
+ 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 'Update data node leaves.'() {
/**
* Get datanodes for the given cm handle by cps path.
*
- * @param cmHandle The identifier for a network function, network element, subnetwork or any other cm object by
- * managed NF-Proxy
- * @param cpsPath cps path
+ * @param cmHandle The identifier for a network function, network element, subnetwork or any other cm
+ * object by managed NF-Proxy
+ * @param cpsPath cps path
+ * @param fetchDescendantsOption defines whether the descendants of the node(s) found by the query should be
+ * included in the output
* @return a collection of datanodes
*/
- Collection<DataNode> queryDataNodes(@NonNull String cmHandle, @NonNull String cpsPath);
+ Collection<DataNode> queryDataNodes(@NonNull String cmHandle, @NonNull String cpsPath,
+ @NonNull FetchDescendantsOption fetchDescendantsOption);
/**
* Updates data node for given cm handle using xpath to parent node.
}
@Override
- public Collection<DataNode> queryDataNodes(final String cmHandle, final String cpsPath) {
- return cpsQueryService.queryDataNodes(NF_PROXY_DATASPACE_NAME, cmHandle, cpsPath);
+ public Collection<DataNode> queryDataNodes(final String cmHandle, final String cpsPath,
+ final FetchDescendantsOption fetchDescendantsOption) {
+ return cpsQueryService.queryDataNodes(NF_PROXY_DATASPACE_NAME, cmHandle, cpsPath, fetchDescendantsOption);
}
@Override
import org.onap.cps.api.CpsDataService
import org.onap.cps.api.CpsQueryService
import org.onap.cps.nfproxy.api.impl.NfProxyDataServiceImpl
+import org.onap.cps.spi.FetchDescendantsOption
import spock.lang.Specification
class NfProxyDataServiceImplSpec extends Specification {
def cmHandle = 'some handle'
def expectedDataspaceName = 'NFP-Operational'
- def 'Query data nodes by cps path.'() {
+ def 'Query data nodes by cps path with #fetchDescendantsOption.'() {
given: 'a cm Handle and a cps path'
def cpsPath = '/cps-path'
when: 'queryDataNodes is invoked'
- objectUnderTest.queryDataNodes(cmHandle, cpsPath)
+ objectUnderTest.queryDataNodes(cmHandle, cpsPath, fetchDescendantsOption)
then: 'the persistence service is called once with the correct parameters'
- 1 * mockcpsQueryService.queryDataNodes(expectedDataspaceName, cmHandle, cpsPath)
+ 1 * mockcpsQueryService.queryDataNodes(expectedDataspaceName, cmHandle, cpsPath, fetchDescendantsOption)
+ where: 'all fetch descendants options are supported'
+ fetchDescendantsOption << FetchDescendantsOption.values()
}
def 'Update data node leaves.'() {
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/cpsPathInQuery'
+ - $ref: 'components.yml#/components/parameters/includeDescendantsOptionInQuery'
responses:
'200':
$ref: 'components.yml#/components/responses/Ok'
import javax.validation.Valid;
import org.onap.cps.api.CpsQueryService;
import org.onap.cps.rest.api.CpsQueryApi;
+import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@Override
public ResponseEntity<Object> getNodesByDataspaceAndAnchorAndCpsPath(final String dataspaceName,
- final String anchorName, @Valid final String cpsPath) {
+ final String anchorName, @Valid final String cpsPath, @Valid final Boolean includeDescendants) {
+ final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants)
+ ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS;
final Collection<DataNode> dataNodes =
- cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath);
+ cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
return new ResponseEntity<>(new Gson().toJson(dataNodes), HttpStatus.OK);
}
}
package org.onap.cps.rest.controller
-import com.google.common.collect.ImmutableMap
+
import com.google.gson.Gson
import org.modelmapper.ModelMapper
import org.onap.cps.api.CpsAdminService
import org.springframework.test.web.servlet.MockMvc
import spock.lang.Shared
import spock.lang.Specification
+import spock.lang.Unroll
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
@WebMvcTest
@Value('${rest.api.cps-base-path}')
def basePath
- def 'Query data node by cps path for the given dataspace and anchor.'() {
+ @Unroll
+ 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 dataNode = new DataNodeBuilder().withXpath('/xpath').build()
def dataspaceName = 'my_dataspace'
def anchorName = 'my_anchor'
- def cpsPath = '/xpath/leaves[@leaf=\'value\']'
- def dataNode = new DataNodeBuilder().withXpath("/xpath")
- .withLeaves(ImmutableMap.of("leaf", "value")).build()
- ArrayList<DataNode> dataNodeList = new ArrayList();
- dataNodeList.add(dataNode)
- mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath) >> dataNodeList
+ def cpsPath = 'some cps-path'
+ mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, expectedCpsDataServiceOption) >> [dataNode]
and: 'the query endpoint'
def dataNodeEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors/$anchorName/nodes/query"
when: 'query data nodes API is invoked'
- def response = mvc.perform(get(dataNodeEndpoint).param('cps-path', cpsPath)).andReturn().response
+ def response = mvc.perform(get(dataNodeEndpoint)
+ .param('cps-path', cpsPath)
+ .param('include-descendants', includeDescendantsOption))
+ .andReturn().response
then: 'the response contains the the datanode in json format'
response.status == HttpStatus.OK.value()
response.getContentAsString().contains(new Gson().toJson(dataNode))
+ 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
}
}
\ No newline at end of file
package org.onap.cps.spi.impl;
import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
}
@Override
- public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath) {
+ public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
+ final FetchDescendantsOption fetchDescendantsOption) {
final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
final CpsPathQuery cpsPathQuery = CpsPathQuery.createFrom(cpsPath);
.getByAnchorAndXpathAndLeafAttributes(anchorEntity.getId(), cpsPathQuery
.getXpathPrefix(), cpsPathQuery.getLeafName(), cpsPathQuery.getLeafValue());
return fragmentEntities.stream()
- .map(fragmentEntity -> toDataNode(fragmentEntity, OMIT_DESCENDANTS))
+ .map(fragmentEntity -> toDataNode(fragmentEntity, fetchDescendantsOption))
.collect(Collectors.toUnmodifiableList());
}
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import org.onap.cps.spi.CpsDataPersistenceService
+import org.onap.cps.spi.FetchDescendantsOption
import org.onap.cps.spi.entities.FragmentEntity
import org.onap.cps.spi.exceptions.AnchorNotFoundException
import org.onap.cps.spi.exceptions.DataNodeNotFoundException
@Sql([CLEAR_DATA, SET_DATA])
def 'Cps Path query for single leaf value with type: #type.'() {
when: 'a query is executed to get a data node by the given cps path'
- def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath)
+ def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath, includeDescendantsOption)
then: 'the correct data is returned'
def leaves ='[common-leaf-name:common-leaf-value, common-leaf-name-int:5.0]'
- result.size() == 1
- def dataNode = result.stream().findFirst().get()
+ DataNode dataNode = result.stream().findFirst().get()
dataNode.getLeaves().toString() == leaves
+ dataNode.getChildDataNodes().size() == expectedNumberOfChidlNodes
where: 'the following data is used'
- type | cpsPath
- 'String' | '/parent-200/child-202[@common-leaf-name=\'common-leaf-value\']'
- 'Integer' | '/parent-200/child-202[@common-leaf-name-int=5]'
+ type | cpsPath | includeDescendantsOption | expectedNumberOfChidlNodes
+ 'String and no descendants' | '/parent-200/child-202[@common-leaf-name=\'common-leaf-value\']' | OMIT_DESCENDANTS | 0
+ 'Integer and descendants' | '/parent-200/child-202[@common-leaf-name-int=5]' | INCLUDE_ALL_DESCENDANTS | 1
}
@Unroll
@Sql([CLEAR_DATA, SET_DATA])
def 'Query for attribute by cps path with cps paths that return no data because of #scenario.'() {
when: 'a query is executed to get datanodes for the given cps path'
- def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath)
+ def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath, FetchDescendantsOption.OMIT_DESCENDANTS)
then: 'no data is returned'
result.isEmpty()
where: 'following cps queries are performed'
scenario | cpsPath
- 'cps path is incomplete' | '/parent-200[@common-leaf-name-int=5]'
+ 'cps path is incomplete' | '/parent-200[@common-leaf-name-int=5]'
'missing / at beginning of path' | 'parent-200/child-202[@common-leaf-name-int=5]'
'leaf value does not exist' | '/parent-200/child-202[@common-leaf-name=\'does not exist\']'
'incomplete end of xpath prefix' | '/parent-200/child-20[@common-leaf-name-int=5]'
(4201, 1001, 3003, null, '/parent-200', '{"leaf-value": "original"}'),
(4202, 1001, 3003, 4201, '/parent-200/child-201', '{"leaf-value": "original"}'),
(4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'),
- (4204, 1001, 3003, 4201, '/parent-200/child-202', '{"common-leaf-name": "common-leaf-value", "common-leaf-name-int" : 5}');
\ No newline at end of file
+ (4204, 1001, 3003, 4201, '/parent-200/child-202', '{"common-leaf-name": "common-leaf-value", "common-leaf-name-int" : 5}'),
+ (4205, 1001, 3003, 4204, '/parent-200/child-202/grand-child-202', '{"common-leaf-name": "common-leaf-value", "common-leaf-name-int" : 5}');
\ No newline at end of file
import java.util.Collection;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
/*
* @param dataspaceName dataspace name
* @param anchorName anchor name
* @param cpsPath cps path
+ * @param fetchDescendantsOption defines whether the descendants of the node(s) found by the query should be
+ * included in the output
* @return a collection of data nodes
*/
Collection<DataNode> queryDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
- @NonNull String cpsPath);
+ @NonNull String cpsPath, @NonNull FetchDescendantsOption fetchDescendantsOption);
}
import java.util.Collection;
import org.onap.cps.api.CpsQueryService;
import org.onap.cps.spi.CpsDataPersistenceService;
+import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Override
public Collection<DataNode> queryDataNodes(final String dataspaceName, final String anchorName,
- final String cpsPath) {
- return cpsDataPersistenceService.queryDataNodes(dataspaceName, anchorName, cpsPath);
+ final String cpsPath, final FetchDescendantsOption fetchDescendantsOption) {
+ return cpsDataPersistenceService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
}
}
\ No newline at end of file
* @param dataspaceName dataspace name
* @param anchorName anchor name
* @param cpsPath cps path
+ * @param fetchDescendantsOption defines whether the descendants of the node(s) found by the query should be
+ * included in the output
* @return the data nodes found i.e. 0 or more data nodes
*/
Collection<DataNode> queryDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
- @NonNull String cpsPath);
+ @NonNull String cpsPath, @NonNull FetchDescendantsOption fetchDescendantsOption);
}
package org.onap.cps.api.impl
import org.onap.cps.spi.CpsDataPersistenceService
+import org.onap.cps.spi.FetchDescendantsOption
import spock.lang.Specification
+import spock.lang.Unroll
+
class CpsQueryServiceImplSpec extends Specification {
def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
objectUnderTest.cpsDataPersistenceService = mockCpsDataPersistenceService
}
- def 'Query data nodes by cps path.'() {
+ @Unroll
+ def 'Query data nodes by cps path with #fetchDescendantsOption.'() {
given: 'a dataspace name, an anchor name and a cps path'
def dataspaceName = 'some dataspace'
def anchorName = 'some anchor'
def cpsPath = '/cps-path'
when: 'queryDataNodes is invoked'
- objectUnderTest.queryDataNodes(dataspaceName, anchorName, cpsPath)
+ objectUnderTest.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption)
then: 'the persistence service is called once with the correct parameters'
- 1 * mockCpsDataPersistenceService.queryDataNodes(dataspaceName, anchorName, cpsPath)
+ 1 * mockCpsDataPersistenceService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption)
+ where: 'all fetch descendants options are supported'
+ fetchDescendantsOption << FetchDescendantsOption.values()
}
}
\ No newline at end of file
schema:
type: string
default: /
+ - name: include-descendants
+ in: query
+ description: include-descendants
+ required: false
+ schema:
+ type: boolean
+ default: false
responses:
'200':
description: OK