2

I'm trying to use Spring Data REST repositories annotated with @RepositoryRestResource annotation together with custom methods implementation. There are 2 cases:

1) I have REST repository annotated with @RepositoryRestResource which is mapped to /users endpoint. Also, I have @RestController which is mapped to the same endpoint. That results in methods (which should be exposed) in @RepositoryRestResource to not be visible and getting 405 result on them. However method validation with @Valid annotation is working on @RestController methods. e.g. this works:

@ResponseBody
@RequestMapping(value = "/users")
public ResponseEntity signUp(@RequestBody @Valid final UserSignUpRequest userSignUpRequest)

2) Controllers which are working together with REST repositories are @RepositoryRestController controllers. This way both methods declared in @RepositoryRestController and @RepositoryRestResource are working. However JSR-303 @Valid annotation on methods stopped working, so I can't use @Valid annotation. This issue is already described DATAREST-593.

Any ideas how at least one of two issues can be solved? The main idea is to use @RepositoryRestResource repositories together with custom controller methods and annotation validation.

yyunikov
  • 5,719
  • 2
  • 43
  • 78

2 Answers2

9

You could also add this to your @RepositoryRestController :

@Inject
private LocalValidatorFactoryBean validator;

@InitBinder
protected void initBinder(WebDataBinder binder) {
    binder.addValidators(validator);
}
Sébastien Nussbaumer
  • 6,202
  • 5
  • 40
  • 58
  • 1
    your solution seems very compact. It works, I get a 400 error but the json of the response is different from the one of "standard" SDR repository, is there a way to customite it? – drenda Nov 30 '17 at 14:11
2

Seems that there are no good solution in this case and @Valid annotation is not supported by default in any way, see DATAREST-593. That why, to support @Valid annotation on @RepositoryRestController methods, I've created the following @ControllerAdvice class:

package com.tivoli.api.application.advice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import javax.validation.Valid;
import javax.validation.ValidationException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

/**
 * Workaround class for making JSR-303 annotation validation work for controller method parameters.
 * Check the issue <a href="https://jira.spring.io/browse/DATAREST-593">DATAREST-593</a>
 */
@ControllerAdvice
public class RequestBodyValidationProcessor extends RequestBodyAdviceAdapter {

    private final Validator validator;

    public RequestBodyValidationProcessor(@Autowired final Validator validator) {
        this.validator = validator;
    }

    @Override
    public boolean supports(final MethodParameter methodParameter, final Type targetType, final Class<? extends
            HttpMessageConverter<?>> converterType) {
        final Annotation[] parameterAnnotations = methodParameter.getParameterAnnotations();
        for (final Annotation annotation : parameterAnnotations) {
            if (annotation.annotationType().equals(Valid.class)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public Object afterBodyRead(final Object body, final HttpInputMessage inputMessage, final MethodParameter
            parameter, final Type targetType, final Class<? extends HttpMessageConverter<?>> converterType) {
        final Object obj = super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
        final BindingResult bindingResult = new BeanPropertyBindingResult(obj, obj.getClass().getCanonicalName());
        validator.validate(obj, bindingResult);
        if (bindingResult.hasErrors()) {
            throw new ValidationException(createErrorMessage(bindingResult));
        }

        return obj;
    }

    private String createErrorMessage(final BindingResult bindingResult) {
        final StringBuilder stringBuilder = new StringBuilder("Invalid parameters specified.");
        if (bindingResult.getFieldErrors() != null && !bindingResult.getFieldErrors().isEmpty()) {
            stringBuilder.append(" Fields:");
            bindingResult.getFieldErrors().forEach(fieldError -> stringBuilder
                    .append(" [ ")
                    .append(fieldError.getField())
                    .append(" : ")
                    .append(fieldError.getRejectedValue())
                    .append(" ] "));
        } else if (bindingResult.getAllErrors() != null && !bindingResult.getAllErrors().isEmpty()) {
            final ObjectError objectError = bindingResult.getAllErrors().get(0); // get the first error
            stringBuilder.append(" Message: ")
                    .append(objectError.getDefaultMessage());
        }

        return stringBuilder.toString();
    }
}
yyunikov
  • 5,719
  • 2
  • 43
  • 78
  • 1
    Thanks for sharing. Your solution product a response that is very different from the one returned from a standard SDR repository. Furthemore in your case is returned a 500 http status instead of a 4xx. Some advice? – drenda Nov 30 '17 at 14:05
  • You can customize the response in createErrorMessage method. For different status code, you just need to catch ValidationException in corresponding exception handler. – yyunikov Nov 30 '17 at 14:08