Skip to content

Commit ed7e430

Browse files
committed
#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.
1 parent 3006c47 commit ed7e430

File tree

4 files changed

+119
-5
lines changed

4 files changed

+119
-5
lines changed

src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2016 the original author or authors.
2+
* Copyright 2012-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -149,10 +149,12 @@ public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationA
149149

150150
Assert.notNull(parameter, "MethodParameter must not be null!");
151151

152+
boolean isOptional = Java8Utils.isJava8Optional(parameter.getParameterType());
153+
152154
this.parameter = parameter;
153155
this.value = value;
154156
this.attribute = attribute;
155-
this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, 0);
157+
this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, isOptional ? 1 : 0);
156158
}
157159

158160
/**

src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilderFactory.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2016 the original author or authors.
2+
* Copyright 2012-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -299,7 +299,7 @@ public boolean isRequired() {
299299

300300
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
301301

302-
if (parameter.getParameterType().getName().equals("java.lang.Optional")) {
302+
if (Java8Utils.isJava8Optional(parameter.getParameterType())) {
303303
return false;
304304
}
305305

@@ -318,11 +318,13 @@ protected Object verifyParameterValue(MethodParameter parameter, Object value) {
318318

319319
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
320320

321+
value = Java8Utils.unwrapJava8Optional(value);
322+
321323
if (value != null) {
322324
return value;
323325
}
324326

325-
if (!annotation.required()) {
327+
if (!annotation.required() || Java8Utils.isJava8Optional(parameter.getParameterType())) {
326328
return SKIP_VALUE;
327329
}
328330

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mvc;
17+
18+
import java.util.Optional;
19+
20+
import org.springframework.util.Assert;
21+
import org.springframework.util.ClassUtils;
22+
23+
/**
24+
* Utilities to reflectively work with JDK 8's {@link Optional} on JDK 6.
25+
*
26+
* @author Oliver Gierke
27+
*/
28+
final class Java8Utils {
29+
30+
private static boolean OPTIONAL_PRESENT = ClassUtils.isPresent("java.util.Optional",
31+
OptionalValueAccessor.class.getClassLoader());
32+
33+
/**
34+
* Returns whether the given type is the JDK 8's {@link Optional}.
35+
*
36+
* @param type must not be {@literal null}.
37+
* @return
38+
*/
39+
static boolean isJava8Optional(Class<?> type) {
40+
41+
Assert.notNull(type, "Type must not be null!");
42+
43+
return OPTIONAL_PRESENT && OptionalValueAccessor.isOptional(type);
44+
}
45+
46+
/**
47+
* Returns whether the given source value is a JDK 8 {@link Optional}.
48+
*
49+
* @param source can be {@literal null}.
50+
* @return
51+
*/
52+
static boolean isJava8Optional(Object source) {
53+
return OPTIONAL_PRESENT && OptionalValueAccessor.isOptional(source);
54+
}
55+
56+
/**
57+
* Unwraps the value contained in a JDK 8 {@link Optional} if the value is one.
58+
*
59+
* @param source can be {@literal null}.
60+
* @return
61+
*/
62+
static Object unwrapJava8Optional(Object source) {
63+
return OPTIONAL_PRESENT && isJava8Optional(source) ? OptionalValueAccessor.unwrapOptional(source) : source;
64+
}
65+
66+
private static class OptionalValueAccessor {
67+
68+
static boolean isOptional(Class<?> type) {
69+
return Optional.class.isAssignableFrom(type);
70+
}
71+
72+
static boolean isOptional(Object source) {
73+
return Optional.class.isInstance(source);
74+
}
75+
76+
static Object unwrapOptional(Object source) {
77+
return ((Optional<?>) source).orElse(null);
78+
}
79+
}
80+
}

src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.lang.reflect.Method;
2323
import java.util.Arrays;
2424
import java.util.List;
25+
import java.util.Optional;
2526

2627
import org.hamcrest.Matchers;
2728
import org.junit.Rule;
@@ -554,6 +555,31 @@ public void createsLinkRelativeToContextRoot() {
554555
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), endsWith("/ctx/people"));
555556
}
556557

558+
/**
559+
* @see #639
560+
*/
561+
@Test
562+
public void considersEmptyOptionalMethodParameterOptional() {
563+
564+
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithJdk8Optional(Optional.<Integer> empty()))
565+
.withSelfRel();
566+
567+
assertThat(link.isTemplated(), is(true));
568+
assertThat(link.getVariableNames(), contains("value"));
569+
}
570+
571+
/**
572+
* @see #639
573+
*/
574+
@Test
575+
public void considersOptionalWithValueMethodParameterOptional() {
576+
577+
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithJdk8Optional(Optional.of(1))).withSelfRel();
578+
579+
assertThat(link.isTemplated(), is(false));
580+
assertThat(link.getHref(), endsWith("?value=1"));
581+
}
582+
557583
private static UriComponents toComponents(Link link) {
558584
return UriComponentsBuilder.fromUriString(link.expand().getHref()).build();
559585
}
@@ -630,6 +656,10 @@ HttpEntity<Void> methodForOptionalNextPage(@RequestParam(required = false) Integ
630656
HttpEntity<Void> methodForOptionalSizeWithDefaultValue(@RequestParam(defaultValue = "10") Integer size) {
631657
return null;
632658
}
659+
660+
HttpEntity<Void> methodWithJdk8Optional(@RequestParam Optional<Integer> value) {
661+
return null;
662+
}
633663
}
634664

635665
@RequestMapping("/parent")

0 commit comments

Comments
 (0)