--- /dev/null
+/*
+ * ============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.config;
+
+import java.util.EnumSet;
+import org.eclipse.jetty.http.UriCompliance;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
+import org.springframework.boot.web.server.WebServerFactoryCustomizer;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configures the Jetty server to allow encoded slashes (%2F) within URI path segments.
+
+ * This customization is essential when path parameters may include encoded slashes,
+ * such as hierarchical identifiers (e.g., {@code SubNetwork=Europe/SubNetwork=Ireland}).
+ * By permitting the {@code AMBIGUOUS_PATH_SEPARATOR} violation, Jetty accepts these
+ * encoded slashes without rejecting the request.
+ *
+ * @see <a href="https://jetty.org/docs/jetty/12/programming-guide/server/compliance.html">Jetty Server Compliance Modes</a>
+ * @see <a href="https://javadoc.jetty.org/jetty-12/org/eclipse/jetty/http/UriCompliance.Violation.html">UriCompliance.Violation</a>
+ */
+@Configuration
+public class JettyConfig implements WebServerFactoryCustomizer<JettyServletWebServerFactory> {
+
+ /**
+ * Customizes the Jetty server factory to allow encoded slashes in URI paths.
+ *
+ * @param jettyServletWebServerFactory the Jetty servlet web server factory to customize
+ */
+ @Override
+ public void customize(final JettyServletWebServerFactory jettyServletWebServerFactory) {
+ jettyServletWebServerFactory.addServerCustomizers(server -> {
+ for (final Connector connector : server.getConnectors()) {
+ for (final ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
+ if (connectionFactory instanceof HttpConnectionFactory) {
+ final HttpConfiguration httpConfiguration
+ = ((HttpConnectionFactory) connectionFactory).getHttpConfiguration();
+ httpConfiguration.setUriCompliance(UriCompliance.from(EnumSet.of(
+ UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR)));
+ }
+ }
+ }
+ });
+ }
+}
+
--- /dev/null
+/*
+ * ============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.config
+
+import org.eclipse.jetty.server.ConnectionFactory
+import org.eclipse.jetty.server.Connector
+import org.eclipse.jetty.server.HttpConfiguration
+import org.eclipse.jetty.server.HttpConnectionFactory
+import org.eclipse.jetty.server.Server
+import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory
+import spock.lang.Specification
+
+class JettyConfigSpec extends Specification {
+
+ def objectUnderTest = new JettyConfig()
+ def server = Mock(Server)
+ def connector = Mock(Connector)
+
+ def 'Enable support for ambiguous path separators in Jetty Http configuration'() {
+ given: 'a Jetty server factory'
+ def jettyServletWebServerFactory = new JettyServletWebServerFactory()
+ and: 'a mocked connection factory (Http or Non-Http)'
+ def connectionFactory = connectionFactoryType == 'http' ? Mock(HttpConnectionFactory) : Mock(ConnectionFactory)
+ and: 'optional mock for HttpConfiguration if applicable'
+ def httpConfig = null
+ if (connectionFactory instanceof HttpConnectionFactory) {
+ httpConfig = Mock(HttpConfiguration)
+ connectionFactory.getHttpConfiguration() >> httpConfig
+ }
+ and: 'mocked components return expected values'
+ connector.getConnectionFactories() >> [connectionFactory]
+ server.getConnectors() >> [connector]
+ when: 'JettyConfig customization is triggered on the server factory'
+ objectUnderTest.customize(jettyServletWebServerFactory)
+ and: 'a server customizer is extracted from the configured factory'
+ def serverCustomizer = jettyServletWebServerFactory.serverCustomizers.first()
+ and: 'the customizer is applied to the mocked Jetty server'
+ serverCustomizer.customize(server)
+ then: 'only the HTTP configuration is updated to allow ambiguous path separators'
+ expectedCalls * httpConfig.setUriCompliance({ it.toString().contains('AMBIGUOUS_PATH_SEPARATOR') })
+ where: 'type of connection factories'
+ scenario | connectionFactoryType || expectedCalls
+ 'HttpConnectionFactory - should configure UriCompliance' | 'http' || 1
+ 'Non-HttpConnectionFactory - should do nothing' | 'non-http' || 0
+ }
+}
'invalid datastore' | 'DELETE' | 'invalid'
}
+ def 'Ensure URI-encoded alternateId with slashes is accepted for #operation - #scenario'() {
+ given: 'A URI-encoded alternateId that includes slashes'
+ def alternateIdWithSlashes = '/some/cps/path'
+ def encodedAlternateId = URLEncoder.encode(alternateIdWithSlashes, 'UTF-8')
+ def url = "$ncmpBasePathV1/ch/${encodedAlternateId}/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=some-value"
+ when: 'A passthrough operation is executed on the URL containing the encoded alternateId'
+ def response = mvc.perform(executeRestOperation('POST', url)).andReturn().response
+ then: 'The API successfully processes the request and returns the expected HTTP status'
+ assert response.status == HttpStatus.CREATED.value()
+ }
+
def executeRestOperation(operation, url) {
if (operation == 'POST') {
return post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)