From: arpitsingh Date: Mon, 9 Jan 2023 14:23:10 +0000 (+0530) Subject: CPS-1401 Implement V2 of GET Data Node API X-Git-Tag: 3.2.3~15^2 X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=0bd192ca12ac2f768e44d0d3482785c79a881904;p=cps.git CPS-1401 Implement V2 of GET Data Node API - Modified the GET Data Node API so it returns all the data nodes when xpath set to root "/" - Fragment Repository now returns a collection of Fragment Entities - Instead of returning only the first Fragment Entity now all fragment entities are returned when xpath is set to root - The Fragemnt Entities are further processed to a Collection of Data Nodes. As opposed to singular Data Node in current implementation. - Finally the DataRestController also returns a Collection of Data Nodes when xpath is set to root and valid data is present - Response body changed from JSON object to JSON Array. - Exception handling for invalid xpath and non-existing xpath is now done separately at persistence layer. - Refactored code against CPS-1422 - Deprecated getDataNode method from Service and Persistence layer - Modified V1 of Get Data Node API to use the getDataNodes method and get the first data node from the collection returned. - Modified NCMP to use getDataNodes method - NCMP still does not support multiple data nodes. It retrieves the first data node from the collection returned by getDataNodes Signed-off-by: arpitsingh Change-Id: I494a5740a53f65376d135fcb9f1e2e8900a2803e --- diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java index b67ae0c19..a98c6008c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -217,8 +218,8 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm final Map queryResult = new HashMap<>(cmHandleIdsByModuleName.size()); if (previousQueryResult == NO_QUERY_TO_EXECUTE) { cmHandleIdsByModuleName.forEach(cmHandleId -> - queryResult.put(cmHandleId, createNcmpServiceCmHandle( - inventoryPersistence.getDataNode("/dmi-registry/cm-handles[@id='" + cmHandleId + "']"))) + queryResult.put(cmHandleId, createNcmpServiceCmHandle(inventoryPersistence + .getDataNode("/dmi-registry/cm-handles[@id='" + cmHandleId + "']").iterator().next())) ); return queryResult; } @@ -298,13 +299,14 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm } private Set getAllCmHandles() { - return inventoryPersistence.getDataNode("/dmi-registry") - .getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet()); + final DataNode dataNode = inventoryPersistence.getDataNode("/dmi-registry").iterator().next(); + return dataNode.getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet()); } private Set getAllCmHandleIds() { - return inventoryPersistence.getDataNode("/dmi-registry", FETCH_DIRECT_CHILDREN_ONLY) - .getChildDataNodes().stream().map(dataNode -> dataNode.getLeaves().get("id").toString()) + final DataNode dataNodes = inventoryPersistence.getDataNode("/dmi-registry", FETCH_DIRECT_CHILDREN_ONLY) + .iterator().next(); + return dataNodes.getChildDataNodes().stream().map(dataNode -> dataNode.getLeaves().get("id").toString()) .collect(Collectors.toSet()); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java index e71b72ab8..d5500b8d7 100755 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java @@ -4,6 +4,7 @@ * Modifications Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -131,8 +132,8 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService public Object getResourceDataOperational(final String cmHandleId, final String resourceIdentifier, final FetchDescendantsOption fetchDescendantsOption) { - return cpsDataService.getDataNode(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId, resourceIdentifier, - fetchDescendantsOption); + return cpsDataService.getDataNodes(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId, resourceIdentifier, + fetchDescendantsOption).iterator().next(); } @Override diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java index f39c2f9b6..bbb2c0f56 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +67,8 @@ public class NetworkCmProxyDataServicePropertyHandler { for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) { final String cmHandleId = ncmpServiceCmHandle.getCmHandleId(); try { - final DataNode existingCmHandleDataNode = inventoryPersistence.getCmHandleDataNode(cmHandleId); + final DataNode existingCmHandleDataNode = inventoryPersistence.getCmHandleDataNode(cmHandleId) + .iterator().next(); processUpdates(existingCmHandleDataNode, ncmpServiceCmHandle); cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandleId)); } catch (final DataNodeNotFoundException e) { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImpl.java index bda0a728b..0f86cb7be 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImpl.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -174,8 +175,8 @@ public class CmHandleQueriesImpl implements CmHandleQueries { private DataNode getCmHandleState(final String cmHandleId) { final String xpath = "/dmi-registry/cm-handles[@id='" + cmHandleId + "']/state"; - return cpsDataPersistenceService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - xpath, OMIT_DESCENDANTS); + return cpsDataPersistenceService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + xpath, OMIT_DESCENDANTS).iterator().next(); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java index 10227cf26..d22cf0e16 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022-2023 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,7 +120,7 @@ public interface InventoryPersistence { * @param xpath xpath * @return data node */ - DataNode getDataNode(String xpath); + Collection getDataNode(String xpath); /** * Get data node via xpath. @@ -128,7 +129,7 @@ public interface InventoryPersistence { * @param fetchDescendantsOption fetch descendants option * @return data node */ - DataNode getDataNode(String xpath, FetchDescendantsOption fetchDescendantsOption); + Collection getDataNode(String xpath, FetchDescendantsOption fetchDescendantsOption); /** * Get collection of data nodes via xpaths. @@ -153,7 +154,7 @@ public interface InventoryPersistence { * @param cmHandleId cmHandle ID * @return data node */ - DataNode getCmHandleDataNode(String cmHandleId); + Collection getCmHandleDataNode(String cmHandleId); /** * Get collection of data nodes of given cm handles. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java index 4d1202b06..1c799c6b2 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022-2023 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,9 +74,9 @@ public class InventoryPersistenceImpl implements InventoryPersistence { @Override public CompositeState getCmHandleState(final String cmHandleId) { - final DataNode stateAsDataNode = cpsDataService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + final DataNode stateAsDataNode = cpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, createCmHandleXPath(cmHandleId) + "/state", - FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); + FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS).iterator().next(); cpsValidator.validateNameCharacters(cmHandleId); return new CompositeStateBuilder().fromDataNode(stateAsDataNode).build(); } @@ -101,7 +102,8 @@ public class InventoryPersistenceImpl implements InventoryPersistence { @Override public YangModelCmHandle getYangModelCmHandle(final String cmHandleId) { cpsValidator.validateNameCharacters(cmHandleId); - return YangDataConverter.convertCmHandleToYangModel(getCmHandleDataNode(cmHandleId), cmHandleId); + final DataNode dataNode = getCmHandleDataNode(cmHandleId).iterator().next(); + return YangDataConverter.convertCmHandleToYangModel(dataNode, cmHandleId); } @Override @@ -169,15 +171,15 @@ public class InventoryPersistenceImpl implements InventoryPersistence { @Override @Timed(value = "cps.ncmp.inventory.persistence.datanode.get", description = "Time taken to get a data node (from ncmp dmi registry)") - public DataNode getDataNode(final String xpath) { + public Collection getDataNode(final String xpath) { return getDataNode(xpath, INCLUDE_ALL_DESCENDANTS); } @Override @Timed(value = "cps.ncmp.inventory.persistence.datanode.get", description = "Time taken to get a data node (from ncmp dmi registry)") - public DataNode getDataNode(final String xpath, final FetchDescendantsOption fetchDescendantsOption) { - return cpsDataService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + public Collection getDataNode(final String xpath, final FetchDescendantsOption fetchDescendantsOption) { + return cpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, fetchDescendantsOption); } @@ -189,12 +191,12 @@ public class InventoryPersistenceImpl implements InventoryPersistence { @Override public Collection getDataNodes(final Collection xpaths, final FetchDescendantsOption fetchDescendantsOption) { - return cpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + return cpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpaths, fetchDescendantsOption); } @Override - public DataNode getCmHandleDataNode(final String cmHandleId) { + public Collection getCmHandleDataNode(final String cmHandleId) { return this.getDataNode(createCmHandleXPath(cmHandleId)); } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy index 05856d0ea..5cd702a6b 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +41,7 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { def partiallyMockedCmHandleQueries = Spy(CmHandleQueriesImpl) def mockInventoryPersistence = Mock(InventoryPersistence) - def static someCmHandleDataNode = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'some-cmhandle-id\']', leaves: ['id':'some-cmhandle-id']) + def static someCmHandleDataNode = [new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'some-cmhandle-id\']', leaves: ['id':'some-cmhandle-id'])] def dmiRegistry = new DataNode(xpath: '/dmi-registry', childDataNodes: createDataNodeList(['PNFDemo1', 'PNFDemo2', 'PNFDemo3', 'PNFDemo4'])) static def queryResultCmHandleMap = createCmHandleMap(['H1', 'H2']) @@ -136,7 +137,7 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { and: 'cmHandles are returned from the module names query' mockInventoryPersistence.getCmHandleIdsWithGivenModules(['some-module-name']) >> anchorsForModuleQuery and: 'cmHandleQueries returns a datanode result' - 2 * cmHandleQueries.queryCmHandleDataNodesByCpsPath(*_) >> [someCmHandleDataNode] + 2 * cmHandleQueries.queryCmHandleDataNodesByCpsPath(*_) >> someCmHandleDataNode when: 'the query is executed for both cm handle ids and details' def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters) @@ -157,9 +158,9 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { given: 'We use an empty query' def cmHandleQueryParameters = new CmHandleQueryServiceParameters() and: 'the inventory persistence returns the dmi registry datanode with just ids' - mockInventoryPersistence.getDataNode("/dmi-registry", FetchDescendantsOption.FETCH_DIRECT_CHILDREN_ONLY) >> dmiRegistry + mockInventoryPersistence.getDataNode("/dmi-registry", FetchDescendantsOption.FETCH_DIRECT_CHILDREN_ONLY) >> [dmiRegistry] and: 'the inventory persistence returns the dmi registry datanode with data' - mockInventoryPersistence.getDataNode("/dmi-registry") >> dmiRegistry + mockInventoryPersistence.getDataNode("/dmi-registry") >> [dmiRegistry] when: 'the query is executed for both cm handle ids and details' def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters) @@ -173,7 +174,7 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { given: 'We query without any parameters' def cmHandleQueryParameters = new CmHandleQueryServiceParameters() and: 'the inventoryPersistence returns all four CmHandleIds' - mockInventoryPersistence.getDataNode(*_) >> dmiRegistry + mockInventoryPersistence.getDataNode(*_) >> [dmiRegistry] when: 'the query executed' def resultSet = objectUnderTest.queryCmHandleIdsForInventory(cmHandleQueryParameters) then: 'the size of the result list equals the size of all cmHandleIds.' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy index 578f7f31c..fe6fa32ae 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy @@ -3,6 +3,7 @@ * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,11 +90,11 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']" - def dataNode = new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService']) + def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])] def 'Write resource data for pass-through running from DMI using POST.'() { given: 'cpsDataService returns valid datanode' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', + mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode when: 'write resource data is called' objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', @@ -107,7 +108,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def 'Get resource data for pass-through operational from DMI.'() { given: 'get data node is called' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', + mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode and: 'get resource data from DMI is called' mockDmiDataOperations.getResourceDataFromDmi( @@ -129,7 +130,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def 'Get resource data for pass-through running from DMI.'() { given: 'cpsDataService returns valid data node' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', + mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode and: 'DMI returns valid response and data' mockDmiDataOperations.getResourceDataFromDmi('testCmHandle', @@ -233,7 +234,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() { given: 'cpsDataService returns valid datanode' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', + mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode when: 'get resource data is called' objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy index 64461fa7e..0df61f4e0 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,11 +48,11 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/additional-properties[@name='additionalProp2']").withLeaves(['name': 'additionalProp2', 'value': 'additionalValue2']).build(), new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/public-properties[@name='publicProp3']").withLeaves(['name': 'publicProp3', 'value': 'publicValue3']).build(), new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/public-properties[@name='publicProp4']").withLeaves(['name': 'publicProp4', 'value': 'publicValue4']).build()] - def static cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: propertyDataNodes) + def static cmHandleDataNodeAsCollection = [new DataNode(xpath: cmHandleXpath, childDataNodes: propertyDataNodes)] def 'Update CM Handle Public Properties: #scenario'() { given: 'the CPS service return a CM handle' - mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNode + mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNodeAsCollection and: 'an update cm handle request with public properties updates' def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: updatedPublicProperties)] when: 'update data node leaves is called with the update request' @@ -73,7 +74,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { def 'Update DMI Properties: #scenario'() { given: 'the CPS service return a CM handle' - mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNode + mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNodeAsCollection and: 'an update cm handle request with DMI properties updates' def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: updatedDmiProperties)] when: 'update data node leaves is called with the update request' @@ -97,7 +98,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { def 'Update CM Handle Properties, remove all properties: #scenario'() { given: 'the CPS service return a CM handle' def cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: originalPropertyDataNodes) - mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNode + mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> [cmHandleDataNode] and: 'an update cm handle request that removes all public properties(existing and non-existing)' def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp3': null, 'publicProp4': null])] when: 'update data node leaves is called with the update request' @@ -145,7 +146,8 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:]), new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:])] and: 'data node can be found for 1st and 3rd cm-handle but not for 2nd cm-handle' - mockInventoryPersistence.getCmHandleDataNode(*_) >> cmHandleDataNode >> { throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') } >> cmHandleDataNode + mockInventoryPersistence.getCmHandleDataNode(*_) >> cmHandleDataNodeAsCollection >> { + throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') } >> cmHandleDataNodeAsCollection when: 'update data node leaves is called using correct parameters' def cmHandleResponseList = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) then: 'response has 3 values' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy index 2800f5c24..771198e28 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -120,8 +121,8 @@ class CmHandleQueriesImplSpec extends Specification { given: 'a cm handle state to compare' def cmHandleState = state and: 'the persistence service returns a list of data nodes' - cpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - '/dmi-registry/cm-handles[@id=\'some-cm-handle\']/state', OMIT_DESCENDANTS) >> new DataNode(leaves: ['cm-handle-state': 'READY']) + cpsDataPersistenceService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', + '/dmi-registry/cm-handles[@id=\'some-cm-handle\']/state', OMIT_DESCENDANTS) >> [new DataNode(leaves: ['cm-handle-state': 'READY'])] when: 'cm handles are compared by state' def result = objectUnderTest.cmHandleHasState('some-cm-handle', cmHandleState) then: 'the returned result matches the expected result from the persistence service' @@ -136,8 +137,8 @@ class CmHandleQueriesImplSpec extends Specification { given: 'a cm handle state to query' def cmHandleState = CmHandleState.READY and: 'cps data service returns a list of data nodes' - cpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - '/dmi-registry/cm-handles[@id=\'some-cm-handle\']/state', OMIT_DESCENDANTS) >> new DataNode(leaves: ['cm-handle-state': 'READY']) + cpsDataPersistenceService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', + '/dmi-registry/cm-handles[@id=\'some-cm-handle\']/state', OMIT_DESCENDANTS) >> [new DataNode(leaves: ['cm-handle-state': 'READY'])] when: 'cm handles are fetched by state and id' def result = objectUnderTest.getCmHandleState('some-cm-handle') then: 'the returned result is a list of data nodes returned by cps data service' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy index 929ea84e3..4a98526b4 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022-2023 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +40,6 @@ import spock.lang.Specification import java.time.OffsetDateTime import java.time.ZoneOffset import java.time.format.DateTimeFormatter -import java.util.stream.Collectors import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMESTAMP import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS @@ -85,7 +85,7 @@ class InventoryPersistenceImplSpec extends Specification { def "Retrieve CmHandle using datanode with #scenario."() { given: 'the cps data service returns a data node from the DMI registry' def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves) - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode + mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode] when: 'retrieving the yang modelled cm handle' def result = objectUnderTest.getYangModelCmHandle(cmHandleId) then: 'the result has the correct id and service names' @@ -112,7 +112,7 @@ class InventoryPersistenceImplSpec extends Specification { def "Handling missing service names as null."() { given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes' def dataNode = new DataNode(childDataNodes:[], leaves: [:]) - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode + mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode] when: 'retrieving the yang modelled cm handle' def result = objectUnderTest.getYangModelCmHandle(cmHandleId) then: 'the service names are returned as null' @@ -126,7 +126,7 @@ class InventoryPersistenceImplSpec extends Specification { def "Retrieve multiple YangModelCmHandles"() { given: 'the cps data service returns 2 data nodes from the DMI registry' def dataNodes = [new DataNode(xpath: xpath), new DataNode(xpath: xpath2)] - mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes + mockCpsDataService.getDataNodesForMultipleXpaths('NCMP-Admin', 'ncmp-dmi-registry', [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes when: 'retrieving the yang modelled cm handle' def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2]) then: 'verify both have returned and cmhandleIds are correct' @@ -139,8 +139,8 @@ class InventoryPersistenceImplSpec extends Specification { def cmHandleId = 'Some-Cm-Handle' def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED']) and: 'cps data service returns a valid data node' - mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', - '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode + mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', + '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode] when: 'get cm handle state is invoked' def result = objectUnderTest.getCmHandleState(cmHandleId) then: 'result has returned the correct cm handle state' @@ -251,7 +251,7 @@ class InventoryPersistenceImplSpec extends Specification { when: 'the method to get data nodes is called' objectUnderTest.getDataNode('sample xPath') then: 'the data persistence service method to get data node is invoked once' - 1 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS) + 1 * mockCpsDataService.getDataNodes('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS) } def 'Get cmHandle data node'() { @@ -260,7 +260,7 @@ class InventoryPersistenceImplSpec extends Specification { when: 'the method to get data nodes is called' objectUnderTest.getCmHandleDataNode('sample cmHandleId') then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath' - 1 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS) + 1 * mockCpsDataService.getDataNodes('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS) } def 'Get CM handles that has given module names'() { diff --git a/cps-rest/docs/openapi/cpsDataV2.yml b/cps-rest/docs/openapi/cpsDataV2.yml index 61663ab3a..ad0c299d7 100644 --- a/cps-rest/docs/openapi/cpsDataV2.yml +++ b/cps-rest/docs/openapi/cpsDataV2.yml @@ -46,4 +46,4 @@ nodeByDataspaceAndAnchor: $ref: 'components.yml#/components/responses/Forbidden' '500': $ref: 'components.yml#/components/responses/InternalServerError' - x-codegen-request-body-name: xpath + x-codegen-request-body-name: xpath \ No newline at end of file diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java index 3a9c764bc..80cfb8ce0 100755 --- a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java +++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java @@ -3,7 +3,7 @@ * Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Nordix Foundation - * Modifications Copyright (C) 2023 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,6 +26,10 @@ package org.onap.cps.rest.controller; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; import javax.validation.ValidationException; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -97,21 +101,27 @@ public class DataRestController implements CpsDataApi { 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.getDataNode(dataspaceName, anchorName, xpath, - fetchDescendantsOption); - final String prefix = prefixResolver.getPrefix(dataspaceName, anchorName, xpath); + final DataNode dataNode = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath, + fetchDescendantsOption).iterator().next(); + final String prefix = prefixResolver.getPrefix(dataspaceName, anchorName, dataNode.getXpath()); return new ResponseEntity<>(DataMapUtils.toDataMapWithIdentifier(dataNode, prefix), HttpStatus.OK); } @Override public ResponseEntity getNodeByDataspaceAndAnchorV2(final String dataspaceName, final String anchorName, - final String xpath, final String fetchDescendantsOptionAsString) { + final String xpath, + final String fetchDescendantsOptionAsString) { final FetchDescendantsOption fetchDescendantsOption = - FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString); - final DataNode dataNode = cpsDataService.getDataNode(dataspaceName, anchorName, xpath, - fetchDescendantsOption); - final String prefix = prefixResolver.getPrefix(dataspaceName, anchorName, xpath); - return new ResponseEntity<>(DataMapUtils.toDataMapWithIdentifier(dataNode, prefix), HttpStatus.OK); + FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString); + final Collection dataNodes = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath, + fetchDescendantsOption); + final List> dataMaps = new ArrayList<>(dataNodes.size()); + for (final DataNode dataNode: dataNodes) { + final String prefix = prefixResolver.getPrefix(dataspaceName, anchorName, dataNode.getXpath()); + final Map dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); + dataMaps.add(dataMap); + } + return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataMaps), HttpStatus.OK); } @Override @@ -162,5 +172,4 @@ public class DataRestController implements CpsDataApi { String.format("observed-timestamp must be in '%s' format", ISO_TIMESTAMP_FORMAT)); } } - } diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy index 16d106ba6..d88a9cdf0 100755 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy @@ -25,7 +25,9 @@ package org.onap.cps.rest.controller import com.fasterxml.jackson.databind.ObjectMapper +import groovy.json.JsonSlurper import org.onap.cps.api.CpsDataService +import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.DataNodeBuilder import org.onap.cps.utils.ContentType @@ -68,7 +70,7 @@ class DataRestControllerSpec extends Specification { @Value('${rest.api.cps-base-path}') def basePath - def dataNodeBaseEndpoint + def dataNodeBaseEndpointV1 def dataNodeBaseEndpointV2 def dataspaceName = 'my_dataspace' def anchorName = 'my_anchor' @@ -87,21 +89,25 @@ class DataRestControllerSpec extends Specification { def expectedXmlData = '\n\n' @Shared - static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath') + 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() def setup() { - dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName" + dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName" dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName" } def 'Create a node: #scenario.'() { given: 'endpoint to create a node' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes" + def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" when: 'post is invoked with datanode endpoint and json' def response = mvc.perform( @@ -124,7 +130,7 @@ class DataRestControllerSpec extends Specification { def 'Create a node with observed-timestamp'() { given: 'endpoint to create a node' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes" + def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" when: 'post is invoked with datanode endpoint and json' def response = mvc.perform( @@ -148,7 +154,7 @@ class DataRestControllerSpec extends Specification { def 'Create a child node #scenario'() { given: 'endpoint to create a node' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes" + def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" and: 'parent node xpath' def parentNodeXpath = 'some xpath' when: 'post is invoked with datanode endpoint and json' @@ -177,7 +183,7 @@ class DataRestControllerSpec extends Specification { given: 'parent node xpath ' def parentNodeXpath = 'parent node xpath' when: 'list-node endpoint is invoked with post (create) operation' - def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes") + def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes") .contentType(MediaType.APPLICATION_JSON) .param('xpath', parentNodeXpath) .content(requestBodyJson) @@ -198,9 +204,9 @@ class DataRestControllerSpec extends Specification { def 'Get data node with leaves'() { given: 'the service returns data node leaves' - def xpath = 'xpath' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node" - mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren + 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)) @@ -208,7 +214,7 @@ class DataRestControllerSpec extends Specification { then: 'a success response is returned' response.status == HttpStatus.OK.value() then: 'the response contains the the datanode in json format' - response.getContentAsString() == '{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}' + 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' @@ -218,8 +224,8 @@ class DataRestControllerSpec extends Specification { def 'Get data node with #scenario.'() { given: 'the service returns data node with #scenario' def xpath = 'some xPath' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node" - mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode + 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( @@ -235,25 +241,68 @@ class DataRestControllerSpec extends Specification { response.contentAsString.contains('"child"') == expectChildInResponse where: scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier - 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false | 'xpath' - 'no descendant explicitly' | dataNodeWithLeavesNoChildren | 'false' || OMIT_DESCENDANTS | false | 'xpath' + '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).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 data node with #scenario using V2.'() { + 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) + .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.getDataNode(dataspaceName, anchorName, xpath, {descendantsOption -> { - assert descendantsOption.depth == 2}}) >> dataNodeWithChild + 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) - .param('xpath', xpath) - .param('descendants', '2')) - .andReturn().response + get(endpoint) + .param('xpath', xpath) + .param('descendants', '2')) + .andReturn().response then: 'a success response is returned' assert response.status == HttpStatus.OK.value() and: 'the response contains the root node identifier' @@ -264,7 +313,7 @@ class DataRestControllerSpec extends Specification { def 'Update data node leaves: #scenario.'() { given: 'endpoint to update a node ' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes" + def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" when: 'patch request is performed' def response = mvc.perform( @@ -286,7 +335,7 @@ class DataRestControllerSpec extends Specification { def 'Update data node leaves with observedTimestamp'() { given: 'endpoint to update a node leaves ' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes" + def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" when: 'patch request is performed' def response = mvc.perform( @@ -309,7 +358,7 @@ class DataRestControllerSpec extends Specification { def 'Replace data node tree: #scenario.'() { given: 'endpoint to replace node' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes" + def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" when: 'put request is performed' def response = mvc.perform( @@ -331,7 +380,7 @@ class DataRestControllerSpec extends Specification { def 'Update data node and descendants with observedTimestamp.'() { given: 'endpoint to replace node' - def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes" + def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" when: 'put request is performed' def response = mvc.perform( @@ -354,7 +403,7 @@ class DataRestControllerSpec extends Specification { def 'Replace list content #scenario.'() { when: 'list-nodes endpoint is invoked with put (update) operation' - def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes") + def putRequestBuilder = put("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes") .contentType(MediaType.APPLICATION_JSON) .param('xpath', 'parent xpath') .content(requestBodyJson) @@ -375,7 +424,7 @@ class DataRestControllerSpec extends Specification { def 'Delete list element #scenario.'() { when: 'list-nodes endpoint is invoked with delete operation' - def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes") + def deleteRequestBuilder = delete("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes") .param('xpath', 'list element xpath') if (observedTimestamp != null) deleteRequestBuilder.param('observed-timestamp', observedTimestamp) @@ -396,7 +445,7 @@ class DataRestControllerSpec extends Specification { given: 'data node xpath' def dataNodeXpath = '/dataNodeXpath' when: 'delete data node endpoint is invoked' - def deleteDataNodeRequest = delete( "$dataNodeBaseEndpoint/anchors/$anchorName/nodes") + def deleteDataNodeRequest = delete( "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes") .param('xpath', dataNodeXpath) and: 'observed timestamp is added to the parameters' if (observedTimestamp != null) diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index 5b310efd5..46439fdb3 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -3,7 +3,7 @@ * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2020-2022 Bell Canada. - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -249,17 +249,22 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } @Override - public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath, - final FetchDescendantsOption fetchDescendantsOption) { - final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath, - fetchDescendantsOption); - return toDataNode(fragmentEntity, fetchDescendantsOption); + public Collection getDataNodes(final String dataspaceName, final String anchorName, + final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { + final String targetXpath = isRootXpath(xpath) ? xpath : CpsPathUtil.getNormalizedXpath(xpath); + final Collection dataNodes = getDataNodesForMultipleXpaths(dataspaceName, anchorName, + Collections.singletonList(targetXpath), fetchDescendantsOption); + if (dataNodes.isEmpty()) { + throw new DataNodeNotFoundException(dataspaceName, anchorName, xpath); + } + return dataNodes; } @Override - public Collection getDataNodes(final String dataspaceName, final String anchorName, - final Collection xpaths, - final FetchDescendantsOption fetchDescendantsOption) { + public Collection getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName, + final Collection xpaths, + final FetchDescendantsOption fetchDescendantsOption) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); @@ -271,7 +276,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService try { normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath)); } catch (final PathParsingException e) { - log.warn("Error parsing xpath \"{}\" in getDataNodes: {}", xpath, e.getMessage()); + log.warn("Error parsing xpath \"{}\" in getDataNodesForMultipleXpaths: {}", xpath, e.getMessage()); } } final Collection fragmentEntities = diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy index e4c552978..336d6035a 100755 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.jdbc.Sql import javax.validation.ConstraintViolationException +import java.nio.file.Path import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS @@ -82,15 +83,13 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { ] @Sql([CLEAR_DATA, SET_DATA]) - def 'Get existing datanode with descendants.'() { - when: 'the node is retrieved by its xpath' - def dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME1, '/parent-1', INCLUDE_ALL_DESCENDANTS) - then: 'the path and prefix are populated correctly' - assert dataNode.xpath == '/parent-1' - and: 'dataNode has no prefix (to be addressed by CPS-1301' - assert dataNode.moduleNamePrefix == null - and: 'the child node has the correct path' - assert dataNode.childDataNodes[0].xpath == '/parent-1/child-1' + def 'Get all datanodes with descendants .'() { + when: 'data nodes are retrieved by their xpath' + def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME1, ['/parent-1'], INCLUDE_ALL_DESCENDANTS) + then: 'same data nodes are returned by getDataNodesForMultipleXpaths method' + assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, '/parent-1', INCLUDE_ALL_DESCENDANTS) == dataNodes + and: 'the dataNodes have no prefix (to be addressed by CPS-1301)' + assert dataNodes[0].moduleNamePrefix == null } @Sql([CLEAR_DATA, SET_DATA]) @@ -102,11 +101,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { def dataNodes = [createDataNodeTree(parentXpath, childXpath, grandChildXpath)] objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, dataNodes) then: 'it can be retrieved by its xpath' - def dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS) - assert dataNode.xpath == parentXpath + def dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS) + assert dataNode[0].xpath == parentXpath and: 'it has the correct child' - assert dataNode.childDataNodes.size() == 1 - def childDataNode = dataNode.childDataNodes[0] + assert dataNode[0].childDataNodes.size() == 1 + def childDataNode = dataNode[0].childDataNodes[0] assert childDataNode.xpath == childXpath and: 'and its grandchild' assert childDataNode.childDataNodes.size() == 1 @@ -236,18 +235,19 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get data node by xpath without descendants.'() { - when: 'data node is requested' - def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT, - inputXPath, OMIT_DESCENDANTS) - then: 'data node is returned with no descendants' - assert result.xpath == XPATH_DATA_NODE_WITH_LEAVES - and: 'expected leaves' - assert result.childDataNodes.size() == 0 - assertLeavesMaps(result.leaves, expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES]) - where: 'the following data is used' + def 'Get all data nodes by single xpath without descendants : #scenario'() { + when: 'data nodes are requested' + def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS, + [inputXPath], OMIT_DESCENDANTS) + then: 'data nodes under root are returned' + assert result.childDataNodes.size() == 2 + and: 'no descendants of parent nodes are returned' + result.each {assert it.childDataNodes.size() == 0} + and: 'same data nodes are returned when V2 of get Data Nodes API is executed' + assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS, + inputXPath, OMIT_DESCENDANTS) == result + where: 'the following xpath is used' scenario | inputXPath - 'some xpath' | '/parent-207' 'root xpath' | '/' 'empty xpath' | '' } @@ -255,51 +255,50 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { @Sql([CLEAR_DATA, SET_DATA]) def 'Cps Path query with syntax error throws a CPS Path Exception.'() { when: 'trying to execute a query with a syntax (parsing) error' - objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS) + objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS) then: 'exception is thrown' - def exceptionThrown = thrown(CpsPathException) - assert exceptionThrown.getDetails().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'') + def exceptionThrown = thrown(PathParsingException) + assert exceptionThrown.getMessage().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'') } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get data node by xpath with all descendants.'() { - when: 'data node is requested with all descendants' - def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT, - inputXPath, INCLUDE_ALL_DESCENDANTS) - def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result) - then: 'data node is returned with all the descendants populated' - assert mappedResult.size() == 4 + def 'Get all data nodes by single xpath with all descendants : #scenario'() { + when: 'data nodes are requested with all descendants' + def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS, + [inputXPath], INCLUDE_ALL_DESCENDANTS) + def mappedResult = multipleTreesToFlatMapByXpath(new HashMap<>(), result) + then: 'data nodes are returned with all the descendants populated' + assert mappedResult.size() == 8 assert result.childDataNodes.size() == 2 - assert mappedResult.get('/parent-207/child-001').childDataNodes.size() == 0 - assert mappedResult.get('/parent-207/child-002').childDataNodes.size() == 1 - and: 'extracted leaves maps are matching expected' - mappedResult.forEach( - (xPath, dataNode) -> assertLeavesMaps(dataNode.leaves, expectedLeavesByXpathMap[xPath])) + assert mappedResult.get('/parent-208/child-001').childDataNodes.size() == 0 + assert mappedResult.get('/parent-208/child-002').childDataNodes.size() == 1 + assert mappedResult.get('/parent-209/child-001').childDataNodes.size() == 0 + assert mappedResult.get('/parent-209/child-002').childDataNodes.size() == 1 + and: 'same data nodes are returned when V2 of Get Data Nodes API is executed' + assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS, + inputXPath, INCLUDE_ALL_DESCENDANTS) == result where: 'the following data is used' scenario | inputXPath - 'some xpath' | '/parent-207' 'root xpath' | '/' 'empty xpath' | '' } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get data node error scenario: #scenario.'() { - when: 'attempt to get data node with #scenario' - objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) - then: 'a #expectedException is thrown' + def 'Get data nodes error scenario : #scenario.'() { + when: 'attempt to get data nodes with #scenario' + objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) + then: 'an #expectedException is thrown' thrown(expectedException) where: 'the following data is used' - scenario | dataspaceName | anchorName | xpath || expectedException - 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException - 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException - 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO-XPATH' || DataNodeNotFoundException - 'invalid xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || CpsPathException + scenario | dataspaceName | anchorName | xpath || expectedException + 'non existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO-XPATH' || DataNodeNotFoundException + 'invalid Xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || PathParsingException } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get multiple data nodes by xpath.'() { + def 'Get data nodes for multiple xpaths.'() { when: 'fetch #scenario.' - def results = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, inputXpaths, OMIT_DESCENDANTS) + def results = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME3, inputXpaths, OMIT_DESCENDANTS) then: 'the expected number of data nodes are returned' assert results.size() == expectedResultSize where: 'following parameters were used' @@ -323,9 +322,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get multiple data nodes error scenario: #scenario.'() { + def 'Get data nodes for collection of xpath error scenario : #scenario.'() { when: 'attempt to get data nodes with #scenario' - objectUnderTest.getDataNodes(dataspaceName, anchorName, ['/not-relevant'], OMIT_DESCENDANTS) + objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, ['/not-relevant'], OMIT_DESCENDANTS) then: 'a #expectedException is thrown' thrown(expectedException) where: 'the following data is used' @@ -599,11 +598,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { def child = new DataNodeBuilder().withXpath(deleteTestChildXpath).withChildDataNodes([grandChild]).build() objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTestParentXPath, child) and: 'number of children before delete is stored' - def numberOfChildrenBeforeDelete = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS).childDataNodes.size() + def numberOfChildrenBeforeDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size() when: 'target node is deleted' objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTarget) then: 'one child has been deleted' - def numberOfChildrenAfterDelete = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS).childDataNodes.size() + def numberOfChildrenAfterDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size() assert numberOfChildrenAfterDelete == numberOfChildrenBeforeDelete - 1 where: scenario | deleteTarget | pathToParentOfDeletedNode @@ -634,13 +633,13 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { and: 'data nodes are deleted' objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion) when: 'verify data nodes are removed' - objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion, INCLUDE_ALL_DESCENDANTS) + objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion, INCLUDE_ALL_DESCENDANTS) then: thrown(DataNodeNotFoundException) and: 'some related object is not deleted' if (xpathSurvivor!=null) { - dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathSurvivor, INCLUDE_ALL_DESCENDANTS) - assert dataNode.xpath == xpathSurvivor + dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathSurvivor, INCLUDE_ALL_DESCENDANTS) + assert dataNode[0].xpath == xpathSurvivor } where: 'following parameters were used' scenario | xpathForDeletion || xpathSurvivor @@ -711,6 +710,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { return flatMap } + def static multipleTreesToFlatMapByXpath(Map flatMap, Collection dataNodeTrees) { + for (DataNode dataNodeTree: dataNodeTrees){ + flatMap.put(dataNodeTree.xpath, dataNodeTree) + dataNodeTree.childDataNodes + .forEach(childDataNode -> multipleTreesToFlatMapByXpath(flatMap, [childDataNode])) + } + return flatMap + } + def keysToXpaths(parent, Collection keys) { return keys.collect { "${parent}/child-list[@key='${it}']".toString() } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy index 5cabc85b3..ac66e8c02 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy @@ -25,7 +25,6 @@ import org.hibernate.StaleStateException import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.entities.AnchorEntity import org.onap.cps.spi.entities.FragmentEntity -import org.onap.cps.spi.entities.FragmentExtract import org.onap.cps.spi.exceptions.ConcurrencyException import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.DataNode @@ -112,10 +111,10 @@ class CpsDataPersistenceServiceSpec extends Specification { given: 'the db has a fragment with an attribute property JSON value of #scenario' mockFragmentWithJson("{\"some attribute\": ${dataString}}") when: 'getting the data node represented by this fragment' - def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor', + def dataNode = objectUnderTest.getDataNodes('my-dataspace', 'my-anchor', '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: 'the leaf is of the correct value and data type' - def attributeValue = dataNode.leaves.get('some attribute') + def attributeValue = dataNode[0].leaves.get('some attribute') assert attributeValue == expectedValue assert attributeValue.class == expectedDataClass where: 'the following Data Type is passed' @@ -136,7 +135,7 @@ class CpsDataPersistenceServiceSpec extends Specification { given: 'a fragment with invalid JSON' mockFragmentWithJson('{invalid json') when: 'getting the data node represented by this fragment' - objectUnderTest.getDataNode('my-dataspace', 'my-anchor', + objectUnderTest.getDataNodes('my-dataspace', 'my-anchor', '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: 'a data validation exception is thrown' thrown(DataValidationException) @@ -151,7 +150,7 @@ class CpsDataPersistenceServiceSpec extends Specification { def fragmentEntity2 = new FragmentEntity(xpath: '/xpath2', childFragments: []) mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['/xpath1', '/xpath2'] as Set) >> [fragmentEntity1, fragmentEntity2] when: 'getting data nodes for 2 xpaths' - def result = objectUnderTest.getDataNodes('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + def result = objectUnderTest.getDataNodesForMultipleXpaths('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: '2 data nodes are returned' assert result.size() == 2 } @@ -243,10 +242,8 @@ class CpsDataPersistenceServiceSpec extends Specification { def mockFragmentWithJson(json) { def anchorEntity = new AnchorEntity(id:123) mockAnchorRepository.getByDataspaceAndName(*_) >> anchorEntity - def mockFragmentExtract = Mock(FragmentExtract) - mockFragmentExtract.getId() >> 456 - mockFragmentExtract.getAttributes() >> json - mockFragmentRepository.findByAnchorIdAndParentXpath(*_) >> [mockFragmentExtract] + def fragmentEntity = new FragmentEntity(xpath: '/parent-01', childFragments: [], attributes: json) + mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['/parent-01'] as Set) >> [fragmentEntity] } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy index 1ecad4e68..30ff11b45 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy @@ -3,6 +3,7 @@ * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021 Bell Canada. + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -60,13 +61,14 @@ class CpsPersistenceSpecBase extends Specification { static final String CLEAR_DATA = '/data/clear-all.sql' - static final String DATASPACE_NAME = 'DATASPACE-001' - static final String SCHEMA_SET_NAME1 = 'SCHEMA-SET-001' - static final String SCHEMA_SET_NAME2 = 'SCHEMA-SET-002' - static final String ANCHOR_NAME1 = 'ANCHOR-001' - static final String ANCHOR_NAME2 = 'ANCHOR-002' - static final String ANCHOR_NAME3 = 'ANCHOR-003' - static final String ANCHOR_FOR_DATA_NODES_WITH_LEAVES = 'ANCHOR-003' - static final String ANCHOR_FOR_SHOP_EXAMPLE = 'ANCHOR-004' - static final String ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT = 'ANCHOR-005' + static def DATASPACE_NAME = 'DATASPACE-001' + static def SCHEMA_SET_NAME1 = 'SCHEMA-SET-001' + static def SCHEMA_SET_NAME2 = 'SCHEMA-SET-002' + static def ANCHOR_NAME1 = 'ANCHOR-001' + static def ANCHOR_NAME2 = 'ANCHOR-002' + static def ANCHOR_NAME3 = 'ANCHOR-003' + static def ANCHOR_FOR_DATA_NODES_WITH_LEAVES = 'ANCHOR-003' + static def ANCHOR_FOR_SHOP_EXAMPLE = 'ANCHOR-004' + static def ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT = 'ANCHOR-005' + static def ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS = 'ANCHOR-006' } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy index 0c4f5ec41..246fd80bb 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy @@ -64,13 +64,13 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { def 'Get data node with many descendants by xpath #scenario'() { when: 'get parent is executed with all descendants' stopWatch.start() - def result = objectUnderTest.getDataNode(PERF_DATASPACE, PERF_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) + def result = objectUnderTest.getDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) stopWatch.stop() def readDurationInMillis = stopWatch.getTotalTimeMillis() then: 'read duration is under 500 milliseconds' recordAndAssertPerformance("Get ${scenario}", 500, readDurationInMillis) and: 'data node is returned with all the descendants populated' - assert countDataNodes(result) == TOTAL_NUMBER_OF_NODES + assert countDataNodes(result[0]) == TOTAL_NUMBER_OF_NODES where: 'the following xPaths are used' scenario || xpath 'parent' || PERF_TEST_PARENT @@ -93,7 +93,7 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { when: 'we query for all grandchildren (except 1 for fun) with the new native method' xpathsToAllGrandChildren.remove(0) stopWatch.start() - def result = objectUnderTest.getDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpathsToAllGrandChildren, INCLUDE_ALL_DESCENDANTS) + def result = objectUnderTest.getDataNodesForMultipleXpaths(PERF_DATASPACE, PERF_ANCHOR, xpathsToAllGrandChildren, INCLUDE_ALL_DESCENDANTS) stopWatch.stop() def readDurationInMillis = stopWatch.getTotalTimeMillis() then: 'the returned number of entities equal to the number of children * number of grandchildren' diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index ad463cffd..caafcd320 100755 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -52,7 +52,8 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES (3001, 'ANCHOR-001', 1001, 2001), (3003, 'ANCHOR-003', 1001, 2001), (3004, 'ncmp-dmi-registry', 1002, 2001), - (3005, 'ANCHOR-005', 1001, 2001); + (3005, 'ANCHOR-005', 1001, 2001), + (3006, 'ANCHOR-006', 1001, 2001); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH) VALUES (4001, 1001, 3001, null, '/parent-1'), @@ -68,6 +69,16 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) (5011, 1001, 3005, 5009, '/parent-207/child-002', '{"second-child-leaf": "second-child-leaf value"}'), (5012, 1001, 3005, 5011, '/parent-207/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}'); +INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES + (5013, 1001, 3006, null, '/parent-208', '{"parent-leaf-1": "parent-leaf value-1"}'), + (5014, 1001, 3006, 5013, '/parent-208/child-001', '{"first-child-leaf": "first-child-leaf value"}'), + (5015, 1001, 3006, 5013, '/parent-208/child-002', '{"second-child-leaf": "second-child-leaf value"}'), + (5016, 1001, 3006, 5015, '/parent-208/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}'), + (5017, 1001, 3006, null, '/parent-209', '{"parent-leaf-2": "parent-leaf value-2"}'), + (5018, 1001, 3006, 5017, '/parent-209/child-001', '{"first-child-leaf": "first-child-leaf value"}'), + (5019, 1001, 3006, 5017, '/parent-209/child-002', '{"second-child-leaf": "second-child-leaf value"}'), + (5020, 1001, 3006, 5019, '/parent-209/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}'); + INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES (4201, 1001, 3003, null, '/parent-200', '{"leaf-value": "original"}'), (4202, 1001, 3003, 4201, '/parent-200/child-201', '{"leaf-value": "original"}'), diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index 174d71f64..9f96df2e4 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -4,6 +4,7 @@ * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada * Modifications Copyright (C) 2022 Deutsche Telekom AG + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,30 +111,31 @@ public interface CpsDataService { Collection jsonDataList, OffsetDateTime observedTimestamp); /** - * Retrieves datanode by XPath for given dataspace and anchor. + * Retrieves all the datanodes by XPath for given dataspace and anchor. * - * @param dataspaceName dataspace name - * @param anchorName anchor name - * @param xpath xpath - * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes - * (recursively) as well - * @return data node object + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param xpath xpath + * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes + * (recursively) as well + * @return collection of data node objects */ - DataNode getDataNode(String dataspaceName, String anchorName, String xpath, - FetchDescendantsOption fetchDescendantsOption); + Collection getDataNodes(String dataspaceName, String anchorName, String xpath, + FetchDescendantsOption fetchDescendantsOption); /** - * Retrieves datanodes by XPath for given dataspace and anchor. + * Retrieves all the datanodes for multiple XPaths for given dataspace and anchor. * - * @param dataspaceName dataspace name - * @param anchorName anchor name - * @param xpaths collection of xpath - * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes - * (recursively) as well - * @return data node object + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param xpaths collection of xpaths + * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes + * (recursively) as well + * @return collection of data node objects */ - Collection getDataNodes(String dataspaceName, String anchorName, Collection xpaths, - FetchDescendantsOption fetchDescendantsOption); + Collection getDataNodesForMultipleXpaths(String dataspaceName, String anchorName, + Collection xpaths, + FetchDescendantsOption fetchDescendantsOption); /** * Updates data node for given dataspace and anchor using xpath to parent node. diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index 06a084538..b149d426e 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -3,7 +3,7 @@ * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -134,21 +134,23 @@ public class CpsDataServiceImpl implements CpsDataService { @Override @Timed(value = "cps.data.service.datanode.get", - description = "Time taken to get a data node") - public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath, - final FetchDescendantsOption fetchDescendantsOption) { + description = "Time taken to get data nodes for an xpath") + public Collection getDataNodes(final String dataspaceName, final String anchorName, + final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - return cpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption); + return cpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption); } @Override @Timed(value = "cps.data.service.datanode.batch.get", description = "Time taken to get a batch of data nodes") - public Collection getDataNodes(final String dataspaceName, final String anchorName, - final Collection xpaths, - final FetchDescendantsOption fetchDescendantsOption) { + public Collection getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName, + final Collection xpaths, + final FetchDescendantsOption fetchDescendantsOption) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - return cpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpaths, fetchDescendantsOption); + return cpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, xpaths, + fetchDescendantsOption); } @Override diff --git a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java index f0cdaee8d..38f898827 100644 --- a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java +++ b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (c) 2021-2022 Bell Canada. * Modifications Copyright (c) 2022 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,8 +77,8 @@ public class CpsDataUpdatedEventFactory { public CpsDataUpdatedEvent createCpsDataUpdatedEvent(final Anchor anchor, final OffsetDateTime observedTimestamp, final Operation operation) { final var dataNode = (operation == Operation.DELETE) ? null : - cpsDataService.getDataNode(anchor.getDataspaceName(), anchor.getName(), - "/", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); + cpsDataService.getDataNodes(anchor.getDataspaceName(), anchor.getName(), + "/", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS).iterator().next(); return toCpsDataUpdatedEvent(anchor, dataNode, observedTimestamp, operation); } diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index 3e0b4475e..0b2cef9bd 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java @@ -3,7 +3,7 @@ * Copyright (C) 2020-2023 Nordix Foundation. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 Bell Canada - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,30 +99,33 @@ public interface CpsDataPersistenceService { Collection> newLists); /** - * Retrieves datanode by XPath for given dataspace and anchor. + * Retrieves multiple datanodes for a single XPath for given dataspace and anchor. + * Multiple data nodes are returned when xPath is set to root '/', otherwise single data node + * is returned when a specific xpath is used (Example: /bookstore). * * @param dataspaceName dataspace name * @param anchorName anchor name - * @param xpath xpath + * @param xpath one xpath * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes * (recursively) as well - * @return data node object + * @return collection of data node object */ - DataNode getDataNode(String dataspaceName, String anchorName, String xpath, - FetchDescendantsOption fetchDescendantsOption); + Collection getDataNodes(String dataspaceName, String anchorName, String xpath, + FetchDescendantsOption fetchDescendantsOption); /** - * Retrieves datanode by XPath for given dataspace and anchor. + * Retrieves multiple datanodes for multiple XPaths, given a dataspace and anchor. * - * @param dataspaceName dataspace name - * @param anchorName anchor name - * @param xpaths collection of xpaths - * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes - * (recursively) as well - * @return data node object - */ - Collection getDataNodes(String dataspaceName, String anchorName, Collection xpaths, - FetchDescendantsOption fetchDescendantsOption); + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param xpaths collection of xpaths + * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes + * (recursively) as well + * @return collection of data node object + */ + Collection getDataNodesForMultipleXpaths(String dataspaceName, String anchorName, + Collection xpaths, + FetchDescendantsOption fetchDescendantsOption); /** * Updates leaves for existing data node. diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index 8bbf4e571..5a74d073a 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -184,13 +184,27 @@ class CpsDataServiceImplSpec extends Specification { thrown(DataValidationException) } - def 'Get data node with option #fetchDescendantsOption.'() { - def xpath = '/xpath' - def dataNode = new DataNodeBuilder().withXpath(xpath).build() + def 'Get all data nodes #scenario.'() { + given: 'persistence service returns data for GET request' + mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode + expect: 'service returns same data if using same parameters' + objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode + where: 'following parameters were used' + scenario | xpath | fetchDescendantsOption | dataNode + 'with root node xpath and descendants' | '/' | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()] + 'with root node xpath and no descendants' | '/' | FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()] + 'with valid xpath and descendants' | '/xpath'| FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()] + 'with valid xpath and no descendants' | '/xpath'| FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()] + } + + def 'Get all data nodes over multiple xpaths with option #fetchDescendantsOption.'() { + def xpath1 = '/xpath-1' + def xpath2 = '/xpath-2' + def dataNode = [new DataNodeBuilder().withXpath(xpath1).build(), new DataNodeBuilder().withXpath(xpath2).build()] given: 'persistence service returns data for get data request' - mockCpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode + mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) >> dataNode expect: 'service returns same data if uses same parameters' - objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode + objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) == dataNode where: 'all fetch options are supported' fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS] } @@ -404,4 +418,4 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L) } -} +} \ No newline at end of file diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy index 6f9a148eb..5dbc2bb04 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (c) 2021-2022 Bell Canada. * Modifications Copyright (c) 2022 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,8 +51,8 @@ class CpsDataUpdateEventFactorySpec extends Specification { and: 'cps data service returns the data node details' def xpath = '/xpath' def dataNode = new DataNodeBuilder().withXpath(xpath).withLeaves(['leafName': 'leafValue']).build() - mockCpsDataService.getDataNode( - 'my-dataspace', 'my-anchorname', '/', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode + mockCpsDataService.getDataNodes( + 'my-dataspace', 'my-anchorname', '/', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode] when: 'CPS data updated event is created' def cpsDataUpdatedEvent = objectUnderTest.createCpsDataUpdatedEvent(anchor, DateTimeUtility.toOffsetDateTime(inputObservedTimestamp), Operation.CREATE) diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/CpsPersistenceSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/CpsPersistenceSpec.groovy index 94bcb0a6f..349f0854e 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/CpsPersistenceSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/CpsPersistenceSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2023 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -36,7 +37,7 @@ class CpsPersistenceSpec extends CpsIntegrationSpecBase{ and: 'The anchor has been persisted successfully' cpsAdminService.getAnchor(TEST_DATASPACE, TEST_ANCHOR).getName() == TEST_ANCHOR and: 'The data nodes have been persisted successfully' - cpsDataService.getDataNode(TEST_DATASPACE, TEST_ANCHOR, '/bookstore', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS).xpath == '/bookstore' + cpsDataService.getDataNodes(TEST_DATASPACE, TEST_ANCHOR, '/bookstore', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS).iterator().next().xpath == '/bookstore' } def 'Test deletion of all test data'() { @@ -56,6 +57,6 @@ class CpsPersistenceSpec extends CpsIntegrationSpecBase{ when: 'data is persisted to the database' saveDataNodes(TEST_DATASPACE, TEST_ANCHOR, "/", "BookstoreDataNodes.json") then: 'the correct data is saved' - cpsDataService.getDataNode(TEST_DATASPACE, TEST_ANCHOR, '/bookstore', FetchDescendantsOption.OMIT_DESCENDANTS).leaves['bookstore-name'] == 'Easons' + cpsDataService.getDataNodes(TEST_DATASPACE, TEST_ANCHOR, '/bookstore', FetchDescendantsOption.OMIT_DESCENDANTS).iterator().next().leaves['bookstore-name'] == 'Easons' } }