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;
}
2
  • Likely through a filter - this might help. Commented Dec 21, 2017 at 19:04
  • 2
    What's wrong with a good old method call? id = leftPad(id)? Commented Dec 21, 2017 at 19:20

3 Answers 3

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
}
Sign up to request clarification or add additional context in comments.

Comments

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>

1 Comment

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.
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()));
                }
            }
        });
    }
}

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.