Skip to content

Add a template variables contribution SPI #1312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,12 +72,11 @@ public interface PreparedWebHandler<T extends LinkBuilder> {

public static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invocationValue,
LinkBuilderCreator<T> creator) {
return linkTo(invocationValue, creator,
(BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder>) null);
return linkTo(invocationValue, creator, null);
}

public static <T extends LinkBuilder> T linkTo(Object invocationValue, LinkBuilderCreator<T> creator,
@Nullable BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder> additionalUriHandler,
@Nullable AdditionalUriHandler additionalUriHandler,
Function<UriMapping, UriComponentsBuilder> finisher, Supplier<ConversionService> conversionService) {

return linkTo(invocationValue, creator, additionalUriHandler).conclude(finisher,
Expand All @@ -87,7 +85,7 @@ public static <T extends LinkBuilder> T linkTo(Object invocationValue, LinkBuild

private static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invocationValue,
LinkBuilderCreator<T> creator,
@Nullable BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder> additionalUriHandler) {
@Nullable AdditionalUriHandler additionalUriHandler) {

Assert.isInstanceOf(LastInvocationAware.class, invocationValue);

Expand Down Expand Up @@ -177,7 +175,9 @@ private static <T extends LinkBuilder> PreparedWebHandler<T> 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) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -28,6 +30,7 @@
*
* @see MethodLinkBuilderFactory#linkTo(Object)
* @author Oliver Gierke
* @author Réda Housni Alaoui
*/
public interface UriComponentsContributor {

Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -150,8 +154,20 @@ public WebMvcLinkBuilder linkTo(Object invocationValue) {
Function<UriMapping, UriComponentsBuilder> 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<UriComponentsContributor> uriComponentsContributors;

private UriComponentsContributorsAdditionalUriHandler(List<UriComponentsContributor> uriComponentsContributors) {
this.uriComponentsContributors = uriComponentsContributors;
}

@Override
public UriComponentsBuilder apply(UriComponentsBuilder builder, MethodInvocation invocation) {
String[] primaryParams = SpringAffordanceBuilder.DISCOVERER.getParams(invocation.getMethod());

if (primaryParams.length > 0) {
Expand Down Expand Up @@ -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<ConversionService> getConversionService() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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}")
Expand All @@ -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 {
Expand Down