I'm developing a Spring boot with Spring MVC web app using rest controllers. I'd like to redirect URLs that have empty parameters to the same URL but without its parameters; the idea being to explicitly remove the semantic ambiguity between an “empty filter” and “no filter”. I'm using the Links to Controllers to do this.
Sample:
@RestController
@RequestMapping(path = "/foo")
public class MyRestController {
@GetMapping(path = "/bar")
public ResponseEntity<List<Integer>> search(@ModelAttribute @NotNull BarRequest barRequest, @RequestParam(defaultValue = "10") Integer limit) {
final BarRequest disambiguatedBarRequest = barRequest.disambiguated();
if (barRequest != disambiguatedBarRequest) {
return ResponseEntity
.status(HttpStatus.PERMANENT_REDIRECT)
.header(HttpHeaders.LOCATION, MvcUriComponentsBuilder
.fromMethodName(this.getClass(), "search", disambiguatedBarRequest, limit).build()
.encode().toUri().toASCIIString())
.build();
}
return ResponseEntity.ok(generateRandomArray(limit, 0, 100));
}
// credits: Ashish Lahoti
public static List<Integer> generateRandomArray(int size, int min, int max) {
return IntStream
.generate(() -> min + new Random().nextInt(max - min + 1))
.limit(size).boxed().toList();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class BarRequest {
private Set<String> categories;
private Set<String> otherFilters;
public BarRequest disambiguated() {
BarRequest candidate = BarRequest.builder()
.categories(disambiguateStringSet(categories))
.otherFilters(disambiguateStringSet(otherFilters))
.build();
if (Objects.equals(candidate, this)) {
return this;
} else {
return candidate;
}
}
private Set<String> disambiguateStringSet(Set<String> set) {
return Optional.ofNullable(set)
.map(categories -> categories.stream().filter(Objects::nonNull).collect(Collectors.toSet()))
.filter(c -> !c.isEmpty())
.orElse(null);
}
}
}
In this example, the URI /foo/bar?categories=not_empty&otherFilters=&limit=3
is expected to result in a redirect to /foo/bar?categories=not_empty&limit=3
but I am redirected to /foo/bar?limit=3
instead.
The problem comes from MvcUriComponentsBuilder which only prepares the url for “simple” method parameters (Integer, String, Set
@jcagarcia explains well how MvcUriComponentsBuilder works in this post and it allows us to to identify how these "contributors" are chosen and used (CompositeUriComponentsContributor):
@Override
public void contributeMethodArgument(
MethodParameter parameter, Object value,
UriComponentsBuilder builder,
Map<String, Object> uriVariables,
ConversionService conversionService) {
for (Object contributor : this.contributors) {
if (contributor instanceof UriComponentsContributor) {
UriComponentsContributor ucc = (UriComponentsContributor) contributor;
if (ucc.supportsParameter(parameter)) {
ucc.contributeMethodArgument(parameter, value, builder, uriVariables, conversionService);
break;
}
}
else if (contributor instanceof HandlerMethodArgumentResolver) {
if (((HandlerMethodArgumentResolver) contributor).supportsParameter(parameter)) {
break;
}
}
}
}
In my example, it is contributor ServletModelAttributeMethodProcessor who is chosen but as he does not implement UriComponentsContributor then MvcUriComponentsBuilder will simply ignore these parameters, resulting in an incomplete URL.
You will find a ready-to-run example on this repository.
Comment From: rstoyanchev
Currently we don't support use of form-back objects from a client perspective for sending requests. This is just one of a number of instances with similar requirements. So this is related to #32142, and possible depends on it, but either way the need to be considered together.
A specific challenge with this is that if the model attribute BarRequest
could have additional properties not intended to appear as query params.