Spring boot rest controller not converting request body to custom object - java

I have spring boot application which used spring rest controller .
This is the controller , below is the response an getting. Am using postman tool for sending request to this controller. And am sending content type as application/json
#RequestMapping(value = "/test", method = RequestMethod.POST)
public String test(#RequestBody WebApp webapp, #RequestBody String propertyFiles, #RequestBody String) {
System.out.println("webapp :"+webapp);
System.out.println("propertyFiles :"+propertyFiles);
System.out.println("propertyText :"+propertyText);
return "ok good";
}
2018-03-21 12:18:47.732  WARN 8520 --- [nio-8099-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved exception caused by Handler execution: org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: Stream closed
This is my postman request
{
"webapp":{"webappName":"cavion17","path":"ud1","isQA":true},
"propertyFiles":"vchannel",
"propertytText":"demo property"}
I tried by removing the RequestBody annotation, then able to hit the service , but param objects are received as null.
So please suggest how to retrieve objects in the restcontroller?

You cannot use multiple #RequestBody annotations in Spring. You need to wrap all these in an object.
Some like this
// some imports here
public class IncomingRequestBody {
private Webapp webapp;
private String propertryFiles;
private String propertyText;
// add getters and setters here
}
And in your controller
#RequestMapping(value = "/test", method = RequestMethod.POST)
public String test(#RequestBody IncomingRequestBody requestBody) {
System.out.println(requestBody.getPropertyFiles());
// other statement
return "ok good";
}
Read more here
Passing multiple variables in #RequestBody to a Spring MVC controller using Ajax

Based on the sample postman payload you gave, you will need:
public class MyObject {
private MyWebapp webapp;
private String propertyFiles;
private String propertytText;
// your getters /setters here as needed
}
and
public class MyWebapp {
private String webappName;
private String path;
private boolean isQA;
// getters setters here
}
Then on your controller change it to:
#RequestMapping(value = "/test", method = RequestMethod.POST)
public String test(#RequestBody MyObject payload) {
// then access the fields from the payload like
payload.getPropertyFiles();
return "ok good";
}

Related

multipart/form-data MutlipartFile is not set through all args constructor

Background
The problem came up on my day job while implementing a POST multipart/form-data endpoint for a file upload with some meta information. I am not an expert in the Spring Boot ecosystem; it is likely that the problem is solved by a simple fix and that I am just missing the right term to search for.
Problem statement
To implement an endpoint for a file-upload with additional meta information, I wrote the following #RestController:
#RestController
#RequestMapping(Resource.ROOT)
#AllArgsConstructor(onConstructor = #__({#Inject}))
public class Resource {
public static final String ROOT = "/test";
private final Logger logger;
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON)
public ResponseEntity<Void> test(#Valid final Request request) {
logger.info("request = {}", request);
return ResponseEntity.ok().build();
}
}
With Request being specified as:
#Value
#AllArgsConstructor
public class Request {
#NotNull
String name;
#NotNull
MultipartFile file;
}
And a small happy path test:
#SpringBootTest
#AutoConfigureMockMvc
class TestCase {
#Autowired
private MockMvc mockMvc;
#Test
void shouldReturnOk() throws Exception {
// GIVEN
final byte[] content = Files.readAllBytes(Path.of(".", "src/test/resources/PenPen.png"));
final String name = "name";
// WHEN
// #formatter:off
mockMvc.perform(MockMvcRequestBuilders
.multipart(Resource.ROOT)
.file("file", content)
.param("name", name))
// THEN
.andExpect(status().isOk());
// #formatter:on
}
}
A complete MRE can be found on Bitbucket, branch problem-with-immutable-request.
When running the test (./mvnw test), it fails with the endpoint returning a 400 BAD REQUEST instead of 200 OK. Reading the logs reveals that request parameter file is null:
...
Content type = text/plain;charset=UTF-8
Body = file: must not be null.
...
I partially understand why it is null. With this partial knowledge, I was able to circumvent the problem by making the field file in Request mutable:
#ToString
#Getter
#AllArgsConstructor
public class Request {
#NotNull
private final String name;
#Setter
#NotNull
private MultipartFile file;
}
The code "fixing" the problem can be found on Bitbucket, branch problem-solved-by-making-field-mutable.
This, however, makes the Request mutable, which I would like to prevent. To further investigate, I unrolled the lombok annotations on Request and added some logging:
public class Request {
private static final Logger LOGGER = LoggerFactory.getLogger(Request.class);
#NotNull
private final String name;
#NotNull
private MultipartFile file;
public Request(final String name, final MultipartFile file) {
this.name = name;
this.setFile(file);
}
public #NotNull String getName() {
return this.name;
}
public #NotNull MultipartFile getFile() {
return this.file;
}
public String toString() {
return "Request(name=" + this.getName() + ", file=" + this.getFile() + ")";
}
public void setFile(final MultipartFile file) {
LOGGER.info("file = {}", file);
this.file = file;
}
}
Code of unrolled version can be found on Bitbucket, branch lombok-unrolled-for-debugging.
When looking at the log statements of the now successful test, we can see that Request::setFile is called twice:
2020-09-05 09:42:31.049 INFO 11012 --- [ main] d.turing85.springboot.multipart.Request : file = null
2020-09-05 09:42:31.056 INFO 11012 --- [ main] d.turing85.springboot.multipart.Request : file = org.springframework.mock.web
The first call comes from the constructor invocation. The second call, I imagine, comes from somewhere within Spring's mapping mechanism for the form parameters.
I know that there is the possibility to define the form parameters individually on the endpoint and constructing the Request instance within the method:
public class Resource {
...
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON)
public ResponseEntity<Void> test(
#RequestPart(name = "name") final String name,
#RequestPart(name = "file") final MultipartFile file) {
final Request request = new Request(name, file);
logger.info("request = {}", request);
return ResponseEntity.ok().build();
}
}
This will, however, result in other problems. For example, we would have to add an additional exception mapper for MissingServletRequestPartException and align the returned HTTP response with the existing response for BindException. I would like to avoid this if possible.
A search on the topic turned up Spring Boot controller - Upload Multipart and JSON to DTO. The solution, however, did not work for me since I do not use MVC (I think).
Question
Is there a possibility to keep Request immutable such that Spring is able to pass the MultipartFile to the all args constructor instead of setting it through the setter afterwards? Writing a custom mapper/converter is acceptable, but I did not find a possibility to write a mapper for either a specific endpoint or a specific type.
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> test(#Valid #ModelAttribute final RequestDto request) {
return ResponseEntity.ok().build();
}
It is still working with rest api call. But i really do not get immutability concern of yours.
If you define setter the multipart data you can use ModelAttribute.
#SpringBootTest
#AutoConfigureMockMvc
class FileUploadControllerIT {
#Autowired
private MockMvc mockMvc;
#Test
void shouldReturnOk() throws Exception {
// GIVEN
final byte[] content = Files.readAllBytes(Paths.get(Thread.currentThread().getContextClassLoader().getResource("text.txt").toURI()));
final String name = "name";
// WHEN
// #formatter:off
mockMvc.perform(MockMvcRequestBuilders
.multipart("/context/api/v1")
.file("multipartFile", content)
.param("name", name))
// THEN
.andExpect(status().isOk());
// #formatter:on
}
}
The above code works with ModelAttribute.
Also you are giving absolute path, i guess it is wrong. You can get file with classloader.

