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

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);
}

Related

Spring Boot Rest Controller with multipart-form-data

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 ?

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";
}
}

Spring Boot - how to allow CORS on REST Controller HTTP PUT

I'm my Spring Boot REST controller, I'm able to allow CORS for HTTP POST, but for some reason HTTP PUT is still being blocked.
I have placed my CORS decorator at the Controller level - HTTP PUT handler still being blocked.
Here is my controller:
package com.khoubyari.example.api.rest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import com.khoubyari.example.domain.Hotel;
import com.khoubyari.example.exception.DataFormatException;
import com.khoubyari.example.service.HotelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
#RestController
#RequestMapping(value = "/example/v1/hotels")
#Api(tags = {"hotels"})
//#CrossOrigin(origins = "http://localhost:4200")
#CrossOrigin( origins = "*" , allowedHeaders = "*")
public class HotelController extends AbstractRestHandler {
#Autowired
private HotelService hotelService;
#RequestMapping(value = "",
method = RequestMethod.POST,
consumes = {"application/json", "application/xml"},
produces = {"application/json", "application/xml"})
#ResponseStatus(HttpStatus.CREATED)
#ApiOperation(value = "Create a hotel resource.", notes = "Returns the URL of the new resource in the Location header.")
public void createHotel(#RequestBody Hotel hotel,
HttpServletRequest request, HttpServletResponse response) {
Hotel createdHotel = this.hotelService.createHotel(hotel);
response.setHeader("Location", request.getRequestURL().append("/").append(createdHotel.getId()).toString());
}
#RequestMapping(value = "",
method = RequestMethod.GET,
produces = {"application/json", "application/xml"})
#ResponseStatus(HttpStatus.OK)
#ApiOperation(value = "Get a paginated list of all hotels.", notes = "The list is paginated. You can provide a page number (default 0) and a page size (default 100)")
public
#ResponseBody
Page<Hotel> getAllHotel(#ApiParam(value = "The page number (zero-based)", required = true)
#RequestParam(value = "page", required = true, defaultValue = DEFAULT_PAGE_NUM) Integer page,
#ApiParam(value = "Tha page size", required = true)
#RequestParam(value = "size", required = true, defaultValue = DEFAULT_PAGE_SIZE) Integer size,
HttpServletRequest request, HttpServletResponse response) {
return this.hotelService.getAllHotels(page, size);
}
#RequestMapping(value = "/{id}",
method = RequestMethod.GET,
produces = {"application/json", "application/xml"})
#ResponseStatus(HttpStatus.OK)
#ApiOperation(value = "Get a single hotel.", notes = "You have to provide a valid hotel ID.")
public
#ResponseBody
Hotel getHotel(#ApiParam(value = "The ID of the hotel.", required = true)
#PathVariable("id") Long id,
HttpServletRequest request, HttpServletResponse response) throws Exception {
Hotel hotel = this.hotelService.getHotel(id);
checkResourceFound(hotel);
return hotel;
}
#RequestMapping(value = "/{id}",
method = RequestMethod.PUT,
consumes = {"application/json", "application/xml"},
produces = {"application/json", "application/xml"})
#ResponseStatus(HttpStatus.NO_CONTENT)
#ApiOperation(value = "Update a hotel resource.", notes = "You have to provide a valid hotel ID in the URL and in the payload. The ID attribute can not be updated.")
public void updateHotel(#ApiParam(value = "The ID of the existing hotel resource.", required = true)
#PathVariable("id") Long id, #RequestBody Hotel hotel,
HttpServletRequest request, HttpServletResponse response) {
checkResourceFound(this.hotelService.getHotel(id));
if (id != hotel.getId()) throw new DataFormatException("ID doesn't match!");
this.hotelService.updateHotel(hotel);
}
//todo: #ApiImplicitParams, #ApiResponses
#RequestMapping(value = "/{id}",
method = RequestMethod.DELETE,
produces = {"application/json", "application/xml"})
#ResponseStatus(HttpStatus.NO_CONTENT)
#ApiOperation(value = "Delete a hotel resource.", notes = "You have to provide a valid hotel ID in the URL. Once deleted the resource can not be recovered.")
public void deleteHotel(#ApiParam(value = "The ID of the existing hotel resource.", required = true)
#PathVariable("id") Long id, HttpServletRequest request,
HttpServletResponse response) {
checkResourceFound(this.hotelService.getHotel(id));
this.hotelService.deleteHotel(id);
}
}
What should I change in order for the HTTP PUT handler to allow updates?
You may need to specify allowed methods explicitely like this in your CORS config (I'm using Kotlin and implementing WebMvcConfigurer):
override fun addCorsMappings(registry: CorsRegistry) {
log.info("CORS: {}", origins)
registry.addMapping("/**").allowedOrigins(*origins)
.allowedMethods("GET", "POST", "PUT", "OPTIONS")
}
PUT is not allowed by default, as can be seen in CorsConfiguration#DEFAULT_PERMIT_METHODS:
private static final List<String> DEFAULT_PERMIT_METHODS = Collections.unmodifiableList(
Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));

#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)
);
}

How to create Exception with HttpStatus and send it using Resource of Spring-Hateoas?

I am using Spring-Boot 1.2.7 for developing Spring-Hateoas application with Spring-Data-JPA.
I have developed controller class with methods which returns Resource.
I want to create Exception with HttpStatus and use it in controller class for GET, POST, PUT and DELETE. Please assist me, I am new to this.
Controller Class - ArticleController
#RestController
#RequestMapping(value = "/api/articles")
public class ArticleController {
#Autowired
private ArticleService articleService;
#Autowired
private ArticleRepository articleRepository;
#Autowired
private ArticleResourceAssembler articleResourceAssembler;
/*#RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Collection<Resource<Article>> getArticles() {
Collection<Article> articles = articleService.findAll();
List<Resource<Article>> resources = new ArrayList<Resource<Article>>();
for (Article article : articles) {
resources.add(getArticleResource(article));
}
return resources;
}*/
#RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public PagedResources<Article> getArticles(Pageable pageable, PagedResourcesAssembler assembler) {
Page<Article> articles = articleService.findAll(pageable);
return assembler.toResource(articles, articleResourceAssembler);
}
#RequestMapping(value = "/{article_id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Resource<Article> getArticle(#PathVariable(value = "article_id") long article_id) {
Article article = articleService.findOne(article_id);
if (article == null) {
ResponseEntity.status(HttpStatus.NOT_FOUND);
}
return getArticleResource(article);
}
// Insert Article
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Article> createArtilce(#RequestBody Article article) {
article.setCreated(new Date());
Article savedArticle = articleService.create(article);
article.add(linkTo(methodOn(ArticleController.class).getArticle(savedArticle.getArticle_id()))
.withSelfRel());
// I want to return here HttpStatus.NOT_FOUND
}
private Resource<Article> getArticleResource(Article article) {
Resource<Article> resource = new Resource<Article>(article);
// Link to Article
resource.add(linkTo(methodOn(ArticleController.class).getArticle(article.getArticle_id())).withSelfRel());
return resource;
}
}
You need to implement a class for the exception (that is extends from RuntimeException) and annotated with #ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) or another Status code, and where you want throw that status, you need throw your CustomException.
For Exceptions you didn't write, can use a exception handler method inside your controller like this:
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
#ExceptionHandler({ExternalException.class})
public void metodoCuandoExcepcionEsLanzada(){
//logging and processing
}

Resources