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.

donahchoo
  • 275
  • 3
  • 14

2 Answers2

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

indybee
  • 1,507
  • 13
  • 17
  • 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. – donahchoo Jan 09 '22 at 20:00
  • both the @GetMapping and the validation annotations can be moved into an interface for reuse – indybee Jan 09 '22 at 21:44
  • and the exception handling can be moved into a @RestControllerAdvice class – indybee Jan 09 '22 at 21:49
  • 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. – donahchoo Jan 09 '22 at 22:19
  • Sorry I misunderstood your question. Can these path variables be converted to query params? – indybee Jan 10 '22 at 00:19
  • no, too many legacy clients at this point. :-( – donahchoo Jan 10 '22 at 01:10
  • 1
    I think this is the best path forward. Define high level path elements but then use validation to make sure they fit the patter. – donahchoo Jan 26 '22 at 20:16
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();
    }
}
indybee
  • 1,507
  • 13
  • 17