From ed7e430628375e92c985d1d71624dd0bcf119bac Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 5 Oct 2017 15:25:57 +0200 Subject: [PATCH] #639 - Added support for Optional controller method parameters in ControllerLinkBuilder. We now properly consider Optional controller method parameters and correctly map its values to URIs created and optionality of the parameter derived. --- .../AnnotatedParametersParameterAccessor.java | 6 +- .../mvc/ControllerLinkBuilderFactory.java | 8 +- .../hateoas/mvc/Java8Utils.java | 80 +++++++++++++++++++ .../mvc/ControllerLinkBuilderUnitTest.java | 30 +++++++ 4 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/mvc/Java8Utils.java diff --git a/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java b/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java index 45ed083b..8d985660 100644 --- a/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java +++ b/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -149,10 +149,12 @@ class AnnotatedParametersParameterAccessor { Assert.notNull(parameter, "MethodParameter must not be null!"); + boolean isOptional = Java8Utils.isJava8Optional(parameter.getParameterType()); + this.parameter = parameter; this.value = value; this.attribute = attribute; - this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, 0); + this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, isOptional ? 1 : 0); } /** diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java index 3a48f4d2..e21e0bb7 100644 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -299,7 +299,7 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory type) { + + Assert.notNull(type, "Type must not be null!"); + + return OPTIONAL_PRESENT && OptionalValueAccessor.isOptional(type); + } + + /** + * Returns whether the given source value is a JDK 8 {@link Optional}. + * + * @param source can be {@literal null}. + * @return + */ + static boolean isJava8Optional(Object source) { + return OPTIONAL_PRESENT && OptionalValueAccessor.isOptional(source); + } + + /** + * Unwraps the value contained in a JDK 8 {@link Optional} if the value is one. + * + * @param source can be {@literal null}. + * @return + */ + static Object unwrapJava8Optional(Object source) { + return OPTIONAL_PRESENT && isJava8Optional(source) ? OptionalValueAccessor.unwrapOptional(source) : source; + } + + private static class OptionalValueAccessor { + + static boolean isOptional(Class type) { + return Optional.class.isAssignableFrom(type); + } + + static boolean isOptional(Object source) { + return Optional.class.isInstance(source); + } + + static Object unwrapOptional(Object source) { + return ((Optional) source).orElse(null); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java index ab16a086..b5a09f3a 100644 --- a/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java @@ -22,6 +22,7 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.Optional; import org.hamcrest.Matchers; import org.junit.Rule; @@ -554,6 +555,31 @@ public class ControllerLinkBuilderUnitTest extends TestUtils { assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), endsWith("/ctx/people")); } + /** + * @see #639 + */ + @Test + public void considersEmptyOptionalMethodParameterOptional() { + + Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithJdk8Optional(Optional. empty())) + .withSelfRel(); + + assertThat(link.isTemplated(), is(true)); + assertThat(link.getVariableNames(), contains("value")); + } + + /** + * @see #639 + */ + @Test + public void considersOptionalWithValueMethodParameterOptional() { + + Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithJdk8Optional(Optional.of(1))).withSelfRel(); + + assertThat(link.isTemplated(), is(false)); + assertThat(link.getHref(), endsWith("?value=1")); + } + private static UriComponents toComponents(Link link) { return UriComponentsBuilder.fromUriString(link.expand().getHref()).build(); } @@ -630,6 +656,10 @@ public class ControllerLinkBuilderUnitTest extends TestUtils { HttpEntity methodForOptionalSizeWithDefaultValue(@RequestParam(defaultValue = "10") Integer size) { return null; } + + HttpEntity methodWithJdk8Optional(@RequestParam Optional value) { + return null; + } } @RequestMapping("/parent")