1

I'm migrating some routes from a jax-rs based application to SpringBoot. In jax-rs I can use @Path to define a regex that contains multiple URL path elements:

@Path("{id:[^/]+/y[\d]{4}/m[\d]{1,2}/d[\d]{1,2}/h[\d]{1,2}}/")

The id variable in the method body will then be the matching segment of the URL and I can go about my day.

With @RequestMapping in Spring this doesn't work. As soon as you put a forward slash into the regex you get a PatternParseException.

PathContainer pathContainingSlash = PathContainer.parsePath("/api/test/y1978/m07/d15");
PathPatternParser parser = new PathPatternParser();
assertThrows(PatternParseException.class, () -> 
parser.parse("/api/test/{ticketId:y[\\d]{4}/m[\\d]{1,2}/d[\\d]{1,2}}"));

This same problem appears to happen with AntPathMatcher.

AntPathMatcher antPathMatcher = new AntPathMatcher();
assertThrows(IllegalStateException.class, () -> 
  antPathMatcher.extractUriTemplateVariables(
    "/api/test/{ticketId:y[\\d]{4}/m[\\d]{1,2}/d[\\d]{1,2}}",
    "/api/test/y1978/m07/d15"));

This is a problem because I have about 78 of these URL patterns. I'm going to have to define each pattern individually with each path element being a separate variable. Then I'm going to have to use String concatenation to combine them back together in the format of a path.

@GetMapping("/{year:y[\\d]{4}}/{month:m[\\d]1,2}/{day:d[\\d]{1,2}")
public ResponseEntity<Void> foo(@PathVariable String year,
  @PathVariable String month, 
  @PathVariable String day) {
    String date = year + "/" + month + "/" + day;
}

Other than using Jax-rs in my SpringBoot app, is there accomplish this? It's possible to write them all like this but it seems sub-optimal.

For clarity, I really want a way to extract multiple path elements from a URL into a @PathVariable. I would like something like this:

@GetMapping("/api/test/{date:y[\\d]{4}/m[\\d]{1,2}/d[\\d]{1,2}}")
public ResponseEntity<Void> foo(@PathVariable String date) {}

So that date is now equal to y1978/m07/d15

Also, this is just one example pattern. There are 78 unique patterns, that have a varying number of a path elements and contents of the elements. In Jax-RS using @Path I can OR these regexes together and create one route and the path variable is accessible inside the method.

0

2 Answers 2

2

how about adding spring-boot-starter-validation for validation

  • requires addition of following jar org.springframework.boot:spring-boot-starter-validation

  • add @org.springframework.validation.annotation.Validated on top of controller class

  • add @javax.validation.constraints.Pattern with regex attribute to the @PathVariable method params


    @GetMapping("{year}/{month}/{day}")
    public ResponseEntity<Void> foo(
            @PathVariable @Pattern(regexp = "[\\d]{4}", message = "year must be ..") String year,
            @PathVariable @Pattern(regexp = "[\\d]{1,2}", message = "month must ..") String month,
            @PathVariable @Pattern(regexp = "[\\d]{1,2}", message= "day must be ..") String day) {
        String date = year + "/" + month + "/" + day;
  • to return http 400 status, add a method to handle the ConstraintViolationException

    @ExceptionHandler(value = { ConstraintViolationException.class })
    protected ResponseEntity<List<String>> handleConstraintViolations(ConstraintViolationException ex, WebRequest request) {
        List<String> errorMessages = ex.getConstraintViolations().stream()
                .map(violation -> violation.getMessage()).collect(Collectors.toList());
        return new ResponseEntity<List<String>>(errorMessages, HttpStatus.BAD_REQUEST);
    }

more validation examples here: https://reflectoring.io/bean-validation-with-spring-boot/

more exception handling options here: https://www.baeldung.com/exception-handling-for-rest-with-spring

Sign up to request clarification or add additional context in comments.

7 Comments

This doesn't provide a way to extract a section of the URL into a @PathVariable. That means I'm still stuck with 78 individual routes that use the exact same logic.
both the @GetMapping and the validation annotations can be moved into an interface for reuse
and the exception handling can be moved into a @RestControllerAdvice class
Sorry, but I'm completely missing how that prevents me from having lots of @GetMappings for each regex pattern I need to match. The patterns are not just year/month/day but very widely. Some have 3 path elements, but there are also ones with 4, 5 and 6 path elements.
I think this is the best path forward. Define high level path elements but then use validation to make sure they fit the patter.
|
0

a possible option using path rewrite from this thread Spring MVC Getting PathVariables containing dots and slashes

add

<dependency>
    <groupId>org.tuckey</groupId>
    <artifactId>urlrewritefilter</artifactId>
    <version>4.0.3</version>
</dependency>

add rewrite rules to src/main/webapp/WEB-INF/urlrewrite.xml

<urlrewrite>
    <rule>
       <from>^/api/test/(y[\d]{4})/(m[\d]{2})/(d[\d]{2})$</from>
       <to>/api/test?date=$1/$2/$3</to>
    </rule>
</urlrewrite>

create controller methods matching the to path of the rewrite rule with query params

@GetMapping("/api/test")
public ResponseEntity<Void> foo(@RequestParam String date) {
    
    System.out.println(date);
    return new ResponseEntity<Void>(HttpStatus.OK);
    
}

add configuration class to register the rewrite filter with urlPatterns to filter

@Configuration
public class FiltersConfig {
    @Bean
    public FilterRegistrationBean<Filter> someFilterRegistration() {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<Filter>();
        registration.setFilter(rewriteFilter());

        // add paths to filter
        registration.addUrlPatterns("/api/*");

        registration.setName("urlRewriteFilter");
        registration.setOrder(1);
        return registration;
    }

    public Filter rewriteFilter() {
        return new UrlRewriteFilter();
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.