From ae36bafc81bf06a836fd69374b86c1d75c6f230c Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 6 Nov 2024 07:36:36 +0100 Subject: [PATCH 1/3] REST Client build time fix for DevServices --- .../config/RestClientsBuildTimeConfig.java | 23 +++++++++++++++---- .../config/RestClientConfigTest.java | 14 +++++++++++ ...vServicesRestClientHttpProxyProcessor.java | 9 ++++---- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java index e4caffdc8453f..f3aa7ead6abe7 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java @@ -83,10 +83,15 @@ interface RestClientBuildConfig { * @return a {@link RestClientsBuildTimeConfig} with the discovered registered REST Clients configuration only. */ default RestClientsBuildTimeConfig get(List restClients) { - SmallRyeConfig config = new SmallRyeConfigBuilder() + return getConfig(restClients).getConfigMapping(RestClientsBuildTimeConfig.class); + } + + default SmallRyeConfig getConfig(List restClients) { + return new SmallRyeConfigBuilder() .withSources( new ConfigSource() { final SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); + final ConfigSource defaultsSource = getDefaultsSource(); @Override public Set getPropertyNames() { @@ -97,13 +102,25 @@ public Set getPropertyNames() { @Override public String getValue(final String propertyName) { - return config.getRawValue(propertyName); + ConfigValue configValue = config.getConfigValue(propertyName); + if (configValue != null && !defaultsSource.getName().equals(configValue.getSourceName())) { + return configValue.getValue(); + } + return null; } @Override public String getName() { return "SmallRye Config"; } + + private ConfigSource getDefaultsSource() { + ConfigSource configSource = null; + for (ConfigSource source : config.getConfigSources()) { + configSource = source; + } + return configSource; + } }) .withCustomizers(new SmallRyeConfigBuilderCustomizer() { @Override @@ -119,7 +136,5 @@ public List getRestClients() { .withMapping(RestClientsBuildTimeConfig.class) .withMappingIgnore("quarkus.**") .build(); - - return config.getConfigMapping(RestClientsBuildTimeConfig.class); } } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java index 8476badf6a4d2..2a8c2621726c5 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -232,6 +233,19 @@ public List getRestClients() { assertThat(restClientConfig.uri().get()).isEqualTo("http://localhost:8082"); } + @Test + void buildTimeConfig() { + SmallRyeConfig config = ConfigUtils.emptyConfigBuilder() + .withMapping(RestClientsBuildTimeConfig.class) + .build(); + assertNotNull(config); + + RestClientsBuildTimeConfig buildTimeConfig = config.getConfigMapping(RestClientsBuildTimeConfig.class) + .get(List.of(new RegisteredRestClient(ConfigKeyRestClient.class, "key"))); + + assertFalse(buildTimeConfig.clients().get(ConfigKeyRestClient.class.getName()).removesTrailingSlash()); + } + private void verifyConfig(RestClientConfig config) { assertTrue(config.url().isPresent()); assertThat(config.url().get()).isEqualTo("http://localhost:8080"); diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java index 913697d6af206..6e2eb61e3f12c 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java @@ -17,7 +17,6 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.exception.UncheckedException; -import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.IndexView; import org.jboss.logging.Logger; @@ -32,6 +31,7 @@ import io.quarkus.rest.client.reactive.deployment.RegisteredRestClientBuildItem; import io.quarkus.rest.client.reactive.spi.DevServicesRestClientProxyProvider; import io.quarkus.rest.client.reactive.spi.RestClientHttpProxyBuildItem; +import io.quarkus.restclient.config.RegisteredRestClient; import io.quarkus.restclient.config.RestClientsBuildTimeConfig; import io.quarkus.restclient.config.RestClientsBuildTimeConfig.RestClientBuildConfig; import io.smallrye.config.SmallRyeConfig; @@ -60,14 +60,15 @@ public void determineRequiredProxies( CombinedIndexBuildItem combinedIndexBuildItem, List registeredRestClientBuildItems, BuildProducer producer) { - Map configs = clientsConfig.get(toRegisteredRestClients(registeredRestClientBuildItems)) - .clients(); + + List registeredRestClients = toRegisteredRestClients(registeredRestClientBuildItems); + Map configs = clientsConfig.get(registeredRestClients).clients(); if (configs.isEmpty()) { return; } IndexView index = combinedIndexBuildItem.getIndex(); - SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); + SmallRyeConfig config = clientsConfig.getConfig(registeredRestClients); for (var configEntry : configs.entrySet()) { if (!configEntry.getValue().enableLocalProxy()) { log.trace("Ignoring config key: '" + configEntry.getKey() + "' because enableLocalProxy is false"); From 606143b7a90aefd96252786f9caa8b88320c0f0e Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 6 Nov 2024 07:38:16 +0100 Subject: [PATCH 2/3] REST Server/Client allows configuring the removal of trailing slashs Before these changes, the REST server and client extensions were removing the trailing slashes that were set by users in the `@Path` annotations. For example, when creating a resource like: ```java @Path("/a/") @Produces(MediaType.TEXT_PLAIN) public class HelloResource { @GET public String echo() { // ... } } ``` The effective path was `/a` instead of `/a/`. Note that it does not matter whether we place the `@Path` annotation at method level because the behaviour will be the same. At the client side, when having the following client: ``` @RegisterRestClient(configKey = "test") @Path("/a/") <1> public interface HelloClient { @GET @Produces(MediaType.TEXT_PLAIN) @Path("/b/") <2> String echo(); } ``` In this case, the trailing slash will be removed from <1> but not from <2>. After these changes, we are adding the following properties: - `quarkus.rest.removes-trailing-slash` To configure the server extension to not remove any trailing slash from the `@Path` annotations. - `quarkus.rest-client.removes-trailing-slash` To configure the client extension to not remove any traling slash from the `@Path` annotations used at interface level. The annotations set at method level will behave the same. Note that this does not introduce any change in behaviour, so everything should keep working as before unless users use the new properties. --- .../config/RestClientsBuildTimeConfig.java | 18 +++ .../src/test/resources/application.properties | 1 + .../JaxrsClientReactiveProcessor.java | 31 ++++- ...tDisableRemovalTrailingSlashBuildItem.java | 22 +++ .../RestClientReactiveProcessor.java | 40 ++++-- .../reactive/SlashPathRestClientTest.java | 127 ++++++++++++++++++ .../runtime/ResteasyReactiveConfig.java | 6 + .../deployment/ResteasyReactiveProcessor.java | 1 + .../scanning/ClientEndpointIndexer.java | 9 +- .../common/processor/EndpointIndexer.java | 5 +- .../ResteasyReactiveDeploymentManager.java | 10 +- .../processor/ServerEndpointIndexer.java | 17 ++- 12 files changed, 266 insertions(+), 21 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/RestClientDisableRemovalTrailingSlashBuildItem.java create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/SlashPathRestClientTest.java diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java index f3aa7ead6abe7..192c41fd1c72d 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java @@ -13,11 +13,13 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigValue; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; import io.smallrye.config.SmallRyeConfigBuilderCustomizer; import io.smallrye.config.WithDefault; import io.smallrye.config.WithDefaults; +import io.smallrye.config.WithName; import io.smallrye.config.WithParentName; @ConfigMapping(prefix = "quarkus.rest-client") @@ -30,6 +32,14 @@ public interface RestClientsBuildTimeConfig { @WithDefaults Map clients(); + /** + * If true, the extension will automatically remove the trailing slash in the paths if any. + * This property is not applicable to the RESTEasy Client. + */ + @WithName("removes-trailing-slash") + @WithDefault("true") + boolean removesTrailingSlash(); + interface RestClientBuildConfig { /** @@ -69,6 +79,14 @@ interface RestClientBuildConfig { * */ Optional localProxyProvider(); + + /** + * If true, the extension will automatically remove the trailing slash in the paths if any. + * This property is not applicable to the RESTEasy Client. + */ + @WithName("removes-trailing-slash") + @WithDefault("true") + boolean removesTrailingSlash(); } /** diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties b/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties index 70ac360842fd8..fb083bea6f36b 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties @@ -15,6 +15,7 @@ quarkus.rest-client."io.quarkus.restclient.config.FullNameRestClient".connection quarkus.rest-client."io.quarkus.restclient.config.FullNameRestClient".connection-pool-size=10 quarkus.rest-client."io.quarkus.restclient.config.FullNameRestClient".max-chunk-size=1024 +quarkus.rest-client.key.removes-trailing-slash=false quarkus.rest-client.key.url=http://localhost:8080 quarkus.rest-client.key.uri=http://localhost:8081 quarkus.rest-client.key.scope=Singleton diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 3733ef6ec9ef0..2f4ac2e80302f 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -288,7 +288,9 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, List defaultConsumes, List defaultProduces, List disableSmartDefaultProduces, + List disableRemovalTrailingSlashProduces, List parameterContainersBuildItems) { + String defaultConsumesType = defaultMediaType(defaultConsumes, MediaType.APPLICATION_OCTET_STREAM); String defaultProducesType = defaultMediaType(defaultProduces, MediaType.TEXT_PLAIN); @@ -403,8 +405,9 @@ public void accept(EndpointIndexer.ResourceMethodCallbackEntry entry) { ClassInfo clazz = index.getClassByName(i.getKey()); //these interfaces can also be clients //so we generate client proxies for them - MaybeRestClientInterface maybeClientProxy = clientEndpointIndexer.createClientProxy(clazz, - i.getValue()); + String path = sanitizePath(i.getValue(), + isRemovalTrailingSlashEnabled(i.getKey(), disableRemovalTrailingSlashProduces)); + MaybeRestClientInterface maybeClientProxy = clientEndpointIndexer.createClientProxy(clazz, path); if (maybeClientProxy.exists()) { RestClientInterface clientProxy = maybeClientProxy.getRestClientInterface(); try { @@ -3076,6 +3079,30 @@ private void addCookieParam(BytecodeCreator invoBuilderEnricher, AssignableResul invocationBuilder, notNullValue.load(paramName), cookieParamHandle)); } + private String sanitizePath(String path, boolean removesTrailingSlash) { + if (!path.startsWith("/")) { + path = "/" + path; + } + + // For the client side, by default, we're only removing the trailing slash for the + // `@Path` annotations at class level which is configurable. + if (removesTrailingSlash && path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return path; + } + + private boolean isRemovalTrailingSlashEnabled(DotName restClientName, + List buildItems) { + for (RestClientDisableRemovalTrailingSlashBuildItem buildItem : buildItems) { + if (buildItem.getClients().contains(restClientName)) { + return false; + } + } + + return true; + } + private enum ReturnCategory { BLOCKING, COMPLETION_STAGE, diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/RestClientDisableRemovalTrailingSlashBuildItem.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/RestClientDisableRemovalTrailingSlashBuildItem.java new file mode 100644 index 0000000000000..f0c0f1dbf4438 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/RestClientDisableRemovalTrailingSlashBuildItem.java @@ -0,0 +1,22 @@ +package io.quarkus.jaxrs.client.reactive.deployment; + +import java.util.List; + +import org.jboss.jandex.DotName; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * This build item disables the removal of trailing slashes from the paths. + */ +public final class RestClientDisableRemovalTrailingSlashBuildItem extends MultiBuildItem { + private final List clients; + + public RestClientDisableRemovalTrailingSlashBuildItem(List clients) { + this.clients = clients; + } + + public List getClients() { + return clients; + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index dac01ae18370f..d913bca3c997e 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -100,6 +100,7 @@ import io.quarkus.jaxrs.client.reactive.deployment.JaxrsClientReactiveEnricherBuildItem; import io.quarkus.jaxrs.client.reactive.deployment.RestClientDefaultConsumesBuildItem; import io.quarkus.jaxrs.client.reactive.deployment.RestClientDefaultProducesBuildItem; +import io.quarkus.jaxrs.client.reactive.deployment.RestClientDisableRemovalTrailingSlashBuildItem; import io.quarkus.jaxrs.client.reactive.deployment.RestClientDisableSmartDefaultProduces; import io.quarkus.rest.client.reactive.runtime.AnnotationRegisteredProviders; import io.quarkus.rest.client.reactive.runtime.HeaderCapturingServerFilter; @@ -108,6 +109,7 @@ import io.quarkus.rest.client.reactive.runtime.RestClientReactiveConfig; import io.quarkus.rest.client.reactive.runtime.RestClientRecorder; import io.quarkus.rest.client.reactive.spi.RestClientAnnotationsTransformerBuildItem; +import io.quarkus.restclient.config.RegisteredRestClient; import io.quarkus.restclient.config.RestClientsBuildTimeConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.restclient.config.deployment.RestClientConfigUtils; @@ -159,15 +161,32 @@ ServiceProviderBuildItem nativeSpiSupport() { } @BuildStep - void setUpDefaultMediaType(BuildProducer consumes, + void setUpClientBuildTimeProperties(BuildProducer consumes, BuildProducer produces, BuildProducer disableSmartProduces, - RestClientReactiveConfig config) { + BuildProducer disableRemovalTrailingSlash, + RestClientReactiveConfig config, + RestClientsBuildTimeConfig configsPerClient, + List registeredRestClientBuildItems) { consumes.produce(new RestClientDefaultConsumesBuildItem(MediaType.APPLICATION_JSON, 10)); produces.produce(new RestClientDefaultProducesBuildItem(MediaType.APPLICATION_JSON, 10)); if (config.disableSmartProduces()) { disableSmartProduces.produce(new RestClientDisableSmartDefaultProduces()); } + + List registeredRestClients = toRegisteredRestClients(registeredRestClientBuildItems); + RestClientsBuildTimeConfig buildTimeConfig = configsPerClient.get(registeredRestClients); + + List clientsToDisable = new ArrayList<>(); + for (RegisteredRestClientBuildItem registeredRestClient : registeredRestClientBuildItems) { + if (removesTrailingSlashIsDisabled(buildTimeConfig, registeredRestClient)) { + clientsToDisable.add(registeredRestClient.getClassInfo().name()); + } + } + + if (!clientsToDisable.isEmpty()) { + disableRemovalTrailingSlash.produce(new RestClientDisableRemovalTrailingSlashBuildItem(clientsToDisable)); + } } @BuildStep @@ -412,7 +431,6 @@ void determineRegisteredRestClients(CombinedIndexBuildItem combinedIndexBuildIte Set seen = new HashSet<>(); List actualInstances = index.getAnnotations(REGISTER_REST_CLIENT); - List registeredRestClientBuildItems = new ArrayList<>(); for (AnnotationInstance instance : actualInstances) { AnnotationTarget annotationTarget = instance.target(); ClassInfo classInfo = annotationTarget.asClass(); @@ -841,11 +859,17 @@ private boolean isRestMethod(MethodInfo method) { return false; } - private Optional getConfigKey(AnnotationInstance registerRestClientAnnotation) { - AnnotationValue configKeyValue = registerRestClientAnnotation.value("configKey"); - return configKeyValue != null - ? Optional.of(configKeyValue.asString()) - : Optional.empty(); + private boolean removesTrailingSlashIsDisabled(RestClientsBuildTimeConfig config, + RegisteredRestClientBuildItem registeredRestClient) { + // is disabled for all the clients + if (!config.removesTrailingSlash()) { + return true; + } + + // is disabled for this concrete client + return !config.clients() + .get(registeredRestClient.getClassInfo().name().toString()) + .removesTrailingSlash(); } private ScopeInfo computeDefaultScope(Capabilities capabilities, Config config, diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/SlashPathRestClientTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/SlashPathRestClientTest.java new file mode 100644 index 0000000000000..14b7dff0c5665 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/SlashPathRestClientTest.java @@ -0,0 +1,127 @@ +package io.quarkus.rest.client.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class SlashPathRestClientTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(HelloClientRemovingSlashes.class, + HelloClientUsingConfigKey.class, + HelloClientUsingSimpleName.class, + HelloClientUsingClassName.class, + HelloResource.class)) + // disable the removal of trailing slash at client side + .overrideConfigKey("quarkus.rest-client.test.removes-trailing-slash", "false") + .overrideConfigKey("quarkus.rest-client.HelloClientUsingSimpleName.removes-trailing-slash", "false") + .overrideConfigKey( + "quarkus.rest-client.\"io.quarkus.rest.client.reactive.SlashPathRestClientTest$HelloClientUsingClassName\".removes-trailing-slash", + "false") + // disable the removal of trailing slash at server side + .overrideConfigKey("quarkus.rest.removes-trailing-slash", "false") + .overrideRuntimeConfigKey("quarkus.rest-client.test.url", + "http://localhost:${quarkus.http.test-port:8081}") + .overrideRuntimeConfigKey("quarkus.rest-client.default.url", + "http://localhost:${quarkus.http.test-port:8081}") + .overrideRuntimeConfigKey("quarkus.rest-client.HelloClientUsingSimpleName.url", + "http://localhost:${quarkus.http.test-port:8081}") + .overrideRuntimeConfigKey( + "quarkus.rest-client.\"io.quarkus.rest.client.reactive.SlashPathRestClientTest$HelloClientUsingClassName\".url", + "http://localhost:${quarkus.http.test-port:8081}"); + + @RestClient + HelloClientRemovingSlashes clientUsingDefaultBehaviour; + + @RestClient + HelloClientUsingConfigKey clientUsingConfigKey; + + @RestClient + HelloClientUsingSimpleName clientUsingSimpleName; + + @RestClient + HelloClientUsingClassName clientUsingClassName; + + @Test + void shouldRemoveTrailingSlashByDefault() { + assertThat(clientUsingDefaultBehaviour.echo()).isEqualTo("/slash/without"); + } + + @Test + void shouldRemoveTrailingSlashUsingConfigKey() { + assertThat(clientUsingConfigKey.echo()).isEqualTo("/slash/with/"); + } + + @Test + void shouldRemoveTrailingSlashUsingSimpleName() { + assertThat(clientUsingSimpleName.echo()).isEqualTo("/slash/with/"); + } + + @Test + void shouldRemoveTrailingSlashUsingClassName() { + assertThat(clientUsingClassName.echo()).isEqualTo("/slash/with/"); + } + + @RegisterRestClient(configKey = "default") + @Path("/slash/without/") + public interface HelloClientRemovingSlashes { + @GET + @Produces(MediaType.TEXT_PLAIN) + String echo(); + } + + @RegisterRestClient(configKey = "test") + @Path("/slash/with/") + public interface HelloClientUsingConfigKey { + @GET + @Produces(MediaType.TEXT_PLAIN) + String echo(); + } + + @RegisterRestClient + @Path("/slash/with/") + public interface HelloClientUsingSimpleName { + @GET + @Produces(MediaType.TEXT_PLAIN) + String echo(); + } + + @RegisterRestClient + @Path("/slash/with/") + public interface HelloClientUsingClassName { + @GET + @Produces(MediaType.TEXT_PLAIN) + String echo(); + } + + @Path("/slash") + @Produces(MediaType.TEXT_PLAIN) + public static class HelloResource { + + @GET + @Path("/without") + public String withoutSlash(@Context UriInfo uriInfo) { + return uriInfo.getPath(); + } + + @GET + @Path("/with/") + public String usingSlash(@Context UriInfo uriInfo) { + return uriInfo.getPath(); + } + } +} diff --git a/extensions/resteasy-reactive/rest-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java b/extensions/resteasy-reactive/rest-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java index 919653a156e04..66a00527bb7d4 100644 --- a/extensions/resteasy-reactive/rest-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java +++ b/extensions/resteasy-reactive/rest-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ResteasyReactiveConfig.java @@ -75,4 +75,10 @@ public interface ResteasyReactiveConfig { */ @WithDefault("false") boolean resumeOn404(); + + /** + * If true, the extension will automatically remove the trailing slash in the paths if any. + */ + @WithDefault("true") + boolean removesTrailingSlash(); } diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 67adec4015f7f..2fd62b912d680 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -538,6 +538,7 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, .setInjectableBeans(injectableBeans) .setAdditionalWriters(additionalWriters) .setDefaultBlocking(appResult.getBlockingDefault()) + .setRemovesTrailingSlash(config.removesTrailingSlash()) .setApplicationScanningResult(appResult) .setMultipartReturnTypeIndexerExtension( new GeneratedHandlerMultipartReturnTypeIndexerExtension(classOutput)) diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java index 1daece4915b77..b97922ad22dc7 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java @@ -60,19 +60,12 @@ public ClientEndpointIndexer(AbstractBuilder builder, String defaultProduces, bo this.smartDefaultProduces = smartDefaultProduces; } - public MaybeRestClientInterface createClientProxy(ClassInfo classInfo, - String path) { + public MaybeRestClientInterface createClientProxy(ClassInfo classInfo, String path) { try { RestClientInterface clazz = new RestClientInterface(); clazz.setClassName(classInfo.name().toString()); clazz.setEncoded(classInfo.hasDeclaredAnnotation(ENCODED)); if (path != null) { - if (!path.startsWith("/")) { - path = "/" + path; - } - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } clazz.setPath(path); } List methods = createEndpoints(classInfo, classInfo, new HashSet<>(), new HashSet<>(), diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 56a9601c012ad..25633cdd60993 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -283,7 +283,7 @@ public Optional createEndpoints(ClassInfo classInfo, boolean cons clazz.setClassName(classInfo.name().toString()); if (path != null) { if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); + path = handleTrailingSlash(path); } if (!path.startsWith("/")) { path = "/" + path; @@ -502,6 +502,9 @@ private static List collectEndpoints(ClassInfo currentClassInfo, return ret; } + /** + * By default, we are not removing the trailing slash. + */ protected String handleTrailingSlash(String path) { return path; } diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java index 9d9c50bc8fe3d..2435451e36e41 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ResteasyReactiveDeploymentManager.java @@ -101,6 +101,8 @@ public static class ScanStep { */ private boolean defaultProduces; + private boolean removesTrailingSlash = true; + private Map additionalResources = new HashMap<>(); private Map additionalResourcePaths = new HashMap<>(); private Set excludedClasses = new HashSet<>(); @@ -199,6 +201,11 @@ public ScanStep setApplicationPath(String applicationPath) { return this; } + public ScanStep setRemovesTrailingSlash(boolean removesTrailingSlash) { + this.removesTrailingSlash = removesTrailingSlash; + return this; + } + public ScanResult scan() { ApplicationScanningResult applicationScanningResult = ResteasyReactiveScanner.scanForApplicationClass(index, @@ -231,7 +238,8 @@ public ScanResult scan() { new ResteasyReactiveConfig(inputBufferSize, minChunkSize, outputBufferSize, singleDefaultProduces, defaultProduces)) .setHttpAnnotationToMethod(resources.getHttpAnnotationToMethod()) - .setApplicationScanningResult(applicationScanningResult); + .setApplicationScanningResult(applicationScanningResult) + .setRemovesTrailingSlash(removesTrailingSlash); for (MethodScanner scanner : methodScanners) { builder.addMethodScanner(scanner); } diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java index 8ec88c0e5b89e..7ba93d0418f01 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java @@ -111,6 +111,7 @@ public class ServerEndpointIndexer protected final List methodScanners; protected final FieldInjectionIndexerExtension fieldInjectionHandler; protected final ConverterSupplierIndexerExtension converterSupplierIndexerExtension; + protected final boolean removesTrailingSlash; protected ServerEndpointIndexer(AbstractBuilder builder) { super(builder); @@ -118,6 +119,7 @@ protected ServerEndpointIndexer(AbstractBuilder builder) { this.methodScanners = new ArrayList<>(builder.methodScanners); this.fieldInjectionHandler = builder.fieldInjectionIndexerExtension; this.converterSupplierIndexerExtension = builder.converterSupplierIndexerExtension; + this.removesTrailingSlash = builder.removesTrailingSlash; } @Override @@ -551,9 +553,16 @@ protected void handlePathSegmentParam(ServerIndexedParameter builder) { builder.setConverter(new PathSegmentParamConverter.Supplier()); } + /** + * For the server side, by default, we are removing the trailing slash unless is not configured otherwise. + */ @Override protected String handleTrailingSlash(String path) { - return path.substring(0, path.length() - 1); + if (removesTrailingSlash) { + return path.substring(0, path.length() - 1); + } + + return path; } @Override @@ -705,6 +714,7 @@ public static class AbstractBuilder methodScanners = new ArrayList<>(); private FieldInjectionIndexerExtension fieldInjectionIndexerExtension; private ConverterSupplierIndexerExtension converterSupplierIndexerExtension = new ReflectionConverterIndexerExtension(); + private boolean removesTrailingSlash = true; public EndpointInvokerFactory getEndpointInvokerFactory() { return endpointInvokerFactory; @@ -735,6 +745,11 @@ public B setFieldInjectionIndexerExtension(FieldInjectionIndexerExtension fieldI return (B) this; } + public B setRemovesTrailingSlash(boolean removesTrailingSlash) { + this.removesTrailingSlash = removesTrailingSlash; + return (B) this; + } + @Override public ServerEndpointIndexer build() { return new ServerEndpointIndexer(this); From 5a2d46c9f578a3cc24f77bd401bb09080d12c2d8 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Wed, 6 Nov 2024 12:02:09 +0000 Subject: [PATCH 3/3] Avoid possible concurrency issues with RestClientsConfig.RestClientKeysProvider.KEYS in build time --- .../config/AbstractRestClientConfigBuilder.java | 16 ++++++++++++++-- .../config/RestClientsBuildTimeConfig.java | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java index 2b607e31f7619..a4e2e0f7bcdbc 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java @@ -53,9 +53,19 @@ public abstract class AbstractRestClientConfigBuilder implements ConfigBuilder { private static final String REST_CLIENT_PREFIX = "quarkus.rest-client."; + private final boolean runtime; + + public AbstractRestClientConfigBuilder() { + this.runtime = true; + RestClientsConfig.RestClientKeysProvider.KEYS.clear(); + } + + public AbstractRestClientConfigBuilder(boolean runtime) { + this.runtime = runtime; + } + @Override public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) { - RestClientsConfig.RestClientKeysProvider.KEYS.clear(); List restClients = getRestClients(); Map quarkusFallbacks = new HashMap<>(); @@ -63,7 +73,9 @@ public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) // relocates [All Combinations] -> quarkus.rest-client."FQN".* Map relocates = new HashMap<>(); for (RegisteredRestClient restClient : restClients) { - RestClientsConfig.RestClientKeysProvider.KEYS.add(restClient.getFullName()); + if (runtime) { + RestClientsConfig.RestClientKeysProvider.KEYS.add(restClient.getFullName()); + } // FQN -> Simple Name String quotedFullName = "\"" + restClient.getFullName() + "\""; diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java index 192c41fd1c72d..47df726193404 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java @@ -143,7 +143,7 @@ private ConfigSource getDefaultsSource() { .withCustomizers(new SmallRyeConfigBuilderCustomizer() { @Override public void configBuilder(final SmallRyeConfigBuilder builder) { - new AbstractRestClientConfigBuilder() { + new AbstractRestClientConfigBuilder(false) { @Override public List getRestClients() { return restClients;