spring-projects / spring-projects/spring-framework

Support links to controller method with a model attribute in MvcUriComponentsBuilder

Open
#33,989 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

in: web type: enhancement
Dominant language
Java
Stars
60.2k
Forks
38.8k
Avg merge
5d 2h
Merged PRs (30d)
27

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with MvcUriComponentsBuilder and CompositeUriComponentsContributor, then inspect how ServletModelAttributeMethodProcessor is selected for the BarRequest parameter. Run the ready-to-run example in the linked mvc-uri-comp-builder repository and verify that the generated redirect preserves non-empty model attributes, producing /foo/bar?categories=not_empty&limit=3.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.