Skip to content

Commit d0ff83c

Browse files
committed
#118 - Use MVC ConversionService for parameter binding in link creation.
We now look up the ConversionService available in the ApplicationContext from Web(Mvc|Flux)LinkBuilder. Some API tweaks to WebHandler to allow the lookup from the current request. The general fallback is now the invocation of …toString() on the parameter value. Fixes #118, #352, #144, #149.
1 parent 3da23a7 commit d0ff83c

File tree

5 files changed

+193
-43
lines changed

5 files changed

+193
-43
lines changed

src/main/java/org/springframework/hateoas/server/core/WebHandler.java

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
import java.util.concurrent.ConcurrentHashMap;
2727
import java.util.function.BiFunction;
2828
import java.util.function.Function;
29+
import java.util.function.Supplier;
2930
import java.util.stream.Collectors;
3031

3132
import org.springframework.core.MethodParameter;
3233
import org.springframework.core.convert.ConversionService;
3334
import org.springframework.core.convert.TypeDescriptor;
34-
import org.springframework.format.support.DefaultFormattingConversionService;
3535
import org.springframework.hateoas.Affordance;
3636
import org.springframework.hateoas.TemplateVariable;
3737
import org.springframework.hateoas.TemplateVariables;
@@ -71,7 +71,7 @@ public interface LinkBuilderCreator<T extends LinkBuilder> {
7171
}
7272

7373
public interface PreparedWebHandler<T extends LinkBuilder> {
74-
T conclude(Function<String, UriComponentsBuilder> finisher);
74+
T conclude(Function<String, UriComponentsBuilder> finisher, ConversionService conversionService);
7575
}
7676

7777
public static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invocationValue,
@@ -82,9 +82,9 @@ public static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invoca
8282

8383
public static <T extends LinkBuilder> T linkTo(Object invocationValue, LinkBuilderCreator<T> creator,
8484
@Nullable BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder> additionalUriHandler,
85-
Function<String, UriComponentsBuilder> finisher) {
85+
Function<String, UriComponentsBuilder> finisher, Supplier<ConversionService> conversionService) {
8686

87-
return linkTo(invocationValue, creator, additionalUriHandler).conclude(finisher);
87+
return linkTo(invocationValue, creator, additionalUriHandler).conclude(finisher, conversionService.get());
8888
}
8989

9090
private static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invocationValue,
@@ -104,7 +104,7 @@ private static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invoc
104104

105105
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod());
106106

