Spring Boot Rest Controller with multipart-form-data - spring-boot

I have those 2 classes.
#Data
public class Book {
private String author;
private String title;
}
and
#Data
public class Annex {
private String name;
private String mimetype;
private MultipartFile file;
}
My controller looks like
#RestController
public class BookController {
#PostMapping(value = "/books", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> book() {
return new ResponseEntity<>("{\"status\": \"Ok\"}", HttpStatus.CREATED);
}
}
I can do something like
#PostMapping(value = "/books", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> book(
#RequestPart("book") Book book,
#ModelAttribute Annex annex
) {
System.out.println(book);
System.out.println(annex);
return new ResponseEntity<>("{\"status\": \"Ok\"}", HttpStatus.CREATED);
}
My postman request is
curl --location --request POST 'http://127.0.0.1:8091/books' --form 'book=#"/C:/Downloads/book.json"' --form 'name="Index"' --form 'mimetype="application/pdf"' --form 'file=#"/C:/Downloads/book.json"'
and it is working.
But how can I do if I want multiple annexes ?
I tried with
#PostMapping(value = "/books2", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> book2(
#RequestPart("book") Book book,
#ModelAttribute List<Annex> annexes
) {
System.out.println(book);
System.out.println(annexes);
return new ResponseEntity<>("{\"status\": \"Ok\"}", HttpStatus.CREATED);
}
with the same postman request I have
{
"code": 500,
"status": "INTERNAL_SERVER_ERROR",
"message": "No primary or single unique constructor found for interface java.util.List",
"timestamp": "2022-09-27 10:24:09"
}
If I try with a #RequestPart("annex") for the List<Annex> annexes how can I put the file in postman ?

Related

Spring boot: Sending a JSON to a post request that uses a model as a param

Lets say I have a predefined post mapping as such:
#PostMapping(value = "/add", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> addVal(#RequestBody final valDetail newVal) {
//Do Stuff
}
and the valDetail object as follows:
#Data
#Component
#Entity
#Table(name = "val_portal")
public class valDetail {
#Id
#Column(name = "valcode")
private String valCode;
#Column(name = "valname")
private String valName;
}
How would I go about actually sending JSON values from a separate service to this /add endpoint so that they are properly received as a valDetail object?
Currently I tried this implementation but I keep getting a 415 response code.
JSONObject valDetail = new JSONObject();
valDetail.put("valCode",request.getAppCode().toLowerCase());
valDetail.put("valName", request.getProjectName());
String accessToken = this.jwtUtility.retrieveToken().get("access_token").toString();
HttpHeaders authHeaders = new HttpHeaders();
authHeaders.setBearerAuth(accessToken);
authHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<String>(valDetail.toString(), authHeaders);
ResponseEntity<String> loginResponse = restTemplate.exchange(uri,
HttpMethod.POST,
entity,
String.class);
If you want to pass data as json you don't want to take Model try to use #ResponseBody annotation to transfer data through json.
#PostMapping(value = "/add", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public ResponseEntity<String> addVal(#RequestBody final valDetail newVal) {
//Do Stuff
}

How can I hide #ApiResponse form #ControllerAdvice for an endpoint?

