Encoding the special characters in the resource identifier 88/141888/6 master
authormpriyank <priyank.maheshwari@est.tech>
Thu, 21 Aug 2025 13:01:32 +0000 (14:01 +0100)
committermpriyank <priyank.maheshwari@est.tech>
Thu, 4 Sep 2025 09:57:13 +0000 (10:57 +0100)
- 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 <priyank.maheshwari@est.tech>
dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/RestTemplateAddressType.java [new file with mode: 0644]
dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/client/SdncRestconfClient.java
dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoder.java [new file with mode: 0644]
dmi-service/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java
dmi-service/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/ResourceIdentifierEncoderSpec.groovy [new file with mode: 0644]

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 (file)
index 0000000..37be156
--- /dev/null
@@ -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
+
+}
index 179707a..2fbbce8 100644 (file)
@@ -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<String> 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<String> httpOperationWithJsonDataWithUri(final HttpMethod httpMethod,
+            final String resourceUrl,
+            final String jsonData,
+            final HttpHeaders httpHeaders) {
+        return executeHttpOperation(httpMethod, resourceUrl, jsonData, httpHeaders, RestTemplateAddressType.URI);
+    }
+
+    private ResponseEntity<String> 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<String> httpEntity;
-        if (jsonData == null) {
-            httpEntity = new HttpEntity<>(httpHeaders);
-        } else {
-            httpEntity = new HttpEntity<>(jsonData, httpHeaders);
+
+        final HttpEntity<String> 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 (file)
index 0000000..a00fece
--- /dev/null
@@ -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<String> encodedSegments = parseAndEncodeSegments(trimmedResourcePath);
+        final String encodedResourcePath = String.join(PATH_SEPARATOR, encodedSegments);
+
+        return hasLeadingPathSeparator ? PATH_SEPARATOR + encodedResourcePath : encodedResourcePath;
+    }
+
+    private static List<String> parseAndEncodeSegments(final String trimmedResourcePath) {
+        final List<String> 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;
+    }
+}
index 1484366..d694497 100644 (file)
@@ -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<OperationEnum, HttpMethod> operationToHttpMethodMap = new EnumMap<>(OperationEnum.class);
+    private static final EnumMap<OperationEnum, HttpMethod> 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<String, String> 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<String, String> 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<ModuleSchema> convertToModuleSchemas(final String modulesListAsJson) {
         try {
             return JsonPath.using(jsonPathConfiguration).parse(modulesListAsJson).read(
-                PATH_TO_MODULE_SCHEMAS, new TypeRef<List<ModuleSchema>>() {
+                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 (file)
index 0000000..aeca062
--- /dev/null
@@ -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'
+    }
+}