From bb1e1efeec5b7877fb8ccb33bcf16b6701a5731f Mon Sep 17 00:00:00 2001 From: mpriyank Date: Thu, 21 Aug 2025 14:01:32 +0100 Subject: [PATCH] Encoding the special characters in the resource identifier - during passthrough requests, if the resource identifier had special characters then the resource was not returned by the sdnc - fix is to do proper encoding when the passthrough read operation is made - Introduced a utility to convert the resource-id to encoded resource id and then make the same request using resttemplate - Added testware for the same - Inspected code for the changed files and fixed the issues Issue-ID: CPS-2939 Change-Id: I940156e09640c36a090afd2518080f619c93b440 Signed-off-by: mpriyank --- .../service/client/RestTemplateAddressType.java | 27 ++++++ .../dmi/service/client/SdncRestconfClient.java | 54 ++++++++--- .../operation/ResourceIdentifierEncoder.java | 102 +++++++++++++++++++++ .../ncmp/dmi/service/operation/SdncOperations.java | 40 +++++--- .../operation/ResourceIdentifierEncoderSpec.groovy | 39 ++++++++ 5 files changed, 236 insertions(+), 26 deletions(-) create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/RestTemplateAddressType.java create mode 100644 dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoder.java create mode 100644 dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoderSpec.groovy diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/RestTemplateAddressType.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/RestTemplateAddressType.java new file mode 100644 index 00000000..37be1564 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/RestTemplateAddressType.java @@ -0,0 +1,27 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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.dmi.service.client; + +public enum RestTemplateAddressType { + + URL_STRING, URI + +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java index 179707ab..2fbbce85 100644 --- a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2022 Nordix Foundation + * Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,9 @@ package org.onap.cps.ncmp.dmi.service.client; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.dmi.config.DmiConfiguration.SdncProperties; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -28,16 +31,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +@Slf4j @Component +@RequiredArgsConstructor public class SdncRestconfClient { - private SdncProperties sdncProperties; - private RestTemplate restTemplate; - - public SdncRestconfClient(final SdncProperties sdncProperties, final RestTemplate restTemplate) { - this.sdncProperties = sdncProperties; - this.restTemplate = restTemplate; - } + private final SdncProperties sdncProperties; + private final RestTemplate restTemplate; /** * restconf get operation on sdnc. @@ -57,7 +57,7 @@ public class SdncRestconfClient { * @return the response entity */ public ResponseEntity getOperation(final String getResourceUrl, final HttpHeaders httpHeaders) { - return httpOperationWithJsonData(HttpMethod.GET, getResourceUrl, null, httpHeaders); + return httpOperationWithJsonDataWithUri(HttpMethod.GET, getResourceUrl, null, httpHeaders); } /** @@ -73,15 +73,41 @@ public class SdncRestconfClient { final String resourceUrl, final String jsonData, final HttpHeaders httpHeaders) { + return executeHttpOperation(httpMethod, resourceUrl, jsonData, httpHeaders, RestTemplateAddressType.URL_STRING); + } + + /** + * restconf http operations on sdnc. + * + * @param httpMethod HTTP Method + * @param resourceUrl sdnc resource url + * @param jsonData json data + * @param httpHeaders HTTP Headers + * @return response entity + */ + public ResponseEntity httpOperationWithJsonDataWithUri(final HttpMethod httpMethod, + final String resourceUrl, + final String jsonData, + final HttpHeaders httpHeaders) { + return executeHttpOperation(httpMethod, resourceUrl, jsonData, httpHeaders, RestTemplateAddressType.URI); + } + + private ResponseEntity executeHttpOperation(final HttpMethod httpMethod, final String resourceUrl, + final String jsonData, final HttpHeaders httpHeaders, + final RestTemplateAddressType restTemplateAddressType) { final String sdncBaseUrl = sdncProperties.getBaseUrl(); final String sdncRestconfUrl = sdncBaseUrl.concat(resourceUrl); httpHeaders.setBasicAuth(sdncProperties.getAuthUsername(), sdncProperties.getAuthPassword()); - final HttpEntity httpEntity; - if (jsonData == null) { - httpEntity = new HttpEntity<>(httpHeaders); - } else { - httpEntity = new HttpEntity<>(jsonData, httpHeaders); + + final HttpEntity httpEntity = + jsonData == null ? new HttpEntity<>(httpHeaders) : new HttpEntity<>(jsonData, httpHeaders); + + if (RestTemplateAddressType.URI.equals(restTemplateAddressType)) { + final URI sdncRestconfUri = URI.create(sdncRestconfUrl); + log.debug("sdncRestconfUri: {}", sdncRestconfUri); + return restTemplate.exchange(sdncRestconfUri, httpMethod, httpEntity, String.class); } + log.debug("sdncRestconfUrl: {}", sdncRestconfUrl); return restTemplate.exchange(sdncRestconfUrl, httpMethod, httpEntity, String.class); } } diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoder.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoder.java new file mode 100644 index 00000000..a00fece5 --- /dev/null +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoder.java @@ -0,0 +1,102 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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.dmi.service.operation; + + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.web.util.UriUtils; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ResourceIdentifierEncoder { + + private static final String EQUALS_SIGN = "="; + private static final String PATH_SEPARATOR = "/"; + private static final String ENCODED_EQUALS = "%3D"; + + /** + * Encode a nested resource path by URL-encoding path segments while preserving + * key-value structure. Supports spaces, slashes, and '=' characters in values. + * + * @param rawResourcePath input path (may start with '/') + * @return encoded resource path + */ + public static String encodeNestedResourcePath(final String rawResourcePath) { + + final boolean hasLeadingPathSeparator = rawResourcePath.startsWith(PATH_SEPARATOR); + final String trimmedResourcePath = hasLeadingPathSeparator ? rawResourcePath.substring(1) : rawResourcePath; + + final List encodedSegments = parseAndEncodeSegments(trimmedResourcePath); + final String encodedResourcePath = String.join(PATH_SEPARATOR, encodedSegments); + + return hasLeadingPathSeparator ? PATH_SEPARATOR + encodedResourcePath : encodedResourcePath; + } + + private static List parseAndEncodeSegments(final String trimmedResourcePath) { + final List encodedResourcePathSegments = new ArrayList<>(); + final String[] resourcePathParts = trimmedResourcePath.split(PATH_SEPARATOR); + + int pathPartIndex = 0; + while (pathPartIndex < resourcePathParts.length) { + final String resourcePathPart = resourcePathParts[pathPartIndex]; + + if (resourcePathPart.contains(EQUALS_SIGN)) { + final StringBuilder resourcePathPartSegment = new StringBuilder(resourcePathPart); + pathPartIndex++; + // Continue collecting parts until we hit another key-value pair or end + while (pathPartIndex < resourcePathParts.length && !resourcePathParts[pathPartIndex].contains( + EQUALS_SIGN)) { + resourcePathPartSegment.append(PATH_SEPARATOR).append(resourcePathParts[pathPartIndex]); + pathPartIndex++; + } + encodedResourcePathSegments.add(encodePathSegment(resourcePathPartSegment.toString())); + } else { + // Simple resource path segment without equals + encodedResourcePathSegments.add(encodePathSegment(resourcePathPart)); + pathPartIndex++; + } + } + return encodedResourcePathSegments; + } + + private static String encodePathSegment(final String segment) { + if (segment.contains(EQUALS_SIGN)) { + return encodePathSegmentWithEqualsSign(segment); + } + return UriUtils.encodePathSegment(segment, StandardCharsets.UTF_8); + + } + + private static String encodePathSegmentWithEqualsSign(final String segment) { + final int indexOfEqualSign = segment.indexOf(EQUALS_SIGN); + final String key = segment.substring(0, indexOfEqualSign); + final String value = segment.substring(indexOfEqualSign + 1); + + // encode both key and value, and replace '=' with %3D in the value + final String encodedKey = UriUtils.encodePathSegment(key, StandardCharsets.UTF_8); + final String encodedValue = + UriUtils.encodePathSegment(value, StandardCharsets.UTF_8).replace(EQUALS_SIGN, ENCODED_EQUALS); + return encodedKey + EQUALS_SIGN + encodedValue; + } +} diff --git a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java index 1484366f..d6944971 100644 --- a/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java +++ b/dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved. * Modifications Copyright (C) 2021-2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ package org.onap.cps.ncmp.dmi.service.operation; import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum; +import static org.onap.cps.ncmp.dmi.service.operation.ResourceIdentifierEncoder.encodeNestedResourcePath; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.JsonPath; @@ -36,6 +37,7 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.dmi.config.DmiConfiguration.SdncProperties; import org.onap.cps.ncmp.dmi.exception.SdncException; import org.onap.cps.ncmp.dmi.model.DataAccessRequest; @@ -51,6 +53,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; +@Slf4j @Component public class SdncOperations { @@ -64,7 +67,8 @@ public class SdncOperations { private static final int QUERY_PARAM_VALUE_INDEX = 1; private static final int QUERY_PARAM_NAME_INDEX = 0; - private static EnumMap operationToHttpMethodMap = new EnumMap<>(OperationEnum.class); + private static final EnumMap operationToHttpMethodMap = + new EnumMap<>(OperationEnum.class); static { operationToHttpMethodMap.put(OperationEnum.READ, HttpMethod.GET); @@ -79,7 +83,7 @@ public class SdncOperations { private final String topologyUrlData; private final String topologyUrlOperational; - private Configuration jsonPathConfiguration = Configuration.builder() + private final Configuration jsonPathConfiguration = Configuration.builder() .mappingProvider(new JacksonMappingProvider()) .jsonProvider(new JacksonJsonProvider()) .build(); @@ -209,32 +213,43 @@ public class SdncOperations { private String prepareResourceDataUrl(final String nodeId, final String resourceId, final MultiValueMap queryMap) { - return addQuery(addResource(addTopologyDataUrlwithNode(nodeId), resourceId), queryMap); + return addQuery(addResourceEncoded(addTopologyDataUrlwithNode(nodeId), resourceId), queryMap); } private String addResource(final String url, final String resourceId) { + return UriComponentsBuilder.fromUriString(url) .pathSegment(resourceId) .buildAndExpand().toUriString(); } + private String addResourceEncoded(final String url, final String resourceId) { + + final String encodedNestedResourcePath = encodeNestedResourcePath(resourceId); + log.debug("Raw resourceId : {} , EncodedResourcePath : {}", resourceId, encodedNestedResourcePath); + return addResource(url, encodedNestedResourcePath); + } + private String addQuery(final String url, final MultiValueMap queryMap) { - return UriComponentsBuilder.fromUriString(url) - .queryParams(queryMap) - .buildAndExpand().toUriString(); + + return UriComponentsBuilder + .fromUriString(url) + .queryParams(queryMap) + .buildAndExpand().toUriString(); } private String addTopologyDataUrlwithNode(final String nodeId) { - return UriComponentsBuilder.fromUriString(topologyUrlData) - .pathSegment("node={nodeId}") - .pathSegment("yang-ext:mount") - .buildAndExpand(nodeId).toUriString(); + return UriComponentsBuilder + .fromUriString(topologyUrlData) + .pathSegment("node={nodeId}") + .pathSegment("yang-ext:mount") + .buildAndExpand(nodeId).toUriString(); } private List convertToModuleSchemas(final String modulesListAsJson) { try { return JsonPath.using(jsonPathConfiguration).parse(modulesListAsJson).read( - PATH_TO_MODULE_SCHEMAS, new TypeRef>() { + PATH_TO_MODULE_SCHEMAS, new TypeRef<>() { }); } catch (final JsonPathException jsonPathException) { throw new SdncException("SDNC Response processing failed", @@ -267,4 +282,5 @@ public class SdncOperations { queryParam -> queryParam[QUERY_PARAM_NAME_INDEX], queryParam -> queryParam[QUERY_PARAM_VALUE_INDEX])); } + } diff --git a/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoderSpec.groovy b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoderSpec.groovy new file mode 100644 index 00000000..aeca0622 --- /dev/null +++ b/dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoderSpec.groovy @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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.dmi.service.operation + +import spock.lang.Specification + +class ResourceIdentifierEncoderSpec extends Specification { + + def 'encodeNestedResourcePath should handle valid paths correctly: #scenario'() { + when: 'we encode a valid resource path' + def result = ResourceIdentifierEncoder.encodeNestedResourcePath(input) + then: 'the result matches expectedEncodedString encoded format' + assert result == expectedEncodedString + where: 'following scenarios are used' + scenario | input || expectedEncodedString + 'simple path without leading path separator' | 'container' || 'container' + 'list entry with space in key value' | '/list-name=My Container/leaf=leaf with space' || '/list-name=My%20Container/leaf=leaf%20with%20space' + 'key value containing path separator' | '/container/list=id/with/slashes/leaf=Some Leaf' || '/container/list=id%2Fwith%2Fslashes/leaf=Some%20Leaf' + 'equals signs in key value' | '/list=Key=Value=Another' || '/list=Key%3DValue%3DAnother' + } +} -- 2.16.6