diff options
57 files changed, 1793 insertions, 953 deletions
diff --git a/cps-application/src/test/java/org/onap/cps/architecture/ArchitectureTestBase.java b/cps-application/src/test/java/org/onap/cps/architecture/ArchitectureTestBase.java index c1d65758c7..28ff7c307c 100644 --- a/cps-application/src/test/java/org/onap/cps/architecture/ArchitectureTestBase.java +++ b/cps-application/src/test/java/org/onap/cps/architecture/ArchitectureTestBase.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2024 Nordix Foundation + * Copyright (C) 2024-2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ public class ArchitectureTestBase { "java..", "lombok..", "org.apache..", + "org.aspectj..", "org.mapstruct..", "org.opendaylight..", "org.slf4j..", diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java index 317f6b70e1..d7b38d1a46 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2021-2024 Nordix Foundation + * Modifications Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2021 highstreet technologies GmbH * Modifications Copyright (C) 2021-2022 Bell Canada * ================================================================================ @@ -57,6 +57,7 @@ import org.onap.cps.ncmp.rest.model.RestOutputCmHandle; import org.onap.cps.ncmp.rest.model.RestOutputCmHandleCompositeState; import org.onap.cps.ncmp.rest.model.RestOutputCmHandlePublicProperties; import org.onap.cps.ncmp.rest.util.CmHandleStateMapper; +import org.onap.cps.ncmp.rest.util.CountCmHandleSearchExecution; import org.onap.cps.ncmp.rest.util.DataOperationRequestMapper; import org.onap.cps.ncmp.rest.util.DeprecationHelper; import org.onap.cps.ncmp.rest.util.NcmpRestInputMapper; @@ -256,6 +257,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { */ @Override @SuppressWarnings("deprecation") // mapOldConditionProperties method will be removed in Release 12 + @CountCmHandleSearchExecution(methodName = "searchCmHandles", interfaceName = "CPS-E-05") public ResponseEntity<List<RestOutputCmHandle>> searchCmHandles( final CmHandleQueryParameters cmHandleQueryParameters) { final CmHandleQueryApiParameters cmHandleQueryApiParameters = @@ -276,6 +278,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { * @return collection of cm handle ids */ @Override + @CountCmHandleSearchExecution(methodName = "searchCmHandleIds", interfaceName = "CPS-E-05") public ResponseEntity<List<String>> searchCmHandleIds(final CmHandleQueryParameters cmHandleQueryParameters, final Boolean outputAlternateId) { final CmHandleQueryApiParameters cmHandleQueryApiParameters = diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java index 0e27ba9355..e412107753 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Bell Canada - * Modifications Copyright (C) 2022-2024 Nordix Foundation + * Modifications Copyright (C) 2022-2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.onap.cps.ncmp.rest.model.CmHandleQueryParameters; import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse; import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse; import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration; +import org.onap.cps.ncmp.rest.util.CountCmHandleSearchExecution; import org.onap.cps.ncmp.rest.util.NcmpRestInputMapper; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -60,6 +61,7 @@ public class NetworkCmProxyInventoryController implements NetworkCmProxyInventor * @return list of cm handle IDs */ @Override + @CountCmHandleSearchExecution(methodName = "searchCmHandleIds", interfaceName = "CPS-NCMP-I-01") public ResponseEntity<List<String>> searchCmHandleIds(final CmHandleQueryParameters cmHandleQueryParameters, final Boolean outputAlternateId) { final CmHandleQueryServiceParameters cmHandleQueryServiceParameters = ncmpRestInputMapper diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounter.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounter.java new file mode 100644 index 0000000000..ecd248d89f --- /dev/null +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounter.java @@ -0,0 +1,84 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.rest.util; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.validation.Valid; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.onap.cps.ncmp.rest.model.CmHandleQueryParameters; +import org.onap.cps.ncmp.rest.model.ConditionProperties; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class CmHandleSearchExecutionCounter { + + private static final String NO_CONDITION = "NONE"; + + private final MeterRegistry meterRegistry; + + /** + * Counts the number of invocations of the methods annotated with @CountCmHandleSearchExecution based on the search + * conditions dynamically added. If search is executed without condition then it would be tagged as NONE, otherwise + * the conditions are concatenated with _ as separator. + * + * @param joinPoint join point + * @param countCmHandleSearchExecution count the cm handle search conditions + */ + @Before("@annotation(countCmHandleSearchExecution)") + public void cmHandleSearchExecutionCounter(final JoinPoint joinPoint, + final CountCmHandleSearchExecution countCmHandleSearchExecution) { + final Object[] args = joinPoint.getArgs(); + + if (args.length == 0 || !(args[0] instanceof CmHandleQueryParameters cmHandleQueryParameters)) { + log.warn("Method {} is missing required CmHandleQueryParameters argument", joinPoint.getSignature()); + return; + } + + final String conditionTag = Optional.ofNullable(cmHandleQueryParameters.getCmHandleQueryParameters()) + .filter(conditionTypes -> !conditionTypes.isEmpty()) + .map(CmHandleSearchExecutionCounter::conditionTag) + .orElse(NO_CONDITION); + + Counter.builder("cm_handle_search_invocations") + .tag("method", countCmHandleSearchExecution.methodName()) + .tag("cps-interface", countCmHandleSearchExecution.interfaceName()) + .tag("conditions", conditionTag) + .description("Number of invocations of search methods based on condition types") + .register(meterRegistry) + .increment(); + } + + private static String conditionTag(final List<@Valid ConditionProperties> conditionTypes) { + return conditionTypes.stream().map(ConditionProperties::getConditionName).sorted() + .collect(Collectors.joining("_")); + } +} diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CountCmHandleSearchExecution.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CountCmHandleSearchExecution.java new file mode 100644 index 0000000000..27a0c4a87a --- /dev/null +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/CountCmHandleSearchExecution.java @@ -0,0 +1,45 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.rest.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CountCmHandleSearchExecution { + + /** + * Capture the method name for which the number of invocations needs to be tracked. + * + * @return the search method name + */ + String methodName(); + + /** + * Capture the CPS and NCMP interface name of the called method. + * + * @return the CPS and NCMP interface name + */ + String interfaceName(); +} diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounterSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounterSpec.groovy new file mode 100644 index 0000000000..bdadfc8689 --- /dev/null +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/CmHandleSearchExecutionCounterSpec.groovy @@ -0,0 +1,128 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.rest.util + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.Signature +import org.onap.cps.ncmp.rest.model.CmHandleQueryParameters +import org.onap.cps.ncmp.rest.model.ConditionProperties +import spock.lang.Specification + +class CmHandleSearchExecutionCounterSpec extends Specification { + + def meterRegistry = new SimpleMeterRegistry() + def mockJoinPoint = Mock(JoinPoint) + def mockCountCmHandleSearchExecutionAnnotation = Mock(CountCmHandleSearchExecution) + def mockSignature = Mock(Signature) + + def objectUnderTest = new CmHandleSearchExecutionCounter(meterRegistry) + + def setup() { + mockCountCmHandleSearchExecutionAnnotation.methodName() >> 'testMethod' + mockCountCmHandleSearchExecutionAnnotation.interfaceName() >> 'testInterface' + mockSignature.toString() >> 'testSignature' + mockJoinPoint.getSignature() >> mockSignature + } + + def 'should track search with conditions'() { + given: 'CmHandleQueryParameters with conditions' + def cmHandleQueryParameters = new CmHandleQueryParameters() + def condition1 = new ConditionProperties(conditionName: 'condition1') + def condition2 = new ConditionProperties(conditionName: 'condition2') + cmHandleQueryParameters.addCmHandleQueryParametersItem(condition1).addCmHandleQueryParametersItem(condition2) + and: 'joinPoint returns the parameters' + mockJoinPoint.getArgs() >> [cmHandleQueryParameters] + when: 'the annotated method is called' + objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation) + then: 'the counter should be registered' + def counter = findCounter('cm_handle_search_invocations', [ + 'method' : 'testMethod', + 'cps-interface': 'testInterface', + 'conditions' : 'condition1_condition2' + ]) + and: 'is incremented once' + assert counter.count() == 1 + } + + def 'should track search with no conditions as NONE'() { + given: 'empty CmHandleQueryParameters' + def cmHandleQueryParameters = new CmHandleQueryParameters() + and: 'joinPoint returns the parameters' + mockJoinPoint.getArgs() >> [cmHandleQueryParameters] + when: 'the annotated method is called' + objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation) + then: 'the counter should be registered with NONE tag' + def counter = findCounter('cm_handle_search_invocations', [ + method : 'testMethod', + 'cps-interface': 'testInterface', + conditions : 'NONE' + ]) + and: 'is incremented once' + assert counter.count() == 1 + } + + def 'should not create counter when args are empty'() { + given: 'joinPoint with empty args' + mockJoinPoint.getArgs() >> [] + when: 'the aspect method is called' + objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation) + then: 'no counter should be registered' + assert meterRegistry.find('cm_handle_search_invocations').counters().isEmpty() + } + + def 'should not create counter when first arg is not CmHandleQueryParameters'() { + given: 'joinPoint with non-CmHandleQueryParameters arg' + mockJoinPoint.getArgs() >> ['not a CmHandleQueryParameters'] + when: 'the aspect method is called' + objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation) + then: 'no counter should be registered' + assert meterRegistry.find('cm_handle_search_invocations').counters().isEmpty() + } + + def 'should sort condition names alphabetically'() { + given: 'CmHandleQueryParameters with unsorted conditions' + def cmHandleQueryParameters = new CmHandleQueryParameters() + def condition1 = new ConditionProperties(conditionName: 'zCondition') + def condition2 = new ConditionProperties(conditionName: 'aCondition') + cmHandleQueryParameters.addCmHandleQueryParametersItem(condition1).addCmHandleQueryParametersItem(condition2) + and: 'joinPoint returns our parameters' + mockJoinPoint.getArgs() >> [cmHandleQueryParameters] + when: 'the aspect method is called' + objectUnderTest.cmHandleSearchExecutionCounter(mockJoinPoint, mockCountCmHandleSearchExecutionAnnotation) + then: 'the counter should be registered with alphabetically sorted tags' + def counter = findCounter('cm_handle_search_invocations', [ + 'method' : 'testMethod', + 'cps-interface': 'testInterface', + 'conditions' : 'aCondition_zCondition' + ]) + and: 'counter is incremented once' + assert counter.count() == 1 + } + + def findCounter(name, tags) { + def counterSearch = meterRegistry.find(name) + tags.each { key, value -> + counterSearch = counterSearch.tag(key, value) + } + return counterSearch.counter() + } +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java index 2d33234478..1b5dd2f853 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2024 Nordix Foundation + * Copyright (C) 2022-2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,8 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle */ public Collection<DataNode> executeRequest(final String cmHandleId, final String resourceIdentifier, final boolean includeDescendants) { - final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants); + final FetchDescendantsOption fetchDescendantsOption + = FetchDescendantsOption.getFetchDescendantsOption(includeDescendants); return networkCmProxyQueryService.queryResourceDataOperational(cmHandleId, resourceIdentifier, fetchDescendantsOption); } @@ -59,7 +60,8 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle final String requestId, final boolean includeDescendants, final String authorization) { - final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants); + final FetchDescendantsOption fetchDescendantsOption + = FetchDescendantsOption.getFetchDescendantsOption(includeDescendants); final DataNode dataNode = cpsDataService.getDataNodes(cmResourceAddress.getDatastoreName(), cmResourceAddress.resolveCmHandleReferenceToId(), @@ -68,8 +70,4 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle return Mono.justOrEmpty(dataNode); } - private static FetchDescendantsOption getFetchDescendantsOption(final boolean includeDescendants) { - return includeDescendants ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS - : FetchDescendantsOption.OMIT_DESCENDANTS; - } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminer.java index 429a3790d4..f200aa2ad7 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminer.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2024 Nordix Foundation + * Copyright (C) 2024-2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,8 @@ package org.onap.cps.ncmp.impl.datajobs; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -31,7 +31,9 @@ import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest; import org.onap.cps.ncmp.api.datajobs.models.DmiWriteOperation; import org.onap.cps.ncmp.api.datajobs.models.ProducerKey; import org.onap.cps.ncmp.api.datajobs.models.WriteOperation; -import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; +import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle; +import org.onap.cps.ncmp.impl.dmi.DmiServiceNameResolver; +import org.onap.cps.ncmp.impl.inventory.ParameterizedCmHandleQueryService; import org.onap.cps.ncmp.impl.models.RequiredDmiService; import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher; import org.springframework.stereotype.Service; @@ -42,6 +44,7 @@ import org.springframework.stereotype.Service; public class WriteRequestExaminer { private final AlternateIdMatcher alternateIdMatcher; + private final ParameterizedCmHandleQueryService parameterizedCmHandleQueryService; private static final String PATH_SEPARATOR = "/"; /** @@ -52,25 +55,35 @@ public class WriteRequestExaminer { * @return {@code Map} map of Dmi Write Operations per Producer Key */ public Map<ProducerKey, List<DmiWriteOperation>> splitDmiWriteOperationsFromRequest( - final String dataJobId, - final DataJobWriteRequest dataJobWriteRequest) { + final String dataJobId, final DataJobWriteRequest dataJobWriteRequest) { final Map<ProducerKey, List<DmiWriteOperation>> dmiWriteOperationsPerProducerKey = new HashMap<>(); + final Map<String, NcmpServiceCmHandle> cmHandlePerAlternateId = getAllNcmpServiceCmHandlesWithoutProperties(); for (final WriteOperation writeOperation : dataJobWriteRequest.data()) { - examineWriteOperation(dataJobId, dmiWriteOperationsPerProducerKey, writeOperation); + examineWriteOperation(dataJobId, dmiWriteOperationsPerProducerKey, writeOperation, cmHandlePerAlternateId); } return dmiWriteOperationsPerProducerKey; } + private Map<String, NcmpServiceCmHandle> getAllNcmpServiceCmHandlesWithoutProperties() { + final Map<String, NcmpServiceCmHandle> ncmpServiceCmHandles = new HashMap<>(); + for (final NcmpServiceCmHandle ncmpServiceCmHandle + : parameterizedCmHandleQueryService.getAllCmHandlesWithoutProperties()) { + ncmpServiceCmHandles.put(ncmpServiceCmHandle.getAlternateId(), ncmpServiceCmHandle); + } + return ncmpServiceCmHandles; + } + private void examineWriteOperation(final String dataJobId, final Map<ProducerKey, List<DmiWriteOperation>> dmiWriteOperationsPerProducerKey, - final WriteOperation writeOperation) { + final WriteOperation writeOperation, + final Map<String, NcmpServiceCmHandle> cmHandlePerAlternateId) { log.debug("data job id for write operation is: {}", dataJobId); - final YangModelCmHandle yangModelCmHandle = alternateIdMatcher - .getYangModelCmHandleByLongestMatchingAlternateId(writeOperation.path(), PATH_SEPARATOR); + final NcmpServiceCmHandle ncmpServiceCmHandle = alternateIdMatcher + .getCmHandleByLongestMatchingAlternateId(writeOperation.path(), PATH_SEPARATOR, cmHandlePerAlternateId); - final DmiWriteOperation dmiWriteOperation = createDmiWriteOperation(writeOperation, yangModelCmHandle); + final DmiWriteOperation dmiWriteOperation = createDmiWriteOperation(writeOperation, ncmpServiceCmHandle); - final ProducerKey producerKey = createProducerKey(yangModelCmHandle); + final ProducerKey producerKey = createProducerKey(ncmpServiceCmHandle); final List<DmiWriteOperation> dmiWriteOperations; if (dmiWriteOperationsPerProducerKey.containsKey(producerKey)) { dmiWriteOperations = dmiWriteOperationsPerProducerKey.get(producerKey); @@ -81,27 +94,20 @@ public class WriteRequestExaminer { dmiWriteOperations.add(dmiWriteOperation); } - private ProducerKey createProducerKey(final YangModelCmHandle yangModelCmHandle) { - return new ProducerKey(yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA), - yangModelCmHandle.getDataProducerIdentifier()); + private ProducerKey createProducerKey(final NcmpServiceCmHandle ncmpServiceCmHandle) { + final String dmiDataServiceName = + DmiServiceNameResolver.resolveDmiServiceName(RequiredDmiService.DATA, ncmpServiceCmHandle); + return new ProducerKey(dmiDataServiceName, ncmpServiceCmHandle.getDataProducerIdentifier()); } private DmiWriteOperation createDmiWriteOperation(final WriteOperation writeOperation, - final YangModelCmHandle yangModelCmHandle) { + final NcmpServiceCmHandle ncmpServiceCmHandle) { return new DmiWriteOperation( writeOperation.path(), writeOperation.op(), - yangModelCmHandle.getModuleSetTag(), + ncmpServiceCmHandle.getModuleSetTag(), writeOperation.value(), writeOperation.operationId(), - getPrivatePropertiesFromDataNode(yangModelCmHandle)); + Collections.emptyMap()); // TODO: Private properties will be removed as part of CPS-2693. } - - private Map<String, String> getPrivatePropertiesFromDataNode(final YangModelCmHandle yangModelCmHandle) { - final Map<String, String> cmHandleDmiProperties = new LinkedHashMap<>(); - yangModelCmHandle.getDmiProperties() - .forEach(dmiProperty -> cmHandleDmiProperties.put(dmiProperty.getName(), dmiProperty.getValue())); - return cmHandleDmiProperties; - } - } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryService.java index f1f71dc57c..9cbc6b0650 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryService.java @@ -84,6 +84,16 @@ public interface CmHandleQueryService { Collection<DataNode> queryNcmpRegistryByCpsPath(String cpsPath, FetchDescendantsOption fetchDescendantsOption); /** + * Method to return data nodes representing the cm handles. + * + * @param cpsPath cps path for which the cmHandle is requested + * @param queryResultLimit the maximum number of data nodes to return; if less than 1, returns all matching nodes + * @return a list of data nodes representing the cm handles. + */ + Collection<DataNode> queryNcmpRegistryByCpsPath(String cpsPath, FetchDescendantsOption fetchDescendantsOption, + int queryResultLimit); + + /** * Method to check the state of a cm handle with given id. * * @param cmHandleId cm handle id diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImpl.java index 890522ca60..74e862691a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImpl.java @@ -26,6 +26,7 @@ import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DATASPACE_NA import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR; import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT; +import com.hazelcast.map.IMap; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -54,6 +55,7 @@ import org.springframework.stereotype.Component; public class CmHandleQueryServiceImpl implements CmHandleQueryService { private static final String ANCESTOR_CM_HANDLES = "/ancestor::cm-handles"; private static final String ALTERNATE_ID = "alternate-id"; + private static final Integer NO_LIMIT = 0; private final CpsDataService cpsDataService; private final CpsQueryService cpsQueryService; @@ -61,7 +63,7 @@ public class CmHandleQueryServiceImpl implements CmHandleQueryService { private final Map<String, TrustLevel> trustLevelPerDmiPlugin; @Qualifier(TrustLevelCacheConfig.TRUST_LEVEL_PER_CM_HANDLE) - private final Map<String, TrustLevel> trustLevelPerCmHandleId; + private final IMap<String, TrustLevel> trustLevelPerCmHandleId; private final CpsValidator cpsValidator; @@ -99,8 +101,15 @@ public class CmHandleQueryServiceImpl implements CmHandleQueryService { @Override public Collection<DataNode> queryNcmpRegistryByCpsPath(final String cpsPath, final FetchDescendantsOption fetchDescendantsOption) { + return queryNcmpRegistryByCpsPath(cpsPath, fetchDescendantsOption, NO_LIMIT); + } + + @Override + public Collection<DataNode> queryNcmpRegistryByCpsPath(final String cpsPath, + final FetchDescendantsOption fetchDescendantsOption, + final int queryResultLimit) { return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cpsPath, - fetchDescendantsOption); + fetchDescendantsOption, queryResultLimit); } @Override @@ -144,14 +153,15 @@ public class CmHandleQueryServiceImpl implements CmHandleQueryService { final TrustLevel dmiTrustLevel = mapEntry.getValue(); final Collection<String> candidateCmHandleIds = getCmHandleReferencesByDmiPluginIdentifier( dmiPluginIdentifier, false); - for (final String candidateCmHandleId : candidateCmHandleIds) { - final TrustLevel candidateCmHandleTrustLevel = trustLevelPerCmHandleId.get(candidateCmHandleId); - final TrustLevel effectiveTrustlevel = - candidateCmHandleTrustLevel.getEffectiveTrustLevel(dmiTrustLevel); - if (targetTrustLevel.equals(effectiveTrustlevel)) { - selectedCmHandleReferences.add(candidateCmHandleId); + final Set<String> candidateCmHandleIdsSet = new HashSet<>(candidateCmHandleIds); + final Map<String, TrustLevel> trustLevelPerCmHandleIdInBatch = + trustLevelPerCmHandleId.getAll(candidateCmHandleIdsSet); + trustLevelPerCmHandleIdInBatch.forEach((cmHandleId, trustLevel) -> { + final TrustLevel effectiveTrustLevel = trustLevel.getEffectiveTrustLevel(dmiTrustLevel); + if (targetTrustLevel.equals(effectiveTrustLevel)) { + selectedCmHandleReferences.add(cmHandleId); } - } + }); } if (outputAlternateId) { return getAlternateIdsByCmHandleIds(selectedCmHandleReferences); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java index 75c52f3c60..ea8c3ca78d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java @@ -71,7 +71,7 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class CmHandleRegistrationService { - private static final int DELETE_BATCH_SIZE = 100; + private static final int DELETE_BATCH_SIZE = 300; private final CmHandleRegistrationServicePropertyHandler cmHandleRegistrationServicePropertyHandler; private final InventoryPersistence inventoryPersistence; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java index 7f6fe76dcc..cf98b31614 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java @@ -59,7 +59,7 @@ import org.springframework.stereotype.Component; @Component public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements InventoryPersistence { - private static final int CMHANDLE_BATCH_SIZE = 100; + private static final int CMHANDLE_BATCH_SIZE = 300; private final CpsModuleService cpsModuleService; private final CpsValidator cpsValidator; @@ -145,7 +145,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv final Collection<DataNode> cmHandlesAsDataNodes = cmHandleQueryService.queryNcmpRegistryByCpsPath( - cpsPathForCmHandlesByReferences, INCLUDE_ALL_DESCENDANTS); + cpsPathForCmHandlesByReferences, INCLUDE_ALL_DESCENDANTS, cmHandleReferences.size()); return YangDataConverter.toYangModelCmHandles(cmHandlesAsDataNodes); } @@ -195,7 +195,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv public YangModelCmHandle getYangModelCmHandleByAlternateId(final String alternateId) { final String cpsPathForCmHandleByAlternateId = getCpsPathForCmHandleByAlternateId(alternateId); final Collection<DataNode> dataNodes = cmHandleQueryService - .queryNcmpRegistryByCpsPath(cpsPathForCmHandleByAlternateId, OMIT_DESCENDANTS); + .queryNcmpRegistryByCpsPath(cpsPathForCmHandleByAlternateId, OMIT_DESCENDANTS, 1); if (dataNodes.isEmpty()) { throw new CmHandleNotFoundException(alternateId); } @@ -209,7 +209,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv } final String cpsPathForCmHandlesByAlternateIds = getCpsPathForCmHandlesByAlternateIds(alternateIds); final Collection<DataNode> dataNodes = cmHandleQueryService.queryNcmpRegistryByCpsPath( - cpsPathForCmHandlesByAlternateIds, INCLUDE_ALL_DESCENDANTS); + cpsPathForCmHandlesByAlternateIds, INCLUDE_ALL_DESCENDANTS, alternateIds.size()); return YangDataConverter.toYangModelCmHandles(dataNodes); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryService.java index 3db4920d3e..fc8884433c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryService.java @@ -75,4 +75,14 @@ public interface ParameterizedCmHandleQueryService { * @return collection of cm handles */ Collection<NcmpServiceCmHandle> getAllCmHandles(); + + /** + * Retrieves all {@code NcmpServiceCmHandle} instances without their associated properties. + * This method fetches the relevant data nodes from the inventory persistence layer and + * converts them into {@code NcmpServiceCmHandle} objects. Only the handles are returned, + * without any additional properties. + * + * @return a collection of {@code NcmpServiceCmHandle} instances without properties. + */ + Collection<NcmpServiceCmHandle> getAllCmHandlesWithoutProperties(); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryServiceImpl.java index 4c1032efad..c44234d28c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryServiceImpl.java @@ -97,7 +97,16 @@ public class ParameterizedCmHandleQueryServiceImpl implements ParameterizedCmHan @Override public Collection<NcmpServiceCmHandle> getAllCmHandles() { - final DataNode dataNode = inventoryPersistence.getDataNode(NCMP_DMI_REGISTRY_PARENT).iterator().next(); + return toNcmpServiceCmHandles(inventoryPersistence.getDataNode(NCMP_DMI_REGISTRY_PARENT)); + } + + @Override + public Collection<NcmpServiceCmHandle> getAllCmHandlesWithoutProperties() { + return toNcmpServiceCmHandles(inventoryPersistence.getDataNode(NCMP_DMI_REGISTRY_PARENT, DIRECT_CHILDREN_ONLY)); + } + + private Collection<NcmpServiceCmHandle> toNcmpServiceCmHandles(final Collection<DataNode> dataNodes) { + final DataNode dataNode = dataNodes.iterator().next(); return dataNode.getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet()); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncWatchdog.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncWatchdog.java index 6eefedb633..8c9ec03dd9 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncWatchdog.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncWatchdog.java @@ -44,7 +44,7 @@ public class ModuleSyncWatchdog { private final ModuleSyncTasks moduleSyncTasks; private final IMap<String, String> cpsAndNcmpLock; - private static final int MODULE_SYNC_BATCH_SIZE = 100; + private static final int MODULE_SYNC_BATCH_SIZE = 300; private static final String VALUE_FOR_HAZELCAST_IN_PROGRESS_MAP = "Started"; /** diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java index 750a5050f2..b8e4e7feda 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java @@ -20,12 +20,12 @@ package org.onap.cps.ncmp.impl.utils; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.onap.cps.ncmp.api.exceptions.CmHandleNotFoundException; +import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle; import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException; import org.onap.cps.ncmp.impl.inventory.InventoryPersistence; -import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; import org.onap.cps.utils.CpsValidator; import org.springframework.stereotype.Service; @@ -37,24 +37,26 @@ public class AlternateIdMatcher { private final CpsValidator cpsValidator; /** - * Get yang model cm handle that matches longest alternate id by removing elements + * Get cm handle that matches longest alternate id by removing elements * (as defined by the separator string) from right to left. * If alternate id contains a hash then all elements after that hash are ignored. * - * @param alternateId alternate ID - * @param separator a string that separates each element from the next. - * @return yang model cm handle + * @param alternateId alternate ID + * @param separator a string that separates each element from the next. + * @param cmHandlePerAlternateId all CM-handles by alternate ID + * @return ncmp service cm handle */ - public YangModelCmHandle getYangModelCmHandleByLongestMatchingAlternateId(final String alternateId, - final String separator) { + public NcmpServiceCmHandle getCmHandleByLongestMatchingAlternateId( + final String alternateId, final String separator, + final Map<String, NcmpServiceCmHandle> cmHandlePerAlternateId) { final String[] splitPath = alternateId.split("#", 2); String bestMatch = splitPath[0]; while (StringUtils.isNotEmpty(bestMatch)) { - try { - return inventoryPersistence.getYangModelCmHandleByAlternateId(bestMatch); - } catch (final CmHandleNotFoundException ignored) { - bestMatch = getParentPath(bestMatch, separator); + final NcmpServiceCmHandle ncmpServiceCmHandle = cmHandlePerAlternateId.get(bestMatch); + if (ncmpServiceCmHandle != null) { + return ncmpServiceCmHandle; } + bestMatch = getParentPath(bestMatch, separator); } throw new NoAlternateIdMatchFoundException(alternateId); } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminerSpec.groovy index 6aa84d1c7f..d051927b3d 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminerSpec.groovy @@ -23,24 +23,27 @@ package org.onap.cps.ncmp.impl.datajobs import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest import org.onap.cps.ncmp.api.datajobs.models.WriteOperation import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle -import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle +import org.onap.cps.ncmp.impl.inventory.ParameterizedCmHandleQueryService import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher import spock.lang.Specification class WriteRequestExaminerSpec extends Specification { def mockAlternateIdMatcher = Mock(AlternateIdMatcher) - def objectUnderTest = new WriteRequestExaminer(mockAlternateIdMatcher) + def mockParameterizedCmHandleQueryService = Mock(ParameterizedCmHandleQueryService) + def objectUnderTest = new WriteRequestExaminer(mockAlternateIdMatcher, mockParameterizedCmHandleQueryService) def setup() { - def ch1 = new YangModelCmHandle(id: 'ch1', dmiServiceName: 'dmiA', dataProducerIdentifier: 'p1', dmiProperties: []) - def ch2 = new YangModelCmHandle(id: 'ch2', dmiServiceName: 'dmiA', dataProducerIdentifier: 'p1', dmiProperties: []) - def ch3 = new YangModelCmHandle(id: 'ch3', dmiServiceName: 'dmiA', dataProducerIdentifier: 'p2', dmiProperties: []) - def ch4 = new YangModelCmHandle(id: 'ch4', dmiServiceName: 'dmiB', dataProducerIdentifier: 'p1', dmiProperties: []) - mockAlternateIdMatcher.getYangModelCmHandleByLongestMatchingAlternateId('fdn1', '/') >> ch1 - mockAlternateIdMatcher.getYangModelCmHandleByLongestMatchingAlternateId('fdn2', '/') >> ch2 - mockAlternateIdMatcher.getYangModelCmHandleByLongestMatchingAlternateId('fdn3', '/') >> ch3 - mockAlternateIdMatcher.getYangModelCmHandleByLongestMatchingAlternateId('fdn4', '/') >> ch4 + def ch1 = new NcmpServiceCmHandle(cmHandleId: 'ch1', dmiServiceName: 'dmiA', moduleSetTag: 'someModuleSetTag', alternateId: 'fdn1', dataProducerIdentifier: 'p1') + def ch2 = new NcmpServiceCmHandle(cmHandleId: 'ch2', dmiServiceName: 'dmiA', moduleSetTag: 'someModuleSetTag', alternateId: 'fdn2', dataProducerIdentifier: 'p1') + def ch3 = new NcmpServiceCmHandle(cmHandleId: 'ch3', dmiServiceName: 'dmiA', moduleSetTag: 'someModuleSetTag', alternateId: 'fdn3', dataProducerIdentifier: 'p2') + def ch4 = new NcmpServiceCmHandle(cmHandleId: 'ch4', dmiServiceName: 'dmiB', moduleSetTag: 'someModuleSetTag', alternateId: 'fdn4', dataProducerIdentifier: 'p1') + def cmHandlePerAlternateId = ['fdn1': ch1, 'fdn2': ch2, 'fdn3': ch3, 'fdn4': ch4] + mockAlternateIdMatcher.getCmHandleByLongestMatchingAlternateId('fdn1', '/', cmHandlePerAlternateId) >> ch1 + mockAlternateIdMatcher.getCmHandleByLongestMatchingAlternateId('fdn2', '/', cmHandlePerAlternateId) >> ch2 + mockAlternateIdMatcher.getCmHandleByLongestMatchingAlternateId('fdn3', '/', cmHandlePerAlternateId) >> ch3 + mockAlternateIdMatcher.getCmHandleByLongestMatchingAlternateId('fdn4', '/', cmHandlePerAlternateId) >> ch4 + mockParameterizedCmHandleQueryService.getAllCmHandlesWithoutProperties() >> [ch1, ch2, ch3, ch4] } def 'Create a map of dmi write requests per producer key with #scenario.'() { @@ -83,9 +86,9 @@ class WriteRequestExaminerSpec extends Specification { def 'Validate the creation of a ProducerKey with correct dmiservicename.'() { given: 'yangModelCmHandles with service name: "#dmiServiceName" and data service name: "#dataServiceName"' - def yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, dataServiceName, '', new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1'), '', '', 'dpi1') + def ncmpServiceCmHandle = new NcmpServiceCmHandle(dmiServiceName: dmiServiceName, dmiDataServiceName: dataServiceName, dataProducerIdentifier: 'dpi1') when: 'the ProducerKey is created' - def result = objectUnderTest.createProducerKey(yangModelCmHandle).toString() + def result = objectUnderTest.createProducerKey(ncmpServiceCmHandle).toString() then: 'we get the ProducerKey with the correct service name' assert result == expectedProducerKey where: 'the following services are registered' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImplSpec.groovy index 884d968c4f..12e4d6b85d 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryServiceImplSpec.groovy @@ -131,7 +131,7 @@ class CmHandleQueryServiceImplSpec extends Specification { def cmHandleState = CmHandleState.ADVISED and: 'the persistence service returns a list of data nodes' mockCpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - "//state[@cm-handle-state='ADVISED']", OMIT_DESCENDANTS) >> sampleDataNodes + "//state[@cm-handle-state='ADVISED']", OMIT_DESCENDANTS, 0) >> sampleDataNodes when: 'cm handles are fetched by state' def result = objectUnderTest.queryCmHandleIdsByState(cmHandleState) then: 'the returned result matches the result from the persistence service' @@ -171,7 +171,7 @@ class CmHandleQueryServiceImplSpec extends Specification { def 'Retrieve Cm Handles By Operational Sync State : UNSYNCHRONIZED'() { given: 'cps data service returns a list of data nodes' mockCpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes + '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS, 0) >> sampleDataNodes when: 'cm handles are fetched by the UNSYNCHRONIZED operational sync state' def result = objectUnderTest.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) then: 'the returned result is a list of data nodes returned by cps data service' @@ -184,7 +184,7 @@ class CmHandleQueryServiceImplSpec extends Specification { def cpsPath = "//state[@cm-handle-state='LOCKED']" and: 'cps data service returns a valid data node for cm handle ancestor' mockCpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - cpsPath + '/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) + cpsPath + '/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS, 0) >> Arrays.asList(cmHandleDataNode) when: 'get cm handles by cps path is invoked' def result = objectUnderTest.queryCmHandleAncestorsByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS) @@ -198,7 +198,7 @@ class CmHandleQueryServiceImplSpec extends Specification { def cpsPath = "//cm-handles[@alternate-id='1']" and: 'cps data service returns a valid data node' mockCpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - cpsPath, INCLUDE_ALL_DESCENDANTS) + cpsPath, INCLUDE_ALL_DESCENDANTS, 0) >> Arrays.asList(cmHandleDataNode) when: 'get cm handles by cps path is invoked' def result = objectUnderTest.queryCmHandleAncestorsByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS) @@ -241,7 +241,7 @@ class CmHandleQueryServiceImplSpec extends Specification { mockCpsQueryService.queryDataLeaf(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@dmi-data-service-name=\'my-dmi-plugin-identifier\']/@alternate-id', _) >> [pnfDemo.getLeaves().get('alternate-id'), pnfDemo4.getLeaves().get('alternate-id')] mockCpsQueryService.queryDataLeaf(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@dmi-model-service-name=\'my-dmi-plugin-identifier\']/@alternate-id', _) >> [pnfDemo2.getLeaves().get('alternate-id'), pnfDemo4.getLeaves().get('alternate-id')] mockCpsQueryService.queryDataLeaf(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'PNFDemo\']/@alternate-id', _) >> [pnfDemo.getLeaves().get('alternate-id')] - mockCpsQueryService.queryDataLeaf(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'PNFDemo2\' or @id=\'PNFDemo\' or @id=\'PNFDemo4\']/@alternate-id', _) >> [pnfDemo2.getLeaves().get('alternate-id'), pnfDemo.getLeaves().get('alternate-id'), pnfDemo4.getLeaves().get('alternate-id')] + mockCpsQueryService.queryDataLeaf(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'PNFDemo2\' or @id=\'PNFDemo4\' or @id=\'PNFDemo\']/@alternate-id', _) >> [pnfDemo2.getLeaves().get('alternate-id'), pnfDemo.getLeaves().get('alternate-id'), pnfDemo4.getLeaves().get('alternate-id')] mockCpsQueryService.queryDataLeaf(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '//public-properties[@name=\'Contact\' and @value=\'newemailforstore@bookstore.com\']/ancestor::cm-handles/@id',_) >> [pnfDemo.getLeaves().get('id'), pnfDemo2.getLeaves().get('id'), pnfDemo4.getLeaves().get('id')] mockCpsQueryService.queryDataLeaf(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '//public-properties[@name=\'Contact\' and @value=\'newemailforstore@bookstore.com\']/ancestor::cm-handles/@alternate-id',_) >> [pnfDemo.getLeaves().get('alternate-id'), pnfDemo2.getLeaves().get('alternate-id'), pnfDemo4.getLeaves().get('alternate-id')] diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy index 2ba8505aaa..5619c5ac57 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy @@ -164,7 +164,7 @@ class InventoryPersistenceImplSpec extends Specification { def "Retrieve multiple YangModelCmHandles using cm handle references"() { given: 'the cps data service returns 2 data nodes from the DMI registry' def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId, 'alternate-id':alternateId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2,'alternate-id':alternateId2])] - mockCmHandleQueries.queryNcmpRegistryByCpsPath(_, INCLUDE_ALL_DESCENDANTS) >> dataNodes + mockCmHandleQueries.queryNcmpRegistryByCpsPath(_, INCLUDE_ALL_DESCENDANTS, _) >> dataNodes when: 'retrieving the yang modelled cm handle' def results = objectUnderTest.getYangModelCmHandlesFromCmHandleReferences([cmHandleId, cmHandleId2]) then: 'verify both have returned and cmhandleIds are correct' @@ -311,7 +311,7 @@ class InventoryPersistenceImplSpec extends Specification { def expectedXPath = '/dmi-registry/cm-handles[@alternate-id=\'alternate id\']' def expectedDataNode = new DataNode(xpath: expectedXPath, leaves: [id: 'id', alternateId: 'alternate id']) and: 'query service is invoked with expected xpath' - mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS) >> [expectedDataNode] + mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS, _) >> [expectedDataNode] mockYangDataConverter.toYangModelCmHandle(expectedDataNode) >> new YangModelCmHandle(id: 'id') expect: 'getting the yang model cm handle' assert objectUnderTest.getYangModelCmHandleByAlternateId('alternate id') == new YangModelCmHandle(id: 'id') diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy index b59dd1a55f..098e88a644 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy @@ -20,7 +20,7 @@ package org.onap.cps.ncmp.impl.utils -import org.onap.cps.ncmp.api.exceptions.CmHandleNotFoundException +import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException import org.onap.cps.ncmp.impl.inventory.InventoryPersistence import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle @@ -37,16 +37,12 @@ class AlternateIdMatcherSpec extends Specification { def objectUnderTest = new AlternateIdMatcher(mockInventoryPersistence, new CpsValidatorImpl()) - def setup() { - given: 'cm handle in the registry with alternate id /a/b' - mockInventoryPersistence.getYangModelCmHandleByAlternateId('/a/b') >> new YangModelCmHandle() - and: 'no other cm handle' - mockInventoryPersistence.getYangModelCmHandleByAlternateId(_) >> { throw new CmHandleNotFoundException('') } - } - def 'Finding longest alternate id matches.'() { + given: 'a cm handle with alternate id /a/b in the cached map of all cm handles' + def ch1 = new NcmpServiceCmHandle(cmHandleId: 'ch1', alternateId: '/a/b') + def cmHandlePerAlternateId = ['/a/b': ch1] expect: 'querying for alternate id a matching result found' - assert objectUnderTest.getYangModelCmHandleByLongestMatchingAlternateId(targetAlternateId, '/') != null + assert objectUnderTest.getCmHandleByLongestMatchingAlternateId(targetAlternateId, '/', cmHandlePerAlternateId) != null where: 'the following parameters are used' scenario | targetAlternateId 'exact match' | '/a/b' @@ -61,7 +57,7 @@ class AlternateIdMatcherSpec extends Specification { def 'Attempt to find longest alternate id match without any matches.'() { when: 'attempt to find alternateId' - objectUnderTest.getYangModelCmHandleByLongestMatchingAlternateId(targetAlternateId, '/') + objectUnderTest.getCmHandleByLongestMatchingAlternateId(targetAlternateId, '/', [:]) then: 'no alternate id match found exception thrown' def thrown = thrown(NoAlternateIdMatchFoundException) and: 'the exception has the relevant details from the error response' diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java index 4ede0d9c90..2c896dc3cd 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022-2024 Nordix Foundation + * Modifications Copyright (C) 2025 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,6 +40,9 @@ import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; @NoArgsConstructor(access = AccessLevel.PACKAGE) public class CpsPathUtil { + public static final String ROOT_NODE_XPATH = "/"; + public static final String NO_PARENT_PATH = ""; + /** * Returns a normalized xpath path query. * @@ -46,6 +50,9 @@ public class CpsPathUtil { * @return a normalized xpath String. */ public static String getNormalizedXpath(final String xpathSource) { + if (ROOT_NODE_XPATH.equals(xpathSource)) { + return NO_PARENT_PATH; + } return getCpsPathBuilder(xpathSource).build().getNormalizedXpath(); } diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy index 29bb3c7b58..03aecc2acd 100644 --- a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy +++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022-2024 Nordix Foundation + * Modifications Copyright (C) 2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +25,11 @@ import spock.lang.Specification class CpsPathUtilSpec extends Specification { + def 'Normalized xpath for root.'() { + expect: 'root node xpath is parsed' + assert CpsPathUtil.getNormalizedXpath('/') == '' + } + def 'Normalized xpaths for list index values using #scenario'() { when: 'xpath with #scenario is parsed' def result = CpsPathUtil.getNormalizedXpath(xpath) @@ -36,7 +42,7 @@ class CpsPathUtilSpec extends Specification { 'single quotes' | "/parent/child[@common-leaf-name='123']" } - def 'Normalized parent paths of absolute paths'() { + def 'Normalized parent paths of absolute paths.'() { when: 'a given cps path is parsed' def result = CpsPathUtil.getNormalizedParentXpath(cpsPath) then: 'the result is the expected parent path' @@ -54,7 +60,7 @@ class CpsPathUtilSpec extends Specification { '/parent/child/name[text()="value"]' || '/parent' } - def 'Normalized parent paths of descendant paths'() { + def 'Normalized parent paths of descendant paths.'() { when: 'a given cps path is parsed' def result = CpsPathUtil.getNormalizedParentXpath(cpsPath) then: 'the result is the expected parent path' @@ -72,7 +78,7 @@ class CpsPathUtilSpec extends Specification { '//parent/child/name[text()="value"]' || '//parent' } - def 'Get node ID sequence for given xpath'() { + def 'Get node ID sequence for given xpath with #scenario.'() { when: 'a given xpath with #scenario is parsed' def result = CpsPathUtil.getXpathNodeIdSequence(xpath) then: 'the result is the expected node ID sequence' @@ -89,7 +95,7 @@ class CpsPathUtilSpec extends Specification { 'does not include ancestor node' | '/parent/child/ancestor::grandparent' || ["parent","child"] } - def 'Recognizing (absolute) xpaths to List elements'() { + def 'Recognizing (absolute) xpaths to List elements.'() { expect: 'check for list returns the correct values' assert CpsPathUtil.isPathToListElement(xpath) == expectList where: 'the following xpaths are used' @@ -101,7 +107,7 @@ class CpsPathUtilSpec extends Specification { '/parent/ancestor::grandparent[@id=1]' || false } - def 'Parsing Exception'() { + def 'Parsing Exception.'() { when: 'a invalid xpath is parsed' CpsPathUtil.getNormalizedXpath('///') then: 'a path parsing exception is thrown' 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 be552ecc6a..b6a2e42a14 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 @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2021-2024 Nordix Foundation + * Modifications Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ @@ -30,24 +30,19 @@ import io.micrometer.core.annotation.Timed; import jakarta.validation.ValidationException; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.onap.cps.api.CpsAnchorService; import org.onap.cps.api.CpsDataService; -import org.onap.cps.api.model.Anchor; -import org.onap.cps.api.model.DataNode; +import org.onap.cps.api.CpsFacade; import org.onap.cps.api.model.DeltaReport; import org.onap.cps.api.parameters.FetchDescendantsOption; import org.onap.cps.rest.api.CpsDataApi; import org.onap.cps.utils.ContentType; -import org.onap.cps.utils.DataMapUtils; import org.onap.cps.utils.JsonObjectMapper; -import org.onap.cps.utils.PrefixResolver; import org.onap.cps.utils.XmlFileUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -64,10 +59,9 @@ public class DataRestController implements CpsDataApi { private static final String ISO_TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; private static final DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_FORMAT); + private final CpsFacade cpsFacade; private final CpsDataService cpsDataService; - private final CpsAnchorService cpsAnchorService; private final JsonObjectMapper jsonObjectMapper; - private final PrefixResolver prefixResolver; @Override public ResponseEntity<String> createNode(final String apiVersion, @@ -116,24 +110,20 @@ public class DataRestController implements CpsDataApi { } @Override - @Timed(value = "cps.data.controller.datanode.get.v1", - description = "Time taken to get data node") + @Timed(value = "cps.data.controller.datanode.get.v1", description = "Time taken to get data node") public ResponseEntity<Object> getNodeByDataspaceAndAnchor(final String dataspaceName, final String anchorName, final String xpath, final Boolean includeDescendants) { - final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants) - ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS; - final DataNode dataNode = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath, - fetchDescendantsOption).iterator().next(); - final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); - return new ResponseEntity<>(DataMapUtils.toDataMapWithIdentifier(dataNode, prefix), HttpStatus.OK); + final FetchDescendantsOption fetchDescendantsOption = + FetchDescendantsOption.getFetchDescendantsOption(includeDescendants); + final Map<String, Object> dataNodeAsMap = + cpsFacade.getFirstDataNodeByAnchor(dataspaceName, anchorName, xpath, fetchDescendantsOption); + return new ResponseEntity<>(dataNodeAsMap, HttpStatus.OK); } @Override - @Timed(value = "cps.data.controller.datanode.get.v2", - description = "Time taken to get data node") + @Timed(value = "cps.data.controller.datanode.get.v2", description = "Time taken to get data node") public ResponseEntity<Object> getNodeByDataspaceAndAnchorV2(final String dataspaceName, final String anchorName, final String xpath, final String fetchDescendantsOptionAsString, @@ -141,16 +131,9 @@ public class DataRestController implements CpsDataApi { final ContentType contentType = ContentType.fromString(contentTypeInHeader); final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString); - final Collection<DataNode> dataNodes = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath, - fetchDescendantsOption); - final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size()); - final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - for (final DataNode dataNode: dataNodes) { - final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); - final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); - dataMaps.add(dataMap); - } - return buildResponseEntity(dataMaps, contentType); + final List<Map<String, Object>> dataNodesAsMaps = + cpsFacade.getDataNodesByAnchor(dataspaceName, anchorName, xpath, fetchDescendantsOption); + return buildResponseEntity(dataNodesAsMaps, contentType); } @Override diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java index f8833094cf..11713ad5e7 100644 --- a/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java +++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2024 Nordix Foundation + * Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada. * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. * ================================================================================ @@ -23,23 +23,15 @@ package org.onap.cps.rest.controller; import io.micrometer.core.annotation.Timed; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.onap.cps.api.CpsAnchorService; -import org.onap.cps.api.CpsQueryService; -import org.onap.cps.api.model.Anchor; -import org.onap.cps.api.model.DataNode; +import org.onap.cps.api.CpsFacade; import org.onap.cps.api.parameters.FetchDescendantsOption; import org.onap.cps.api.parameters.PaginationOption; import org.onap.cps.rest.api.CpsQueryApi; import org.onap.cps.utils.ContentType; -import org.onap.cps.utils.DataMapUtils; import org.onap.cps.utils.JsonObjectMapper; -import org.onap.cps.utils.PrefixResolver; import org.onap.cps.utils.XmlFileUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -51,27 +43,24 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class QueryRestController implements CpsQueryApi { - private final CpsQueryService cpsQueryService; - private final CpsAnchorService cpsAnchorService; + private final CpsFacade cpsFacade; private final JsonObjectMapper jsonObjectMapper; - private final PrefixResolver prefixResolver; @Override - @Timed(value = "cps.data.controller.datanode.query.v1", - description = "Time taken to query data nodes") + @Timed(value = "cps.data.controller.datanode.query.v1", description = "Time taken to query data nodes") public ResponseEntity<Object> getNodesByDataspaceAndAnchorAndCpsPath(final String dataspaceName, final String anchorName, final String cpsPath, final Boolean includeDescendants) { - final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants) - ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS; - return executeNodesByDataspaceQueryAndCreateResponse(dataspaceName, anchorName, cpsPath, - fetchDescendantsOption, ContentType.JSON); + final FetchDescendantsOption fetchDescendantsOption = + FetchDescendantsOption.getFetchDescendantsOption(includeDescendants); + final List<Map<String, Object>> dataNodesAsMaps + = cpsFacade.executeAnchorQuery(dataspaceName, anchorName, cpsPath, fetchDescendantsOption); + return buildResponseEntity(dataNodesAsMaps, ContentType.JSON); } @Override - @Timed(value = "cps.data.controller.datanode.query.v2", - description = "Time taken to query data nodes") + @Timed(value = "cps.data.controller.datanode.query.v2", description = "Time taken to query data nodes") public ResponseEntity<Object> getNodesByDataspaceAndAnchorAndCpsPathV2(final String dataspaceName, final String anchorName, final String cpsPath, @@ -80,8 +69,9 @@ public class QueryRestController implements CpsQueryApi { final ContentType contentType = ContentType.fromString(contentTypeInHeader); final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString); - return executeNodesByDataspaceQueryAndCreateResponse(dataspaceName, anchorName, cpsPath, - fetchDescendantsOption, contentType); + final List<Map<String, Object>> dataNodesAsMaps + = cpsFacade.executeAnchorQuery(dataspaceName, anchorName, cpsPath, fetchDescendantsOption); + return buildResponseEntity(dataNodesAsMaps, contentType); } @Override @@ -96,65 +86,21 @@ public class QueryRestController implements CpsQueryApi { FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString); final PaginationOption paginationOption = (pageIndex == null || pageSize == null) ? PaginationOption.NO_PAGINATION : new PaginationOption(pageIndex, pageSize); - final Collection<DataNode> dataNodes = cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, - cpsPath, fetchDescendantsOption, paginationOption); - final List<Map<String, Object>> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size()); - String prefix = null; - final Map<String, List<DataNode>> dataNodesPerAnchor = groupDataNodesPerAnchor(dataNodes); - for (final Map.Entry<String, List<DataNode>> dataNodesPerAnchorEntry : dataNodesPerAnchor.entrySet()) { - final String anchorName = dataNodesPerAnchorEntry.getKey(); - if (prefix == null) { - final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - prefix = prefixResolver.getPrefix(anchor, dataNodesPerAnchorEntry.getValue().get(0).getXpath()); - } - final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifierAndAnchor( - dataNodesPerAnchorEntry.getValue(), anchorName, prefix); - dataNodesAsListOfMaps.add(dataMap); - } - final Integer totalPages = getTotalPages(dataspaceName, cpsPath, paginationOption); - return ResponseEntity.ok().header("total-pages", - totalPages.toString()).body(jsonObjectMapper.asJsonString(dataNodesAsListOfMaps)); - } + final List<Map<String, Object>> dataNodesAsMaps + = cpsFacade.executeDataspaceQuery(dataspaceName, cpsPath, fetchDescendantsOption, paginationOption); - private Integer getTotalPages(final String dataspaceName, final String cpsPath, - final PaginationOption paginationOption) { - if (paginationOption == PaginationOption.NO_PAGINATION) { - return 1; - } - final int totalAnchors = cpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath); - return totalAnchors <= paginationOption.getPageSize() ? 1 - : (int) Math.ceil((double) totalAnchors / paginationOption.getPageSize()); - } - - private static Map<String, List<DataNode>> groupDataNodesPerAnchor(final Collection<DataNode> dataNodes) { - return dataNodes.stream().collect(Collectors.groupingBy(DataNode::getAnchorName)); - } - - private ResponseEntity<Object> executeNodesByDataspaceQueryAndCreateResponse(final String dataspaceName, - final String anchorName, final String cpsPath, final FetchDescendantsOption fetchDescendantsOption, - final ContentType contentType) { - final Collection<DataNode> dataNodes = - cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption); - final List<Map<String, Object>> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size()); - final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - String prefix = null; - for (final DataNode dataNode : dataNodes) { - if (prefix == null) { - prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); - } - final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); - dataNodesAsListOfMaps.add(dataMap); - } - return buildResponseEntity(dataNodesAsListOfMaps, contentType); + final int totalPages = cpsFacade.countAnchorsInDataspaceQuery(dataspaceName, cpsPath, paginationOption); + return ResponseEntity.ok().header("total-pages", String.valueOf(totalPages)) + .body(jsonObjectMapper.asJsonString(dataNodesAsMaps)); } - private ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataNodesAsListOfMaps, + private ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataNodesAsMaps, final ContentType contentType) { final String responseData; if (ContentType.XML.equals(contentType)) { - responseData = XmlFileUtils.convertDataMapsToXml(dataNodesAsListOfMaps); + responseData = XmlFileUtils.convertDataMapsToXml(dataNodesAsMaps); } else { - responseData = jsonObjectMapper.asJsonString(dataNodesAsListOfMaps); + responseData = jsonObjectMapper.asJsonString(dataNodesAsMaps); } return new ResponseEntity<>(responseData, HttpStatus.OK); } 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 f2f962422f..e4cd8c4be6 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 @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2024 Nordix Foundation + * Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. * Modifications Copyright (C) 2022 Deutsche Telekom AG @@ -25,17 +25,12 @@ package org.onap.cps.rest.controller import com.fasterxml.jackson.databind.ObjectMapper -import groovy.json.JsonSlurper -import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDataService -import org.onap.cps.api.parameters.FetchDescendantsOption -import org.onap.cps.api.model.DataNode -import org.onap.cps.impl.DataNodeBuilder +import org.onap.cps.api.CpsFacade import org.onap.cps.impl.DeltaReportBuilder import org.onap.cps.utils.ContentType import org.onap.cps.utils.DateTimeUtility import org.onap.cps.utils.JsonObjectMapper -import org.onap.cps.utils.PrefixResolver import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value @@ -44,7 +39,6 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.mock.web.MockMultipartFile import org.springframework.test.web.servlet.MockMvc -import org.springframework.web.multipart.MultipartFile import spock.lang.Shared import spock.lang.Specification @@ -61,17 +55,14 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder class DataRestControllerSpec extends Specification { @SpringBean - CpsDataService mockCpsDataService = Mock() + CpsFacade mockCpsFacade = Mock() @SpringBean - CpsAnchorService mockCpsAnchorService = Mock() + CpsDataService mockCpsDataService = Mock() @SpringBean JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - @SpringBean - PrefixResolver prefixResolver = Mock() - @Autowired MockMvc mvc @@ -97,20 +88,7 @@ class DataRestControllerSpec extends Specification { def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>' @Shared - static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/parent-1') - .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build() - - @Shared - static DataNode dataNodeWithLeavesNoChildren2 = new DataNodeBuilder().withXpath('/parent-2') - .withLeaves([leaf: 'value']).build() - - @Shared - static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent') - .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build() - - @Shared - static MultipartFile multipartYangFile = new MockMultipartFile("file", 'filename.yang', "text/plain", 'content'.getBytes()) - + def multipartYangFile = new MockMultipartFile("file", 'filename.yang', "text/plain", 'content'.getBytes()) def setup() { dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName" @@ -130,7 +108,7 @@ class DataRestControllerSpec extends Specification { ).andReturn().response then: 'a created response is returned' response.status == HttpStatus.CREATED.value() - then: 'the java API was called with the correct parameters' + then: 'the cps data service was called with the correct parameters' 1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, noTimestamp, expectedContentType) where: 'following xpath parameters are are used' scenario | parentNodeXpath | contentType | expectedContentType | requestBody | expectedData @@ -140,7 +118,7 @@ class DataRestControllerSpec extends Specification { 'XML content: xpath parameter point root' | '/' | MediaType.APPLICATION_XML | ContentType.XML | requestBodyXml | expectedXmlData } - def 'Create a node with observed-timestamp'() { + def 'Create a node with observed-timestamp.'() { given: 'endpoint to create a node' def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" when: 'post is invoked with datanode endpoint and json' @@ -154,7 +132,7 @@ class DataRestControllerSpec extends Specification { ).andReturn().response then: 'a created response is returned' response.status == expectedHttpStatus.value() - then: 'the java API was called with the correct parameters' + then: 'the cps data service was called with the correct parameters' expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType) where: @@ -164,7 +142,7 @@ class DataRestControllerSpec extends Specification { 'with invalid observed-timestamp' | 'invalid' | MediaType.APPLICATION_JSON | requestBodyJson || 0 | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON } - def 'Validate data using create a node API'() { + def 'Validate data using create a node API.'() { given: 'an endpoint to create a node' def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" def parentNodeXpath = '/' @@ -181,11 +159,11 @@ class DataRestControllerSpec extends Specification { ).andReturn().response then: 'a 200 OK response is returned' response.status == HttpStatus.OK.value() - then: 'the service was called with correct parameters' + then: 'the cps data service was called with correct parameters' 1 * mockCpsDataService.validateData(dataspaceName, anchorName, parentNodeXpath, requestBodyJson, ContentType.JSON) } - def 'Create a child node #scenario'() { + def 'Create a child node #scenario.'() { given: 'endpoint to create a node' def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" and: 'parent node xpath' @@ -201,7 +179,7 @@ class DataRestControllerSpec extends Specification { mvc.perform(postRequestBuilder).andReturn().response then: 'a created response is returned' response.status == HttpStatus.CREATED.value() - then: 'the java API was called with the correct parameters' + then: 'the cps data service was called with the correct parameters' 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedData, DateTimeUtility.toOffsetDateTime(observedTimestamp), expectedContentType) where: @@ -251,10 +229,10 @@ class DataRestControllerSpec extends Specification { def response = mvc.perform(postRequestBuilder).andReturn().response then: 'a created response is returned' response.status == expectedHttpStatus.value() - then: 'the java API was called with the correct parameters' + then: 'the cps data service was called with the correct parameters when needed' expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, expectedData, { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType) - where: + where: 'the following parameters are used' scenario | observedTimestamp | contentType | requestBody || expectedApiCount | expectedHttpStatus | expectedData | expectedContentType 'Content type JSON with observed-timestamp' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1 | HttpStatus.CREATED | expectedJsonData | ContentType.JSON 'Content type JSON without observed-timestamp' | null | MediaType.APPLICATION_JSON | requestBodyJson || 1 | HttpStatus.CREATED | expectedJsonData | ContentType.JSON @@ -280,34 +258,14 @@ class DataRestControllerSpec extends Specification { ).andReturn().response then: 'a 200 OK response is returned' response.status == HttpStatus.OK.value() - then: 'the service was called with correct parameters' + then: 'the cps data service was called with correct parameters' 1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON) } - def 'Get data node with leaves'() { - given: 'the service returns data node leaves' - def xpath = 'parent-1' - def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node" - mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren] - when: 'get request is performed through REST API' - def response = - mvc.perform(get(endpoint).param('xpath', xpath)) - .andReturn().response - then: 'a success response is returned' - response.status == HttpStatus.OK.value() - then: 'the response contains the the datanode in json format' - response.getContentAsString() == '{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}' - and: 'response contains expected leaf and value' - response.contentAsString.contains('"leaf":"value"') - and: 'response contains expected leaf-list and values' - response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]') - } - - def 'Get data node with #scenario.'() { + def 'Get data nodes [V1] with #scenario.'() { given: 'the service returns data node with #scenario' - def xpath = 'some xPath' + def xpath = 'my/path' def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node" - mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode] when: 'get request is performed through REST API' def response = mvc.perform( @@ -315,121 +273,42 @@ class DataRestControllerSpec extends Specification { .param('xpath', xpath) .param('include-descendants', includeDescendantsOption)) .andReturn().response + then: 'the cps facade is called with the correct parameters' + 1 * mockCpsFacade.getFirstDataNodeByAnchor(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [mocked:'result'] then: 'a success response is returned' response.status == HttpStatus.OK.value() - and: 'the response contains the root node identifier: #expectedRootidentifier' - response.contentAsString.contains(expectedRootidentifier) - and: 'the response contains child is #expectChildInResponse' - response.contentAsString.contains('"child"') == expectChildInResponse - where: - scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier - 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false | 'parent-1' - 'no descendant explicitly' | dataNodeWithLeavesNoChildren | 'false' || OMIT_DESCENDANTS | false | 'parent-1' - 'with descendants' | dataNodeWithChild | 'true' || INCLUDE_ALL_DESCENDANTS | true | 'parent' - } - - def 'Get all the data trees as json array with root node xPath using V2'() { - given: 'the service returns all data node leaves' - def xpath = '/' - def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node" - mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren, dataNodeWithLeavesNoChildren2] - when: 'V2 of get request is performed through REST API' - def response = - mvc.perform(get(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .param('xpath', xpath)) - .andReturn().response - then: 'a success response is returned' - response.status == HttpStatus.OK.value() - and: 'the response contains the datanode in json array format' - response.getContentAsString() == '[{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}},' + - '{"parent-2":{"leaf":"value"}}]' - and: 'the json array contains expected number of data trees' - def numberOfDataTrees = new JsonSlurper().parseText(response.getContentAsString()).iterator().size() - assert numberOfDataTrees == 2 - } - - def 'Get all the data trees using V2 without Content-Type defaults to json'() { - given: 'the service returns all data node leaves' - def xpath = '/' - def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node" - mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren, dataNodeWithLeavesNoChildren2] - when: 'V2 of get request is performed through REST API without specifying content-type header' - def response = - mvc.perform(get(endpoint) - .param('xpath', xpath)) - .andReturn().response - then: 'a success response is returned' - response.status == HttpStatus.OK.value() - and: 'the response contains the datanode in json array format' - response.getContentAsString() == '[{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}},' + - '{"parent-2":{"leaf":"value"}}]' - } - - def 'Get all the data trees as XML with root node xPath using V2'() { - given: 'the service returns all data node leaves' - def xpath = '/' - def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node" - mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren] - when: 'V2 of get request is performed through REST API with XML content type' - def response = - mvc.perform(get(endpoint).contentType(MediaType.APPLICATION_XML).param('xpath', xpath)) - .andReturn().response - then: 'a success response is returned' - response.status == HttpStatus.OK.value() - and: 'the response contains the datanode in XML format' - response.getContentAsString() == '<parent-1><leaf>value</leaf><leafList>leaveListElement1</leafList><leafList>leaveListElement2</leafList></parent-1>' + and: 'the response contains the facade result in json format' + response.getContentAsString() == '{"mocked":"result"}' + where: 'the following parameters are used' + scenario | includeDescendantsOption || expectedCpsDataServiceOption + 'no descendants (default) ' | '' || OMIT_DESCENDANTS + 'with descendants' | 'true' || INCLUDE_ALL_DESCENDANTS } - def 'Get data node with #scenario using V2.'() { + def 'Get data node with #scenario using V2. output type #scenario.'() { given: 'the service returns data nodes with #scenario' def xpath = 'some xPath' def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node" - mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode] when: 'V2 of get request is performed through REST API' def response = - mvc.perform( - get(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .param('xpath', xpath) - .param('descendants', includeDescendantsOption)) - .andReturn().response - then: 'a success response is returned' - response.status == HttpStatus.OK.value() - and: 'the response contains the root node identifier: #expectedRootidentifier' - response.contentAsString.contains(expectedRootidentifier) - and: 'the response contains child is #expectChildInResponse' - response.contentAsString.contains('"child"') == expectChildInResponse - where: - scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier - 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false | 'parent-1' - 'no descendant explicitly' | dataNodeWithLeavesNoChildren | '0' || OMIT_DESCENDANTS | false | 'parent-1' - 'with descendants' | dataNodeWithChild | '-1' || INCLUDE_ALL_DESCENDANTS | true | 'parent' - } - - def 'Get data node using v2 api'() { - given: 'the service returns data node' - def xpath = 'some xPath' - def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node" - mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, { descendantsOption -> { - assert descendantsOption.depth == 2}} as FetchDescendantsOption) >> [dataNodeWithChild] - when: 'get request is performed through REST API' - def response = - mvc.perform( - get(endpoint) - .contentType(MediaType.APPLICATION_JSON) + mvc.perform(get(endpoint) + .contentType(contentType) .param('xpath', xpath) - .param('descendants', '2')) + .param('descendants', 'all')) .andReturn().response - then: 'a success response is returned' + then: 'the cps service facade is called with the correct parameters and returns some data' + 1 * mockCpsFacade.getDataNodesByAnchor(dataspaceName, anchorName, xpath, INCLUDE_ALL_DESCENDANTS) >> [[mocked:'result1'], [mocked:'result2']] + and: 'a success response is returned' assert response.status == HttpStatus.OK.value() - and: 'the response contains the root node identifier' - assert response.contentAsString.contains('parent') - and: 'the response contains child is true' - assert response.contentAsString.contains('"child"') + and: 'the response is in the expected format' + assert response.contentAsString == expectedResult + where: 'the following content types are used' + scenario | contentType || expectedResult + 'XML' | MediaType.APPLICATION_XML || '<mocked>result1</mocked><mocked>result2</mocked>' + 'JSON' | MediaType.APPLICATION_JSON || '[{"mocked":"result1"},{"mocked":"result2"}]' } - def 'Get delta between two anchors'() { + def 'Get delta between two anchors.'() { given: 'the service returns a list containing delta reports' def deltaReports = new DeltaReportBuilder().actionReplace().withXpath('some xpath').withSourceData('some key': 'some value').withTargetData('some key': 'some value').build() def xpath = 'some xpath' @@ -468,7 +347,7 @@ class DataRestControllerSpec extends Specification { assert response.contentAsString.contains("[{\"action\":\"create\",\"xpath\":\"some xpath\"}]") } - def 'Get delta between anchor and JSON payload without multipart file'() { + def 'Get delta between anchor and JSON payload without multipart file.'() { given: 'sample delta report, xpath, and json payload' def deltaReports = new DeltaReportBuilder().actionRemove().withXpath('some xpath').build() def xpath = 'some xpath' @@ -499,7 +378,7 @@ class DataRestControllerSpec extends Specification { .content(requestBody) .param('xpath', inputXpath) ).andReturn().response - then: 'the service method is invoked with expected parameters' + then: 'the cps data service method is invoked with expected parameters' 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, expectedData, null, expectedContentType) and: 'response status indicates success' response.status == HttpStatus.OK.value() @@ -513,7 +392,7 @@ class DataRestControllerSpec extends Specification { 'XML content: some xpath by parent' | '/some/xpath' | MediaType.APPLICATION_XML || '/some/xpath' | requestBodyXml | expectedXmlData | ContentType.XML } - def 'Update data node leaves with observedTimestamp'() { + def 'Update data node leaves with observedTimestamp.'() { given: 'endpoint to update a node leaves ' def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" when: 'patch request is performed' @@ -525,7 +404,7 @@ class DataRestControllerSpec extends Specification { .param('xpath', '/') .param('observed-timestamp', observedTimestamp) ).andReturn().response - then: 'the service method is invoked with expected parameters' + then: 'the cps data service method is invoked with expected parameters' expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', expectedJsonData, { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON) and: 'response status indicates success' @@ -536,7 +415,7 @@ class DataRestControllerSpec extends Specification { 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST } - def 'Validate data using Update a node API'() { + def 'Validate data using Update a node API.'() { given: 'endpoint to update a node leaves' def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" and: 'dryRunEnabled flag is set to true' @@ -552,7 +431,7 @@ class DataRestControllerSpec extends Specification { ).andReturn().response then: 'a 200 OK response is returned' response.status == HttpStatus.OK.value() - then: 'the service was called with correct parameters' + then: 'the cps data service was called with correct parameters' 1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON) } @@ -567,7 +446,7 @@ class DataRestControllerSpec extends Specification { .content(requestBody) .param('xpath', inputXpath)) .andReturn().response - then: 'the service method is invoked with expected parameters' + then: 'the cps data service method is invoked with expected parameters' 1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedData, noTimestamp, expectedContentType) and: 'response status indicates success' response.status == HttpStatus.OK.value() @@ -581,7 +460,7 @@ class DataRestControllerSpec extends Specification { 'XML content: some xpath by parent' | '/some/xpath' | MediaType.APPLICATION_XML || '/some/xpath' | requestBodyXml | expectedXmlData | ContentType.XML } - def 'Validate data using Replace data node API'() { + def 'Validate data using Replace data node API.'() { given: 'endpoint to replace node' def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes" and: 'dryRunEnabled flag is set to true' @@ -597,7 +476,7 @@ class DataRestControllerSpec extends Specification { ).andReturn().response then: 'a 200 OK response is returned' response.status == HttpStatus.OK.value() - then: 'the service was called with correct parameters' + then: 'the cps data service was called with correct parameters' 1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON) } @@ -613,7 +492,7 @@ class DataRestControllerSpec extends Specification { .param('xpath', '') .param('observed-timestamp', observedTimestamp)) .andReturn().response - then: 'the service method is invoked with expected parameters' + then: 'the cps data service method is invoked with expected parameters' expectedApiCount * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData, { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON) and: 'response status indicates success' @@ -635,7 +514,7 @@ class DataRestControllerSpec extends Specification { def response = mvc.perform(putRequestBuilder).andReturn().response then: 'a success response is returned' response.status == expectedHttpStatus.value() - and: 'the java API was called with the correct parameters' + and: 'the cps data service was called with the correct parameters' expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData, { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON) where: @@ -656,7 +535,7 @@ class DataRestControllerSpec extends Specification { def response = mvc.perform(putRequestBuilder).andReturn().response then: 'a success response is returned' response.status == expectedHttpStatus.value() - and: 'the java API was called with the correct parameters' + and: 'the cps data service was called with the correct parameters' expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedXmlData, { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.XML) where: @@ -666,7 +545,7 @@ class DataRestControllerSpec extends Specification { 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST } - def 'Validate data using Replace list content API'() { + def 'Validate data using Replace list content API.'() { given: 'endpoint to replace list-nodes' def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes" and: 'dryRunEnabled flag is set to true' @@ -682,7 +561,7 @@ class DataRestControllerSpec extends Specification { ).andReturn().response then: 'a 200 OK response is returned' response.status == HttpStatus.OK.value() - then: 'the service was called with correct parameters' + then: 'the cps data service was called with correct parameters' 1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON) } @@ -695,7 +574,7 @@ class DataRestControllerSpec extends Specification { def response = mvc.perform(deleteRequestBuilder).andReturn().response then: 'a success response is returned' response.status == expectedHttpStatus.value() - and: 'the java API was called with the correct parameters' + and: 'the cps data service was called with the correct parameters' expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath', { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }) where: @@ -717,7 +596,7 @@ class DataRestControllerSpec extends Specification { def response = mvc.perform(deleteDataNodeRequest).andReturn().response then: 'a successful response is returned' response.status == expectedHttpStatus.value() - and: 'the api is called with the correct parameters' + and: 'the cps data service is called with the correct parameters' expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath, { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }) where: diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy index 2b5c471287..5f6de2ec4c 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2024 Nordix Foundation + * Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2021-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. @@ -24,12 +24,9 @@ package org.onap.cps.rest.controller import com.fasterxml.jackson.databind.ObjectMapper -import org.onap.cps.api.CpsAnchorService -import org.onap.cps.api.CpsQueryService +import org.onap.cps.api.CpsFacade import org.onap.cps.api.parameters.PaginationOption -import org.onap.cps.impl.DataNodeBuilder import org.onap.cps.utils.JsonObjectMapper -import org.onap.cps.utils.PrefixResolver import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value @@ -39,216 +36,98 @@ import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import spock.lang.Specification -import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS +import static org.onap.cps.api.parameters.PaginationOption.NO_PAGINATION import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @WebMvcTest(QueryRestController) class QueryRestControllerSpec extends Specification { @SpringBean - CpsQueryService mockCpsQueryService = Mock() - - @SpringBean - CpsAnchorService mockCpsAnchorService = Mock() + CpsFacade mockCpsFacade = Mock() @SpringBean JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - @SpringBean - PrefixResolver prefixResolver = Mock() - @Autowired MockMvc mvc @Value('${rest.api.cps-base-path}') def basePath - def dataspaceName = 'my_dataspace' - def anchorName = 'my_anchor' - def cpsPath = 'some cps-path' - def dataNodeEndpointV2 - - def setup() { - dataNodeEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName/anchors/$anchorName/nodes/query" - } + def dataNodeAsMap = ['prefixedPath':[path:[leaf:'value']]] - def 'Query data node by cps path for the given dataspace and anchor with #scenario.'() { - given: 'service method returns a list containing a data node' - def dataNode1 = new DataNodeBuilder().withXpath('/xpath') - .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build() - mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, expectedCpsDataServiceOption) >> [dataNode1, dataNode1] - and: 'the query endpoint' - def dataNodeEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors/$anchorName/nodes/query" + def 'Query data node (v1) by cps path for the given dataspace and anchor with #scenario.'() { + given: 'the query endpoint' + def dataNodeEndpoint = "$basePath/v1/dataspaces/my_dataspace/anchors/my_anchor/nodes/query" when: 'query data nodes API is invoked' - def response = - mvc.perform( - get(dataNodeEndpoint) - .param('cps-path', cpsPath) - .param('include-descendants', includeDescendantsOption)) - .andReturn().response + def response = mvc.perform(get(dataNodeEndpoint).param('cps-path', 'my/path').param('include-descendants', includeDescendantsOption)) + .andReturn().response + then: 'the call is delegated to the cps service facade which returns a list containing one data node as a map' + 1 * mockCpsFacade.executeAnchorQuery('my_dataspace', 'my_anchor', 'my/path', expectedCpsDataServiceOption) >> [dataNodeAsMap] then: 'the response contains the the datanode in json format' - response.status == HttpStatus.OK.value() - response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}') + assert response.status == HttpStatus.OK.value() + assert response.getContentAsString() == '[{"prefixedPath":{"path":{"leaf":"value"}}}]' where: 'the following options for include descendants are provided in the request' scenario | includeDescendantsOption || expectedCpsDataServiceOption 'no descendants by default' | '' || OMIT_DESCENDANTS - 'no descendant explicitly' | 'false' || OMIT_DESCENDANTS 'descendants' | 'true' || INCLUDE_ALL_DESCENDANTS } - def 'Query data node v2 API by cps path for the given dataspace and anchor with #scenario and media type JSON'() { - given: 'service method returns a list containing a data node' - def dataNode = new DataNodeBuilder().withXpath('/xpath') - .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build() - mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption -> - assert descendantsOption.depth == expectedDepth - }) >> [dataNode, dataNode] + def 'Query data node (v2) by cps path for given dataspace and anchor with #scenario'() { + given: 'the query endpoint' + def dataNodeEndpointV2 = "$basePath/v2/dataspaces/my_dataspace/anchors/my_anchor/nodes/query" when: 'query data nodes API is invoked' - def response = - mvc.perform( - get(dataNodeEndpointV2) - .contentType(MediaType.APPLICATION_JSON) - .param('cps-path', cpsPath) - .param('descendants', includeDescendantsOptionString)) + def response = mvc.perform(get(dataNodeEndpointV2).contentType(contentType).param('cps-path', 'my/path') .param('descendants', includeDescendantsOptionString)) .andReturn().response - then: 'the response contains the datanode in the expected JSON format' + then: 'the call is delegated to the cps service facade which returns a list containing one data node as a map' + 1 * mockCpsFacade.executeAnchorQuery('my_dataspace', 'my_anchor', 'my/path', + { descendantsOption -> assert descendantsOption.depth == expectedDepth }) >> [dataNodeAsMap] + and: 'the response contains the datanode in the expected format' assert response.status == HttpStatus.OK.value() - assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}') + assert response.getContentAsString() == expectedOutput where: 'the following options for include descendants are provided in the request' - scenario | includeDescendantsOptionString || expectedDepth - 'direct children' | 'direct' || 1 - 'descendants' | '2' || 2 + scenario | includeDescendantsOptionString | contentType || expectedDepth || expectedOutput + 'direct children JSON' | 'direct' | MediaType.APPLICATION_JSON || 1 || '[{"prefixedPath":{"path":{"leaf":"value"}}}]' + 'descendants JSON' | '2' | MediaType.APPLICATION_JSON || 2 || '[{"prefixedPath":{"path":{"leaf":"value"}}}]' + 'descendants XML' | '2' | MediaType.APPLICATION_XML || 2 || '<prefixedPath><path><leaf>value</leaf></path></prefixedPath>' } - def 'Query data node v2 API by cps path for the given dataspace and anchor with #scenario and media type XML'() { - given: 'service method returns a list containing a data node' - def dataNode = new DataNodeBuilder().withXpath('/xpath') - .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build() - mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption -> - assert descendantsOption.depth == expectedDepth - }) >> [dataNode, dataNode] + def 'Query data node by cps path for given dataspace across all anchors'() { + given: 'the query endpoint' + def dataNodeEndpoint = "$basePath/v2/dataspaces/my_dataspace/nodes/query" + and: 'the cps service facade will say there are 123 pages ' + mockCpsFacade.countAnchorsInDataspaceQuery('my_dataspace', 'my/path', new PaginationOption(2,5) ) >> 123 when: 'query data nodes API is invoked' - def response = - mvc.perform( - get(dataNodeEndpointV2) - .contentType(MediaType.APPLICATION_XML) - .param('cps-path', cpsPath) - .param('descendants', includeDescendantsOptionString)) - .andReturn().response - then: 'the response contains the datanode in the expected XML format' - assert response.status == HttpStatus.OK.value() - assert response.getContentAsString().contains('<xpath><leaf>value</leaf><leafList>leaveListElement1</leafList><leafList>leaveListElement2</leafList></xpath>') - where: 'the following options for include descendants are provided in the request' - scenario | includeDescendantsOptionString || expectedDepth - 'direct children' | 'direct' || 1 - 'descendants' | '2' || 2 - } - - def 'Query data node by cps path for the given dataspace across all anchors with #scenario.'() { - given: 'service method returns a list containing a data node from different anchors' - def dataNode1 = new DataNodeBuilder().withXpath('/xpath') - .withAnchor('my_anchor') - .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build() - def dataNode2 = new DataNodeBuilder().withXpath('/xpath') - .withAnchor('my_anchor_2') - .withLeaves([leaf: 'value', leafList: ['leaveListElement3', 'leaveListElement4']]).build() - and: 'second data node for the same anchor' - def dataNode3 = new DataNodeBuilder().withXpath('/xpath') - .withAnchor('my_anchor_2') - .withLeaves([leaf: 'value', leafList: ['leaveListElement5', 'leaveListElement6']]).build() - and: 'the query endpoint' - def dataspaceName = 'my_dataspace' - def cpsPath = 'some/cps/path' - def dataNodeEndpoint = "$basePath/v2/dataspaces/$dataspaceName/nodes/query" - mockCpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, - expectedCpsDataServiceOption, PaginationOption.NO_PAGINATION) >> [dataNode1, dataNode2, dataNode3] - mockCpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath) >> 2 - when: 'query data nodes API is invoked' - def response = - mvc.perform( - get(dataNodeEndpoint) - .param('cps-path', cpsPath) - .param('descendants', includeDescendantsOptionString)) + def response = mvc.perform( + get(dataNodeEndpoint).param('cps-path', 'my/path').param('pageIndex', String.valueOf(2)).param('pageSize', String.valueOf(5))) .andReturn().response - then: 'the response contains the the datanode in json format' - response.status == HttpStatus.OK.value() - response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}') - response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement3","leaveListElement4"]}}') - response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement5","leaveListElement6"]}}') - where: 'the following options for include descendants are provided in the request' - scenario | includeDescendantsOptionString || expectedCpsDataServiceOption - 'no descendants by default' | '' || OMIT_DESCENDANTS - 'no descendant explicitly' | 'none' || OMIT_DESCENDANTS - 'descendants' | 'all' || INCLUDE_ALL_DESCENDANTS - 'direct children' | 'direct' || DIRECT_CHILDREN_ONLY - } - - def 'Query data node by cps path for the given dataspace across all anchors with pagination #scenario.'() { - given: 'service method returns a list containing a data node from different anchors' - def dataNode1 = new DataNodeBuilder().withXpath('/xpath') - .withAnchor('my_anchor') - .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build() - def dataNode2 = new DataNodeBuilder().withXpath('/xpath') - .withAnchor('my_anchor_2') - .withLeaves([leaf: 'value', leafList: ['leaveListElement3', 'leaveListElement4']]).build() - and: 'the query endpoint' - def dataspaceName = 'my_dataspace' - def cpsPath = 'some/cps/path' - def dataNodeEndpoint = "$basePath/v2/dataspaces/$dataspaceName/nodes/query" - mockCpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, - INCLUDE_ALL_DESCENDANTS, new PaginationOption(pageIndex,pageSize)) >> [dataNode1, dataNode2] - mockCpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath) >> totalAnchors - when: 'query data nodes API is invoked' - def response = - mvc.perform( - get(dataNodeEndpoint) - .param('cps-path', cpsPath) - .param('descendants', "all") - .param('pageIndex', String.valueOf(pageIndex)) - .param('pageSize', String.valueOf(pageSize))) - .andReturn().response - then: 'the response contains the the datanode in json format' + then: 'the call is delegated to the cps service facade which returns a list containing one data node as a map' + 1 * mockCpsFacade.executeDataspaceQuery('my_dataspace', 'my/path', OMIT_DESCENDANTS, new PaginationOption(2,5)) >> [dataNodeAsMap] + then: 'the response is OK and contains the the datanode in json format' assert response.status == HttpStatus.OK.value() - assert Integer.valueOf(response.getHeaderValue("total-pages")) == expectedTotalPageSize - assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}') - assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement3","leaveListElement4"]}}') - where: 'the following options for include descendants are provided in the request' - scenario | pageIndex | pageSize | totalAnchors || expectedTotalPageSize - '1st page with all anchors' | 1 | 3 | 3 || 1 - '1st page with less anchors' | 1 | 2 | 3 || 2 + assert response.getContentAsString() == '[{"prefixedPath":{"path":{"leaf":"value"}}}]' + and: 'the header indicates the correct number of pages' + assert response.getHeaderValue('total-pages') == '123' } - def 'Query data node across all anchors with pagination option with #scenario.'() { - given: 'service method returns a list containing a data node from different anchors' - def dataNode1 = new DataNodeBuilder().withXpath('/xpath') - .withAnchor('my_anchor') - .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build() - def dataNode2 = new DataNodeBuilder().withXpath('/xpath') - .withAnchor('my_anchor_2') - .withLeaves([leaf: 'value', leafList: ['leaveListElement3', 'leaveListElement4']]).build() - and: 'the query endpoint' - def dataspaceName = 'my_dataspace' - def cpsPath = 'some/cps/path' - def dataNodeEndpoint = "$basePath/v2/dataspaces/$dataspaceName/nodes/query" - mockCpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, - INCLUDE_ALL_DESCENDANTS, PaginationOption.NO_PAGINATION) >> [dataNode1, dataNode2] - mockCpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath) >> 2 + def 'Query data node across all anchors with pagination option with #scenario i.e. no pagination.'() { + given: 'the query endpoint' + def dataNodeEndpoint = "$basePath/v2/dataspaces/my_dataspace/nodes/query" + and: 'the cps service facade will say there is 1 page ' + mockCpsFacade.countAnchorsInDataspaceQuery('my_dataspace', 'my/path', NO_PAGINATION ) >> 1 when: 'query data nodes API is invoked' - def response = - mvc.perform( - get(dataNodeEndpoint) - .param('cps-path', cpsPath) - .param('descendants', "all") - .param(parameterName, "1")) - .andReturn().response - then: 'the response contains the the datanode in json format' + def response = mvc.perform(get(dataNodeEndpoint).param('cps-path', 'my/path').param(parameterName, '1')) + .andReturn().response + then: 'the call is delegated to the cps service facade which returns a list containing one data node as a map' + 1 * mockCpsFacade.executeDataspaceQuery('my_dataspace', 'my/path', OMIT_DESCENDANTS, PaginationOption.NO_PAGINATION) >> [dataNodeAsMap] + then: 'the response is OK and contains the datanode in json format' assert response.status == HttpStatus.OK.value() - assert Integer.valueOf(response.getHeaderValue("total-pages")) == 1 - assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}') - assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement3","leaveListElement4"]}}') - where: + assert response.getContentAsString() == '[{"prefixedPath":{"path":{"leaf":"value"}}}]' + and: 'the header indicates the correct number of pages' + assert response.getHeaderValue('total-pages') == '1' + where: 'only the following rest parameter is used' scenario | parameterName 'only page size' | 'pageSize' 'only page index' | 'pageIndex' diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy index 4e1d27cda2..0cbdffbdc4 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2020 Pantheon.tech - * Modifications Copyright (C) 2021-2023 Nordix Foundation + * Modifications Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2021 Bell Canada. * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG @@ -26,23 +26,24 @@ package org.onap.cps.rest.exceptions import com.fasterxml.jackson.databind.ObjectMapper import groovy.json.JsonSlurper -import org.onap.cps.api.CpsDataspaceService import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsDataspaceService +import org.onap.cps.api.CpsFacade import org.onap.cps.api.CpsModuleService import org.onap.cps.api.CpsNotificationService import org.onap.cps.api.CpsQueryService -import org.onap.cps.rest.controller.CpsRestInputMapper import org.onap.cps.api.exceptions.AlreadyDefinedException import org.onap.cps.api.exceptions.CpsException import org.onap.cps.api.exceptions.CpsPathException import org.onap.cps.api.exceptions.DataInUseException import org.onap.cps.api.exceptions.DataNodeNotFoundException import org.onap.cps.api.exceptions.DataValidationException +import org.onap.cps.api.exceptions.DataspaceInUseException import org.onap.cps.api.exceptions.ModelValidationException import org.onap.cps.api.exceptions.NotFoundInDataspaceException import org.onap.cps.api.exceptions.SchemaSetInUseException -import org.onap.cps.api.exceptions.DataspaceInUseException +import org.onap.cps.rest.controller.CpsRestInputMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver import org.spockframework.spring.SpringBean @@ -65,6 +66,9 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder class CpsRestExceptionHandlerSpec extends Specification { @SpringBean + CpsFacade mockCpsFacade = Stub() + + @SpringBean CpsDataspaceService mockCpsAdminService = Stub() @SpringBean @@ -86,10 +90,10 @@ class CpsRestExceptionHandlerSpec extends Specification { CpsRestInputMapper cpsRestInputMapper = Stub() @SpringBean - PrefixResolver prefixResolver = Mock() + PrefixResolver prefixResolver = Stub() @SpringBean - CpsNotificationService mockCpsNotificationService = Mock() + CpsNotificationService mockCpsNotificationService = Stub() @Autowired MockMvc mvc diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsFacade.java b/cps-service/src/main/java/org/onap/cps/api/CpsFacade.java new file mode 100644 index 0000000000..8933f02cb4 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/api/CpsFacade.java @@ -0,0 +1,96 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.api; + +import java.util.List; +import java.util.Map; +import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.api.parameters.PaginationOption; + +public interface CpsFacade { + + /** + * Get the first data node for a given dataspace, anchor and xpath. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor + * @param xpath the xpath + * @param fetchDescendantsOption control what level of descendants should be returned + * @return a map representing the data node and its descendants + */ + Map<String, Object> getFirstDataNodeByAnchor(String dataspaceName, + String anchorName, + String xpath, + FetchDescendantsOption fetchDescendantsOption); + + /** + * Get data nodes for a given dataspace, anchor and xpath. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor + * @param xpath the xpath + * @param fetchDescendantsOption control what level of descendants should be returned + * @return a map representing the data nodes and their descendants + */ + List<Map<String, Object>> getDataNodesByAnchor(String dataspaceName, + String anchorName, + String xpath, + FetchDescendantsOption fetchDescendantsOption); + + /** + * Query the given anchor using a cps path expression. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor + * @param cpsPath the xpath i.e. query + * @param fetchDescendantsOption control what level of descendants should be returned + * @return a map representing the data nodes and their descendants + */ + List<Map<String, Object>> executeAnchorQuery(String dataspaceName, + String anchorName, + String cpsPath, + FetchDescendantsOption fetchDescendantsOption); + + /** + * Query the given dataspace (all anchors) using a cps path expression. + * + * @param dataspaceName the name of the dataspace + * @param cpsPath the xpath i.e. query + * @param fetchDescendantsOption control what level of descendants should be returned + * @return a map representing the data nodes and their descendants + */ + List<Map<String, Object>> executeDataspaceQuery(String dataspaceName, + String cpsPath, + FetchDescendantsOption fetchDescendantsOption, + PaginationOption paginationOption); + + /** + * Query how many anchors wil be returned for the given dataspace and a cps path query. + * + * @param dataspaceName the name of the dataspace + * @param cpsPath the xpath i.e. query + * @param paginationOption the options for pagination + * @return the number of anchors involved in the output + */ + int countAnchorsInDataspaceQuery(String dataspaceName, + String cpsPath, + PaginationOption paginationOption); +} diff --git a/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java b/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java new file mode 100644 index 0000000000..1e3410c7f4 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java @@ -0,0 +1,83 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.api; + +import java.util.Collection; +import java.util.Map; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.utils.ContentType; + +public interface DataNodeFactory { + + /** + * Create data nodes using an anchor, xpath, and JSON/XML string. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param xpath xpath of the data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorXpathAndNodeData(Anchor anchor, String xpath, + String nodeData, ContentType contentType); + + /** + * Create data nodes using an anchor, parent data node xpath, and JSON/XML string. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param parentNodeXpath xpath of the parent data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorParentXpathAndNodeData(Anchor anchor, + String parentNodeXpath, + String nodeData, + ContentType contentType); + + /** + * Create data nodes using a map of xpath to JSON/XML data, and anchor name. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param nodesData map of xpath and node JSON/XML data + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorAndXpathToNodeData(Anchor anchor, + Map<String, String> nodesData, + ContentType contentType); + + /** + * Create data nodes using a map of YANG resource name to content, xpath, and JSON/XML string. + * + * @param yangResourcesNameToContentMap map of YANG resource name to content + * @param xpath xpath of the data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithYangResourceXpathAndNodeData( + Map<String, String> yangResourcesNameToContentMap, + String xpath, String nodeData, + ContentType contentType); + +} diff --git a/cps-service/src/main/java/org/onap/cps/api/parameters/FetchDescendantsOption.java b/cps-service/src/main/java/org/onap/cps/api/parameters/FetchDescendantsOption.java index 46022ba46b..05fa366239 100644 --- a/cps-service/src/main/java/org/onap/cps/api/parameters/FetchDescendantsOption.java +++ b/cps-service/src/main/java/org/onap/cps/api/parameters/FetchDescendantsOption.java @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Pantheon.tech - * Copyright (C) 2022-2023 Nordix Foundation + * Copyright (C) 2022-2025 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ package org.onap.cps.api.parameters; import com.google.common.base.Strings; import java.util.regex.Matcher; import java.util.regex.Pattern; +import lombok.Getter; import lombok.RequiredArgsConstructor; import org.onap.cps.api.exceptions.DataValidationException; @@ -44,6 +45,12 @@ public class FetchDescendantsOption { private static final Pattern FETCH_DESCENDANTS_OPTION_PATTERN = Pattern.compile("^$|^all$|^none$|^direct$|^[0-9]+$|^-1$|^1$"); + /** + * Get depth. + * + * @return depth: -1 for all descendants, 0 for no descendants, or positive value for fixed level of descendants + */ + @Getter private final int depth; private final String optionName; @@ -76,15 +83,7 @@ public class FetchDescendantsOption { } /** - * Get depth. - * @return depth: -1 for all descendants, 0 for no descendants, or positive value for fixed level of descendants - */ - public int getDepth() { - return depth; - } - - /** - * get fetch descendants option for given descendant. + * Convert fetch descendants option from string to enum with depth. * * @param fetchDescendantsOptionAsString fetch descendants option string * @return fetch descendants option for given descendant @@ -99,11 +98,22 @@ public class FetchDescendantsOption { } else if ("1".equals(fetchDescendantsOptionAsString) || "direct".equals(fetchDescendantsOptionAsString)) { return FetchDescendantsOption.DIRECT_CHILDREN_ONLY; } else { - final Integer depth = Integer.valueOf(fetchDescendantsOptionAsString); + final int depth = Integer.parseInt(fetchDescendantsOptionAsString); return new FetchDescendantsOption(depth); } } + /** + * Convert include all-descendants boolean parameter to FetchDescendantsOption enum. + * + * @param includedDescendantsOptionAsBoolean fetch descendants option as Boolean + * @return fetch descendants option for given descendant + */ + public static FetchDescendantsOption getFetchDescendantsOption(final Boolean includedDescendantsOptionAsBoolean) { + return Boolean.TRUE.equals(includedDescendantsOptionAsBoolean) + ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS; + } + @Override public String toString() { return optionName; diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java index 9f70ac9132..a93bf9ac82 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java @@ -1,9 +1,9 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2024 Nordix Foundation + * Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,13 +24,16 @@ package org.onap.cps.impl; +import static org.onap.cps.cpspath.parser.CpsPathUtil.NO_PARENT_PATH; +import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH; +import static org.onap.cps.utils.ContentType.JSON; + import io.micrometer.core.annotation.Timed; import java.io.Serializable; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -39,7 +42,7 @@ import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsAnchorService; import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsDeltaService; -import org.onap.cps.api.exceptions.DataValidationException; +import org.onap.cps.api.DataNodeFactory; import org.onap.cps.api.model.Anchor; import org.onap.cps.api.model.DataNode; import org.onap.cps.api.model.DeltaReport; @@ -50,11 +53,9 @@ import org.onap.cps.events.model.Data.Operation; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.utils.ContentType; import org.onap.cps.utils.CpsValidator; -import org.onap.cps.utils.DataMapUtils; +import org.onap.cps.utils.DataMapper; import org.onap.cps.utils.JsonObjectMapper; -import org.onap.cps.utils.PrefixResolver; import org.onap.cps.utils.YangParser; -import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.springframework.stereotype.Service; @Service @@ -62,36 +63,33 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class CpsDataServiceImpl implements CpsDataService { - private static final String ROOT_NODE_XPATH = "/"; - private static final String PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH = ""; private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L; - private static final String NO_DATA_NODES = "No data nodes."; private final CpsDataPersistenceService cpsDataPersistenceService; private final CpsDataUpdateEventsService cpsDataUpdateEventsService; private final CpsAnchorService cpsAnchorService; + private final DataNodeFactory dataNodeFactory; private final CpsValidator cpsValidator; private final YangParser yangParser; private final CpsDeltaService cpsDeltaService; + private final DataMapper dataMapper; private final JsonObjectMapper jsonObjectMapper; - private final PrefixResolver prefixResolver; @Override public void saveData(final String dataspaceName, final String anchorName, final String nodeData, final OffsetDateTime observedTimestamp) { - saveData(dataspaceName, anchorName, nodeData, observedTimestamp, ContentType.JSON); + saveData(dataspaceName, anchorName, nodeData, observedTimestamp, JSON); } @Override - @Timed(value = "cps.data.service.datanode.root.save", - description = "Time taken to save a root data node") + @Timed(value = "cps.data.service.datanode.root.save", description = "Time taken to save a root data node") public void saveData(final String dataspaceName, final String anchorName, final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, ROOT_NODE_XPATH, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, ROOT_NODE_XPATH, nodeData, contentType); cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes); sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.CREATE, observedTimestamp); } @@ -99,34 +97,32 @@ public class CpsDataServiceImpl implements CpsDataService { @Override public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, final OffsetDateTime observedTimestamp) { - saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, ContentType.JSON); + saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, JSON); } @Override - @Timed(value = "cps.data.service.datanode.child.save", - description = "Time taken to save a child data node") + @Timed(value = "cps.data.service.datanode.child.save", description = "Time taken to save a child data node") public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.CREATE, observedTimestamp); } @Override - @Timed(value = "cps.data.service.list.element.save", - description = "Time taken to save list elements") + @Timed(value = "cps.data.service.list.element.save", description = "Time taken to save list elements") public void saveListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> listElementDataNodeCollection = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); - if (isRootNodeXpath(parentNodeXpath)) { + final Collection<DataNode> listElementDataNodeCollection = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); + if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, listElementDataNodeCollection); } else { cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath, @@ -136,8 +132,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.datanode.get", - description = "Time taken to get data nodes for an xpath") + @Timed(value = "cps.data.service.datanode.get", description = "Time taken to get data nodes for an xpath") public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName, final String xpath, final FetchDescendantsOption fetchDescendantsOption) { @@ -146,8 +141,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.datanode.batch.get", - description = "Time taken to get a batch of data nodes") + @Timed(value = "cps.data.service.datanode.batch.get", description = "Time taken to get a batch of data nodes") public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName, final Collection<String> xpaths, final FetchDescendantsOption fetchDescendantsOption) { @@ -163,8 +157,8 @@ public class CpsDataServiceImpl implements CpsDataService { final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodesInPatch = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodesInPatch = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); final Map<String, Map<String, Serializable>> xpathToUpdatedLeaves = dataNodesInPatch.stream() .collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves)); cpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, xpathToUpdatedLeaves); @@ -180,8 +174,9 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodeUpdates = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON); + final Collection<DataNode> dataNodeUpdates = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, dataNodeUpdatesAsJson, + JSON); for (final DataNode dataNodeUpdate : dataNodeUpdates) { processDataNodeUpdate(anchor, dataNodeUpdate); } @@ -210,8 +205,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.get.delta", - description = "Time taken to get delta between anchors") + @Timed(value = "cps.data.service.get.delta", description = "Time taken to get delta between anchors") public List<DeltaReport> getDeltaByDataspaceAndAnchors(final String dataspaceName, final String sourceAnchorName, final String targetAnchorName, final String xpath, @@ -225,9 +219,9 @@ public class CpsDataServiceImpl implements CpsDataService { return cpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes); } + @Override @Timed(value = "cps.data.service.get.deltaBetweenAnchorAndPayload", description = "Time taken to get delta between anchor and a payload") - @Override public List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName, final String sourceAnchorName, final String xpath, final Map<String, String> yangResourceContentPerName, @@ -256,8 +250,8 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp); } @@ -266,31 +260,30 @@ public class CpsDataServiceImpl implements CpsDataService { @Timed(value = "cps.data.service.datanode.descendants.batch.update", description = "Time taken to update a batch of data nodes and descendants") public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName, - final Map<String, String> nodeDataPerXPath, + final Map<String, String> nodeDataPerParentNodeXPath, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = buildDataNodesWithParentNodeXpath(anchor, nodeDataPerXPath, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorAndXpathToNodeData(anchor, nodeDataPerParentNodeXPath, contentType); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); - nodeDataPerXPath.keySet().forEach(nodeXpath -> + nodeDataPerParentNodeXPath.keySet().forEach(nodeXpath -> sendDataUpdatedEvent(anchor, nodeXpath, Operation.UPDATE, observedTimestamp)); } @Override - @Timed(value = "cps.data.service.list.update", - description = "Time taken to update a list") + @Timed(value = "cps.data.service.list.update", description = "Time taken to update a list") public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> newListElements = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> newListElements = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp); } @Override - @Timed(value = "cps.data.service.list.batch.update", - description = "Time taken to update a batch of lists") + @Timed(value = "cps.data.service.list.batch.update", description = "Time taken to update a batch of lists") public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection<DataNode> dataNodes, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); @@ -300,8 +293,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.datanode.delete", - description = "Time taken to delete a datanode") + @Timed(value = "cps.data.service.datanode.delete", description = "Time taken to delete a datanode") public void deleteDataNode(final String dataspaceName, final String anchorName, final String dataNodeXpath, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); @@ -311,8 +303,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.datanode.batch.delete", - description = "Time taken to delete a batch of datanodes") + @Timed(value = "cps.data.service.datanode.batch.delete", description = "Time taken to delete a batch of datanodes") public void deleteDataNodes(final String dataspaceName, final String anchorName, final Collection<String> dataNodeXpaths, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); @@ -348,8 +339,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.list.delete", - description = "Time taken to delete a list or list element") + @Timed(value = "cps.data.service.list.delete", description = "Time taken to delete a list or list element") public void deleteListOrListElement(final String dataspaceName, final String anchorName, final String listNodeXpath, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); @@ -362,156 +352,34 @@ public class CpsDataServiceImpl implements CpsDataService { public void validateData(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, final ContentType contentType) { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final String xpath = ROOT_NODE_XPATH.equals(parentNodeXpath) ? PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH : + final String xpath = ROOT_NODE_XPATH.equals(parentNodeXpath) ? NO_PARENT_PATH : CpsPathUtil.getNormalizedXpath(parentNodeXpath); yangParser.validateData(contentType, nodeData, anchor, xpath); } - private Collection<DataNode> rebuildSourceDataNodes(final String xpath, final Anchor sourceAnchor, + private Collection<DataNode> rebuildSourceDataNodes(final String xpath, + final Anchor sourceAnchor, final Collection<DataNode> sourceDataNodes) { - final Collection<DataNode> sourceDataNodesRebuilt = new ArrayList<>(); if (sourceDataNodes != null) { - final String sourceDataNodesAsJson = getDataNodesAsJson(sourceAnchor, sourceDataNodes); - sourceDataNodesRebuilt.addAll( - buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, sourceDataNodesAsJson, ContentType.JSON)); + final Map<String, Object> sourceDataNodesAsMap = dataMapper.toFlatDataMap(sourceAnchor, sourceDataNodes); + final String sourceDataNodesAsJson = jsonObjectMapper.asJsonString(sourceDataNodesAsMap); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, sourceDataNodesAsJson, JSON); + sourceDataNodesRebuilt.addAll(dataNodes); } return sourceDataNodesRebuilt; } - private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor, final String xpath, + private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor, + final String xpath, final Map<String, String> yangResourceContentPerName, final String targetData) { if (yangResourceContentPerName.isEmpty()) { - return buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, targetData, ContentType.JSON); - } else { - return buildDataNodesWithYangResourceAndXpath(yangResourceContentPerName, xpath, - targetData, ContentType.JSON); + return dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, targetData, JSON); } - } - - private String getDataNodesAsJson(final Anchor anchor, final Collection<DataNode> dataNodes) { - - final List<Map<String, Object>> prefixToDataNodes = prefixResolver(anchor, dataNodes); - final Map<String, Object> targetDataAsJsonObject = getNodeDataAsJsonString(prefixToDataNodes); - return jsonObjectMapper.asJsonString(targetDataAsJsonObject); - } - - private Map<String, Object> getNodeDataAsJsonString(final List<Map<String, Object>> prefixToDataNodes) { - final Map<String, Object> nodeDataAsJson = new HashMap<>(); - for (final Map<String, Object> prefixToDataNode : prefixToDataNodes) { - nodeDataAsJson.putAll(prefixToDataNode); - } - return nodeDataAsJson; - } - - private List<Map<String, Object>> prefixResolver(final Anchor anchor, final Collection<DataNode> dataNodes) { - final List<Map<String, Object>> prefixToDataNodes = new ArrayList<>(dataNodes.size()); - for (final DataNode dataNode: dataNodes) { - final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); - final Map<String, Object> prefixToDataNode = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); - prefixToDataNodes.add(prefixToDataNode); - } - return prefixToDataNodes; - } - - private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor, - final Map<String, String> nodesJsonData, - final ContentType contentType) { - final Collection<DataNode> dataNodes = new ArrayList<>(); - for (final Map.Entry<String, String> nodeJsonData : nodesJsonData.entrySet()) { - dataNodes.addAll(buildDataNodesWithParentNodeXpath(anchor, nodeJsonData.getKey(), - nodeJsonData.getValue(), contentType)); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor, final String parentNodeXpath, - final String nodeData, final ContentType contentType) { - - if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, - anchor, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "No data nodes provided"); - } - return dataNodes; - } - final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(parentNodeXpath); - final ContainerNode containerNode = - yangParser.parseData(contentType, nodeData, anchor, normalizedParentNodeXpath); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withParentNodeXpath(normalizedParentNodeXpath) - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "No data nodes provided"); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithParentNodeXpath( - final Map<String, String> yangResourceContentPerName, final String xpath, - final String nodeData, final ContentType contentType) { - - if (isRootNodeXpath(xpath)) { - final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, - yangResourceContentPerName, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "Data nodes were not found under the xpath " + xpath); - } - return dataNodes; - } - final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(xpath); - final ContainerNode containerNode = - yangParser.parseData(contentType, nodeData, yangResourceContentPerName, normalizedParentNodeXpath); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withParentNodeXpath(normalizedParentNodeXpath) - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "Data nodes were not found under the xpath " + xpath); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithAnchorAndXpath(final Anchor anchor, final String xpath, - final String nodeData, - final ContentType contentType) { - - if (!isRootNodeXpath(xpath)) { - final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath); - if (parentNodeXpath.isEmpty()) { - return buildDataNodesWithParentNodeXpath(anchor, ROOT_NODE_XPATH, nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(anchor, xpath, nodeData, contentType); - } - - private Collection<DataNode> buildDataNodesWithYangResourceAndXpath( - final Map<String, String> yangResourceContentPerName, final String xpath, - final String nodeData, final ContentType contentType) { - if (!isRootNodeXpath(xpath)) { - final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath); - if (parentNodeXpath.isEmpty()) { - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, ROOT_NODE_XPATH, - nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, parentNodeXpath, - nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, xpath, nodeData, contentType); - } - - private static boolean isRootNodeXpath(final String xpath) { - return ROOT_NODE_XPATH.equals(xpath); + return dataNodeFactory + .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, targetData, JSON); } private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) { @@ -523,8 +391,10 @@ public class CpsDataServiceImpl implements CpsDataService { } } - private void sendDataUpdatedEvent(final Anchor anchor, final String xpath, - final Operation operation, final OffsetDateTime observedTimestamp) { + private void sendDataUpdatedEvent(final Anchor anchor, + final String xpath, + final Operation operation, + final OffsetDateTime observedTimestamp) { try { cpsDataUpdateEventsService.publishCpsDataUpdateEvent(anchor, xpath, operation, observedTimestamp); } catch (final Exception exception) { diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java new file mode 100644 index 0000000000..4ac0d5d8e8 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java @@ -0,0 +1,97 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.impl; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.onap.cps.api.CpsDataService; +import org.onap.cps.api.CpsFacade; +import org.onap.cps.api.CpsQueryService; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.api.parameters.PaginationOption; +import org.onap.cps.utils.DataMapper; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class CpsFacadeImpl implements CpsFacade { + + private final CpsDataService cpsDataService; + private final CpsQueryService cpsQueryService; + private final DataMapper dataMapper; + + @Override + public Map<String, Object> getFirstDataNodeByAnchor(final String dataspaceName, + final String anchorName, + final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { + final DataNode dataNode = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath, + fetchDescendantsOption).iterator().next(); + return dataMapper.toDataMap(dataspaceName, anchorName, dataNode); + } + + @Override + public List<Map<String, Object>> getDataNodesByAnchor(final String dataspaceName, + final String anchorName, + final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { + final Collection<DataNode> dataNodes = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath, + fetchDescendantsOption); + return dataMapper.toDataMaps(dataspaceName, anchorName, dataNodes); + } + + @Override + public List<Map<String, Object>> executeAnchorQuery(final String dataspaceName, + final String anchorName, + final String cpsPath, + final FetchDescendantsOption fetchDescendantsOption) { + final Collection<DataNode> dataNodes = + cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption); + return dataMapper.toDataMaps(dataspaceName, anchorName, dataNodes); + } + + @Override + public List<Map<String, Object>> executeDataspaceQuery(final String dataspaceName, + final String cpsPath, + final FetchDescendantsOption fetchDescendantsOption, + final PaginationOption paginationOption) { + final Collection<DataNode> dataNodes = cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, + cpsPath, fetchDescendantsOption, paginationOption); + return dataMapper.toDataMaps(dataspaceName, dataNodes); + } + + @Override + public int countAnchorsInDataspaceQuery(final String dataspaceName, + final String cpsPath, + final PaginationOption paginationOption) { + if (paginationOption == PaginationOption.NO_PAGINATION) { + return 1; + } + final int totalAnchors = cpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath); + return totalAnchors <= paginationOption.getPageSize() ? 1 + : (int) Math.ceil((double) totalAnchors / paginationOption.getPageSize()); + } + +} + diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java index 09ef637965..dc293b26e2 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2025 TechMahindra Ltd. + * Modifications Copyright (C) 2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +23,6 @@ package org.onap.cps.impl; import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -36,8 +36,7 @@ import org.onap.cps.api.model.DataNode; import org.onap.cps.cpspath.parser.CpsPathUtil; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.utils.ContentType; -import org.onap.cps.utils.DataMapUtils; -import org.onap.cps.utils.PrefixResolver; +import org.onap.cps.utils.DataMapper; import org.onap.cps.utils.YangParser; import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.springframework.stereotype.Service; @@ -53,10 +52,10 @@ public class CpsNotificationServiceImpl implements CpsNotificationService { private final YangParser yangParser; - private final PrefixResolver prefixResolver; + private final DataMapper dataMapper; private static final String ADMIN_DATASPACE = "CPS-Admin"; - private static final String ANCHOR_NAME = "cps-notification-subscriptions"; + private static final String CPS_SUBSCRIPTION_ANCHOR_NAME = "cps-notification-subscriptions"; private static final String DATASPACE_SUBSCRIPTION_XPATH_FORMAT = "/dataspaces/dataspace[@name='%s']"; private static final String ANCHORS_SUBSCRIPTION_XPATH_FORMAT = "/dataspaces/dataspace[@name='%s']/anchors"; private static final String ANCHOR_SUBSCRIPTION_XPATH_FORMAT = @@ -65,29 +64,22 @@ public class CpsNotificationServiceImpl implements CpsNotificationService { @Override public void createNotificationSubscription(final String notificationSubscriptionAsJson, final String xpath) { - final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, ANCHOR_NAME); + final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME); final Collection<DataNode> dataNodes = buildDataNodesWithParentNodeXpath(anchor, xpath, notificationSubscriptionAsJson); - cpsDataPersistenceService.addListElements(ADMIN_DATASPACE, ANCHOR_NAME, xpath, dataNodes); + cpsDataPersistenceService.addListElements(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, dataNodes); } @Override public void deleteNotificationSubscription(final String xpath) { - cpsDataPersistenceService.deleteDataNode(ADMIN_DATASPACE, ANCHOR_NAME, xpath); + cpsDataPersistenceService.deleteDataNode(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath); } @Override public List<Map<String, Object>> getNotificationSubscription(final String xpath) { - final Collection<DataNode> dataNodes = - cpsDataPersistenceService.getDataNodes(ADMIN_DATASPACE, ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS); - final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size()); - final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, ANCHOR_NAME); - for (final DataNode dataNode: dataNodes) { - final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); - final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); - dataMaps.add(dataMap); - } - return dataMaps; + final Collection<DataNode> dataNodes = cpsDataPersistenceService + .getDataNodes(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS); + return dataMapper.toDataMaps(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, dataNodes); } @Override @@ -103,7 +95,8 @@ public class CpsNotificationServiceImpl implements CpsNotificationService { private boolean isNotificationEnabledForXpath(final String xpath) { try { - cpsDataPersistenceService.getDataNodes(ADMIN_DATASPACE, ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS); + cpsDataPersistenceService + .getDataNodes(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS); } catch (final DataNodeNotFoundException e) { return false; } diff --git a/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java b/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java new file mode 100644 index 0000000000..76db887c8e --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java @@ -0,0 +1,107 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.impl; + +import static org.onap.cps.cpspath.parser.CpsPathUtil.NO_PARENT_PATH; +import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.onap.cps.api.DataNodeFactory; +import org.onap.cps.api.exceptions.DataValidationException; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.utils.ContentType; +import org.onap.cps.utils.YangParser; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DataNodeFactoryImpl implements DataNodeFactory { + + private final YangParser yangParser; + + @Override + public Collection<DataNode> createDataNodesWithAnchorAndXpathToNodeData(final Anchor anchor, + final Map<String, String> nodesDataPerParentNodeXpath, + final ContentType contentType) { + final Collection<DataNode> dataNodes = new ArrayList<>(); + for (final Map.Entry<String, String> nodeDataToParentNodeXpath : nodesDataPerParentNodeXpath.entrySet()) { + dataNodes.addAll(createDataNodesWithAnchorParentXpathAndNodeData(anchor, nodeDataToParentNodeXpath.getKey(), + nodeDataToParentNodeXpath.getValue(), contentType)); + } + return dataNodes; + } + + @Override + public Collection<DataNode> createDataNodesWithAnchorXpathAndNodeData(final Anchor anchor, final String xpath, + final String nodeData, + final ContentType contentType) { + final String xpathToBuildNodes = isRootNodeXpath(xpath) ? NO_PARENT_PATH : + CpsPathUtil.getNormalizedParentXpath(xpath); + final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, anchor, xpathToBuildNodes); + return convertToDataNodes(xpathToBuildNodes, containerNode); + } + + @Override + public Collection<DataNode> createDataNodesWithAnchorParentXpathAndNodeData(final Anchor anchor, + final String parentNodeXpath, + final String nodeData, + final ContentType contentType) { + + final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(parentNodeXpath); + final ContainerNode containerNode = + yangParser.parseData(contentType, nodeData, anchor, normalizedParentNodeXpath); + return convertToDataNodes(normalizedParentNodeXpath, containerNode); + } + + @Override + public Collection<DataNode> createDataNodesWithYangResourceXpathAndNodeData( + final Map<String, String> yangResourceContentPerName, + final String xpath, final String nodeData, + final ContentType contentType) { + final String normalizedParentNodeXpath = isRootNodeXpath(xpath) ? NO_PARENT_PATH : + CpsPathUtil.getNormalizedParentXpath(xpath); + final ContainerNode containerNode = + yangParser.parseData(contentType, nodeData, yangResourceContentPerName, normalizedParentNodeXpath); + return convertToDataNodes(normalizedParentNodeXpath, containerNode); + } + + private static Collection<DataNode> convertToDataNodes(final String normalizedParentNodeXpath, + final ContainerNode containerNode) { + final Collection<DataNode> dataNodes = new DataNodeBuilder() + .withParentNodeXpath(normalizedParentNodeXpath) + .withContainerNode(containerNode) + .buildCollection(); + if (dataNodes.isEmpty()) { + throw new DataValidationException("No Data Nodes", "The request did not return any data nodes for xpath " + + normalizedParentNodeXpath); + } + return dataNodes; + } + + private static boolean isRootNodeXpath(final String xpath) { + return ROOT_NODE_XPATH.equals(xpath); + } +} diff --git a/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java b/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java new file mode 100644 index 0000000000..6e7eff9132 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java @@ -0,0 +1,133 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.onap.cps.api.CpsAnchorService; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DataMapper { + + private final CpsAnchorService cpsAnchorService; + private final PrefixResolver prefixResolver; + + /** + * Convert a data node to a data map. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor + * @param dataNode the data node to convert + * @return the data node represented as a map of key value pairs + */ + public Map<String, Object> toDataMap(final String dataspaceName, final String anchorName, final DataNode dataNode) { + final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); + return DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); + } + + /** + * Convert a collection of data nodes to a list of data maps. + * + * @param dataspaceName the name dataspace name + * @param anchorName the name of the anchor + * @param dataNodes the data nodes to convert + * @return a list of maps representing the data nodes + */ + public List<Map<String, Object>> toDataMaps(final String dataspaceName, final String anchorName, + final Collection<DataNode> dataNodes) { + final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + return toDataMaps(anchor, dataNodes); + } + + /** + * Convert a collection of data nodes to a list of data maps. + * + * @param anchor the anchor + * @param dataNodes the data nodes to convert + * @return a list of maps representing the data nodes + */ + public List<Map<String, Object>> toDataMaps(final Anchor anchor, final Collection<DataNode> dataNodes) { + final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size()); + for (final DataNode dataNode : dataNodes) { + final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); + final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); + dataMaps.add(dataMap); + } + return dataMaps; + } + + /** + * Convert a collection of data nodes (belonging to multiple anchors) to a list of data maps. + * + * @param dataspaceName the name dataspace name + * @param dataNodes the data nodes to convert + * @return a list of maps representing the data nodes + */ + public List<Map<String, Object>> toDataMaps(final String dataspaceName, final Collection<DataNode> dataNodes) { + final List<Map<String, Object>> dataNodesAsMaps = new ArrayList<>(dataNodes.size()); + final Map<String, List<DataNode>> dataNodesPerAnchor = groupDataNodesPerAnchor(dataNodes); + for (final Map.Entry<String, List<DataNode>> dataNodesPerAnchorEntry : dataNodesPerAnchor.entrySet()) { + final String anchorName = dataNodesPerAnchorEntry.getKey(); + final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + final DataNode dataNode = dataNodesPerAnchorEntry.getValue().get(0); + final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); + final Map<String, Object> dataNodeAsMap = DataMapUtils.toDataMapWithIdentifierAndAnchor( + dataNodesPerAnchorEntry.getValue(), anchorName, prefix); + dataNodesAsMaps.add(dataNodeAsMap); + } + return dataNodesAsMaps; + } + + /** + * Convert a collection of data nodes to a data map. + * + * @param anchor the anchor + * @param dataNodes the data nodes to convert + * @return a map representing the data nodes + */ + public Map<String, Object> toFlatDataMap(final Anchor anchor, final Collection<DataNode> dataNodes) { + final List<Map<String, Object>> dataNodesAsMaps = toDataMaps(anchor, dataNodes); + return flattenDataNodesMaps(dataNodesAsMaps); + } + + private Map<String, Object> flattenDataNodesMaps(final List<Map<String, Object>> dataNodesAsMaps) { + final Map<String, Object> dataNodesAsFlatMap = new HashMap<>(); + for (final Map<String, Object> dataNodeAsMap : dataNodesAsMaps) { + dataNodesAsFlatMap.putAll(dataNodeAsMap); + } + return dataNodesAsFlatMap; + } + + private static Map<String, List<DataNode>> groupDataNodesPerAnchor(final Collection<DataNode> dataNodes) { + return dataNodes.stream().collect(Collectors.groupingBy(DataNode::getAnchorName)); + } + +} diff --git a/cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java b/cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java index bd348a25d1..e59029f916 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java +++ b/cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2024 Nordix Foundation. + * Copyright (C) 2022-2025 Nordix Foundation. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,25 +47,29 @@ public class PrefixResolver { * @return the prefix of the module the top level element of given xpath */ public String getPrefix(final Anchor anchor, final String xpath) { + return getPrefix(anchor.getDataspaceName(), anchor.getSchemaSetName(), xpath); + } + + private String getPrefix(final String dataspaceName, final String schemaSetName, final String xpath) { final CpsPathQuery cpsPathQuery = CpsPathUtil.getCpsPathQuery(xpath); if (cpsPathQuery.getCpsPathPrefixType() != CpsPathPrefixType.ABSOLUTE) { return ""; } - final String topLevelContainerName = cpsPathQuery.getContainerNames().get(0); + final String topLevelContainerName = cpsPathQuery.getContainerNames().get(0); final YangTextSchemaSourceSet yangTextSchemaSourceSet = - yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(), anchor.getSchemaSetName()); + yangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName); final SchemaContext schemaContext = yangTextSchemaSourceSet.getSchemaContext(); return schemaContext.getChildNodes().stream() - .filter(DataNodeContainer.class::isInstance) - .map(SchemaNode::getQName) - .filter(qname -> qname.getLocalName().equals(topLevelContainerName)) - .findFirst() - .map(QName::getModule) - .flatMap(schemaContext::findModule) - .map(Module::getPrefix) - .orElse(""); + .filter(DataNodeContainer.class::isInstance) + .map(SchemaNode::getQName) + .filter(qname -> qname.getLocalName().equals(topLevelContainerName)) + .findFirst() + .map(QName::getModule) + .flatMap(schemaContext::findModule) + .map(Module::getPrefix) + .orElse(""); } } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy index 126e5b197b..508178b419 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation + * Copyright (C) 2022-2025 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,10 @@ package org.onap.cps.api.parameters import org.onap.cps.api.exceptions.DataValidationException import spock.lang.Specification +import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY +import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS +import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS + class FetchDescendantsOptionSpec extends Specification { def 'Has next descendant for fetch descendant option: #scenario'() { @@ -105,11 +109,22 @@ class FetchDescendantsOptionSpec extends Specification { expect: 'each fetch descendant option has the correct String value' assert fetchDescendantsOption.toString() == expectedStringValue where: 'the following option is used' - fetchDescendantsOption || expectedStringValue - FetchDescendantsOption.OMIT_DESCENDANTS || 'OmitDescendants' - FetchDescendantsOption.DIRECT_CHILDREN_ONLY || 'DirectChildrenOnly' - FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS || 'IncludeAllDescendants' - new FetchDescendantsOption(2) || 'Depth=2' + fetchDescendantsOption || expectedStringValue + OMIT_DESCENDANTS || 'OmitDescendants' + DIRECT_CHILDREN_ONLY || 'DirectChildrenOnly' + INCLUDE_ALL_DESCENDANTS || 'IncludeAllDescendants' + new FetchDescendantsOption(2) || 'Depth=2' + } + + def 'Convert include-descendants boolean to fetch descendants option with : #includeDescendants'() { + when: 'convert boolean #includeDescendants' + def result = FetchDescendantsOption.getFetchDescendantsOption(includeDescendants) + then: 'result is the expected option' + assert result == expectedFetchDescendantsOption + where: 'following parameters are used' + includeDescendants || expectedFetchDescendantsOption + true || INCLUDE_ALL_DESCENDANTS + false || OMIT_DESCENDANTS } } diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy index d78c8bb47f..a21a17fabd 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy @@ -20,7 +20,6 @@ package org.onap.cps.impl - import org.onap.cps.utils.CpsValidator import org.onap.cps.spi.CpsAdminPersistenceService import org.onap.cps.spi.CpsDataPersistenceService diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy index abcda6c696..967bcc0aa0 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy @@ -1,9 +1,9 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2024 Nordix Foundation + * Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 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. @@ -30,17 +30,18 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDeltaService -import org.onap.cps.events.CpsDataUpdateEventsService -import org.onap.cps.utils.CpsValidator -import org.onap.cps.spi.CpsDataPersistenceService -import org.onap.cps.api.parameters.FetchDescendantsOption import org.onap.cps.api.exceptions.ConcurrencyException import org.onap.cps.api.exceptions.DataNodeNotFoundExceptionBatch import org.onap.cps.api.exceptions.DataValidationException import org.onap.cps.api.exceptions.SessionManagerException import org.onap.cps.api.exceptions.SessionTimeoutException import org.onap.cps.api.model.Anchor +import org.onap.cps.api.parameters.FetchDescendantsOption +import org.onap.cps.events.CpsDataUpdateEventsService +import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.utils.ContentType +import org.onap.cps.utils.CpsValidator +import org.onap.cps.utils.DataMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver import org.onap.cps.utils.YangParser @@ -68,9 +69,11 @@ class CpsDataServiceImplSpec extends Specification { def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) def mockPrefixResolver = Mock(PrefixResolver) + def dataMapper = new DataMapper(mockCpsAnchorService, mockPrefixResolver) + def dataNodeFactory = new DataNodeFactoryImpl(yangParser) def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, - mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) + dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, dataMapper, jsonObjectMapper) def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) def loggingListAppender @@ -107,8 +110,9 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving #scenario data.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') - when: 'save data method is invoked with test-tree #scenario data' + and: 'JSON/XML data is fetched from resource file' def data = TestUtils.getResourceFileContent(dataFile) + when: 'save data method is invoked with test-tree #scenario data' objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, @@ -131,7 +135,7 @@ class CpsDataServiceImplSpec extends Specification { assert exceptionThrown.message.startsWith(expectedMessage) where: 'given parameters' scenario | invalidData | contentType || expectedMessage - 'no data nodes' | '{}' | ContentType.JSON || 'No data nodes' + 'no data nodes' | '{}' | ContentType.JSON || 'No Data Nodes' 'invalid json' | '{invalid json' | ContentType.JSON || 'Data Validation Failed' 'invalid xml' | '<invalid xml' | ContentType.XML || 'Data Validation Failed' } @@ -139,8 +143,9 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving list element data fragment under Root node.'() { given: 'schema set for given anchor and dataspace references bookstore model' setupSchemaSetMocks('bookstore.yang') - when: 'save data method is invoked with list element json data' + and: 'JSON data associated with bookstore model' def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}' + when: 'save data method is invoked with list element json data' objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp, ContentType.JSON) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, @@ -159,8 +164,8 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving child data fragment under existing node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') - when: 'save data method is invoked with test-tree json data' def jsonData = '{"branch": [{"name": "New"}]}' + when: 'save data method is invoked with test-tree json data' objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree', @@ -169,7 +174,7 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) } - def 'Saving list element data fragment under existing JSON/XML node.'() { + def 'Saving list element data fragment under existing #scenario .'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with list element data' @@ -187,12 +192,13 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) where: - data | contentType - '{"branch": [{"name": "A"}, {"name": "B"}]}' | ContentType.JSON - '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch><branch><name>B</name></branch></test-tree>' | ContentType.XML + scenario | data | contentType + 'JSON data' | '{"branch": [{"name": "A"}, {"name": "B"}]}' | ContentType.JSON + 'XML data' | '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch><branch><name>B</name></branch></test-tree>' | ContentType.XML + } - def 'Saving empty list element data fragment for JSON/XML data.'() { + def 'Saving empty list element data fragment for #scenario.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with an empty list' @@ -200,9 +206,9 @@ class CpsDataServiceImplSpec extends Specification { then: 'invalid data exception is thrown' thrown(DataValidationException) where: - data | contentType - '{"branch": []}' | ContentType.JSON - '<test-tree><branch></branch></test-tree>' | ContentType.XML + scenario | data | contentType + 'JSON data' | '{"branch": []}' | ContentType.JSON + 'XML data' | '<test-tree><branch></branch></test-tree>' | ContentType.XML } def 'Get all data nodes #scenario.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy new file mode 100644 index 0000000000..c754970518 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy @@ -0,0 +1,114 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.impl + +import org.onap.cps.api.CpsAnchorService +import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsQueryService +import org.onap.cps.api.model.DataNode +import org.onap.cps.api.parameters.PaginationOption +import org.onap.cps.utils.DataMapper +import org.onap.cps.utils.PrefixResolver +import spock.lang.Specification + +import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS +import static org.onap.cps.api.parameters.PaginationOption.NO_PAGINATION + +class CpsFacadeImplSpec extends Specification { + + def mockCpsDataService = Mock(CpsDataService) + def mockCpsQueryService = Mock(CpsQueryService) + def mockCpsAnchorService = Mock(CpsAnchorService) + def mockPrefixResolver = Mock(PrefixResolver) + def dataMapper = new DataMapper(mockCpsAnchorService, mockPrefixResolver) + + def myFetchDescendantsOption = OMIT_DESCENDANTS + def myPaginationOption = NO_PAGINATION + + def objectUnderTest = new CpsFacadeImpl(mockCpsDataService, mockCpsQueryService , dataMapper) + + def dataNode1 = new DataNode(xpath:'/path1', anchorName: 'my anchor') + def dataNode2 = new DataNode(xpath:'/path2', anchorName: 'my anchor') + def dataNode3 = new DataNode(xpath:'/path3', anchorName: 'other anchor') + + def setup() { + mockCpsDataService.getDataNodes('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption) >> [ dataNode1, dataNode2] + mockPrefixResolver.getPrefix(_, '/path1') >> 'prefix1' + mockPrefixResolver.getPrefix(_, '/path2') >> 'prefix2' + mockPrefixResolver.getPrefix(_, '/path3') >> 'prefix3' + } + + def 'Get one data node.'() { + when: 'get data node by dataspace and anchor' + def result = objectUnderTest.getFirstDataNodeByAnchor('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption) + then: 'only the first node (from the data service result) is returned' + assert result.size() == 1 + assert result.keySet()[0] == 'prefix1:path1' + } + + def 'Get multiple data nodes.'() { + when: 'get data node by dataspace and anchor' + def result = objectUnderTest.getDataNodesByAnchor('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption) + then: 'all nodes (from the data service result) are returned' + assert result.size() == 2 + assert result[0].keySet()[0] == 'prefix1:path1' + assert result[1].keySet()[0] == 'prefix2:path2' + } + + def 'Execute anchor query.'() { + given: 'the cps query service returns two data nodes' + mockCpsQueryService.queryDataNodes('my dataspace', 'my anchor', 'my cps path', myFetchDescendantsOption) >> [ dataNode1, dataNode2] + when: 'get data node by dataspace and anchor' + def result = objectUnderTest.executeAnchorQuery('my dataspace', 'my anchor', 'my cps path', myFetchDescendantsOption) + then: 'all nodes (from the query service result) are returned' + assert result.size() == 2 + assert result[0].keySet()[0] == 'prefix1:path1' + assert result[1].keySet()[0] == 'prefix2:path2' + } + + def 'Execute dataspace query.'() { + given: 'the cps query service returns two data nodes (on two different anchors)' + mockCpsQueryService.queryDataNodesAcrossAnchors('my dataspace', 'my cps path', myFetchDescendantsOption, myPaginationOption) >> [ dataNode1, dataNode2, dataNode3 ] + when: 'get data node by dataspace and anchor' + def result = objectUnderTest.executeDataspaceQuery('my dataspace', 'my cps path', myFetchDescendantsOption, myPaginationOption) + then: 'all nodes (from the query service result) are returned, grouped by anchor' + assert result.size() == 2 + assert result[0].toString() == '{anchorName=my anchor, dataNodes=[{prefix1:path1={}}, {prefix1:path2={}}]}' + assert result[1].toString() == '{anchorName=other anchor, dataNodes=[{prefix3:path3={}}]}' + } + + def 'How many pages (anchors) could be in the output with #scenario.'() { + given: 'the query service says there are 10 anchors for the given query' + mockCpsQueryService.countAnchorsForDataspaceAndCpsPath('my dataspace', 'my cps path') >> 10 + expect: 'the correct number of pages is returned' + assert objectUnderTest.countAnchorsInDataspaceQuery('my dataspace', 'my cps path', paginationOption) == expectedNumberOfPages + where: 'the following pagination options are used' + scenario | paginationOption || expectedNumberOfPages + 'no pagination' | NO_PAGINATION || 1 + '1 anchor per page' | new PaginationOption(1,1) || 10 + '1 anchor per page, start at 2' | new PaginationOption(2,1) || 10 + '2 anchors per page' | new PaginationOption(1,2) || 5 + '3 anchors per page' | new PaginationOption(1,3) || 4 + '10 anchors per page' | new PaginationOption(1,10) || 1 + '100 anchors per page' | new PaginationOption(1,100) || 1 + } + +} diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy index b7f06456c9..ab7853c8e6 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2025 TechMahindra Ltd. + * Modifications Copyright (C) 2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,22 +22,22 @@ package org.onap.cps.impl import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.exceptions.DataNodeNotFoundException import org.onap.cps.api.exceptions.DataValidationException import org.onap.cps.api.model.Anchor -import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.api.parameters.FetchDescendantsOption import org.onap.cps.spi.CpsDataPersistenceService +import org.onap.cps.utils.DataMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver import org.onap.cps.utils.YangParser -import org.onap.cps.TestUtils import org.onap.cps.utils.YangParserHelper import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.springframework.test.context.ContextConfiguration - import spock.lang.Specification @ContextConfiguration(classes = [ObjectMapper, JsonObjectMapper]) @@ -53,9 +54,9 @@ class CpsNotificationServiceImplSpec extends Specification { def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder) def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder) - def mockPrefixResolver = Mock(PrefixResolver) + def dataMapper = new DataMapper(mockCpsAnchorService, Mock(PrefixResolver)) - def objectUnderTest = new CpsNotificationServiceImpl(mockCpsAnchorService, mockCpsDataPersistenceService, yangParser, mockPrefixResolver) + def objectUnderTest = new CpsNotificationServiceImpl(mockCpsAnchorService, mockCpsDataPersistenceService, yangParser, dataMapper) def 'add notification subscription for list of dataspaces'() { given: 'details for notification subscription and subscription root node xpath' diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy new file mode 100644 index 0000000000..082fb33a61 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy @@ -0,0 +1,196 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.impl + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.core.read.ListAppender +import org.onap.cps.TestUtils +import org.onap.cps.api.CpsAnchorService +import org.onap.cps.api.exceptions.DataValidationException +import org.onap.cps.api.model.Anchor +import org.onap.cps.utils.ContentType +import org.onap.cps.utils.YangParser +import org.onap.cps.utils.YangParserHelper +import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder +import org.onap.cps.yang.YangTextSchemaSourceSet +import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Specification + +class DataNodeFactorySpec extends Specification { + + def mockCpsAnchorService = Mock(CpsAnchorService) + def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) + def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder) + def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder) + def objectUnderTest = new DataNodeFactoryImpl(yangParser) + + def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) + def loggingListAppender + def applicationContext = new AnnotationConfigApplicationContext() + + def dataspaceName = 'some-dataspace' + def anchorName = 'some-anchor' + def schemaSetName = 'some-schema-set' + def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build() + + def setup() { + mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor + logger.setLevel(Level.DEBUG) + loggingListAppender = new ListAppender() + logger.addAppender(loggingListAppender) + loggingListAppender.start() + applicationContext.refresh() + } + + void cleanup() { + ((Logger) LoggerFactory.getLogger(DataNodeFactoryImpl.class)).detachAndStopAllAppenders() + applicationContext.close() + } + + def 'Create data nodes using anchor and map of xpath to #scenario'() { + given:'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorAndXpathToNodeData(anchor, xpathToNodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == expectedDataNodes + and: 'data nodes have expected xpaths' + dataNodes.stream().map { it.getXpath() }.toList().containsAll(expectedXpaths) + where: 'the following data was used' + scenario | xpathToNodeData | contentType || expectedDataNodes | expectedXpaths + 'JSON Data' | ['/' : "{'test-tree': {'branch': []}}", '/test-tree' : "{'branch': [{'name':'Name'}]}"] | ContentType.JSON || 2 | ['/test-tree', "/test-tree/branch[@name='Name']"] + 'XML Data' | ['/test-tree' : '<branch><name>Name</name></branch>'] | ContentType.XML || 1 | ["/test-tree/branch[@name='Name']"] + } + + def 'Create data nodes using anchor, xpath and #scenario string'() { + given:'xpath, json string and schema set for given anchor and dataspace references test-tree model' + def xpath = '/' + def nodeData = TestUtils.getResourceFileContent(data) + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorXpathAndNodeData(anchor, xpath, nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpaths' + dataNodes[0].getXpath() == '/test-tree' + where: 'the following data was used' + scenario | data | contentType + 'JSON' | 'test-tree.json' | ContentType.JSON + 'XML' | 'test-tree.xml' | ContentType.XML + } + + def 'Building data nodes using anchor, xpath and #scenario'() { + given:'xpath, invalid json string and schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithAnchorXpathAndNodeData(anchor, '/test-tree', invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no data nodes' | '{}' | ContentType.JSON || 'No Data Nodes' + 'invalid json' | '{invalid json' | ContentType.JSON || 'Data Validation Failed' + 'invalid xml' | '<invalid xml' | ContentType.XML || 'Data Validation Failed' + } + + def 'Create data nodes using anchor, parent node xpath and #scenario string'() { + given:'parent node xpath, json string and schema set for given anchor and dataspace references test-tree model' + def parentXpath = '/test-tree' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentXpath, nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpaths' + dataNodes[0].getXpath() == "/test-tree/branch[@name='A']" + where: 'the following data was used' + scenario | nodeData | contentType + 'JSON' | '{"branch": [{"name": "A"}]}' | ContentType.JSON + 'XML' | '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch></test-tree>' | ContentType.XML + } + + def 'Create data nodes using anchor, parent node xpath and invalid #scenario string'() { + given:'parent node xpath, invalid json string and schema set for given anchor and dataspace references test-tree model' + def parentXpath = '/test-tree' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentXpath, invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no data nodes' | '{"branch": []}' | ContentType.JSON || 'No Data Nodes' + 'invalid json' | '<test-tree><branch></branch></test-tree>' | ContentType.JSON || 'Data Validation Failed' + } + + def 'Create data nodes using schema, xpath and #scenario string'() { + given:'xpath, json string and schema set for given anchor and dataspace references bookstore model' + def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + setupSchemaSetMocksForDelta(yangResourcesNameToContentMap) + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithYangResourceXpathAndNodeData(yangResourcesNameToContentMap, '/', nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpath' + dataNodes[0].getXpath() == '/bookstore' + where: 'the following data was used' + scenario | nodeData | contentType + 'JSON' | '{"bookstore":{"bookstore-name":"Easons"}}' | ContentType.JSON + 'XML' | "<bookstore xmlns=\"org:onap:ccsdk:sample\"><bookstore-name>Easons</bookstore-name></bookstore>" | ContentType.XML + } + + def 'Create data nodes using schema, xpath and invalid #scenario string'() { + given:'xpath, invalid json string and schema set for given anchor and dataspace references bookstore model' + def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + setupSchemaSetMocksForDelta(yangResourcesNameToContentMap) + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithYangResourceXpathAndNodeData(yangResourcesNameToContentMap, '/', invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no json nodes' | '{}' | ContentType.JSON || 'No Data Nodes' + 'no xml nodes' | '"<bookstore xmlns=\"org:onap:ccsdk:sample\"/>' | ContentType.XML || 'Data Validation Failed' + 'invalid json' | '{invalid' | ContentType.JSON || 'Data Validation Failed' + 'invalid xml' | '<invalid' | ContentType.XML || 'Data Validation Failed' + } + + def setupSchemaSetMocks(String... yangResources) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources) + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext + } + + def setupSchemaSetMocksForDelta(Map<String, String> yangResourcesNameToContentMap) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockTimedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourcesNameToContentMap) >> mockYangTextSchemaSourceSet + mockYangTextSchemaSourceSetCache.get(_, _) >> mockYangTextSchemaSourceSet + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy index db5b4f104e..893cce6687 100755 --- a/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2021-2025 Nordix Foundation. * Modifications Copyright (C) 2021-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,12 +27,13 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDeltaService +import org.onap.cps.api.model.Anchor import org.onap.cps.events.CpsDataUpdateEventsService -import org.onap.cps.utils.CpsValidator import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.CpsModulePersistenceService -import org.onap.cps.api.model.Anchor import org.onap.cps.utils.ContentType +import org.onap.cps.utils.CpsValidator +import org.onap.cps.utils.DataMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver import org.onap.cps.utils.YangParser @@ -42,23 +43,22 @@ import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import spock.lang.Specification class E2ENetworkSliceSpec extends Specification { - def mockModuleStoreService = Mock(CpsModulePersistenceService) - def mockDataStoreService = Mock(CpsDataPersistenceService) + def mockCpsModulePersistenceService = Mock(CpsModulePersistenceService) + def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService) def mockCpsAnchorService = Mock(CpsAnchorService) def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockCpsValidator = Mock(CpsValidator) def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder() def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, timedYangTextSchemaSourceSetBuilder) def mockCpsDeltaService = Mock(CpsDeltaService) + def dataMapper = new DataMapper(mockCpsAnchorService, Mock(PrefixResolver)) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - def mockPrefixResolver = Mock(PrefixResolver) - def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService, - mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) + def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService) - def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, - yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) + def dataNodeFactory = new DataNodeFactoryImpl(yangParser) + def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, dataMapper, jsonObjectMapper) def dataspaceName = 'someDataspace' def anchorName = 'someAnchor' def schemaSetName = 'someSchemaSet' @@ -74,7 +74,7 @@ class E2ENetworkSliceSpec extends Specification { when: 'Create schema set method is invoked' cpsModuleServiceImpl.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockModuleStoreService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) + 1 * mockCpsModulePersistenceService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) } def 'E2E Coverage Area-Tracking Area & TA-Cell mapping model can be parsed by CPS.'() { @@ -84,7 +84,7 @@ class E2ENetworkSliceSpec extends Specification { when: 'Create schema set method is invoked' cpsModuleServiceImpl.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockModuleStoreService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) + 1 * mockCpsModulePersistenceService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) } def 'E2E Coverage Area-Tracking Area & TA-Cell mapping data can be parsed by CPS.'() { @@ -100,31 +100,28 @@ class E2ENetworkSliceSpec extends Specification { new Anchor().builder().name(anchorName).schemaSetName(schemaSetName).dataspaceName(dataspaceName).build() mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName) - mockModuleStoreService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext + mockCpsModulePersistenceService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext when: 'saveData method is invoked' cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >> + 1 * mockCpsDataPersistenceService.storeDataNodes('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]} def child = dataNodeStored[0].childDataNodes[0] assert child.childDataNodes.size() == 1 and: 'list of Tracking Area for a Coverage Area are stored with correct xpath and child nodes ' def listOfTAForCoverageArea = child.childDataNodes[0] - listOfTAForCoverageArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/' + - 'coverage-area[@coverageArea=\'Washington\']' - listOfTAForCoverageArea.childDataNodes[0].leaves.get('nRTAC') == 234 + listOfTAForCoverageArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/coverage-area[@coverageArea=\'Washington\']' + assert listOfTAForCoverageArea.childDataNodes[0].leaves.get('nRTAC') == 234 and: 'list of cells in a tracking area are stored with correct xpath and child nodes ' def listOfCellsInTrackingArea = listOfTAForCoverageArea.childDataNodes[0] - listOfCellsInTrackingArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/' + - 'coverage-area[@coverageArea=\'Washington\']/coverageAreaTAList[@nRTAC=\'234\']' + listOfCellsInTrackingArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/coverage-area[@coverageArea=\'Washington\']/coverageAreaTAList[@nRTAC=\'234\']' listOfCellsInTrackingArea.childDataNodes[0].leaves.get('cellLocalId') == 15709 } def 'E2E Coverage Area-Tracking Area & TA-Cell mapping data can be parsed for RAN inventory.'() { def dataNodeStored given: 'valid yang resource as name-to-content map' - def yangResourceContentPerName = TestUtils.getYangResourcesAsMap( - 'e2e/basic/cps-ran-inventory@2021-01-28.yang') + def yangResourceContentPerName = TestUtils.getYangResourcesAsMap('e2e/basic/cps-ran-inventory@2021-01-28.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName).getSchemaContext() and : 'a valid json is provided for the model' def jsonData = TestUtils.getResourceFileContent('e2e/basic/cps-ran-inventory-data.json') @@ -132,12 +129,11 @@ class E2ENetworkSliceSpec extends Specification { mockCpsAnchorService.getAnchor('someDataspace', 'someAnchor') >> new Anchor().builder().name('someAnchor').schemaSetName('someSchemaSet').dataspaceName(dataspaceName).build() mockYangTextSchemaSourceSetCache.get('someDataspace', 'someSchemaSet') >> YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName) - mockModuleStoreService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext + mockCpsModulePersistenceService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext when: 'saveData method is invoked' cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp) then: 'parameters are validated and processing is delegated to persistence service' - 1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >> - { args -> dataNodeStored = args[2]} + 1 * mockCpsDataPersistenceService.storeDataNodes('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]} and: 'the size of the tree is correct' def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored[0]) assert cpsRanInventory.size() == 4 @@ -146,17 +142,16 @@ class E2ENetworkSliceSpec extends Specification { def ranSlices = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']') def sliceProfilesList = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']/sliceProfilesList[@sliceProfileId=\'f33a9dd8-ae51-4acf-8073-c9390c25f6f1\']') def pLMNIdList = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']/sliceProfilesList[@sliceProfileId=\'f33a9dd8-ae51-4acf-8073-c9390c25f6f1\']/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']') - ranInventory.getChildDataNodes().size() == 1 - ranInventory.getChildDataNodes().find( {it.xpath == ranSlices.xpath}) + assert ranInventory.getChildDataNodes().size() == 1 + assert ranInventory.getChildDataNodes().find( {it.xpath == ranSlices.xpath}) and: 'ranSlices contains the correct child node' - ranSlices.getChildDataNodes().size() == 1 - ranSlices.getChildDataNodes().find( {it.xpath == sliceProfilesList.xpath}) + assert ranSlices.getChildDataNodes().size() == 1 + assert ranSlices.getChildDataNodes().find( {it.xpath == sliceProfilesList.xpath}) and: 'sliceProfilesList contains the correct child node' - sliceProfilesList.getChildDataNodes().size() == 1 - sliceProfilesList.getChildDataNodes().find( {it.xpath == pLMNIdList.xpath}) + assert sliceProfilesList.getChildDataNodes().size() == 1 + assert sliceProfilesList.getChildDataNodes().find( {it.xpath == pLMNIdList.xpath}) and: 'pLMNIdList contains no children' - pLMNIdList.getChildDataNodes().size() == 0 - + assert pLMNIdList.getChildDataNodes().size() == 0 } def 'E2E RAN Schema Model.'(){ diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 3b7cc6063a..49646731e2 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -94,7 +94,7 @@ services: test: wget -q -O - http://localhost:8080/actuator/health/readiness | grep -q '{"status":"UP"}' || exit 1 interval: 10s timeout: 10s - retries: 3 + retries: 10 start_period: 60s nginx: diff --git a/docs/cps-path.rst b/docs/cps-path.rst index eb203d8918..cfaad3ca57 100644 --- a/docs/cps-path.rst +++ b/docs/cps-path.rst @@ -1,6 +1,6 @@ .. This work is licensed under a Creative Commons Attribution 4.0 International License. .. http://creativecommons.org/licenses/by/4.0 -.. Copyright (C) 2021-2023 Nordix Foundation +.. Copyright (C) 2021-2025 Nordix Foundation .. Modifications Copyright (C) 2023 TechMahindra Ltd .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING @@ -183,7 +183,7 @@ General Notes Query Syntax ============ -``( <absolute-path> | <descendant-path> ) [ <leaf-conditions> ] [ <text()-condition> ] [ <contains()-condition> ] [ <ancestor-axis> ]`` +``( <absolute-path> | <descendant-path> ) [ <leaf-conditions> ] [ <text()-condition> ] [ <contains()-condition> ] [ <ancestor-axis> ] [ <attribute-axis> ]`` Each CPS path expression need to start with an 'absolute' or 'descendant' xpath. @@ -310,3 +310,21 @@ The ancestor axis can be added to any CPS path query but has to be the last part **Limitations** - Ancestor list elements can only be addressed using the list key leaf. - List elements with compound keys are not supported. + +attribute-axis +-------------- + +The attribute axis can be added to a CPS path query at the end. It will return only distinct values of a specified leaf. + +**Syntax**: ``<cps-path> ( '/@' <leaf-name> )?`` + - ``cps-path``: Any CPS path query. + - ``leaf-name``: The name of the leaf (attribute) for which values should be returned. + +**Examples** + - ``//categories/@name`` + - ``//categories[@code='1']/books/@price`` + - ``//books/ancestor::bookstore/@bookstore-name`` + +**Notes** + - The output is a list of attribute-value pairs. For example, ``[{"name":"Kids"},{"name":"SciFi"}]`` + - Only unique values will be returned. For example, if 3 books have a price of 5, then 5 will be returned only once. diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy index 9b79af95ff..bb69f2f544 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy @@ -272,16 +272,26 @@ abstract class CpsIntegrationSpecBase extends Specification { } def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset) { - registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy.UNIQUE) + registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy.UNIQUE, { id -> "alt=${id}" }) } - def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy ) { + def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy) { + registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, moduleNameStrategy, { id -> "alt=${id}" }) + } + + def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy, Closure<String> alternateIdGenerator) { + registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, moduleNameStrategy, alternateIdGenerator) + } + + def registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy, Closure<String> alternateIdGenerator) { def cmHandles = [] def id = offset def modulePrefix = moduleNameStrategy.OVERLAPPING.equals(moduleNameStrategy) ? 'same' : moduleSetTag - def moduleReferences = (1..200).collect { "${modulePrefix}Module${it}" } + def moduleReferences = (1..200).collect { "${modulePrefix}Module${it}" } + (1..numberOfCmHandles).each { - def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: "ch-${id}", moduleSetTag: moduleSetTag, alternateId: "alt=${id}") + def alternateId = alternateIdGenerator(id) + def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: "ch-${id}", moduleSetTag: moduleSetTag, alternateId: alternateId) cmHandles.add(ncmpServiceCmHandle) dmiDispatcher1.moduleNamesPerCmHandleId[ncmpServiceCmHandle.cmHandleId] = moduleReferences dmiDispatcher2.moduleNamesPerCmHandleId[ncmpServiceCmHandle.cmHandleId] = moduleReferences diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy index acc95cab8d..53e39ed9c0 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023 Nordix Foundation + * Copyright (C) 2023-2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class QueryPerfTest extends CpsPerfTestBase { def durationInSeconds = resourceMeter.getTotalTimeInSeconds() then: 'the expected number of nodes is returned' assert countDataNodesInTree(result) == expectedNumberOfDataNodes - and: 'all data is read within #durationLimit ms and memory used is within limit' + and: 'all data is read within #durationLimit seconds and memory used is within limit' recordAndAssertResourceUsage("Query 1 anchor ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB()) where: 'the following parameters are used' scenario | cpsPath || durationLimit | memoryLimit | expectedNumberOfDataNodes @@ -60,7 +60,7 @@ class QueryPerfTest extends CpsPerfTestBase { def durationInSeconds = resourceMeter.getTotalTimeInSeconds() then: 'the expected number of nodes is returned' assert countDataNodesInTree(result) == expectedNumberOfDataNodes - and: 'all data is read within #durationLimit ms and memory used is within limit' + and: 'all data is read within #durationLimit seconds and memory used is within limit' recordAndAssertResourceUsage("Query across anchors ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB()) where: 'the following parameters are used' scenario | cpspath || durationLimit | memoryLimit | expectedNumberOfDataNodes @@ -78,7 +78,7 @@ class QueryPerfTest extends CpsPerfTestBase { def durationInSeconds = resourceMeter.getTotalTimeInSeconds() then: 'the expected number of nodes is returned' assert countDataNodesInTree(result) == expectedNumberOfDataNodes - and: 'all data is read within #durationLimit ms and memory used is within limit' + and: 'all data is read within #durationLimit seconds and memory used is within limit' recordAndAssertResourceUsage("Query with ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB()) where: 'the following parameters are used' scenario | fetchDescendantsOption || durationLimit | memoryLimit | expectedNumberOfDataNodes @@ -95,7 +95,7 @@ class QueryPerfTest extends CpsPerfTestBase { def durationInSeconds = resourceMeter.getTotalTimeInSeconds() then: 'the expected number of nodes is returned' assert countDataNodesInTree(result) == expectedNumberOfDataNodes - and: 'all data is read within #durationLimit ms and memory used is within limit' + and: 'all data is read within #durationLimit seconds and memory used is within limit' recordAndAssertResourceUsage("Query ancestors with ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB()) where: 'the following parameters are used' scenario | fetchDescendantsOption || durationLimit | memoryLimit | expectedNumberOfDataNodes @@ -103,4 +103,22 @@ class QueryPerfTest extends CpsPerfTestBase { 'direct descendants' | DIRECT_CHILDREN_ONLY || 0.11 | 8 | 1 + OPENROADM_DEVICES_PER_ANCHOR 'all descendants' | INCLUDE_ALL_DESCENDANTS || 1.34 | 400 | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE } + + def 'Query data leaf with #scenario.'() { + when: 'query data leaf is called' + resourceMeter.start() + def result = objectUnderTest.queryDataLeaf(CPS_PERFORMANCE_TEST_DATASPACE, 'openroadm1', cpsPath, String) + resourceMeter.stop() + def durationInSeconds = resourceMeter.getTotalTimeInSeconds() + then: 'the expected number of results is returned' + assert result.size() == expectedNumberOfValues + and: 'all data is read within #durationLimit seconds and memory used is within limit' + recordAndAssertResourceUsage("Query data leaf ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB()) + where: 'the following parameters are used' + scenario | cpsPath || durationLimit | memoryLimit | expectedNumberOfValues + 'unique leaf value' | '/openroadm-devices/openroadm-device/@device-id' || 0.10 | 8 | OPENROADM_DEVICES_PER_ANCHOR + 'common leaf value' | '/openroadm-devices/openroadm-device/@ne-state' || 0.05 | 1 | 1 + 'non-existing data leaf' | '/openroadm-devices/openroadm-device/@non-existing' || 0.05 | 1 | 0 + } + } diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryByAlternateIdPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryByAlternateIdPerfTest.groovy deleted file mode 100644 index cd2fc6ed7e..0000000000 --- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryByAlternateIdPerfTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2024 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the 'License'); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an 'AS IS' BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.integration.performance.ncmp - -import org.onap.cps.integration.ResourceMeter -import org.onap.cps.integration.performance.base.NcmpPerfTestBase -import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher - -import java.util.stream.Collectors - -import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DATASPACE_NAME -import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR -import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS - -class CmHandleQueryByAlternateIdPerfTest extends NcmpPerfTestBase { - - AlternateIdMatcher objectUnderTest - ResourceMeter resourceMeter = new ResourceMeter() - - def setup() { objectUnderTest = alternateIdMatcher } - - def 'Query cm handle by longest match alternate id'() { - when: 'an alternate id as cps path query' - resourceMeter.start() - def cpsPath = "/a/b/c/d-5/e/f/g/h/i" - def dataNodes = objectUnderTest.getYangModelCmHandleByLongestMatchingAlternateId(cpsPath, '/') - and: 'the ids of the result are extracted and converted to xpath' - def cpsXpaths = dataNodes.stream().map(dataNode -> "/dmi-registry/cm-handles[@id='${dataNode.leaves.id}']".toString() ).collect(Collectors.toSet()) - and: 'a single get is executed to get all the parent objects and their descendants' - cpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cpsXpaths, OMIT_DESCENDANTS) - resourceMeter.stop() - def durationInSeconds = resourceMeter.getTotalTimeInSeconds() - print 'Total time in seconds to query ch handle by alternate id: ' + durationInSeconds - then: 'the required operations are performed within required time and memory limit' - recordAndAssertResourceUsage('Look up cm-handle by longest match alternate-id', 1, durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB()) - } -} diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/WriteDataJobPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/WriteDataJobPerfTest.groovy new file mode 100644 index 0000000000..c71426032d --- /dev/null +++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/WriteDataJobPerfTest.groovy @@ -0,0 +1,67 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.integration.performance.ncmp + +import org.onap.cps.integration.ResourceMeter +import org.onap.cps.integration.base.CpsIntegrationSpecBase +import org.onap.cps.ncmp.api.datajobs.DataJobService +import org.onap.cps.ncmp.api.datajobs.models.DataJobMetadata +import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest +import org.onap.cps.ncmp.api.datajobs.models.WriteOperation +import org.springframework.beans.factory.annotation.Autowired +import spock.lang.Ignore + +/** + * This test does not depend on common performance test data. Hence it just extends the integration spec base. + */ +class WriteDataJobPerfTest extends CpsIntegrationSpecBase { + + @Autowired + DataJobService dataJobService + + def resourceMeter = new ResourceMeter() + + def populateDataJobWriteRequests(int numberOfWriteOperations) { + def writeOperations = [] + for (int i = 1; i <= numberOfWriteOperations; i++) { + def basePath = "/SubNetwork=Europe/SubNetwork=Ireland/MeContext=MyRadioNode${i}/ManagedElement=MyManagedElement${i}" + writeOperations.add(new WriteOperation("${basePath}/SomeChild=child-1", 'operation1', '1', null)) + writeOperations.add(new WriteOperation("${basePath}/SomeChild=child-2", 'operation2', '2', null)) + writeOperations.add(new WriteOperation(basePath, 'operation3', '3', null)) + } + return new DataJobWriteRequest(writeOperations) + } + + @Ignore // CPS-2691 + def 'Performance test for writeDataJob method'() { + given: 'register 10_000 cm handles (with alternative ids)' + registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(DMI1_URL, 'tagA', 10_000, 1, ModuleNameStrategy.UNIQUE, { it -> "/SubNetwork=Europe/SubNetwork=Ireland/MeContext=MyRadioNode${it}/ManagedElement=MyManagedElement${it}" }) + def dataJobWriteRequest = populateDataJobWriteRequests(10_000) + when: 'sending a write job to NCMP with dynamically generated write operations' + resourceMeter.start() + dataJobService.writeDataJob('', '', new DataJobMetadata('d1', '', ''), dataJobWriteRequest) + resourceMeter.stop() + then: 'record the result. Not asserted, just recorded in See https://lf-onap.atlassian.net/browse/CPS-2691' + println "*** CPS-2691 Execution time: ${resourceMeter.totalTimeInSeconds} seconds" + cleanup: 'deregister test cm handles' + deregisterSequenceOfCmHandles(DMI1_URL, 10_000, 1) + } +} diff --git a/k6-tests/make-logs.sh b/k6-tests/make-logs.sh new file mode 100644 index 0000000000..60976247e5 --- /dev/null +++ b/k6-tests/make-logs.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Copyright 2025 Nordix Foundation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +SERVICE_NAME="cps-and-ncmp" +TIMESTAMP=$(date +"%Y%m%d%H%M%S") +LOG_DIR="${WORKSPACE:-.}/logs" +TEMP_DIR="$LOG_DIR/temp_$TIMESTAMP" +ZIP_FILE="$LOG_DIR/${SERVICE_NAME}_logs_$TIMESTAMP.zip" + +mkdir -p "$LOG_DIR" +mkdir -p "$TEMP_DIR" + +# Store logs for cps-and-ncmp containers to temp directory +CONTAINER_IDS=$(docker ps --filter "name=$SERVICE_NAME" --format "{{.ID}}") +for CONTAINER_ID in $CONTAINER_IDS; do + CONTAINER_NAME=$(docker inspect --format="{{.Name}}" "$CONTAINER_ID" | sed 's/\///g') + LOG_FILE="$TEMP_DIR/${CONTAINER_NAME}_logs_$TIMESTAMP.log" + docker logs "$CONTAINER_ID" > "$LOG_FILE" +done + +# Zip the logs +zip -r "$ZIP_FILE" "$TEMP_DIR" +echo "Logs saved to $ZIP_FILE inside workspace" + +# Clean temp files +rm -r "$TEMP_DIR" + +# Delete logs older than 2 weeks +find "$LOG_DIR" -name "${SERVICE_NAME}_logs_*.zip" -mtime +14 -delete diff --git a/k6-tests/ncmp/common/cmhandle-crud.js b/k6-tests/ncmp/common/cmhandle-crud.js index 285028f13c..3b6c3ff7b7 100644 --- a/k6-tests/ncmp/common/cmhandle-crud.js +++ b/k6-tests/ncmp/common/cmhandle-crud.js @@ -51,19 +51,30 @@ export function waitForAllCmHandlesToBeReady() { function createCmHandlePayload(cmHandleIds) { return { "dmiPlugin": DMI_PLUGIN_URL, - "createdCmHandles": cmHandleIds.map((cmHandleId, index) => ({ - "cmHandle": cmHandleId, - "alternateId": cmHandleId.replace('ch-', 'Subnetwork=Europe,ManagedElement='), - "moduleSetTag": MODULE_SET_TAGS[index % MODULE_SET_TAGS.length], - "cmHandleProperties": { - "id": "123" - }, - "publicCmHandleProperties": { - "Color": "yellow", - "Size": "small", - "Shape": "cube" - } - })), + "createdCmHandles": cmHandleIds.map((cmHandleId, index) => { + // Ensure unique networkSegment within range 1-10 + let networkSegmentId = Math.floor(Math.random() * 10) + 1; // Random between 1-10 + let moduleTag = MODULE_SET_TAGS[index % MODULE_SET_TAGS.length]; + + return { + "cmHandle": cmHandleId, + "alternateId": cmHandleId.replace('ch-', 'Region=NorthAmerica,Segment='), + "moduleSetTag": moduleTag, + "cmHandleProperties": { + "segmentId": index + 1, + "networkSegment": `Region=NorthAmerica,Segment=${networkSegmentId}`, // Unique within range 1-10 + "deviceIdentifier": `Element=RadioBaseStation_5G_${index + 1000}`, // Unique per cmHandle + "hardwareVersion": `HW-${moduleTag}`, // Shares uniqueness with moduleSetTag + "softwareVersion": `Firmware_${moduleTag}`, // Shares uniqueness with moduleSetTag + "syncStatus": "ACTIVE", + "nodeCategory": "VirtualNode" + }, + "publicCmHandleProperties": { + "systemId": index + 1, + "systemName": "ncmp" + } + }; + }), }; } diff --git a/k6-tests/ncmp/common/passthrough-crud.js b/k6-tests/ncmp/common/passthrough-crud.js index a3d48fd590..eed1ab5190 100644 --- a/k6-tests/ncmp/common/passthrough-crud.js +++ b/k6-tests/ncmp/common/passthrough-crud.js @@ -67,7 +67,7 @@ export function legacyBatchRead(cmHandleIds) { } function getRandomCmHandleReference(useAlternateId) { - const prefix = useAlternateId ? 'Subnetwork=Europe,ManagedElement=' : 'ch-'; + const prefix = useAlternateId ? 'Region=NorthAmerica,Segment=' : 'ch-'; return `${prefix}${randomIntBetween(1, TOTAL_CM_HANDLES)}`; } diff --git a/k6-tests/ncmp/common/search-base.js b/k6-tests/ncmp/common/search-base.js index af2caf71ec..af7d153416 100644 --- a/k6-tests/ncmp/common/search-base.js +++ b/k6-tests/ncmp/common/search-base.js @@ -51,7 +51,7 @@ const SEARCH_PARAMETERS_PER_SCENARIO = { "cmHandleQueryParameters": [ { "conditionName": "hasAllProperties", - "conditionParameters": [{"Color": "yellow"}] + "conditionParameters": [{"systemName": "ncmp"}] } ] }, diff --git a/k6-tests/ncmp/common/utils.js b/k6-tests/ncmp/common/utils.js index ee3e9c7b4b..57ab2ea17f 100644 --- a/k6-tests/ncmp/common/utils.js +++ b/k6-tests/ncmp/common/utils.js @@ -27,7 +27,7 @@ export const DMI_PLUGIN_URL = testConfig.hosts.dmiStubUrl; export const CONTAINER_UP_TIME_IN_SECONDS = testConfig.hosts.containerUpTimeInSeconds; export const LEGACY_BATCH_TOPIC_NAME = 'legacy_batch_topic'; export const TOTAL_CM_HANDLES = 50000; -export const REGISTRATION_BATCH_SIZE = 100; +export const REGISTRATION_BATCH_SIZE = 2000; export const READ_DATA_FOR_CM_HANDLE_DELAY_MS = 300; // must have same value as in docker-compose.yml export const WRITE_DATA_FOR_CM_HANDLE_DELAY_MS = 670; // must have same value as in docker-compose.yml export const CONTENT_TYPE_JSON_PARAM = {'Content-Type': 'application/json'}; diff --git a/k6-tests/setup.sh b/k6-tests/setup.sh index c01a0f6c60..d990475522 100755 --- a/k6-tests/setup.sh +++ b/k6-tests/setup.sh @@ -26,11 +26,11 @@ docker-compose \ --profile dmi-stub \ up --quiet-pull --detach --wait || exit 1 - if [[ "$testProfile" == "kpi" ]]; then - ACTUATOR_PORT=8883 - elif [[ "$testProfile" == "endurance" ]]; then - ACTUATOR_PORT=8884 - fi +if [[ "$testProfile" == "kpi" ]]; then + ACTUATOR_PORT=8883 +elif [[ "$testProfile" == "endurance" ]]; then + ACTUATOR_PORT=8884 +fi echo "Build information:" curl --silent --show-error http://localhost:$ACTUATOR_PORT/actuator/info diff --git a/k6-tests/teardown.sh b/k6-tests/teardown.sh index 10db7ac7e0..7804a73286 100755 --- a/k6-tests/teardown.sh +++ b/k6-tests/teardown.sh @@ -18,11 +18,9 @@ echo '================================== docker info ==========================' docker ps -a -echo '================================== CPS-NCMP Logs ========================' -for CONTAINER_ID in $(docker ps --filter "name=cps-and-ncmp" --format "{{.ID}}"); do - echo "CPS-NCMP Logs for container: $CONTAINER_ID" - docker logs "$CONTAINER_ID" -done +# Zip and store logs for the containers +chmod +x make-logs.sh +./make-logs.sh testProfile=$1 docker_compose_shutdown_cmd="docker-compose -f ../docker-compose/docker-compose.yml --profile dmi-stub --project-name $testProfile down --volumes" diff --git a/test-tools/generate-metrics-report.sh b/test-tools/generate-metrics-report.sh index 7d94e5b49f..4d99adfdff 100755 --- a/test-tools/generate-metrics-report.sh +++ b/test-tools/generate-metrics-report.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright 2023 Nordix Foundation. +# Copyright 2023-2025 Nordix Foundation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -75,13 +75,13 @@ function generate_report() { grep --invert-match "^#" $TEMP_DIR/metrics-raw.txt | sort | sed 's/,[}]/}\t/' >$TEMP_DIR/metrics-all.txt # Extract useful metrics. - grep -E "^cps_|^spring_data_" $TEMP_DIR/metrics-all.txt >$TEMP_DIR/metrics-cps.txt + grep -E "^cps_|^spring_data_|^http_server_|^http_client_|^tasks_scheduled_execution_|^spring_kafka_template_|^spring_kafka_listener_" $TEMP_DIR/metrics-all.txt >$TEMP_DIR/metrics-cps.txt # Extract into columns. - grep "_count" $TEMP_DIR/metrics-cps.txt | sed 's/_count//' | cut -f 1 >$TEMP_DIR/column1.txt - grep "_count" $TEMP_DIR/metrics-cps.txt | cut -f 2 >$TEMP_DIR/column2.txt - grep "_sum" $TEMP_DIR/metrics-cps.txt | cut -f 2 >$TEMP_DIR/column3.txt - grep "_max" $TEMP_DIR/metrics-cps.txt | cut -f 2 >$TEMP_DIR/column4.txt + grep "_count" $TEMP_DIR/metrics-cps.txt | sed 's/_count//' | cut -d ' ' -f 1 >$TEMP_DIR/column1.txt + grep "_count" $TEMP_DIR/metrics-cps.txt | cut -d ' ' -f 2 >$TEMP_DIR/column2.txt + grep "_sum" $TEMP_DIR/metrics-cps.txt | cut -d ' ' -f 2 >$TEMP_DIR/column3.txt + grep "_max" $TEMP_DIR/metrics-cps.txt | cut -d ' ' -f 2 >$TEMP_DIR/column4.txt # Combine columns into report. paste $TEMP_DIR/column{1,2,3,4}.txt >$TEMP_DIR/report.txt |