2

This question is very similar to this one, but I dont know where to start.

Suppose I have an action like this:

@GetMapping("/foo/{id}")
public Collection<Foo> listById(@PathVariable("id") string id) {
    return null;
}

How could one intercept the listById method and change the value of id (Eg.: concat a string, pad with zeros etc)?

My scenario is that mostly of the IDs are left-padded with zeros (lengths differ) and I dont want to leave this to my ajax calls.

Expected solution:

@GetMapping("/foo/{id}")
public Collection<Foo> listById(@PathVariablePad("id", 4) string id) {
    // id would be "0004" on "/foo/4" calls
    return null;
}
Yves Calaci
  • 1,019
  • 1
  • 11
  • 37
  • Likely through a filter - [this](https://stackoverflow.com/questions/27504696/how-to-change-the-posted-values-with-a-spring-mvc-interceptor) might help. – Andrew S Dec 21 '17 at 19:04
  • 2
    What's wrong with a good old method call? `id = leftPad(id)`? – JB Nizet Dec 21 '17 at 19:20

3 Answers3

4

Ok, here is how I've done it.

Since we can't inherit annotations and thus @PathVariable's target are only parameters, we have to create a new annotation, as follows:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface PathVariablePad {

    int zeros() default 0;

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;

}

Now we need to create a HandlerMethodArgumentResolver. In this case, since all I want is to left-pad a @PathVariable with zeros, we're going to inherit PathVariableMethodArgumentResolver, like this:

public class PathVariablePadderMethodArgumentResolver extends PathVariableMethodArgumentResolver {

    private String leftPadWithZeros(Object target, int zeros) {
        return String.format("%1$" + zeros + "s", target.toString()).replace(' ', '0'); // Eeeewwwwwwwwwwww!
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(PathVariablePad.class);
    }

    @Override
    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
        PathVariablePad pvp = parameter.getParameterAnnotation(PathVariablePad.class);

        return new NamedValueInfo(pvp.name(), pvp.required(), leftPadWithZeros("", pvp.zeros()));
    }

    @Override
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        PathVariablePad pvp = parameter.getParameterAnnotation(PathVariablePad.class);

        return leftPadWithZeros(super.resolveName(name, parameter, request), pvp.zeros());
    }

}

Finally, let's register our method argument resolver (xml):

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="my.package.PathVariablePadderMethodArgumentResolver" />
    </mvc:argument-resolvers>
</mvc:annotation-driven>

The usage is pretty simple and heres how to do this:

@GetMapping("/ten/{id}")
public void ten(@PathVariablePad(zeros = 10) String id) {
    // id would be "0000000001" on "/ten/1" calls
}

@GetMapping("/five/{id}")
public void five(@PathVariablePad(zeros = 5) String id) {
    // id would be "00001" on "/five/1" calls
}
Yves Calaci
  • 1,019
  • 1
  • 11
  • 37
2

Spring @InitBinder annotation and WebDataBinder class will help you to intercept parameter and process it's value before controller method call.

Documentation:

Full code pattern:

@RestController
public class FooController {

    @InitBinder
    private void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(String.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                super.setValue("000" + text);
            }
        } );
    }

    @GetMapping(value = "/foo/{id}")
    public Foo sayHello(
            @PathVariable(value = "id") String id
    ) {
        return new Foo(id);
    }

    @XmlRootElement
    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Foo {
        @XmlElement(name = "id")
        private String id;

        public Foo(String id) {
            this.id = id;
        }

        public Foo() {
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }
    }
}

And the usage:

curl http://localhost:8080/foo/10 | xmllint --format -

Response:

<foo>
<id>00010</id>
</foo>
yanefedor
  • 2,132
  • 1
  • 21
  • 37
  • Hey, thank you for your time! Although your answer only works for a fixed length and pads all the string parameters within the controller actions. Also, copying this `@InitBinder` to every controller is out of mind. – Yves Calaci Dec 21 '17 at 20:34
0

This is quite similar, but for Decoding the @PathVariable value, as brought here by @yanefedor, but applied to all Controllers in the application:

 @org.springframework.web.bind.annotation.ControllerAdvice
public class ControllerAdvice {

    /**
     * Just to decode the data parsed into the Controller's methods parameters annotated with @PathVariable.
     *
     * @param binder
     */
    @InitBinder
    private void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(String.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                if (text == null) {
                    super.setValue(null);
                } else {
                    super.setValue(UriUtils.decode(text, Charset.defaultCharset()));
                }
            }
        });
    }
}
Igor
  • 19
  • 1
  • 6