1

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;
}
Henri
  • 716
  • 7
  • 28
  • 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

2 Answers2

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
}
Henri
  • 716
  • 7
  • 28
0

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
  • 1,802
  • 1
  • 20
  • 33
  • 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. – Henri Dec 21 '17 at 20:34