Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping URL parameters with dashes to object in Spring Web MVC

Mapping URL request parameters with Spring MVC to an object is fairly straightforward if you're using camelCase parameters in your request, but when presented with hyphen delimited values, how do you map these to an object?

Example for reference:

Controller:

@RestController
public class MyController {

    @RequestMapping(value = "/search", method = RequestMethod.GET)
    public ResponseEntity<String> search(RequestParams requestParams) {
        return new ResponseEntity<>("my-val-1: " + requestParams.getMyVal1() + " my-val-2: " + requestParams.getMyVal2(), HttpStatus.OK);
    }

}

Object to hold parameters:

public class RequestParams {

    private String myVal1;
    private String myVal2;

    public RequestParams() {}

    public String getMyVal1() {
        return myVal1;
    }

    public void setMyVal1(String myVal1) {
        this.myVal1 = myVal1;
    }

    public String getMyVal2() {
        return myVal2;
    }

    public void setMyVal2(String myVal2) {
        this.myVal2 = myVal2;
    }
}

A request made like this works fine:

GET http://localhost:8080/search?myVal1=foo&myVal2=bar

But, what I want is for a request with hyphens to map to the object, like so:

GET http://localhost:8080/search?my-val-1=foo&my-val-2=bar

What do I need to configure in Spring to map url request parameters with hyphens to fields in an object? Bear in mind that we may have many parameters, so using a @RequestParam annotation for each field is not ideal.

like image 767
Andrew Avatar asked Nov 25 '15 18:11

Andrew


1 Answers

I extended ServletRequestDataBinder and ServletModelAttributeMethodProcessor to solve the problem.

Consider that your domain object may already be annotated with @JsonProperty or @XmlElement for serialization. This example assumes this is the case. But you could also create your own custom annotation for this purpose e.g. @MyParamMapping.

An example of your annotated domain class is:

public class RequestParams {

    @XmlElement(name = "my-val-1" )
    @JsonProperty(value = "my-val-1")
    private String myVal1;

    @XmlElement(name = "my-val-2")
    @JsonProperty(value = "my-val-2")
    private String myVal2;

    public RequestParams() {
    }

    public String getMyVal1() {
        return myVal1;
    }

    public void setMyVal1(String myVal1) {
        this.myVal1 = myVal1;
    }

    public String getMyVal2() {
        return myVal2;
    }

    public void setMyVal2(String myVal2) {
        this.myVal2 = myVal2;
    }
}

You will need a SerletModelAttributeMethodProcessor to analyze the target class, generate a mapping, invoke your ServletRequestDataBinder.

    public class KebabCaseProcessor extends ServletModelAttributeMethodProcessor {

    public KebabCaseProcessor(boolean annotationNotRequired) {
        super(annotationNotRequired);
    }

    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
        Object target = binder.getTarget();
        Class<?> targetClass = target.getClass();
        if (!replaceMap.containsKey(targetClass)) {
            Map<String, String> mapping = analyzeClass(targetClass);
            replaceMap.put(targetClass, mapping);
        }
        Map<String, String> mapping = replaceMap.get(targetClass);
        ServletRequestDataBinder kebabCaseDataBinder = new KebabCaseRequestDataBinder(target, binder.getObjectName(), mapping);
        requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(kebabCaseDataBinder, nativeWebRequest);
        super.bindRequestParameters(kebabCaseDataBinder, nativeWebRequest);
    }

    private static Map<String, String> analyzeClass(Class<?> targetClass) {
        Field[] fields = targetClass.getDeclaredFields();
        Map<String, String> renameMap = new HashMap<String, String>();
        for (Field field : fields) {
            XmlElement xmlElementAnnotation = field.getAnnotation(XmlElement.class);
            JsonProperty jsonPropertyAnnotation = field.getAnnotation(JsonProperty.class);
            if (xmlElementAnnotation != null && !xmlElementAnnotation.name().isEmpty()) {
                renameMap.put(xmlElementAnnotation.name(), field.getName());
            } else if (jsonPropertyAnnotation != null && !jsonPropertyAnnotation.value().isEmpty()) {
                renameMap.put(jsonPropertyAnnotation.value(), field.getName());
            }
        }
        if (renameMap.isEmpty())
            return Collections.emptyMap();
        return renameMap;
    }
}

This KebabCaseProcessor will use reflection to get a list of mappings for your request object. It will then invoke the KebabCaseDataBinder - passing in the mappings.

@Configuration
public class KebabCaseRequestDataBinder extends ExtendedServletRequestDataBinder {

    private final Map<String, String> renameMapping;

    public KebabCaseRequestDataBinder(Object target, String objectName, Map<String, String> mapping) {
        super(target, objectName);
        this.renameMapping = mapping;
    }

    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        super.addBindValues(mpvs, request);
        for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
            String from = entry.getKey();
            String to = entry.getValue();
            if (mpvs.contains(from)) {
                mpvs.add(to, mpvs.getPropertyValue(from).getValue());
            }
        }
    }
}

All that remains now is to add this behavior to your configuration. The following configuration overrides the default configuration that the @EnableWebMVC delivers and adds this behavior to your request processing.

@Configuration
public static class WebContextConfiguration extends WebMvcConfigurationSupport {
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(kebabCaseProcessor());
    }

    @Bean
    protected KebabCaseProcessor kebabCaseProcessor() {
        return new KebabCaseProcessor(true);
    }
} 

Credit should be given to @Jkee. This solution is derivative of an example he posted here: How to customize parameter names when binding spring mvc command objects.

like image 145
Bjorn Loftis Avatar answered Sep 30 '22 07:09

Bjorn Loftis