PageNotFound - Request method 'GET' not supported

I am getting this error while calling an API from postman, after I hosted my spring app in VM. Locally it works. But Get methods in my VMs are working.
[http-nio-8081-exec-4] PageNotFound - Request method 'GET' not supported
My controller method looks like this:
#RestController
#RequestMapping("/orders/")
public class OrdersController {}
#PostMapping(value = "create", produces = "text/plain")
private String createOrder(#RequestBody POCreationRequest request) throws ParseException {
The API request running forever and dont get any response. I found the exception in my log. Any idea on this issue?
You created two urls there:
url/orders/ -> accepts get/post/etc... (though its not implemented)
url/orders/create -> accepts post
#RestController
#RequestMapping("/orders")
public class OrdersController {
#PostMapping(value = "create", produces = "text/plain")
private String createOrder(#RequestBody POCreationRequest request) throws ParseException {
System.out.println(request)}
}
You can try the above code.
You are trying to make a GET request on an only POST endpoint, thus then not loading the page. Your endpoint should be of type GET. You can also have the same endpoint for GET and POST requests as follows:
#RestController
#RequestMapping("/orders/")
public class OrdersController {}
#PostMapping(value = "create", produces = "text/plain")
private String createOrder(#RequestBody POCreationRequest request) throws ParseException {
//Parse post requests
}
#GetMapping(value= "create")
private String servePage() {
return create; //create is the name of the html view.
}
Now when going to localhost:8080/orders/create it should serve the view.
You can also make the GET mapping return a JSON object by:
#GetMapping(value= "create")
private String serveJSON() {
return "{\"hello\": \"world\"}"; //The string is treated as JSON and not as a view.
}

How to access Spring controller's JSON parameter?

I have a Spring controller which maps me JSON to some DTO object via message converter:
#RequestMapping(value = {"bar"},
consumes = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST)
public void foo(#RequestBody paramsDTO httpParams) {
// some logic here
}
Let's imagine, my DTO looks like:
public class paramsDTO {
String name;
String surName;
}
Is it possible to get access to name or surName values via some Servlet or Spring context (not directly from field)? I'm looking for something similar to HttpServletRequest#getParameterValues() but for the Spring JSON converter.

