Understanding How Spring MVC's #RequestMapping POST Works - java

I have a simple Controller that looks like this:-
#Controller
#RequestMapping(value = "/groups")
public class GroupsController {
// mapping #1
#RequestMapping(method = RequestMethod.GET)
public String main(#ModelAttribute GroupForm groupForm, Model model) {
...
}
// mapping #2
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public String changeGroup(#PathVariable Long id, #ModelAttribute GroupForm groupForm, Model model) {
...
}
// mapping #3
#RequestMapping(method = RequestMethod.POST)
public String save(#Valid #ModelAttribute GroupForm groupForm, BindingResult bindingResult, Model model) {
...
}
}
Basically, this page has the following functionalities:-
User visits main page (/groups GET).
User creates a new group (/groups POST) or selects a specific group (/groups/1 GET).
User edits an existing group (/groups/1 POST).
I understand how both GET request mappings work here. Mapping #2 is defined, otherwise (/groups/1 GET) will cause a "No mapping found" exception.
What I'm trying to understand here is why mapping #3 handles both (/groups POST) and (/groups/1 POST)? It makes sense that it should handle (/groups POST) here since the request mapping matches the URI. Why (/groups/1 POST) isn't causing a "No mapping found" exception being thrown here? In fact, it almost seems like any POST with URI beginning with /groups (ex: /groups/bla/1 POST) will also be handled by mapping #3.
Can someone provide a clear explanation of this to me? Thanks much.
CLARIFICATION
I understand the fact that I can use more appropriate methods (like GET, POST, PUT or DELETE)... or I can create yet another request mapping to handle /groups/{id} POST.
However, what I want to really know is...
.... "Why does mapping #3 handle /groups/1 POST too?"
The "closest match" reasoning don't seem to hold true because if I remove mapping #2, then I would think mapping #1 will handle /groups/1 GET, but it doesn't and it causes a "No mapping found" exception.
I'm just a little stumped here.

This is complicated, I think it is better to read the code.
In Spring 3.0 The magic is done by method public Method resolveHandlerMethod(HttpServletRequest request) of the inner class ServletHandlerMethodResolver of org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.
An instance of this class exists for every Request Controller Class, and has a field handlerMethods that contains a list of all the request methods.
But let me summarize how I understand it
Spring first checks if at least one handler method matches (this can contain false negatives)
Then it creates a map of all really matching handler methods
Then it sorts the map by request path: RequestSpecificMappingInfoComparator
and takes the first one
The sorting works this way: the RequestSpecificMappingInfoComparator first compares the path with the help of an AntPathMatcher, if two methods are equal according to this, then other metrics (like number of parameters, number of headers, etc.) are taken into account with respect to the request.

Spring tries to find the mapping which matches the closest. Hence, in your case of any POST request, the only map found for the request type is Mapping# 3.
Neither of Mapping 1 or Mapping 2 matches your request type, and hence are ignored.
May be you can try removing the Mapping #3, and see that Spring throws a runtime error since it does not find a match!

I would add a PUT mapping for /groups/{id}. I guess POST would work too but not strictly correct from a HTTP perspective.
adding #RequestMapping("/{id}", POST) should cover it?

add #PathVariable to the Long id parameter in mapping #2

Related

Java Spring multiple url mapping values

