diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriPathMatrix.java b/common/uri/src/main/java/io/helidon/common/uri/UriPathMatrix.java index e0f18761e09..1d5025fef88 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriPathMatrix.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriPathMatrix.java @@ -31,7 +31,7 @@ class UriPathMatrix extends UriPathNoParam { } UriPathMatrix(String rawPath, String noParamPath, UriPath absolute) { - super(noParamPath, absolute); + super(absolute, noParamPath); this.rawPath = rawPath; } diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriPathNoParam.java b/common/uri/src/main/java/io/helidon/common/uri/UriPathNoParam.java index 2234c4b2cc7..178999cb5a1 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriPathNoParam.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriPathNoParam.java @@ -35,12 +35,9 @@ class UriPathNoParam implements UriPath { this.absolute = this; } - UriPathNoParam(String rawPath, UriPath absolute) { - this.rawPath = rawPath; - this.absolute = absolute; - } - UriPathNoParam(UriPath absolute, String relativePath) { + // relativePath is always decoded, so rawPath will become decoded here. + // This is not really the meaning of rawPath. this.rawPath = relativePath; this.decodedPath = relativePath; this.absolute = absolute; diff --git a/http/http/src/main/java/io/helidon/http/PathMatchers.java b/http/http/src/main/java/io/helidon/http/PathMatchers.java index 9aa0eff66d4..bbaaff18a86 100644 --- a/http/http/src/main/java/io/helidon/http/PathMatchers.java +++ b/http/http/src/main/java/io/helidon/http/PathMatchers.java @@ -24,6 +24,7 @@ import java.util.regex.Pattern; import io.helidon.common.parameters.Parameters; +import io.helidon.common.uri.UriEncoding; import io.helidon.common.uri.UriPath; /** @@ -276,8 +277,9 @@ static final class ExactPathMatcher implements PathMatcher { private final String pathWithTrailingSlash; ExactPathMatcher(String path) { - this.path = path; - this.pathWithTrailingSlash = path + "/"; + // We work with decoded URIs + this.path = UriEncoding.decodeUri(path); + this.pathWithTrailingSlash = this.path + "/"; } @Override diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java index 159bbf5b583..7d4e6a74325 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java @@ -204,6 +204,13 @@ private void doHandle(Context ctx, ServerRequest req, ServerResponse res) { String rawPath = req.path().rawPath(); rawPath = rawPath.startsWith("/") ? rawPath.substring(1) : rawPath; + /* + * rawPath could be a decoded path here because of UriPathNoParam(41). + * Paths generated from decoded URIs could contain whitespace, + * and URIs cannot be created with whitespace (it throws one IllegalException). + * I didn't find a better solution than the next. + */ + rawPath = rawPath.replaceAll(" ", "%20"); if (req.query().isEmpty()) { requestUri = baseUri.resolve(rawPath); } else { diff --git a/microprofile/server/src/test/java/io/helidon/microprofile/server/JaxRsApplicationPathTest.java b/microprofile/server/src/test/java/io/helidon/microprofile/server/JaxRsApplicationPathTest.java new file mode 100644 index 00000000000..330ee2875b8 --- /dev/null +++ b/microprofile/server/src/test/java/io/helidon/microprofile/server/JaxRsApplicationPathTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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. + */ + +package io.helidon.microprofile.server; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Encoded; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class JaxRsApplicationPathTest { + + private static Server server; + private static Client client; + private static int port; + + @BeforeAll + static void beforeAll() { + server = Server.builder().addApplication(MyApplication.class).build(); + server.start(); + port = server.port(); + client = ClientBuilder.newClient(); + } + + @AfterAll + static void afterAll() { + server.stop(); + client.close(); + } + + @Test + void okTest() { + String getResponse = client.target("http://localhost:" + port) + .path("/ApplicationPath!/Resource").request().get(String.class); + assertThat(getResponse, is("ok")); + } + + @Test + void nokTest() { + int status = client.target("http://localhost:" + port) + .path("/Resource").request().get().getStatus(); + assertThat(status, is(404)); + } + + @Test + void emptyParamTest() { + String getResponse = client.target("http://localhost:" + port) + .path("/ApplicationPath!/Resource/pathparam1/%20/%2010").request().get(String.class); + assertThat(getResponse, is("a= b= 10")); + } + + @Test + public void testEncoded() { + String getResponse = client.target("http://localhost:" + port) + .path("/ApplicationPath!/Resource/encoded").queryParam("query", "%dummy23+a") + .request().get(String.class); + assertThat(getResponse, is("true:%25dummy23%2Ba")); + } + + @Test + public void testDecoded() { + String getResponse = client.target("http://localhost:" + port) + .path("/ApplicationPath!/Resource/decoded").queryParam("query", "%dummy23+a") + .request().get(String.class); + assertThat(getResponse, is("true:%dummy23+a")); + } + + @Test + @Disabled("Enable to investigate TCK issue") + void encodedEntityTest() { + String getResponse = client.target("http://localhost:" + port) + .path("/ApplicationPath!/Resource/ParamEntityWithFromString/test%21/test!/test!/test%21") + .request().get(String.class); + assertThat(getResponse, is("test%21test!test%21test!")); + } + + @ApplicationPath("/ApplicationPath%21") + static class MyApplication extends Application { + + @Override + public java.util.Set> getClasses() { + Set> resources = new HashSet>(); + resources.add(TestResource.class); + return resources; + } + } + + @Path("/Resource") + public static class TestResource { + + @GET + public String param() { + return "ok"; + } + + @GET + @Path("/pathparam1/{a}/{b}") + public String pathparamTest1(@Context UriInfo info) { + StringBuilder buf = new StringBuilder(); + for (String param : info.getPathParameters(true).keySet()) { + buf.append(param + "=" + info.getPathParameters(true).getFirst(param)); + } + return buf.toString(); + } + + @GET + @Path("/ParamEntityWithFromString/{encoded}/{notEncoded}/{encoded2}/{notEncoded2}") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String stringParamHandlingFromString( + @BeanParam PathBeanParamEntity bean) { + return bean.encoded + bean.notEncoded + bean.encoded2 + bean.notEncoded2; + } + + @GET + @Path("encoded") + public String getEncoded(@Encoded @QueryParam("query") String queryParam) { + return queryParam.equals("%25dummy23%2Ba") + ":" + queryParam; + } + + @GET + @Path("decoded") + public String getDecoded(@QueryParam("query") String queryParam) { + return queryParam.equals("%dummy23+a") + ":" + queryParam; + } + } + + static class PathBeanParamEntity { + @Encoded + @PathParam("encoded") + public String encoded; + @PathParam("notEncoded") + public String notEncoded; + @Encoded + @PathParam("encoded2") + public String encoded2; + @PathParam("notEncoded2") + public String notEncoded2; + } +} diff --git a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java index ee94075fecf..25c2732d115 100644 --- a/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java +++ b/microprofile/tests/arquillian/src/main/java/io/helidon/microprofile/arquillian/HelidonDeployableContainer.java @@ -58,6 +58,7 @@ import jakarta.enterprise.inject.spi.CDI; import jakarta.enterprise.inject.spi.DefinitionException; import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -417,7 +418,11 @@ void startServer(RunContext context, Path[] classPath) Map properties = new HashMap<>(); for (Class app : context.applications) { String key = app.getName() + ".routing-path.path"; + ApplicationPath path = app.getDeclaredAnnotation(ApplicationPath.class); String value = "/" + context.rootContext; + if (path != null && !"/".equals(path.value())) { + value = value + "/" + path.value(); + } properties.put(key, value); } configBuilder.withSources(MpConfigSources.create(properties));