Enable percent encoding servlet context path and servlet path

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletContainer.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletContainer.java
index 9a91ec9..522ba33 100644
--- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletContainer.java
+++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/ServletContainer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -288,16 +288,8 @@
          * We need to work around this and not use getPathInfo
          * for the decodedPath.
          */
-        final String decodedBasePath = request.getContextPath() + servletPath + "/";
-
-        final String encodedBasePath = UriComponent.encode(decodedBasePath,
-                UriComponent.Type.PATH);
-
-        if (!decodedBasePath.equals(encodedBasePath)) {
-            setResponseForInvalidUri(response, new ProcessingException("The servlet context path and/or the "
-                    + "servlet path contain characters that are percent encoded"));
-            return;
-        }
+        final String encodedBasePath = UriComponent.contextualEncode(
+                request.getContextPath() + servletPath, UriComponent.Type.PATH) + "/";
 
         final URI baseUri;
         final URI requestUri;
diff --git a/containers/jersey-servlet-core/src/test/java/org/glassfish/jersey/servlet/internal/ContextPathEncodingTest.java b/containers/jersey-servlet-core/src/test/java/org/glassfish/jersey/servlet/internal/ContextPathEncodingTest.java
new file mode 100644
index 0000000..9ab2e92
--- /dev/null
+++ b/containers/jersey-servlet-core/src/test/java/org/glassfish/jersey/servlet/internal/ContextPathEncodingTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.servlet.internal;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.net.URI;
+
+/**
+ * Context encoding test. See Jersey-4949.
+ */
+public class ContextPathEncodingTest {
+    private static final String PATH = "A%20B";
+    private static final String CONTEXT = "c%20ntext";
+
+    @Test
+    public void contextEncodingTest() throws ServletException, IOException {
+        // In jetty maven plugin, context is set by
+        //<configuration>
+        //    <scan>10</scan>
+        //    <webApp>
+        //        <contextPath>/c ntext</contextPath>
+        //    </webApp>
+        //</configuration>
+
+        //Servlet path is not encoded, context is encoded
+        final ServletRequestValues servletRequestValues = new ServletRequestValues(
+                "/" + CONTEXT,
+                "",
+                "/" + CONTEXT + "/" + PATH
+        );
+        final EncodingTestServletContainer testServletContainer = new EncodingTestServletContainer(
+                "/" + CONTEXT + "/",
+                "/" + CONTEXT + "/" + PATH
+        );
+        EncodingTestData testData = new EncodingTestData(servletRequestValues, testServletContainer);
+
+        testData.test();
+    }
+
+    @Test
+    public void servletPathEncodingTest() throws ServletException, IOException {
+        //Servlet path is not encoded, context is encoded
+        final ServletRequestValues servletRequestValues = new ServletRequestValues(
+                "/",
+                "A B",
+                "/" + PATH + "/" + PATH
+        );
+        final EncodingTestServletContainer testServletContainer = new EncodingTestServletContainer(
+                "/" + PATH + "/",
+                "/" + PATH + "/" + PATH
+        );
+        EncodingTestData testData = new EncodingTestData(servletRequestValues, testServletContainer);
+
+        testData.test();
+    }
+
+    static class EncodingTestData {
+        final ServletRequestValues servletRequestValues;
+        final EncodingTestServletContainer encodingTestServletContainer;
+        final HttpServletRequest httpServletRequest;
+
+        EncodingTestData(ServletRequestValues servletRequestValues, EncodingTestServletContainer encodingTestServletContainer) {
+            this.servletRequestValues = servletRequestValues;
+            this.encodingTestServletContainer = encodingTestServletContainer;
+            this.httpServletRequest = (HttpServletRequest) Proxy.newProxyInstance(getClass().getClassLoader(),
+                    new Class[]{HttpServletRequest.class}, new InvocationHandler() {
+                        @Override
+                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+                           return servletRequestValues.handle(method.getName());
+                        }
+                    });
+        }
+
+        public void test() throws ServletException, IOException {
+            encodingTestServletContainer.service(httpServletRequest, (HttpServletResponse) null);
+        }
+
+    }
+
+    static class ServletRequestValues {
+        final String servletPath;
+        final String requestUri;
+        final String contextPath;
+
+        ServletRequestValues(String contextPath, String servletPath, String requestUri) {
+            this.servletPath = servletPath;
+            this.requestUri = requestUri;
+            this.contextPath = contextPath;
+        }
+
+        Object handle(String name) {
+            switch (name) {
+                case "getServletPath":
+                    return servletPath;
+                case "getRequestURI":
+                    return requestUri;
+                case "getRequestURL":
+                    return new StringBuffer(requestUri);
+                case "getContextPath":
+                    return contextPath;
+                default:
+                    return null;
+            }
+        }
+    }
+
+    static class EncodingTestServletContainer extends ServletContainer {
+        final String baseUri;
+        final String requestUri;
+
+        EncodingTestServletContainer(String baseUri, String requestUri) {
+            this.baseUri = baseUri;
+            this.requestUri = requestUri;
+        }
+
+        @Override
+        public Value<Integer> service(URI baseUri, URI requestUri, HttpServletRequest request, HttpServletResponse response) {
+            Assert.assertEquals(this.baseUri, baseUri.toASCIIString());
+            Assert.assertEquals(this.requestUri, requestUri.toASCIIString());
+            return Values.of(0);
+        }
+
+        //Update visibility
+        public void service(final HttpServletRequest request, final HttpServletResponse response)
+                throws ServletException, IOException {
+            super.service(request, response);
+        }
+    };
+}
diff --git a/tests/integration/jersey-4949/pom.xml b/tests/integration/jersey-4949/pom.xml
new file mode 100644
index 0000000..02cca4d
--- /dev/null
+++ b/tests/integration/jersey-4949/pom.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+
+    This program and the accompanying materials are made available under the
+    terms of the Eclipse Public License v. 2.0, which is available at
+    http://www.eclipse.org/legal/epl-2.0.
+
+    This Source Code may also be made available under the following Secondary
+    Licenses when the conditions for such availability set forth in the
+    Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+    version 2 with the GNU Classpath Exception, which is available at
+    https://www.gnu.org/software/classpath/license.html.
+
+    SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.glassfish.jersey.tests.integration</groupId>
+        <artifactId>project</artifactId>
+        <version>2.36-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>jersey-4949</artifactId>
+    <packaging>war</packaging>
+    <name>jersey-tests-integration-jersey-4949</name>
+
+    <description>Servlet integration test - JERSEY-4949 - Encoded Jetty Path</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-servlet-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-external</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-maven-plugin</artifactId>
+                <configuration>
+                    <scan>10</scan>
+                    <webApp>
+                        <contextPath>/c ntext</contextPath>
+                    </webApp>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/tests/integration/jersey-4949/src/main/java/org/glassfish/jersey/tests/integration/jersey4949/Issue4949Resource.java b/tests/integration/jersey-4949/src/main/java/org/glassfish/jersey/tests/integration/jersey4949/Issue4949Resource.java
new file mode 100644
index 0000000..b6d34a7
--- /dev/null
+++ b/tests/integration/jersey-4949/src/main/java/org/glassfish/jersey/tests/integration/jersey4949/Issue4949Resource.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.integration.jersey4949;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * Test resource.
+ */
+@Path("/")
+public class Issue4949Resource {
+    public static final String PATH = "0.0.2%20-%20Market%20Data%20Import";
+    @GET
+    @Path(PATH)
+    public String get(@Context UriInfo uriInfo) {
+        return uriInfo.getRequestUri().toASCIIString();
+    }
+
+    @GET
+    @Path("echo")
+    public String echo(@Context UriInfo uriInfo) {
+        return uriInfo.getRequestUri().toASCIIString();
+    }
+}
diff --git a/tests/integration/jersey-4949/src/main/java/org/glassfish/jersey/tests/integration/jersey4949/Jersey4949.java b/tests/integration/jersey-4949/src/main/java/org/glassfish/jersey/tests/integration/jersey4949/Jersey4949.java
new file mode 100644
index 0000000..6c625fc
--- /dev/null
+++ b/tests/integration/jersey-4949/src/main/java/org/glassfish/jersey/tests/integration/jersey4949/Jersey4949.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.integration.jersey4949;
+
+import org.glassfish.jersey.server.ResourceConfig;
+
+/**
+ * JAX-RS application for the JERSEY-4949 reproducer test.
+ */
+public class Jersey4949 extends ResourceConfig {
+
+    public Jersey4949() {
+        register(Issue4949Resource.class);
+    }
+}
diff --git a/tests/integration/jersey-4949/src/main/webapp/WEB-INF/web.xml b/tests/integration/jersey-4949/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..b337657
--- /dev/null
+++ b/tests/integration/jersey-4949/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+
+    This program and the accompanying materials are made available under the
+    terms of the Eclipse Public License v. 2.0, which is available at
+    http://www.eclipse.org/legal/epl-2.0.
+
+    This Source Code may also be made available under the following Secondary
+    Licenses when the conditions for such availability set forth in the
+    Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+    version 2 with the GNU Classpath Exception, which is available at
+    https://www.gnu.org/software/classpath/license.html.
+
+    SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
+    <servlet>
+        <servlet-name>jersey4949Servlet</servlet-name>
+        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
+        <init-param>
+            <param-name>javax.ws.rs.Application</param-name>
+            <param-value>org.glassfish.jersey.tests.integration.jersey4949.Jersey4949</param-value>
+        </init-param>
+        <load-on-startup>1</load-on-startup>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>jersey4949Servlet</servlet-name>
+        <url-pattern>/A B/*</url-pattern>
+    </servlet-mapping>
+</web-app>
diff --git a/tests/integration/jersey-4949/src/test/java/org/glassfish/jersey/tests/integration/jersey4949/Jersey4949ITCase.java b/tests/integration/jersey-4949/src/test/java/org/glassfish/jersey/tests/integration/jersey4949/Jersey4949ITCase.java
new file mode 100644
index 0000000..9ddaa1e
--- /dev/null
+++ b/tests/integration/jersey-4949/src/test/java/org/glassfish/jersey/tests/integration/jersey4949/Jersey4949ITCase.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.integration.jersey4949;
+
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.external.ExternalTestContainerFactory;
+import org.glassfish.jersey.test.spi.TestContainerException;
+import org.glassfish.jersey.test.spi.TestContainerFactory;
+
+import org.junit.Assert;
+import org.junit.Test;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Reproducer tests for JERSEY-4949.
+ */
+public class Jersey4949ITCase extends JerseyTest {
+
+    private static final String CONTEXT_PATH = "c%20ntext";
+    private static final String SERVLET_PATH = "A%20B";
+
+    @Override
+    protected Application configure() {
+        return new Jersey4949();
+    }
+
+    @Override
+    protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
+        //return new org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory();
+        return new ExternalTestContainerFactory();
+    }
+
+    /**
+     * Reproducer method for JERSEY-4949.
+     */
+    @Test
+    public void testJersey4949Fix() {
+        try (Response response = target(CONTEXT_PATH).path(SERVLET_PATH).path(Issue4949Resource.PATH).request().get()) {
+            assertThat(response.getStatus(), is(200));
+
+            String entity = response.readEntity(String.class);
+            Assert.assertTrue(entity.contains(CONTEXT_PATH));
+            Assert.assertTrue(entity.contains(SERVLET_PATH));
+            Assert.assertTrue(entity.contains(Issue4949Resource.PATH));
+        }
+    }
+}
diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml
index e44118d..385ee00 100644
--- a/tests/integration/pom.xml
+++ b/tests/integration/pom.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-    Copyright (c) 2011, 2021 Oracle and/or its affiliates. All rights reserved.
+    Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved.
     Copyright (c) 2018 Payara Foundation and/or its affiliates. All rights reserved.
 
     This program and the accompanying materials are made available under the
@@ -91,6 +91,7 @@
         <module>jersey-4542</module>
         <module>jersey-4697</module>
         <module>jersey-4722</module>
+        <module>jersey-4949</module>
         <module>jetty-response-close</module>
         <module>microprofile</module>
         <module>portability-jersey-1</module>