#1575 - Support for non-composite request parameters.
This commit introduces @NonComposite, an annotation to be used with collection or array typed @RequestParam handler method parameters. Using the annotation causes the rendering of request parameters to use the non-composite way of rendering URI template values (param=value1,value2) rather than the default, composite flavor of param=value1¶m=value2. A bit of polish in TemplateVariable, which now also exposes a prepareAndEncode(Object) method that renders a given value according to the rules defined in the URI template spec for the particular variable type and state (composite VS. non-composite).
This commit is contained in:
@@ -71,8 +71,8 @@ return new ResponseEntity<PersonModel>(headers, HttpStatus.CREATED);
|
||||
----
|
||||
====
|
||||
|
||||
[[fundamentals.obtaining-links.builder.methods]]
|
||||
=== Building links that point to methods
|
||||
[[server.link-builder.webmvc.methods]]
|
||||
=== [[fundamentals.obtaining-links.builder.methods]] Building links that point to methods
|
||||
|
||||
You can even build links that point to methods or create dummy controller method invocations.
|
||||
The first approach is to hand a `Method` instance to the `WebMvcLinkBuilder`.
|
||||
@@ -105,6 +105,39 @@ assertThat(link.getHref()).endsWith("/people/2");
|
||||
* The return type has to be capable of proxying, as we need to expose the method invocation on it.
|
||||
* The parameters handed into the methods are generally neglected (except the ones referred to through `@PathVariable`, because they make up the URI).
|
||||
|
||||
[[server.link-builder.webmvc.methods.request-params]]
|
||||
==== Controlling the rendering of request parameters
|
||||
|
||||
Collection-valued request parameters can actually be materialized in two different ways.
|
||||
The URI template specification lists the composite way of rendering them that repeats the parameter name for each value (`param=value1¶m=value2`), and the non-composite one that separates values by a comma (`param=value1,value2`).
|
||||
Spring MVC properly parses the collection out of both formats.
|
||||
Rendering the values defaults to the composite style by default.
|
||||
If you want the values to be rendered in the non-composite style, you can use the `@NonComposite` annotation with the request parameter handler method parameter:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
----
|
||||
@Controller
|
||||
class PersonController {
|
||||
|
||||
@GetMapping("/people")
|
||||
HttpEntity<PersonModel> showAll(
|
||||
@NonComposite @RequestParam Collection<String> names) { … } <1>
|
||||
}
|
||||
|
||||
var values = List.of("Matthews", "Beauford");
|
||||
var link = linkTo(methodOn(PersonController.class).showAll(values)).withSelfRel(); <2>
|
||||
|
||||
assertThat(link.getHref()).endsWith("/people?names=Matthews,Beauford"); <3>
|
||||
----
|
||||
<1> We use the `@NonComposite` annotation to declare we want values to be rendered comma-separated.
|
||||
<2> We invoke the method using a list of values.
|
||||
<3> See how the request parameter is rendered in the expected format.
|
||||
====
|
||||
|
||||
NOTE: The reason we're exposing `@NonComposite` is that the composite way of rendering request parameters is baked into the internals of Spring's `UriComponents` builder and we only introduced that non-composite style in Spring HATEOAS 1.4.
|
||||
If we started from scratch today, we'd probably default to that style and rather let users opt into the composite style explicitly rather than the other way around.
|
||||
|
||||
[[server.link-builder.webflux]]
|
||||
== Building links in Spring WebFlux
|
||||
|
||||
|
||||
36
src/main/java/org/springframework/hateoas/NonComposite.java
Normal file
36
src/main/java/org/springframework/hateoas/NonComposite.java
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2021 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;
|
||||
|
||||
import static java.lang.annotation.ElementType.*;
|
||||
import static java.lang.annotation.RetentionPolicy.*;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/**
|
||||
* Annotation to be used in combination with {@link RequestParam} to indicate that collection based values are supposed
|
||||
* to be rendered as non-composite values, i.e. like {@code param=value1,value2,value3} rather than
|
||||
* {@code param=value1¶m=value2} when generating links by pointing to controller methods.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 1.4
|
||||
*/
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, ANNOTATION_TYPE })
|
||||
public @interface NonComposite {}
|
||||
@@ -325,18 +325,13 @@ public final class TemplateVariable implements Serializable, UriTemplate.Expanda
|
||||
return null;
|
||||
}
|
||||
|
||||
return prepareValue(value);
|
||||
return handleComposite(prepareAndEncode(value));
|
||||
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String prepareValue(Map<String, ?> parameters) {
|
||||
return prepareValue(parameters.get(name));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
String prepareValue(@Nullable Object value) {
|
||||
public String prepareAndEncode(@Nullable Object value) {
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
@@ -352,20 +347,20 @@ public final class TemplateVariable implements Serializable, UriTemplate.Expanda
|
||||
return null;
|
||||
}
|
||||
|
||||
return handleComposite(StreamSupport.stream(source.spliterator(), false)
|
||||
return StreamSupport.stream(source.spliterator(), false)
|
||||
.map(it -> prepareElement(it, false))
|
||||
.collect(Collectors.joining(separator)));
|
||||
.collect(Collectors.joining(separator));
|
||||
|
||||
} else if (value instanceof Map) {
|
||||
|
||||
String keyValueSeparator = isComposite() ? "=" : DEFAULT_SEPARATOR;
|
||||
|
||||
return handleComposite(((Map<Object, Object>) value).entrySet().stream()
|
||||
return ((Map<Object, Object>) value).entrySet().stream()
|
||||
.map(it -> it.getKey().toString().concat(keyValueSeparator).concat(prepareElement(it.getValue(), true)))
|
||||
.collect(Collectors.joining(separator)));
|
||||
.collect(Collectors.joining(separator));
|
||||
|
||||
} else {
|
||||
return handleComposite(prepareElement(value, false));
|
||||
return prepareElement(value, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +431,10 @@ public final class TemplateVariable implements Serializable, UriTemplate.Expanda
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
@@ -454,6 +453,10 @@ public final class TemplateVariable implements Serializable, UriTemplate.Expanda
|
||||
&& Objects.equals(this.description, that.description);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.name, this.type, this.description);
|
||||
|
||||
@@ -438,7 +438,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
|
||||
public String expand(Map<String, ?> parameters) {
|
||||
|
||||
return type.join(variables.stream()
|
||||
.map(it -> it.prepareValue(parameters))
|
||||
.map(it -> it.expand(parameters))
|
||||
.filter(it -> it != null)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.hateoas.Affordance;
|
||||
import org.springframework.hateoas.NonComposite;
|
||||
import org.springframework.hateoas.TemplateVariable;
|
||||
import org.springframework.hateoas.TemplateVariables;
|
||||
import org.springframework.hateoas.server.LinkBuilder;
|
||||
@@ -224,9 +225,16 @@ public class WebHandler {
|
||||
|
||||
if (value instanceof Collection) {
|
||||
|
||||
for (Object element : (Collection<?>) value) {
|
||||
if (key != null) {
|
||||
builder.queryParam(key, encodeParameter(element));
|
||||
if (parameter.isNonComposite()) {
|
||||
|
||||
TemplateVariable variable = TemplateVariable.requestParameter(key);
|
||||
builder.queryParam(key, variable.prepareAndEncode(value));
|
||||
|
||||
} else {
|
||||
for (Object element : (Collection<?>) value) {
|
||||
if (key != null) {
|
||||
builder.queryParam(key, encodeParameter(element));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (SKIP_VALUE.equals(value)) {
|
||||
@@ -310,6 +318,7 @@ public class WebHandler {
|
||||
private final MethodParameter parameter;
|
||||
private final AnnotationAttribute attribute;
|
||||
private final TypeDescriptor typeDescriptor;
|
||||
private final boolean isNonComposite;
|
||||
|
||||
private String variableName;
|
||||
|
||||
@@ -328,6 +337,18 @@ public class WebHandler {
|
||||
int nestingIndex = Optional.class.isAssignableFrom(parameter.getParameterType()) ? 1 : 0;
|
||||
|
||||
this.typeDescriptor = TypeDescriptor.nested(parameter, nestingIndex);
|
||||
this.isNonComposite = parameter.hasParameterAnnotation(NonComposite.class);
|
||||
|
||||
if (isNonComposite) {
|
||||
|
||||
Assert.isTrue(parameter.hasParameterAnnotation(RequestParam.class),
|
||||
"@NonComposite can only be used in combination with @RequestParam!");
|
||||
|
||||
Class<?> parameterType = parameter.getParameterType();
|
||||
|
||||
Assert.isTrue(parameterType.isArray() || Collection.class.isAssignableFrom(parameterType),
|
||||
"@NonComposite can only be used with collections or arrays!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,6 +373,15 @@ public class WebHandler {
|
||||
return attribute.getAnnotationType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean isNonComposite() {
|
||||
return isNonComposite;
|
||||
}
|
||||
|
||||
public String getVariableName() {
|
||||
|
||||
if (variableName == null) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -29,6 +30,7 @@ import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.hateoas.IanaLinkRelations;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.NonComposite;
|
||||
import org.springframework.hateoas.TemplateVariable;
|
||||
import org.springframework.hateoas.TemplateVariable.VariableType;
|
||||
import org.springframework.hateoas.TestUtils;
|
||||
@@ -627,6 +629,15 @@ class WebMvcLinkBuilderUnitTest extends TestUtils {
|
||||
assertThat(linkTo(method, new Object[] { null }).withSelfRel().getHref()).endsWith("?id={id}");
|
||||
}
|
||||
|
||||
@Test // #1575
|
||||
void buildsNonCompositeRequestParamUri() {
|
||||
|
||||
Link link = linkTo(methodOn(ControllerWithMethods.class).nonCompositeRequestParam(Arrays.asList("first", "second")))
|
||||
.withSelfRel();
|
||||
|
||||
assertThat(link.getHref()).endsWith("?foo=first,second");
|
||||
}
|
||||
|
||||
private static UriComponents toComponents(Link link) {
|
||||
return UriComponentsBuilder.fromUriString(link.expand().getHref()).build();
|
||||
}
|
||||
@@ -717,6 +728,11 @@ class WebMvcLinkBuilderUnitTest extends TestUtils {
|
||||
HttpEntity<Void> methodWithMapRequestParam(@RequestParam Map<String, String> params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@RequestMapping("/non-composite")
|
||||
HttpEntity<Void> nonCompositeRequestParam(@NonComposite @RequestParam("foo") Collection<String> params) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping("/parent")
|
||||
|
||||
Reference in New Issue
Block a user