Spring Boot REST: #DeleteMapping that consuming form_urlencoded not work as expect

I'm using Spring boot 1.4.0, Consider below code in a #RestController, what I expect is, the server side will receive a http body with form_urlencoded content type, but unfortunately it demands me a query parameter type with email and token. What's the problem here and how to fix?
#DeleteMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
#ResponseStatus(HttpStatus.NO_CONTENT)
public void removeAdmin(#RequestParam(value = "email") String email, #RequestParam(value = "token") String token) {
//...
}
#DeleteMapping is only a convenience extension the provides #RequestMapping(method=DELETE) It will not handle request paramters. You will still have to map those in the controllers method signature if you need the data to perform the work.
Since you want a body, You could create an object and mark it as #RequestBody:
public class DeleteBody {
public String email;
public String token;
}
public void removeAdmin(#RequestBody DeleteBody deleteBody) {
...
}

How to validate request parameter if it is not a bean in spring MVC?

Below is a POST end point in my spring MVC REST service. I want to use spring validation frame work to make sure that list I receive is not empty. How do I do it? Do I have to provide wrapper bean to around listOfLongs?
#RequestMapping(value = "/some/path", method = RequestMethod.POST)
#ResponseBody
public Foo bar(#Valid #NotEmpty #RequestBody List<Long> listOfLongs) {
/* if (listOfLongs.size() == 0) {
throw new InvalidRequestException();
}
*/
// do some useful work
}
What should be the Request Body?
1) [123,456,789]
2) { listOfLongs : [123,456,789]}
Providing a wrapper bean is a good practice.
class LongList {
#NotEmpty
private List<Long> listOfLongs;
// Setters and Getters ...
}
Then, the Request Body should be { listOfLongs : [123,456,789]}
#RequestMapping(value = "/some/path", method = RequestMethod.POST)
#ResponseBody
public Foo bar(#Valid #RequestBody LongList listOfLongs) {
// do some useful work
}

Categories