107-
return finisher -> {
107+
return (finisher, conversionService) -> {
108108

109109
UriComponentsBuilder builder = finisher.apply(mapping);
110110
UriTemplate template = UriTemplateFactory.templateFor(mapping == null ? "/" : mapping);
@@ -120,16 +120,18 @@ private static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invoc
120120

121121
HandlerMethodParameters parameters = HandlerMethodParameters.of(invocation.getMethod());
122122
Object[] arguments = invocation.getArguments();
123+
ConversionService resolved = conversionService;
123124

124125
for (HandlerMethodParameter parameter : parameters.getParameterAnnotatedWith(PathVariable.class, arguments)) {
125-
values.put(parameter.getVariableName(), encodePath(parameter.getValueAsString(arguments)));
126+
values.put(parameter.getVariableName(),
127+
encodePath(parameter.getValueAsString(arguments, resolved)));
126128
}
127129

128130
List<String> optionalEmptyParameters = new ArrayList<>();
129131

130132
for (HandlerMethodParameter parameter : parameters.getParameterAnnotatedWith(RequestParam.class, arguments)) {
131133

132-
bindRequestParameters(builder, parameter, arguments);
134+
bindRequestParameters(builder, parameter, arguments, conversionService);
133135

134136
if (SKIP_VALUE.equals(parameter.getVerifiedValue(arguments))) {
135137

@@ -177,7 +179,7 @@ private static <T extends LinkBuilder> PreparedWebHandler<T> linkTo(Object invoc
177179
*/
178180
@SuppressWarnings("unchecked")
179181
private static void bindRequestParameters(UriComponentsBuilder builder, HandlerMethodParameter parameter,
180-
Object[] arguments) {
182+
Object[] arguments, ConversionService conversionService) {
181183

182184
Object value = parameter.getVerifiedValue(arguments);
183185

@@ -223,7 +225,7 @@ private static void bindRequestParameters(UriComponentsBuilder builder, HandlerM
223225

224226
} else {
225227
if (key != null) {
226-
builder.queryParam(key, encodeParameter(parameter.getValueAsString(arguments)));
228+
builder.queryParam(key, encodeParameter(parameter.getValueAsString(arguments, conversionService)));
227229
}
228230
}
229231
}
@@ -335,7 +337,6 @@ public List<HandlerMethodParameter> getParameterAnnotatedWith(Class<? extends An
335337

336338
private abstract static class HandlerMethodParameter {
337339

338-
private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
339340
private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
340341
private static final Map<Class<? extends Annotation>, Function<MethodParameter, ? extends HandlerMethodParameter>> FACTORY;
341342
private static final String NO_PARAMETER_NAME = "Could not determine name of parameter %s! Make sure you compile with parameter information or explicitly define a parameter name in %s.";
@@ -400,7 +401,7 @@ public String getVariableName() {
400401
return variableName;
401402
}
402403

403-
public String getValueAsString(Object[] values) {
404+
public String getValueAsString(Object[] values, ConversionService conversionService) {
404405

405406
Object value = values[parameter.getParameterIndex()];
406407

@@ -414,11 +415,9 @@ public String getValueAsString(Object[] values) {
414415

415416
value = ObjectUtils.unwrapOptional(value);
416417

417-
// Try to lookup ConversionService from the request's context
418-
419-
// Guard with ….canConvert(…)
420-
// if not, fall back to ….toString();
421-
Object result = CONVERSION_SERVICE.convert(value, typeDescriptor, STRING_DESCRIPTOR);
418+
Object result = conversionService.canConvert(typeDescriptor, STRING_DESCRIPTOR)
419+
? conversionService.convert(value, typeDescriptor, STRING_DESCRIPTOR)
420+
: value == null ? null : value.toString();
422421

423422
if (result == null) {
424423
throw new IllegalArgumentException(String.format("Conversion of value %s resulted in null!", value));

src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@
2323
import java.util.List;
2424
import java.util.Map;
2525
import java.util.function.Function;
26+
import java.util.function.Supplier;
27+
28+
import javax.servlet.ServletContext;
2629

2730
import org.springframework.core.MethodParameter;
31+
import org.springframework.core.convert.ConversionService;
32+
import org.springframework.format.support.DefaultFormattingConversionService;
2833
import org.springframework.hateoas.Link;
2934
import org.springframework.hateoas.server.MethodLinkBuilderFactory;
3035
import org.springframework.hateoas.server.core.LinkBuilderSupport;
3136
import org.springframework.hateoas.server.core.MethodParameters;
3237
import org.springframework.hateoas.server.core.WebHandler;
38+
import org.springframework.web.context.WebApplicationContext;
39+
import org.springframework.web.context.request.RequestAttributes;
40+
import org.springframework.web.context.request.RequestContextHolder;
41+
import org.springframework.web.context.request.ServletRequestAttributes;
42+
import org.springframework.web.context.support.WebApplicationContextUtils;
3343
import org.springframework.web.util.UriComponentsBuilder;
3444

3545
/**
@@ -47,6 +57,8 @@
4757
*/
4858
public class WebMvcLinkBuilderFactory implements MethodLinkBuilderFactory<WebMvcLinkBuilder> {
4959

60+
private static ConversionService FALLBACK_CONVERSION_SERVICE = new DefaultFormattingConversionService();
61+
5062
private List<UriComponentsContributor> uriComponentsContributors = new ArrayList<>();
5163

5264
/**
@@ -124,7 +136,7 @@ public WebMvcLinkBuilder linkTo(Object invocationValue) {
124136

125137
return builder;
126138

127-
}, builderFactory);
139+
}, builderFactory, getConversionService());
128140
}
129141

130142
/*
@@ -135,4 +147,24 @@ public WebMvcLinkBuilder linkTo(Object invocationValue) {
135147
public WebMvcLinkBuilder linkTo(Method method, Object... parameters) {
136148
return WebMvcLinkBuilder.linkTo(method, parameters);
137149
}
150+
151+
@SuppressWarnings("null")
152+
private Supplier<ConversionService> getConversionService() {
153+
154+
return () -> {
155+
156+
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
157+
158+
if (!ServletRequestAttributes.class.isInstance(attributes)) {
159+
return null;
160+
}
161+
162+
ServletContext servletContext = ((ServletRequestAttributes) attributes).getRequest().getServletContext();
163+
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext);
164+
165+
return context == null || !context.containsBean("mvcConversionService")
166+
? FALLBACK_CONVERSION_SERVICE
167+
: context.getBean("mvcConversionService", ConversionService.class);
168+
};
169+
}
138170
}

src/main/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilder.java

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import java.util.List;
2323
import java.util.function.Function;
2424

25+
import org.springframework.context.ApplicationContext;
26+
import org.springframework.core.convert.ConversionService;
27+
import org.springframework.core.convert.support.DefaultConversionService;
2528
import org.springframework.hateoas.Affordance;
2629
import org.springframework.hateoas.IanaLinkRelations;
2730
import org.springframework.hateoas.Link;
@@ -75,7 +78,7 @@ public static WebFluxBuilder linkTo(Object invocation) {
7578
* @param exchange must not be {@literal null}.
7679
*/
7780
public static WebFluxBuilder linkTo(Object invocation, ServerWebExchange exchange) {
78-
return new WebFluxBuilder(linkToInternal(invocation, Mono.just(getBuilder(exchange))));
81+
return new WebFluxBuilder(linkToInternal(invocation, CurrentRequest.of(exchange)));
7982
}
8083

8184
/**
@@ -87,7 +90,6 @@ public static WebFluxBuilder linkTo(Object invocation, ServerWebExchange exchang
8790
* @return
8891
*/
8992
public static <T> T methodOn(Class<T> controller, Object... parameters) {
90-
9193
return DummyInvocationUtils.methodOn(controller, parameters);
9294
}
9395

@@ -245,40 +247,58 @@ public Mono<Link> toMono(Function<Link, Link> finisher) {
245247
}
246248
}
247249

250+
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation) {
251+
252+
return linkToInternal(invocation,
253+
Mono.deferContextual(
254+
context -> CurrentRequest.of(context.getOrDefault(EXCHANGE_CONTEXT_ATTRIBUTE, null))));
255+
}
256+
257+
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation, Mono<CurrentRequest> builder) {
258+
259+
PreparedWebHandler<WebFluxLinkBuilder> handler = WebHandler.linkTo(invocation, WebFluxLinkBuilder::new);
260+
261+
return builder.map(it -> handler.conclude(path -> it.builder.path(path), it.conversionService));
262+
}
263+
248264
/**
249-
* Returns a {@link UriComponentsBuilder} obtained from the {@link ServerWebExchange}.
265+
* Access to components we can obtain from the current request or fallbacks in case no current request is available.
250266
*
251-
* @param exchange
267+
* @author Oliver Drotbohm
252268
*/
253-
private static UriComponentsBuilder getBuilder(@Nullable ServerWebExchange exchange) {
269+
private static class CurrentRequest {
254270

255-
if (exchange == null) {
256-
return UriComponentsBuilder.fromPath("/");
257-
}
271+
private static final ConversionService FALLBACK_CONVERSION_SERVICE = new DefaultConversionService();
258272

259-
ServerHttpRequest request = exchange.getRequest();
260-
PathContainer contextPath = request.getPath().contextPath();
273+
private UriComponentsBuilder builder;
274+
private ConversionService conversionService;
261275

262-
return UriComponentsBuilder.fromHttpRequest(request) //
263-
.replacePath(contextPath.toString()) //
264-
.replaceQuery("");
265-
}
276+
public static Mono<CurrentRequest> of(@Nullable ServerWebExchange exchange) {
266277

267-
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation) {
278+
CurrentRequest result = new CurrentRequest();
268279

269-
return linkToInternal(invocation,
270-
Mono.subscriberContext().map(context -> getBuilder(context.getOrDefault(EXCHANGE_CONTEXT_ATTRIBUTE, null))));
271-
}
280+
if (exchange == null) {
272281

273-
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation, Mono<UriComponentsBuilder> builder) {
282+
result.builder = UriComponentsBuilder.fromPath("/");
283+
result.conversionService = FALLBACK_CONVERSION_SERVICE;
274284

275-
PreparedWebHandler<WebFluxLinkBuilder> handler = WebHandler.linkTo(invocation, WebFluxLinkBuilder::new);
285+
return Mono.just(result);
286+
}
276287

277-
return builder.map(WebFluxLinkBuilder::getBuilderCreator) //
278-
.map(handler::conclude);
279-
}
288+
ServerHttpRequest request = exchange.getRequest();
289+
PathContainer contextPath = request.getPath().contextPath();
290+
291+
result.builder = UriComponentsBuilder.fromHttpRequest(request) //
292+
.replacePath(contextPath.toString()) //
293+
.replaceQuery("");
294+
295+
ApplicationContext context = exchange.getApplicationContext();
280296

281-
private static Function<String, UriComponentsBuilder> getBuilderCreator(UriComponentsBuilder builder) {
282-
return path -> builder.path(path);
297+
result.conversionService = context != null && context.containsBean("webFluxConversionService")
298+
? context.getBean("webFluxConversionService", ConversionService.class)
299+
: FALLBACK_CONVERSION_SERVICE;
300+
301+
return Mono.just(result);
302+
}
283303
}
284304
}

src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
1919
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
2020
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
21+
import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.*;
2122

2223
import reactor.core.publisher.Flux;
2324
import reactor.core.publisher.Mono;
@@ -31,6 +32,7 @@
3132
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3233
import org.springframework.context.annotation.Bean;
3334
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.format.FormatterRegistry;
3436
import org.springframework.hateoas.CollectionModel;
3537
import org.springframework.hateoas.EntityModel;
3638
import org.springframework.hateoas.IanaLinkRelations;
@@ -41,7 +43,11 @@
4143
import org.springframework.hateoas.server.core.TypeReferences.CollectionModelType;
4244
import org.springframework.hateoas.server.core.TypeReferences.EntityModelType;
4345
import org.springframework.hateoas.support.Employee;
46+
import org.springframework.http.HttpEntity;
47+
import org.springframework.http.HttpStatus;
4448
import org.springframework.http.MediaType;
49+
import org.springframework.http.ResponseEntity;
50+
import org.springframework.stereotype.Controller;
4551
import org.springframework.test.web.reactive.server.WebTestClient;
4652
import org.springframework.web.bind.annotation.GetMapping;
4753
import org.springframework.web.bind.annotation.PathVariable;
@@ -50,6 +56,7 @@
5056
import org.springframework.web.bind.annotation.RequestBody;
5157
import org.springframework.web.bind.annotation.RestController;
5258
import org.springframework.web.reactive.config.EnableWebFlux;
59+
import org.springframework.web.reactive.config.WebFluxConfigurer;
5360

5461
/**
5562
* @author Greg Turnquist
@@ -67,7 +74,7 @@ void setUp(Class<?> context) {
6774
ctx.register(context);
6875
ctx.refresh();
6976

70-
HypermediaWebTestClientConfigurer configurer = ctx.getBean(HypermediaWebTestClientConfigurer.class);
77+
HypermediaWebTestClientConfigurer configurer = ctx.getBean(HypermediaWebTestClientConfigurer.class);
7178

7279
this.testClient = WebTestClient.bindToApplicationContext(ctx).build().mutateWith(configurer);
7380
}
@@ -322,6 +329,24 @@ void reactorTypesShouldWork() {
322329
}).verifyComplete();
323330
}
324331

332+
@Test // #118
333+
void linkCreationConsidersRegisteredConverters() throws Exception {
334+
335+
setUp(WithConversionService.class);
336+
337+
this.testClient.get().uri("/sample/4711").exchange() //
338+
.expectStatus().isEqualTo(HttpStatus.I_AM_A_TEAPOT) //
339+
.returnResult(String.class).getResponseBody()
340+
.as(StepVerifier::create)
341+
.expectNextMatches(it -> {
342+
343+
assertThat(it).isEqualTo("/sample/sample");
344+
345+
return true;
346+
})
347+
.verifyComplete();
348+
}
349+
325350
private void verifyRootUriServesHypermedia(MediaType mediaType) {
326351
verifyRootUriServesHypermedia(mediaType, mediaType);
327352
}
@@ -555,4 +580,33 @@ public void addLinks(CollectionModel<EntityModel<Employee>> resources) {
555580
}
556581
}
557582

583+
// #118
584+
585+
@Configuration
586+
static class WithConversionService extends HalWebFluxConfig implements WebFluxConfigurer {
587+
588+
/*
589+
* (non-Javadoc)
590+
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurer#addFormatters(org.springframework.format.FormatterRegistry)
591+
*/
592+
@Override
593+
public void addFormatters(FormatterRegistry registry) {
594+
registry.addConverter(Sample.class, String.class, source -> "sample");
595+
registry.addConverter(String.class, Sample.class, source -> new Sample());
596+
}
597+
598+
static class Sample {}
599+
600+
@Controller
601+
static class SampleController {
602+
603+
@GetMapping("/sample/{sample}")
604+
Mono<HttpEntity<?>> sample(@PathVariable Sample sample) {
605+
606+
return linkTo(methodOn(SampleController.class).sample(new Sample())).withSelfRel()
607+
.toMono()
608+
.map(it -> new ResponseEntity<>(it.getHref(), HttpStatus.I_AM_A_TEAPOT));
609+
}
610+
}
611+
}
558612
}

0 commit comments

Comments
 (0)