ControllerAdvice is not called without #RequestBody - spring

If i use #RequestBody, everything works fine. However, i cannot use #RequestBody when sending files.
#PostMapping
public ResponseEntity<?> postProduct(#Valid PostProduct postProduct, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
for (ObjectError objectError : bindingResult.getAllErrors()) {
System.out.println(objectError.toString());
}
}
return ResponseEntity.ok().build();
}
The error is still thrown but the handleMethodArgumentNotValid of ControllerAdvice is not invoked.
I don't understand this behavior.
UPDATE 1
Here's the PostProduct class
#Data
public class PostProduct {
#NotBlank(message = "product name must not be blank")
private String name;
#NotEmpty
#UniqueElements
private List<#NotNull #Positive Integer> materials;
#NotEmpty
#UniqueElements
private List<#NotNull #Positive Integer> colors;
#NotNull
#Positive
private Integer price;
#NotNull
private List<#NotNull #ProductImageConstraint MultipartFile> images;
}
The ControllerAdvice:
#ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleMethodArgumentNotValid");
Map<String, String> errors = new HashMap<>();
List<ObjectError> objectErrors = ex.getBindingResult().getAllErrors();
return returnError(objectErrors);
}
public static ResponseEntity<Object> returnError(List<ObjectError> objectErrors) {
Map<String, String> errors = new HashMap<>();
objectErrors.forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest().body(errors);
}
}
I use 1 custom validator, don't know it has anything to do with this strange behavior but i'll post it anyway.
#Documented
#Constraint(validatedBy = ProductImageValidator.class)
#Target( { FIELD, TYPE_USE })
#Retention(RetentionPolicy.RUNTIME)
public #interface ProductImageConstraint {
String message() default "not image or file empty";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class ProductImageValidator implements
ConstraintValidator<ProductImageConstraint, MultipartFile> {
#Override
public void initialize(ProductImageConstraint constraintAnnotation) {
}
#Override
public boolean isValid(MultipartFile image, ConstraintValidatorContext context) {
boolean isValid = false;
String contentType = image.getContentType();
if (contentType != null && contentType.contains("image") && !image.isEmpty()) {
isValid = true;
}
return isValid;
}
}

Found the Solution here BindException thrown instead of MethodArgumentNotValidException
Basically, when your content type is not application/json, BindException will be thrown, otherwise MethodArgumentNotValidException will be.

Related

How can I moderate #PathVariable?

I have following methods.
public ResponseEntity some(#PathVariable("id") final String id_) {
final Long id = ((Supplier<Long>) () -> {
try {
if (id_ == null || id_.equals("dontcare")) {
return null;
}
return Long.parseLong(id_);
} catch (final NumberFormatException nfe) {
throw new RuntimeException(nfe);
}
}).get();
}
As you can assume I want my parameter value \d+ or dontcare.
How or What can I use for do following?
public ResponseEntity some(
#Dontcare #PathVariable("id") final Long id) {
}
I found WebArgumentHandler, HandlerMethodArgumentResolver, and PathVariableMethodArgumentResolver, but I can't find any good example.
Please help me.
UPDATE
I prepared following class.
public class DontcarePathVariableResolver
extends PathVariableMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
final boolean flag = parameter.getMethodAnnotation(
DontcarePathVariable.class) != null;
logger.info("flag: {}", flag);
return flag;
}
#Override
protected Object resolveName(String name, MethodParameter parameter,
NativeWebRequest request)
throws Exception {
#SuppressWarnings({"unchecked"})
final Map<String, String> templateVariables
= (Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST);
final String pathValue = templateVariables.get(name);
return new DontcareConverter().convert(pathValue);
}
}
And added it.
#Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new DontcarePathVariableMethodArgumentResolver());
logger.info("resolver added");
//super.addArgumentResolvers(resolvers);
}
}
But it did come to play.
I think a default handler for Long type is already in play.

Spring MVC: The JsonView And ResponseBodyAdvice Beans

I have a ResponseBodyAdvice bean which wraps the returning objects from rest controller with a specific form:
public class Response<T> {
private T data; //the data is from the rest controller
private int code;
private String message;
}
#RestControllerAdvice
public class ResponseAdviceConfig implements ResponseBodyAdvice {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Response) {
return body;
} else {
return new Response<>(body);
}
}
}
#Data
public class User {
#JsonView(SummaryView.class)
private String account;
#JsonView(SummaryView.class)
private String avatar;
private String realname;
#JsonView(SummaryView.class)
private String nickname;
private String password;
public interface SummaryView {
}
}
#RestController
#RequestMapping("/v1/user")
public class UserAPI {
#GetMapping("/follows/list")
#JsonView(User.SummaryView.class)
public List<User> followsList(#RequestParam String account){
return userService.followsList(account);
}
}
But the problem is that the #JsonView annotated method in controller doesn't work anymore and the final response just a empty object {}. I guess that this ResponseBodyAdvice bean may conflict with the internal JsonViewResponseBodyAdvice. I debuged but without result.

Throwing Custom Exception with HTTP Response From Spring Validator

