Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding snake_case request parameters to a Spring form

I'm implementing a simple RESTful service using Spring Boot, with the interface defined by a .NET (I think) client. Their parameter names are snake_case, rather than camelCase, which obviously means I need to customise how they are mapped.

In the case of JSON input/output, that's fine, I've just customised the ObjectMapper, like so:

@Bean
public ObjectMapper objectMapper() {
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
  return objectMapper;
}

That works fine. Now my problem is form data. I have a Spring form like:

public class MyForm {
  private String myValue;

  public String getMyValue() {return myValue;}
  public void setMyValue(String myValue) {this.myValue = myValue;}
}

But the requests I need to accept will look like:

POST /foo/bar HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

my_value=5

I feel like there must be some simple hook into Spring's binding, like the equivalent setting in Jackon's ObjectMapper, but I'm struggling to find one. The only somewhat-relevant post I can find on here is this one, about completely changing the parameter names, which has some suggestions that seem like overkill for my use case.

The simple solution is simply to use snake case for the fields in MyForm, which works fine, but is a bit ugly.

A final suggestion I've seen elsewhere is to use an interceptor to modify the request parameters on the way in, which seems like it would be straightforward but it feels like there are bound to be exceptions that make it non-trivial, and I'm concerned that having code hidden away in an interceptor makes it really hard to find when you hit the one obscure case where it doesn't work.

Is there some 'proper' Spring-y way of handling this that I'm missing, or do I just need to pick one of the above not-quite-perfect solutions?

like image 514
DaveyDaveDave Avatar asked Jan 22 '16 11:01

DaveyDaveDave


1 Answers

probably you already have solved this issue, I was fighting with this today and answered a question on StackOverflow PT.

So here is the deal:

Create a filter to be executed before the request reach the controller, and format the parameters accordingly (from snake case to camel case on my scenario).

Talk is cheap, show me the code!

import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;

import com.google.common.base.CaseFormat;

@Configuration
public class AppConfig {

    @Bean
    public Filter snakeConverter() {
        return new OncePerRequestFilter() {

            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
                final Map<String, String[]> formattedParams = new ConcurrentHashMap<>();

                for (String param : request.getParameterMap().keySet()) {
                    String formattedParam = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, param);
                    formattedParams.put(formattedParam, request.getParameterValues(param));
                }

                filterChain.doFilter(new HttpServletRequestWrapper(request) {
                    @Override
                    public String getParameter(String name) {
                        return formattedParams.containsKey(name) ? formattedParams.get(name)[0] : null;
                    }

                    @Override
                    public Enumeration<String> getParameterNames() {
                        return Collections.enumeration(formattedParams.keySet());
                    }

                    @Override
                    public String[] getParameterValues(String name) {
                        return formattedParams.get(name);
                    }

                    @Override
                    public Map<String, String[]> getParameterMap() {
                        return formattedParams;
                    }
                }, response);
            }
        };
    }

}

The snakeConverter do the magic.

In there, the doFilterInternal is executed always before the request reach the controller, the parameters are stored in a new Map in their formatted form, and are forwarded to the controller through the filterChain.doFilter.

The HttpServletRequestWrapper do the job of provide our new parameters to the controller.

This code is completely based on the azhawkes filter.


Testing it using a simple controller in the the following URL: http://localhost:8080/snakecase?foo_bar=123

enter image description here

like image 67
nortontgueno Avatar answered Nov 12 '22 19:11

nortontgueno