diff --git a/src/main/java/org/springframework/hateoas/server/core/AdditionalUriHandler.java b/src/main/java/org/springframework/hateoas/server/core/AdditionalUriHandler.java new file mode 100644 index 000000000..b1f1c72e4 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/server/core/AdditionalUriHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.hateoas.server.core; + +import org.springframework.hateoas.TemplateVariables; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Réda Housni Alaoui + */ +public interface AdditionalUriHandler { + + UriComponentsBuilder apply(UriComponentsBuilder uriComponentsBuilder, MethodInvocation methodInvocation); + + TemplateVariables apply(TemplateVariables templateVariables, UriComponents uriComponents, MethodInvocation methodInvocation); +} diff --git a/src/main/java/org/springframework/hateoas/server/core/WebHandler.java b/src/main/java/org/springframework/hateoas/server/core/WebHandler.java index 294201eed..5f59f94f7 100644 --- a/src/main/java/org/springframework/hateoas/server/core/WebHandler.java +++ b/src/main/java/org/springframework/hateoas/server/core/WebHandler.java @@ -24,7 +24,6 @@ import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -73,12 +72,11 @@ public interface PreparedWebHandler { public static PreparedWebHandler linkTo(Object invocationValue, LinkBuilderCreator creator) { - return linkTo(invocationValue, creator, - (BiFunction) null); + return linkTo(invocationValue, creator, null); } public static T linkTo(Object invocationValue, LinkBuilderCreator creator, - @Nullable BiFunction additionalUriHandler, + @Nullable AdditionalUriHandler additionalUriHandler, Function finisher, Supplier conversionService) { return linkTo(invocationValue, creator, additionalUriHandler).conclude(finisher, @@ -87,7 +85,7 @@ public static T linkTo(Object invocationValue, LinkBuild private static PreparedWebHandler linkTo(Object invocationValue, LinkBuilderCreator creator, - @Nullable BiFunction additionalUriHandler) { + @Nullable AdditionalUriHandler additionalUriHandler) { Assert.isInstanceOf(LastInvocationAware.class, invocationValue); @@ -177,7 +175,9 @@ private static PreparedWebHandler linkTo(Object invoc ? builder.buildAndExpand(values) // : additionalUriHandler.apply(builder, invocation).buildAndExpand(values); - TemplateVariables variables = NONE; + TemplateVariables variables = additionalUriHandler == null + ? NONE + : additionalUriHandler.apply(NONE, components, invocation); for (String parameter : optionalEmptyParameters) { diff --git a/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsContributor.java b/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsContributor.java index 2f2661c7a..42d487471 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsContributor.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsContributor.java @@ -16,9 +16,11 @@ package org.springframework.hateoas.server.mvc; import org.springframework.core.MethodParameter; +import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.server.MethodLinkBuilderFactory; import org.springframework.lang.Nullable; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -28,6 +30,7 @@ * * @see MethodLinkBuilderFactory#linkTo(Object) * @author Oliver Gierke + * @author Réda Housni Alaoui */ public interface UriComponentsContributor { @@ -47,4 +50,15 @@ public interface UriComponentsContributor { * @param value can be {@literal null}. */ void enhance(UriComponentsBuilder builder, @Nullable MethodParameter parameter, @Nullable Object value); + + /** + * Enhance the given {@link TemplateVariables} + * + * @param templateVariables will never be {@literal null}. + * @param uriComponents will never be {@literal null}. + * @param parameter will never be {@literal null}. + */ + default TemplateVariables enhance(TemplateVariables templateVariables, UriComponents uriComponents, MethodParameter parameter){ + return templateVariables; + } } diff --git a/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java b/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java index 7fc48e127..fd9f65a59 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java @@ -31,8 +31,11 @@ import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.hateoas.Link; +import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.server.MethodLinkBuilderFactory; +import org.springframework.hateoas.server.core.AdditionalUriHandler; import org.springframework.hateoas.server.core.LinkBuilderSupport; +import org.springframework.hateoas.server.core.MethodInvocation; import org.springframework.hateoas.server.core.MethodParameters; import org.springframework.hateoas.server.core.SpringAffordanceBuilder; import org.springframework.hateoas.server.core.UriMapping; @@ -44,6 +47,7 @@ import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.mvc.condition.NameValueExpression; import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -150,8 +154,20 @@ public WebMvcLinkBuilder linkTo(Object invocationValue) { Function builderFactory = mapping -> UriComponentsBuilderFactory .forMapping(mapping); - return WebHandler.linkTo(invocationValue, WebMvcLinkBuilder::new, (builder, invocation) -> { + return WebHandler.linkTo(invocationValue, WebMvcLinkBuilder::new, + new UriComponentsContributorsAdditionalUriHandler(uriComponentsContributors), builderFactory, getConversionService()); + } + + private static class UriComponentsContributorsAdditionalUriHandler implements AdditionalUriHandler { + + private final List uriComponentsContributors; + + private UriComponentsContributorsAdditionalUriHandler(List uriComponentsContributors) { + this.uriComponentsContributors = uriComponentsContributors; + } + @Override + public UriComponentsBuilder apply(UriComponentsBuilder builder, MethodInvocation invocation) { String[] primaryParams = SpringAffordanceBuilder.DISCOVERER.getParams(invocation.getMethod()); if (primaryParams.length > 0) { @@ -189,8 +205,23 @@ public WebMvcLinkBuilder linkTo(Object invocationValue) { } return builder; + } + + @Override + public TemplateVariables apply(TemplateVariables templateVariables, UriComponents uriComponents, MethodInvocation invocation) { + MethodParameters parameters = MethodParameters.of(invocation.getMethod()); + + for (MethodParameter parameter : parameters.getParameters()) { + + for (UriComponentsContributor contributor : uriComponentsContributors) { + if (contributor.supportsParameter(parameter)) { + templateVariables = contributor.enhance(templateVariables, uriComponents, parameter); + } + } + } - }, builderFactory, getConversionService()); + return templateVariables; + } } private static Supplier getConversionService() { diff --git a/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactoryUnitTest.java b/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactoryUnitTest.java index b457f4839..68ebfd03c 100644 --- a/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactoryUnitTest.java +++ b/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactoryUnitTest.java @@ -31,6 +31,8 @@ import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Link; +import org.springframework.hateoas.TemplateVariable; +import org.springframework.hateoas.TemplateVariables; import org.springframework.hateoas.TestUtils; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderUnitTest.ControllerWithMethods; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderUnitTest.PersonControllerImpl; @@ -41,6 +43,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -184,6 +187,17 @@ void linksToMethodWithPrimaryParam() { assertThat(link.getHref()).endsWith("/something/foo?a=1&b=2"); } + @Test + void appliesTemplateVariableIfContributorConfigured() { + + WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory(); + factory.setUriComponentsContributors(Collections.singletonList(new SampleUriComponentsContributor())); + + Link link = factory.linkTo(methodOn(SampleController.class).sampleMethod(1L, null)).withSelfRel(); + assertPointsToMockServer(link); + assertThat(link.getHref()).endsWith("/sample/1{?foo}"); + } + interface SampleController { @RequestMapping("/sample/{id}") @@ -208,8 +222,19 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) { + if (value == null) { + return; + } builder.queryParam("foo", ((SpecialType) value).parameterValue); } + + @Override + public TemplateVariables enhance(TemplateVariables templateVariables, UriComponents uriComponents, MethodParameter parameter) { + if (uriComponents.getQueryParams().containsKey("foo")) { + return templateVariables; + } + return templateVariables.concat(TemplateVariable.requestParameter("foo")); + } } static class SpecialType {