1

Edit I added more detail to help others and left the original question for history

Background I have prototyped a REST call that returns JSON in a Spring Controller that works with my client software. The client software has a specific way it queries for data. That query is not compatible with the my Spring code, so I had a few lines that did the conversion. I refactored the conversion code into its own object. Instead of creating each time in my REST methods that require it, I would like to have it pre-populated before it gets to my method.

Question In a Spring Controller can I have Spring pre-populate an object from the values in the URL and the header, similar to how Spring populates and object from a form?

Current code

@RequestMapping(value="", headers = "Accept=application/json", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<String> searchUserProjects(
          @RequestParam(required = false) String projectName, 
          @RequestParam(required = false) String sortBy, 
          @RequestHeader(value = "Range") String range) {

Original Question I know in Spring you can take the properties of a form and map them to an object. In addition, I know you can map a field to property converter object, I cannot remember the exact name, but I have done it. My question, is it possible to have Spring populate an object from values in the URL and the header and then pass that into the method instead of declaring them at the method signature of the controller?

Edit:

The registration method in the applicationContext.xml

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="app.util.dojo.DojoQueryProcessorHandlerMethodArgumentResolver"/>
    </mvc:argument-resolvers>
</mvc:annotation-driven>

And the handler method with parameter

public ResponseEntity<String> searchUserProjects(@RequestParam(required = false) String projectName, @ProcessDojoQuery DojoRestQueryProcessor dojoQueryResults) {

DojoRestQueryProcessor.java

package app.util.dojo;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;

public class DojoRestQueryProcessor {

    protected String[] rangeArray;
    protected String range;
    protected String sortBy;
    protected int startIndex;
    protected int endIndex;

    public DojoRestQueryProcessor() {
    }
    public DojoRestQueryProcessor(String range, String sortBy) {
        if (range== null && sortBy == null)
            return;
        if (range.length() <= 3 || !range.contains("-"))
            throw new DojoRestQueryProcessorException("Range value does not meet spec. " + range);
        this.rangeArray = range.substring(6).split("-");
        this.range = range;
        this.sortBy = sortBy;
    }

    public PageRequest createPageRequest() {
        startIndex = Integer.parseInt(rangeArray[0]);
        endIndex = Integer.parseInt(rangeArray[1]);
        if (startIndex >= endIndex)
            throw new IllegalArgumentException("The starting index for a range needs to be less than the end index.");

        Sort.Order[] sortOrders = null;
        if (sortBy != null && sortBy.length() > 2)
            sortOrders = convertDojoSortValuesToSpringSorts(sortBy.split(","));
        int pageSize = endIndex-startIndex+1;
        int pageNum = ((endIndex+1)/pageSize)-1;
        PageRequest pageRequest = null;
        if (sortOrders != null)
            pageRequest = new PageRequest(pageNum, pageSize, new Sort(sortOrders));
        else 
            pageRequest = new PageRequest(pageNum, pageSize);
        return pageRequest;
    }

    public static Sort.Order[] convertDojoSortValuesToSpringSorts(String[] sortStrings) {
        if (sortStrings == null)
            return null;
        Sort.Order[] sortOrders = new Sort.Order[sortStrings.length];
        for (int i = 0; i < sortStrings.length; i++) {
            String sortString = sortStrings[i];
            if (sortString.startsWith("-")) {
                sortOrders[i] = new Sort.Order(Direction.DESC, sortString.substring(1));
            } else {
                sortOrders[i] = new Sort.Order(Direction.ASC, sortString.substring(1));
            }
        }
        return sortOrders;
    }

    public int getStartIndex() {
        return startIndex;
    }

    public int getEndIndex() {
        return endIndex;
    }

    public String getRange() {
        return range;
    }

    public String getSortBy() {
        return sortBy;
    }


}

My Method Handler:

package app.util.dojo;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;

public class DojoQueryProcessorHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {


    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(ProcessDojoQuery.class) && parameter.getParameterType().equals(DojoRestQueryProcessor.class) ;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
            throws Exception {
        String rangeField = parameter.getParameterAnnotation(ProcessDojoQuery.class).rangeField();
        String sortByField = parameter.getParameterAnnotation(ProcessDojoQuery.class).sortByField();

        String range = getRangeValue(rangeField, webRequest);
        String sortBy = getSortByValue(sortByField, webRequest);
        return new DojoRestQueryProcessor(range, sortBy);
    }

    private String getSortByValue(String rangeField, NativeWebRequest webRequest) {
        Map<String, String> pathVariables = getPathVariables(webRequest);
        return pathVariables.get(rangeField);
    }

    private Map<String, String> getPathVariables(NativeWebRequest webRequest) {
        HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        return (Map<String, String>) httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
    }

    private String getHeaderValue(String headerName, NativeWebRequest webRequest) {
        HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        return httpServletRequest.getHeader(headerName);
    }

    private String getRangeValue(String rangeField, NativeWebRequest webRequest) {
        return getHeaderValue(rangeField, webRequest);
    }

}
Joe
  • 747
  • 3
  • 8
  • 21

2 Answers2

2

It is possible, but you would have to do it yourself (once).

The interface for this is HandlerMethodArgumentResolver. The way I see it is you would create an annotation, like @FromUrlAndHeaders and use that to annotate the parameter in the method:

@RequestMapping(value = "/someRequest/path")
public String doBusiness(@FromUrlAndHeaders CustomObject customObject) {
    // do business with customObject
}

Then the fun part is creating your own HandlerMethodArgumentResolver.

public class FromUrlAndHeadersHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(FromUrlAndHeaders.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                ModelAndViewContainer mavContainer,
                NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // use the various objects here
        // request to get parameters and headers
        // mavContainer for model attributes (if you need) 
        // parameter for class type and annotation attributes
        // etc.     

        // note that the parameter class type matters, are your creating a CustomObject, a String, a DifferentClassObject, etc...
    }
}

You can then register this HandlerMethodArgumentResolver and let it do work.


The DispatcherServlet stack uses a list of HandlerMethodArgumentResolver implementation instances to decide what argument to pass to your method. There's one for @ModelAttribute, for @PathVariable, for @RequestParam, for @RequestBody, for ModelMap, for HttpServletRequest, for HttpServletResponse, basically for each parameter type supported by default. You can see all of them in the javadoc.

Related:

Community
  • 1
  • 1
Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • 1
    Thanks for your answer. I find it very useful. – SerotoninChase Sep 20 '13 at 21:18
  • This looks like it will work. I am traveling right now, so I will test it tomorrow. – Joe Sep 20 '13 at 23:22
  • I have created the code and registered it. During start up the argument resolver is being picked up. However, the object is being created by Spring and is not calling my code. Any thoughts on how to fix this one? – Joe Sep 21 '13 at 21:16
  • @Joe Have you annotated the parameter with anything else? – Sotirios Delimanolis Sep 21 '13 at 21:18
  • Nope. The new code is the last parameter: public ResponseEntity searchUserProjects(@RequestParam(required = false) String projectName, @ProcessDojoQuery DojoRestQueryProcessor dojoQueryResults) {. It creates the DojoRestQueryProcessor object and send that in. Thanks for the quick response – Joe Sep 21 '13 at 21:27
  • @Joe Please edit your question with that and also add how you say you registered the Resolver. – Sotirios Delimanolis Sep 21 '13 at 21:30
  • @Joe Also, please, in your question, show the class declaration of `DojoRestQueryProcessor`. – Sotirios Delimanolis Sep 21 '13 at 21:47
  • @Joe I've tried what you posted and it works as intended for me. Put a breakpoint in `RequestMappingHandlerAdapter#getDefaultArgumentResolvers()` method and debug. When the application starts up you should be able to see if your custom resolver was registered. Pay attention to the `getCustomArgumentResolvers()` call. – Sotirios Delimanolis Sep 21 '13 at 23:52
  • @Sotirios I was using my existing unit test to test this code and it was configured with a standalone mockmvc. Your recommendation to debug that method led me to the problem. I had to reconfigure the test to use the web configuration context and that fixed it! Thanks for your help! – Joe Sep 22 '13 at 05:31
0

maybe i didn't get your question and this is not what your looking for, but if you want all parameters to be injected in action method,just declare it as :

@RequestMapping(method = { RequestMethod.POST })
    public ResponseEntity doSomethingCool(@RequestParam Map<String, String> parameters) {
...
}
Eugen Halca
  • 1,775
  • 2
  • 13
  • 26