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);