I've been trying to implement a generic interface to support multiple access controls.
Currently, I have the following controller:
#RequestMapping(path = "/api")
public interface MyController {
#GetMapping(value = {"/public/workers/name/{workerString}"})
ResponseEntity<T> searchByName(#PathVariable String workerString);
#GetMapping("/hr/worker/general/{id}")
ResponseEntity<T> general(#PathVariable int id);
}
I have an interceptor which catches the request and checks wheather the user has the "hr" permissions.
Now I have the need to add another scenario, where the user can have "hd" permission and he can execute the same procedure.
The easy solution is to do the following:
#GetMapping(value={"/hr/worker/general/{id}","/hd/worker/general/{id}"})
ResponseEntity<T> general(#PathVariable int id);
and my interceptor will check in a generic manner whether the user has the permission depends on the url.
My issue with this is scalability. what if tomorrow I have to add another type of permission? "hm" "hs" and so on.. I don't want to add more urls to the mapping.
Is that possible to do something like that? :
#GetMapping("/{permission}/worker/general/{id}")
ResponseEntity<T> general(#PathVariable int id);
Obviously this doesn't work since I have to consume the declared path variable.
what are the alternatives here and what would be considered a good practice?
I would appreciate any thought or help here. Thanks.

Having alias param names to accept Url Encoded Form data values

In my Spring web application, I have an API that accepts requests with application/x-www-form-urlencoded content type.
#RequestMapping(value = "/do-it", method = {RequestMethod.POST})
public String test(#ModelAttribute("request")RequestDTO request,HttpServletRequest
httpServletRequest, Map<String, Object> model, RedirectAttributes redirectAttributes){
.....
}
My RequestDTO has following fields in it.
public class RequestDTO {
private String paramOne;
private String paramTwo;
// standard getters and setters
}
This implementation works fine, all the request params get mapped to the request dto as expected. However, now I have this requirement to accept the requests with the fields in following pattern.
param_one, param_two
I understand that, using #JsonProperty annotation on the fields in my request dto is not gonna work in my case since the request is not in the type of application/json.
The only way I have found to solve the issue is creating new setters like following (which looks ugly in my opinion when it comes to naming convention).
public void setParam_one(String param_one) {
this.paramOne = param_one;
}
Can some one help me to find a better way to get this done? I cannot change the param names in original request dto.
Thank you..!
I was able to get this done. Thanks to #neetash for guiding me.
Everything I needed to have was a Custom HandlerMethodArgumentResolver to map the post request body data to the object that I wanted to get.
I followed following linked tutorial to implement it. It contains every single thing someone needs to know to create a HandlerMethodArgumentResolver.
https://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-creating-a-custom-handlermethodargumentresolver/

Weird Spring MVC 4.2.x behaviour on controller method with ResponseBody and returning Model class instance

I have some legacy controller which puts some amount of data into the Model object (needed for Thymeleaf template).
Now I have to return the same data as JSON in REST service.
For these purposes I have wrapped the block of data preparation into separate method to use in two places: in the old method which is used for thymeleaf template and in the new one:
#RequestMapping(value = "/index", method = RequestMethod.GET)
public String index(Model model) {
prepareIndexModel(model);
return "index";
}
#RequestMapping(value = "/index/model", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
#ResponseBody
public Map<String, Object> indexModel(Model model) {
prepareIndexModel(model);
return model.asMap();
}
private void prepareIndexModel(Model model) {
model.addAttribute("prop1", ...);
...
}
However, when I try access via GET /index/model I receive the following error:
org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "/WEB-INF/templates/index/model.html")
so it simply considers my method not as REST method. I guess, that it is because method is actually returns instance of ExtendedModelMap class which implements both interfaces: Model and Map.
Therefore, after chaning /index/model method to:
#RequestMapping(value = "/index/model", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
#ResponseBody
public Map<String, Object> indexModel(Model model) {
prepareIndexModel(model);
return new LinkedHashMap<>(model.asMap());
}
everything started working expected: GET /index/model returns me desired JSON. So basically I just wrapped the model into LinkedHashMap.
My question is: is this a specified behaviour or simply a bug? I expected that by annotating a method with #ResponseBody annotation, Spring should ignore the fact that returning object is instance of Model. Shouldn't it?
UPDATE: #Sotirios Delimanolis provided the link to the very similar question, which was about version 3.2. But my question is not about why, but is it a bug of Spring or is it specified some where in the documentation?
UPDATE-2: Please, also note, that in the linked question a method has Model returning type, and its behaviour is described. In my case I have Map<...> returning type, what in my opinion makes this behaviour strange and inconsistent!
is this a specified behaviour or simply a bug?
I would say it's undefined behavior.
The Spring Framework Reference defines that a return type of Model means a View with a Model. It also defines that a return type of Map means a View with the Map as the Model. These are both well-defined.
It also specifies that for a method annotated with #ResponseBody, the returned value is written to the response HTTP body. Again, this is well-defined.
What it doesn't specify, is what happens when a #ResponseBody returns a Model or a Map.
It is very common to return a Map to be encoded as JSON, and that works correctly, i.e. #ResponseBody takes precedence over return type Map. This makes sense.
However, a Model is specifically for the purpose of model attributes for a View, so the fact that return type Model takes precedence over #ResponseBody makes some sense. But as I said, it's not specified behavior, but neither is it a bug. It is undefined.
If you asked me for what it should do, I'd leave it undefined, since Model as #ResponseBody doesn't make any sense to me.
Also, the documentation doesn't make the distinction between declared return type and actual return type, so it is undefined which is used.
The implementation uses the actual return type, which has the advantage that your handler method can return Object and then return e.g. a ModelAndView or an HttpEntity, depending on conditions. This flexibility makes sense, but it isn't explicitly defined, as far as I can see.
So, result of the combination of #ResponseBody, declared return type of Map, but actual return type of Model, is undefined.
If you ask me (and you kind of did), I'd say that your code is bugged. If you want to return a Map to be sent as the response body, why not just create the Map yourself. Asking the Spring framework for a view Model makes no sense at all to me.
Even reading the code, I'd be unsure what you actually intended, given the mixed signals of using a Model and specifying #ResponseBody.
Conclusion: Don't be lazy, and create the Map yourself.

Spring MVC Binding Command Object Using Get Request

I need to implement a controller that has a command object that is backing a filtering form for a search across multiple entries.
The problem is that the i was asked to do that without using POST request, instead using GET request only, and there before loosing the functionality of the default data binding that springs makes happily for us.
So i tried to implement a method, inside my controller, that looks like this:
#Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (isSearchRequest(request)) {
MyCommandObject myCommandObject = (MyCommandObject) getCommand(request);
System.out.println(managePositionsForm);
}
return super.handleRequestInternal(request, response);
}
But the getCommand returns me a brand new CommandObject with no values, despite that the values are present in the request object (i could retrieve then using the getParameter method of HttpServletRequest). But there isn't any binding.
So the question :
1) Is there any way to archive this?
2) Also is very important, that all the values in the form, are lost and, eventually (if this problem is solved) i will need to "persist" the filters for the users in order to avoid re entering after the first search.
Auto Response : setSessionForm(true); looks like can do the work! (According to javadoc)
Thanks to all!
Greetings
Victor.
Okey, i found a way to archive what a was looking for.
I will explain for the sake of those have the same problem before, and hoping to find a experienced user to validate this method... some quiet common is there a multiple ways to do a same thing and as human beings is very difficult to know without proper acknowledge the right path.. so this i a found looking inside the AbstractFormController (that is excellently documented with javadoc).
So what i did was the following, on my controller constructor i add these lines at the end :
setSessionForm(true);
setBindOnNewForm(true);
That all the magic!
But is not enought with setSessionForm(true). According to javadoc the setBindOnNewForm(boolean) method does the following :
/**
* Set if request parameters should be bound to the form object
* in case of a non-submitting request, i.e. a new form.
*/
So my guess are that these two flags are necessary to be marked as true, because :
The setSessionForm makes posible to store as a session attribute the form object, so "is stored in the session to keep the form object instance between requests, instead of creating a new one on each request" (according to javadoc of the setSessionForm method).
The setBindOnNewForm allows the population of the form object with the initial request (despites what type of request method we have). According the javadoc found the AbstractFormController "Only if bindOnNewForm is set to true, then ServletRequestDataBinder gets applied to populate the new form object with initial request parameters..."
But still i noticed, following the controller flow with a debugger, that the population is happening inside the method "getErrorsForNewForm(HttpServletRequest request)".. that is where a concrete object of type ServletRequestDataBinder is used IF the setBindOnNewForm is true, and later (as the javadoc stated) the onBindOnNewForm method is invoked, allowing the programmer to overwrite it with custom behavior, the default behavior is just empty (again this was double checked against the code of AbstractFormController).
I have an strong felling to validate my thoughts here, so if anyone can help me, that would be alright, besides the problem is solved!
Thanks to all in advance!
Greetings.

