Spring model and request objects - spring

In Spring, why can't we simply use a request object to carry data around in the application? Why did we switch over to using a model object? Also I read on a popular Spring book that it is still a request object (even in a Spring application) that does most of the hardwork of carrying data. Does it mean that the models we create in Spring are somehow linked to a request object underneath the hood?

In Spring, why can't we simply use a request object to carry data
around in the application?
You can use HttpServletRequest or even WebRequest as method argument and then inspect those two in order to extract the data out of it. For example, if you want to get the value of a query string named page:
#RequestMapping("/users")
public void listUsers(HttpServletRequest request) {
String page = request.getParameter("page");
...
}
Why did we switch over to using a model object?
You can simply read from a HttpServletRequest and finally write to a HttpServletResponse in any controller method but spring also provides nicer abstractions for handling typical web scenarios. For example if your page parameter is a required int parameter with the default value of 1, you can simply write:
#RequestMapping("/users")
public void listUsers(#RequestParam(defaultValue = "1") Integer page) {
...
}
Spring automatically does all the string to int conversion and populating with default values for you. Or suppose you want to automatically validate your json or xml Request Body and return validation errors. With these higher abstractions you will define a model object that contains all the validation logic in it and simply add #Valid annotation before its reference it method arguments, that's it!
#RequestMappting(value = "/users", method = POST)
public void addNewUser(#RequestBody #Valid User user) {
// automatically validates the user
// and send validation error, if user wasn't valid
}
Of course you can do the same by inspecting HttpServletRequest but it would be so cumbersome.
Does it mean that the models we create in Spring are somehow linked to
a request object underneath the hood?
They're not necessarily link to them but usually these model objects will be populated from request parameters.
For example, spring convert the request body to the method argument by using an HttpMessageConverter. HttpMessageConverter is responsible for converting from the HTTP request message to an object and converting from an object to the HTTP response body. For instance FormHttpMessageConverter converts form data to/from a MultiValueMap<String, String>. This class has a read method that reads the request body and converts it to a MultiValueMap<String, String>. This method looks like this:
public MultiValueMap<String, String> read(Class<? extends MultiValueMap<String, ?>> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = (contentType.getCharSet() != null ? contentType.getCharSet() : this.charset);
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>(pairs.length);
for (String pair : pairs) {
int idx = pair.indexOf('=');
if (idx == -1) {
result.add(URLDecoder.decode(pair, charset.name()), null);
}
else {
String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
result.add(name, value);
}
}
return result;
}
By String body = StreamUtils.copyToString(inputMessage.getBody(), charset);, it actually reads the request body and converts it to a String.

Related

Ignoring Jackson InvalidFormatException to use #ExceptionHandler for #Valid #RequestBody

Basically, I'm trying to validate inputs from the #RequestBody of a REST controller using an #ExceptionHandler to catch MethodArgumentNotValidException constraints.
#PostMapping(value = "/createbaseline")
public ResponseEntity<?> createBaseline(#Valid #RequestPart Baseline baseline){
//...
return ResponseEntity.ok("...");
}
Before I can do that Jackson is throwing an InvalidFormatException when it fails to parse a string to a Date, thus preventing my #ExceptionHandler from validating the remaining inputs. Here is my #ExceptionHandler method that I want to use for validating inputs.
#ExceptionHandler(MethodArgumentNotValidException.class)
protected final ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex){
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String[] mapping = error.getDefaultMessage().split(":");
errors.put(mapping[0], mapping[1]);
});
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
I've considered just changing the Date property of the object to a String, but I know that's against best practices. The reason why I want to use the handleMethodArgumentNotValid is because I have custom constraint annotations on fields in my objects that specify a key that I can access via MethodArgumentNotValidException.getBindingResult() like so:
#RequiredFieldConstraint(key = "label")
private String label;
The client can then use those keys to determine which input field will show an error. If I used the method below to catch the InvalidFormatException then I don't have access to those keys specified by the field annotations.
#ExceptionHandler(value = {InvalidFormatException.class})
protected ResponseEntity<Object> handleException(Exception e){
//can't access annotation keys in this method body
return ResponseEntity.badRequest().body("bad");
}
I need to be able to validate ALL inputs sent in the #ResponseBody in order to send the appropriate error message back to the client but I'd still like to utilize deserializing the #RequestBody directly into an object. Does anyone have ideas on how to approach or work around this?
I found a work around for this by implementing a custom deserializer for Dates so that it catches the InvalidFormatException but still returns a new Date() with .setTime(-1) that I can check for in my handleMethodArgumentNotValid method along with all the other inputs.