I'm trying to migrate our manually writen OpenAPI (swagger) to a generated OpenAPI using springdoc-openapi for our Spring-Boot application. We got some issues, because the controller responses (mostly ErrorCodes) didn't match to the documentatation.
We already used a #ControllerAdvice annotated handler configuration. Here a snippet:
#ControllerAdvice
public class ExceptionHandler {
#ResponseStatus(code = HttpStatus.NOT_FOUND)
#ApiResponse(responseCode = "404", description = "(NOT FOUND) Resource does not exist!", content = #Content)
#ExceptionHandler(NotFoundException.class)
public void handleException(NotFoundException e) {
log.warn("Returning {} due to a NotFoundException: {}", HttpStatus.NOT_FOUND, e.toString());
}
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ApiResponse(responseCode = "400", description = "(BAD REQUEST) Given resource is invalid!", content = #Content)
#ExceptionHandler(InvalidResourceException.class)
public void handleException(InvalidResourceExceptione) {
log.error("Invalid resource: {}", e.toString());
}
The generated API now showed all defined ApiResponses as responses for all controllers and endpoints. So I splittet the handler config using #ControllerAdvice(basePackageClasses = MyController.class) to group the possible exceptions. But there are still responses that are not fitting to all endpoints of a controller. Like:
#RestController
public class MyController {
#ResponseStatus(HttpStatus.CREATED)
#Operation(summary = "Create", description = "Create myResource!")
#PostMapping(value = "/myResources/", produces = {"application/json"})
#ResponseBody
public Integer create(#RequestBody MyResource newResource) throws InvalidResourceException {
return creationService.createResource(newResource).getId();
}
#ResponseStatus(HttpStatus.OK)
#Operation(summary = "Update", description = "Update myResource!")
#PutMapping(value = "/myResources/{id}", produces = {"application/json"})
public void update(#PathVariable("id") Integer id, #RequestBody MyResource newResource)
throws ResourceNotFoundException, InvalidResourceException {
return updateService.updateResource(id, newResource);
}
#ResponseStatus(HttpStatus.OK)
#Operation(summary = "Get", description = "Get myResource!")
#GetMapping(value = "/myResources/{id}", produces = {"application/json"})
#ResponseBody
public MyResource get(#PathVariable("id") Integer id) throws ResourceNotFoundException {
return loadingService.getResource(id);
}
}
POST will never respond with my 'business' 404 and GET will never respond with my 'business' 400. Is it possible to annotate an endpoint, so that not possible response codes are hidden in the API?
I tried to override the responses, but didn't work as intended:
#ResponseStatus(HttpStatus.OK)
#Operation(summary = "Get", description = "Get myResource!")
#ApiResponses({#ApiResponse(responseCode = "200", description = "(OK) Returning myResource"),
#ApiResponse(responseCode = "404", description = "(NOT FOUND) Resource does not exist!")})
#GetMapping(value = "/myResources/{id}", produces = {"application/json"})
#ResponseBody
public MyResource get(#PathVariable("id") Integer id) throws ResourceNotFoundException {
return loadingService.getResource(id);
}
400 still shows up...
You need to remove the #ApiResponse from your #ControllerAdvice class and need to add the respective response in your controller class, as mentioned by you.
#ResponseStatus(HttpStatus.OK)
#Operation(summary = "Get", description = "Get myResource!")
#ApiResponses({#ApiResponse(responseCode = "200", description = "(OK) Returning myResource"),
#ApiResponse(responseCode = "404", description = "(NOT FOUND) Resource does not exist!")})
#GetMapping(value = "/myResources/{id}", produces = {"application/json"})
#ResponseBody
public MyResource get(#PathVariable("id") Integer id) throws ResourceNotFoundException {
return loadingService.getResource(id);
}

Overloading SpringBoot #PostMapping controller method not working

I have faced some challenge and to describe shortly I created test application. Code you can see and error you can see below.
#RestController
public class TestController {
#PostMapping(value = "/test",params = { "a", "b" })
public String test(#RequestPart MultipartFile a, #RequestPart(required = false) MultipartFile b) {
return "test1";
}
#PostMapping(value = "/test", params = { "b" })
public String test(#RequestPart MultipartFile b) {
return "test2";
}
}
I`m trying to execute this request from postman:
And I`m getting such error in logs:
Resolved [org.springframework.web.bind.UnsatisfiedServletRequestParameterException:
Parameter conditions "a, b" OR "b" not met for actual request parameters: ]
The thing is, if I will put parameters also in postman (not in body, in request url: localhost:8080/test?b=anything) it will work fine, but I don`t need request params in url.
Is there some possible way to make it work?
I am able to override #PostMapping. But the type of the parameter should be different.
#PostMapping(value="/test" )
public String testApi(#ModelAttribute MultipartDTO multipartDTO) {
return "test1";
}
#PostMapping(value="/test" ,params = { "b" })
public String test(#RequestParam String b) {
return "test2";
}
/** DTO **/
#Data
public class MultipartDTO{
private MultipartFile a;
private MultipartFile b;
}
you can not map the same signature twice which contains the same Http methods then below error will occur.
java.lang.IllegalStateException: Ambiguous handler methods
try this one
#RestController
public class TestController {
#PostMapping("/test")
public String test(#RequestParam MultipartFile a, #RequestParam(required = false) MultipartFile b) {
return "test1";
}
#PostMapping("/test2")
public String test(#RequestParam MultipartFile b) {
return "test2";
}
}
You should try something like below.
#RestController
public class TestController {
#PostMapping(value = "/test")
public String test(#RequestParam MultipartFile a, #RequestParam(required = false) MultipartFile b) {
return "test1";
}
#PostMapping(value = "/test")
public String test(#RequestParam MultipartFile b) {
return "test2";
}
}

Use 2 objects in a controller function

I am new in SpringBoot.
I have 2 objects
public class Phone {
private long id;
private String phone;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
public class MailID {
private long id;
private String mail;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getMail() {
return mail;
}
public void setMail(String mail) {
this.mail = mail;
}
}
I have created a controller to get these objects.
#RestController
#RequestMapping("/person")
public class PersonController {
#RequestMapping(
value = "/mail",
method = RequestMethod.POST,
consumes = "application/json",
produces = "application/json"
)
public String getMail(#RequestBody MailID mail) {
System.out.println(mail.getId()+" "+mail.getMail());
return mail.getMail();
}
#RequestMapping(
value = "/phone",
method = RequestMethod.POST,
consumes = "application/json",
produces = "application/json"
)
public String getPhone(#RequestBody Phone phone) {
System.out.println(phone.getId()+" "+phone.getPhone());
return phone.getPhone();
}
#RequestMapping(
value = "/info",
method = RequestMethod.POST,
consumes = "application/json",
produces = "application/json"
)
public String mysqlToEs( #RequestBody MailID mail, #RequestBody Phone phone) {
System.out.println(mail.getMail()+" "+phone.getPhone());
return mail.getMail()+" "+phone.getPhone();
}
}
This commands works fine. It accept the MailID object values perfectly.
curl -X POST -d '{"id":1, "mail":"abc#yahoo.com"}' -H "Content-type:application/json" http://localhost:8084/person/mail
It accept the Phone object values perfectly.
curl -X POST -d '{"id":1, "phone":"3333212"}' -H "Content-type:application/json" http://localhost:8084/person/phone
Now i want to pass both MailID and Phone values in a single POST request. I tried the following curl request. BUt it didnt work.
curl -X POST -d '{"phone":{"id":1, "phone":"3333212"}}' -H "Content-type:application/json" http://localhost:8084/person/info
I have read that we can solve this issue sby creating a new object with fields as MailId and Phone. Is that the proper way of doing? Is there any other way, so that I can access both the object parameters in a single function itself?
Thanks
I suggest you to create a new object that holds the other two. Something like this:
class MyNewObjectRequest{
private MailID mailId;
private Phone phone;
... some getters/setters
}
Then you need to change the controller to receive the holder.
#RequestMapping(
value = "/info",
method = RequestMethod.POST,
consumes = "application/json",
produces = "application/json"
)
public String mysqlToEs( #RequestBody MyNewObjectRequest rq) {
System.out.println(rq.getEmailId().getMail()+" "+rq.getPhone().getPhone());
return rq.getEmailId().getMail()+" "+rq.getPhone().getPhone();
}
And the request json looks like this:
{"emailId" : {"id":1, "mail":"abc#yahoo.com"}, "phone" : {"id":1, "phone":"3333212"}}
if you have more than one object in the request body, you will have to use the
#RequestPart
annotation instead of
#RequestBody
for this to work, you have to name the fields (easy if it is an HTML form, not sure how to do that in curl).
one thing to consider is that this is bad style for REST APIs, because it would be asymmetric (one POST to create, but two different methods to GET). if you create one object with both fields, it is symmetric again.
because of that, i would consider NOT doing two create operations in one call.

#PathVariable Validation in Spring 4

How can i validate my path variable in spring. I want to validate id field, since its only single field i do not want to move to a Pojo
#RestController
public class MyController {
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public ResponseEntity method_name(#PathVariable String id) {
/// Some code
}
}
I tried doing adding validation to the path variable but its still not working
#RestController
#Validated
public class MyController {
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public ResponseEntity method_name(
#Valid
#Nonnull
#Size(max = 2, min = 1, message = "name should have between 1 and 10 characters")
#PathVariable String id) {
/// Some code
}
}
You need to create a bean in your Spring configuration:
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
You should leave the #Validated annotation on your controller.
And you need an Exceptionhandler in your MyController class to handle theConstraintViolationException :
#ExceptionHandler(value = { ConstraintViolationException.class })
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public String handleResourceNotFoundException(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
StringBuilder strBuilder = new StringBuilder();
for (ConstraintViolation<?> violation : violations ) {
strBuilder.append(violation.getMessage() + "\n");
}
return strBuilder.toString();
}
After those changes you should see your message when the validation hits.
P.S.: I just tried it with your #Size validation.
To archive this goal I have apply this workaround for getting a response message equals to a real Validator:
#GetMapping("/check/email/{email:" + Constants.LOGIN_REGEX + "}")
#Timed
public ResponseEntity isValidEmail(#Email #PathVariable(value = "email") String email) {
return userService.getUserByEmail(email).map(user -> {
Problem problem = Problem.builder()
.withType(ErrorConstants.CONSTRAINT_VIOLATION_TYPE)
.withTitle("Method argument not valid")
.withStatus(Status.BAD_REQUEST)
.with("message", ErrorConstants.ERR_VALIDATION)
.with("fieldErrors", Arrays.asList(new FieldErrorVM("", "isValidEmail.email", "not unique")))
.build();
return new ResponseEntity(problem, HttpStatus.BAD_REQUEST);
}).orElse(
new ResponseEntity(new UtilsValidatorResponse(EMAIL_VALIDA), HttpStatus.OK)
);
}

Resources