Spring - adding BindingResult to newly created model attribute

My task is - to create a model attribute by given request parameters, to validate it (in same method) and to give it whole to the View.
I was given this example code:
#Controller
class PromotionController {
#RequestMapping("promo")
public String showPromotion(#RequestParam String someRequestParam, Model model) {
//Create the model attribute by request parameters
Promotion promotion = Promotions.get(someRequestParam);
//Add the attribute to the model
model.addAttribute("promotion", promotion);
if (!promotion.validate()) {
BindingResult errors = new BeanPropertyBindingResult(promotion, "promotion");
errors.reject("promotion.invalid");
//TODO: This is the part I don't like
model.put(BindingResult.MODEL_KEY_PREFIX + "promotion", errors);
}
return
}
}
This thing sure works, but that part with creating key with MODEL_KEY_PREFIX and attribute name looks very hackish and not a Spring style to me. Is there a way to make the same thing prettier?
Skaffman answered the question but disappeared, so I will answer it for him.
The binding validation thing is there to bind and validate parameters, not arbitrary business objects.
That means, that if I need to do some custom validation of some general data that was not submitted by the user - I need to add some custom variable to hold that status and not use BindingResult.
This answers all the questions I had with BindingResult, as I thought it had to be used as a container for any kind of errors.
Again, thanks #Skaffman.

Categories