Get request body as string/json to validate with a json schema- Spring boot REST API

I'm trying to validate JSON (passed by a client as a request body) before it is converted into a model in Controller method.
If validation passes then return nothing, let the process continue as it was (spring boot to convert JSON into a model marked as #RequestBody). Throw error in case validation fails (everit-org/json-schema).
I tried to two way:
Implement HandlerMethodArgumentResolver, but resolveArgument() doesn't give request body details as it is already read and stored in ContentCachingRequestWrapper.
NOTE: inputStream in ContentCachingRequestWrapper doesn't have any request body details.
Using spring Interceptor. But this doesn't help me to find request body type passed in the request. As JSON schema is different for each request.
Any other approaches I can try with?
I cannot add a comment ... so ...
What kind of validation do you need? If you only want to validate the fields like length of a string or range of a number and so on. I recommend you use #Validated on controller mehtod parameter, and model:
#NotNull
#Size(min = 32, max = 32)
private String id;
controller:
#PatchMapping
public Object update(#RequestBody #Validated User user, Errors errors) {
...
}
If there is something wrong, errors.hasErrors() will return true.
edit:
OK, I did some tests, in a filter :
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
ServletInputStream inputStream = httpServletRequest.getInputStream();
byte[] a = new byte[1024];
inputStream.read(a);
System.out.println(IOUtils.toString(a));
I got a json string (a piece of request body) :
{"template":"5AF78355A4F0D58E03CE9F55AFA850F8","bd":"" ...

How to accept RequestBody of different class types dynamically

I am using Spring Boot . Writing rest api's
where for the same api url , the request json structure varies
Is there any way we can apply Factory design or some thing else
#RequestMapping(value = "/myservice/{type}", method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<?> myServiceApi(#PathVariable String type,
#RequestBody SomeClass1 somereq) {
// here based on type , the RequestBody can be either SomeClass1 or SomeClass2
// both the SomeClass1 and SomeClass2 has nothing in common .
}
The above code will work only if the request json is in SomeClass1 format , but i needed it to accept among {SomeClass1 , SomeClass2}
You could do this by passing the JSON as a String into your controller method and then mapping this to whichever object you expect to need:
#PostMapping(value = "/myservice/{type}")
public ResponseEntity<?> myServiceApi(#PathVariable String type,
#RequestBody String somereq) {
ObjectMapper mapper = new ObjectMapper();
if (<something that indicates SomeClass1>) {
SomeClass1 someClass1 = mapper.readValue(somereq, SomeClass1.class);
} else if (<something that indicates SomeClass2>) {
SomeClass2 someClass2 = mapper.readValue(somereq, SomeClass2.class);
}
}
Although to be honest if you really are expecting bodies with completely different structures my advice would be to just make separate API calls for these.

Spring framework does not map list request parameter correctly

I have a web request like
/path?param=value1&param=value2
which maps to a List<String> param on the Controller side.
The important aspect to note is that value1 and value2 can have a comma(,) in their values.
I see 2 different behaviors with spring request parameter mapping.
Case 1. /path?param=part1%2Cpart2 (url encoded comma)
Spring request parameter maps this to a List of size 2 with part1 and part2 as the elements, but HttpServletRequest.getParameterValues("param") is correctly assigned to an array of size 1 with value=part1,part2
Case 2. /path?param=part1%2Cpart2&param=part3%2Cpart4
In this case Spring correctly maps this to a list of 2 values, and so does the HttpServletRequest parameter.
I guess Spring is supporting mapping List parameter with both csv values and repeating the parameter. Is there a way to tell Spring to use a particular mapping method?
I'm using spring-mvc 3.2.13
#Controller
public class MyController {
#RequestMapping(value = "/mymethod", method = RequestMethod.POST)
public #ResponseBody Boolean method(MyRequest myReq, HttpServletRequest request) {
}
}
public class MyRequest {
List<String> param;
}
I've actually never seen this before, but have done a bit of reading and the recommended way to do it for POST seems to be to pass the values as follow:
/path?param=value1&param=value2&param=value3
And then in the #Controller, have a #RequestParam which contains a value of param[] - note the square brackets.
#RequestMapping(value="/path", method = RequestMethod.POST)
public void doSomething(#RequestParam(value = "param[]") String[] paramValues){...}
I haven't tested POST, but I have tested GET and I got it working using:
/path?code=1&height=300&width=300&param=3&param=5&param=7
And then in my #Controller
#ResponseBody
#RequestMapping(value = "/path", method = RequestMethod.GET)
public void blah(
HttpServletResponse response,
#RequestParam("code") String code,
#RequestParam("width") Integer width,
#RequestParam("height") Integer height,
#RequestParam(value = "param") String[] params) {
for (int i = 0; i < params.length; i++){
System.out.println(params[i]);
}
And this printed
3
5
7

how to capture multiple parameters using #RequestParam using spring mvc?

Suppose a hyperlink is clicked and an url is fired with the following parameter list myparam=myValue1&myparam=myValue2&myparam=myValue3 . Now how can I capture all the parameters using #RequestParam in spring mvc?
My requirement is I have to capture all the params and put them in a map.
Please help!
#RequestMapping(value = "users/newuser", method = RequestMethod.POST)
public String saveUser(#RequestParam Map<String,String> requestParams) throws Exception{
String userName=requestParams.get("email");
String password=requestParams.get("password");
//perform DB operations
return "profile";
}
You could use RequestParam in the above mentioned manner.
It seems you can't get
Map<String,String>
because all your params have same name "myparam"
Try this instead:
public ModelAndView method(#RequestParam("myparam") List<String> params) { }
To get all parameters at once try this:
public ModelAndView postResultPage(#RequestParam MultiValueMap<String, String> params)
This feature is described in the #RequestParam java doc (3. Paragraph):
Annotation which indicates that a method parameter should be bound to a web request parameter. Supported for annotated handler methods in Servlet and Portlet environments.
If the method parameter type is Map and a request parameter name is specified, then the request parameter value is converted to a Map assuming an appropriate conversion strategy is available.
If the method parameter is Map<String, String> or MultiValueMap<String, String> and a parameter name is not specified, then the map parameter is populated with all request parameter names and values.
As of Spring 3.0, you can also use MultiValueMap to achieve this:
A rudimentary example would be:
public String someMethod(#RequestParam MultiValueMap<String,String> params) {
final Iterator<Entry<String, List<String>>> it = params.entrySet().iterator();
while(it.hasNext()) {
final String k = it.next().getKey();
final List<String> values = it.next().getValue();
}
return "dummy_response";
}
If anyone is trying to do the same in Spring Boot, use RequestBody in place of RequestParam
Spring mvc can support List<Object>, Set<Object> and Map<Object> param, but without #RequestParam.
Take List<Object> as example, if your object is User.java, and it like this:
public class User {
private String name;
private int age;
// getter and setter
}
And you want pass a param of List<User>, you can use url like this
http://127.0.0.1:8080/list?users[0].name=Alice&users[0].age=26&users[1].name=Bob&users[1].age=16
Remember to encode the url, the url after encoded is like this:
http://127.0.0.1:8080/list?users%5B0%5D.name=Alice&users%5B0%5D.age=26&users%5B1%5D.name=Bob&users%5B1%5D.age=16
Example of List<Object>, Set<Object> and Map<Object> is displayed in my github.
You can use for multiple Params as such
public String saveUser(#RequestParam("email") String userName, #RequestParam("password") String password) throws Exception{
//your code
//perform DB operations
return "profile";
}
For params with same name, you can use MultiValueMap<String ,String>. Then all the values would be present as List
You can use multiple #RequestParam annotations as shown below.
#RequestParam(value="myparam1", required = true) <Datatype> myparam1,
#RequestParam(value = "myparam2", required = false) <Datatype> myparam2,

Resources