I have implemented a custom Validator in Spring which is called inside an overridden Jackson de-serializer. If validation fails, I want the HTTP response code to be a 403 Forbidden as defined in my ControllerAdvice.
However, the response is always 400 Bad Request.
public class InterceptedDeserializer extends StdDeserializer<Object> implements ResolvableDeserializer
{
public InterceptedDeserializer(JsonDeserializer<?> defaultDeserializer)
{
super(Object.class);
this.defaultDeserializer = defaultDeserializer;
}
#Override public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, My403Exception
{
this.classFieldValidator = ServletUtils.findWebApplicationContext().getBean(ClassFieldValidator.class);
Object deserializedObject = defaultDeserializer.deserialize(jp, ctxt);
Errors errors = new BeanPropertyBindingResult(deserializedObject, deserializedObject.getClass().getName());
classFieldValidator.validate(deserializedObject, errors);
if(errors.hasErrors() || errors.hasFieldErrors()){
throw new My403Exception("No funny business");
}
return deserializedObject;
}
}
#ControllerAdvice
public class ValidationControllerAdvice {
private static final Logger log = LoggerFactory.getLogger(ValidationControllerAdvice.class);
private final StringWriter sw = new StringWriter();
#ResponseBody
#ExceptionHandler(My403Exception.class)
#ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorResponse my403Exception(My403Exception e) {
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setErrorCode("my403");
errorResponse.setDescription(e.getMessage());
errorResponse.setMessage(e.getMessage());
e.printStackTrace(new PrintWriter(sw));
String eStackTrace = sw.toString();
log.error("My403 error message: " + e.getMessage() + "\nException Class:" + e.getClass() + "\nStack Trace:" + eStackTrace);
return errorResponse;
}
}
#ResponseStatus(value = HttpStatus.FORBIDDEN)
public class My403Exception extends RuntimeException{
private String message;
public My403Exception(String message) {
super(message);
this.message = message;
}
public My403Exception() {
}
#Override
public String getMessage() {
return message;
}
}
#ResponseStatus(HttpStatus.CREATED)
#RequestMapping(method = RequestMethod.POST, path = "/thing")
public void createmyThing(#RequestParam(value = "thing") String thing, #RequestBody() #Valid MyThing thing) throws My403Exception {
thingService.createThing(thing);
}

spring-mvc-rest-return-standard-response not working using #ControllerAdvice

I want to create a standard response using #ControllerAdvice
I have written a Custom Annotation for setting status code and message.
I am getting class cast exception in
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
JsonFilter annotation = returnType.getMethodAnnotation(JsonFilter.class);
String statusCode = annotation.status();
String message = annotation.message();
ResponseHeader<Object> responseHeader = new ResponseHeader(statusCode,message,body);
System.out.println(responseHeader);
return responseHeader;
}
when I am using JsonFilter annotation = returnType.getMethodAnnotation(JsonFilter.class);
Here is my classes that I have created -
#RestControllerAdvice
public class RestResponseHandler implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return (AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null ||
returnType.getMethodAnnotation(ResponseBody.class) != null);
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
JsonFilter annotation = returnType.getMethodAnnotation(JsonFilter.class);
String statusCode = annotation.status();
String message = annotation.message();
ResponseHeader<Object> responseHeader = new ResponseHeader(statusCode,message,body);
System.out.println(responseHeader);
return responseHeader;
}
public class CommentController {
#RequestMapping(value = "/repos", method = RequestMethod.GET)
#JsonFilter(status="0",message="done")
public #ResponseBody String repos() throws IOException {
return null;
}
}
#Target(ElementType.METHOD)
#Documented
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonFilter {
String status() default "0";
String message() ;
}
public class ResponseHeader<Object> {
private String code;
private String message;
private Object body;
public ResponseHeader(String code, String message,Object body) {
super();
this.code = code;
this.message = message;
this.body = body;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
public Object getBody() {
return body;
}
Need to override the supports method.
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
List<Annotation> annotations = Arrays.asList(returnType.getMethodAnnotations());
return annotations.stream().anyMatch(annotation -> annotation.annotationType().equals(JsonFilter.class));
}

#RestController: Validate the #ResponseBody (not the #RequestBody)

How can I achive that the #ResponseBody (in my case a class of type SomePojoInterface) is automatically validated (lets say through JSR-303 validation). Nice to have would be, that in case of a validation-failure the handler would throw an Exception which can be handled in some #ControllerAdvice annotated class.
My code so far.
#RestController
public class MyRestController {
#GetMapping(value = "validate", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
protected SomePojo validateResponse() {
return new SomePojo();
}
}
#ControllerAdvice
class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.XXX)
#ExceptionHandler(MyResponseValidationException.class)
public void handleResponseValidationException() {
// ...
}
}
public class SomePojo implements SomePojoInterface {
#NotNull
private String someValue;
// getter / setter
}
If you have annotated your class SomePojo, then:
#GetMapping(value = "validate", produces = MediaType.APPLICATION_JSON_VALUE)
protected SomePojo validateResponse() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
SomePojo somePojo = new SomePojo(null);
Set<ConstraintViolation<Car>> constraintViolations = validator.validate(somePojo);
// Other stuff
}
#Valid annotation is for request. More examples from their docs. I am not sure what all you want to validate
I managed to achieve this through the #RestControllerAdvice.
#RestControllerAdvice
public class RestPostProcessingAdvice implements ResponseBodyAdvice<SomePojoInterface> {
#Inject
private Validator validator;
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if(doSomeChecksIfEligiable(returnType, converterType)) {
return true;
}
return false;
}
#Override
public SomePojoInterface beforeBodyWrite(SomePojoInterface body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(body);
if(constraintViolations.size() > 0) {
response.setStatusCode(HttpStatus.XXX);
LOG.fatal("Sorry, I'm sending crap");
}
return body;
}
}
Be aware that throwing an Exception and catching it in an #ExceptionHandler that is sending the same (mofified) object out in the #ResponseBody could lead to an endless loop, since the object will be checked again this #RestControllerAdvice.

Resources