简体   繁体   中英

Bean validation (JSR-303) errors not serialized with Spring DATA REST

I'm using Spring Boot 2.1.11, Spring DATA REST, Hibernate. I'm trying make to work JRS 303 with SDR, so to get a JSON response for validations errors.

So far it works but only when I do a POST, when I do a PATCH I've an unexpected response. It seems the ConstraintViolationException is wrapped up as described here .

To give a complete scenario, this is my configuration:

CustomConfiguration.java:

@Configuration
@EnableRetry
@EnableTransactionManagement
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
// To activate the Spring Data Envers repository factory
@EnableJpaRepositories(basePackages = "server.repositories", repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
public class CustomConfiguration {
    public static CustomConfiguration INSTANCE;

    @PostConstruct
    public void init() {
        INSTANCE = this;
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public static SpringSecurityAuditorAware springSecurityAuditorAware() {
        return new SpringSecurityAuditorAware();
    }

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("classpath:/i18n/messages");

        messageSource.setUseCodeAsDefaultMessage(false);
        messageSource.setCacheSeconds((int) TimeUnit.HOURS.toSeconds(1));
        messageSource.setFallbackToSystemLocale(false);
        return messageSource;
    }

    @Bean
    public MessageSourceAccessor messageSourceAccessor() {
        return new MessageSourceAccessor(messageSource());
    }

    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setValidationMessageSource(messageSource());
        return factoryBean;
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator());
        return methodValidationPostProcessor;
    }

    @Bean
    public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
        return new SecurityEvaluationContextExtension();
    }


    @Bean
    FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
        FilterRegistrationBean<ForwardedHeaderFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new ForwardedHeaderFilter());
        return bean;
    }
}

GlobalRepositoryRestConfigurer.java

@Configuration
public class GlobalRepositoryRestConfigurer implements RepositoryRestConfigurer {

    @Autowired(required = false)
    private Jackson2ObjectMapperBuilder objectMapperBuilder;

    @Autowired
    private Validator validator;


    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.getCorsRegistry().addMapping(corsMapping).exposedHeaders(corsExposedHeaders).allowedOrigins(corsAllowedOrigins)
                .allowedHeaders(corsAllowedHeaders).allowedMethods(corsAllowedMethod).maxAge(corsMaxAge);

    }

    @Override
    public void configureConversionService(ConfigurableConversionService conversionService) {

    }

    @Bean
    public ValidationExceptionSerializer validationExceptionSerializer() {
        return new ValidationExceptionSerializer();
    }

    @Bean
    public CustomValidationExceptionSerializer customValidationExceptionSerializer() {
        return new CustomValidationExceptionSerializer();
    }

    @Bean
    public ConstraintViolationExceptionSerializer constraintViolationExceptionSerializer() {
        return new ConstraintViolationExceptionSerializer();
    }


    @Bean
    public Module customJacksonModule() {
        SimpleModule customJacksonModule = new SimpleModule();
        customJacksonModule.addSerializer(ConstraintViolationException.class, constraintViolationExceptionSerializer());
        customJacksonModule.addSerializer(ValidationException.class, validationExceptionSerializer());
        customJacksonModule.addSerializer(it.rebus.server.exceptions.ValidationException.class, customValidationExceptionSerializer());
        return customJacksonModule;
    }



    @Override
    public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
        validatingListener.addValidator("beforeCreate", validator);
        validatingListener.addValidator("beforeSave", validator);
    }

    @Override
    public void configureHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        messageConverters.add(new ResourceHttpMessageConverter());
    }

}

WebMvcConfiguration.java

@Configuration
@EnableHypermediaSupport(type = { HypermediaType.HAL })
public class WebMvcConfiguration implements WebMvcConfigurer {

    private Validator validator;

    @Bean
    public LocaleResolver localeResolver() {
        return new SmartLocaleResolver();
    }

    public class SmartLocaleResolver extends CookieLocaleResolver {
        @Override
        public Locale resolveLocale(HttpServletRequest request) {
            String acceptLanguage = request.getHeader("Accept-Language");
            if (acceptLanguage == null || acceptLanguage.trim().isEmpty()) {
                return super.determineDefaultLocale(request);
            }
            return request.getLocale();
        }
    }

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

    @Override
    public Validator getValidator() {
        return validator;
    }


    @Bean
    public CustomErrorAttributes myCustomErrorAttributes() {
        return new CustomErrorAttributes();
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(corsMapping).exposedHeaders(corsExposedHeaders).allowedOrigins(corsAllowedOrigins)
                .allowedHeaders(corsAllowedHeaders).allowedMethods(corsAllowedMethod).maxAge(corsMaxAge);
    }

}

RequestBodyValidationProcessor.java

@ControllerAdvice
@Log4j2
public class RequestBodyValidationProcessor extends RequestBodyAdviceAdapter {

    @Autowired
    private LocalValidatorFactoryBean 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);

        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(obj);
        if (!constraintViolations.isEmpty()) {
            throw new ConstraintViolationException(constraintViolations);
        }
        return obj;
    }

}

ApplicationExceptionHandler.java

@RestControllerAdvice
@Log4j2
public class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {

    @Autowired
    private ErrorLogRepository errorLogRepository;

    @Autowired
    private MessageSource messageSource;

    private MessageSourceAccessor messageSourceAccessor = null;

    @PostConstruct
    public void postConstruct() {
        Assert.notNull(messageSource, "MessageSource must not be null!");
        this.messageSourceAccessor = new MessageSourceAccessor(messageSource);
    }



    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status,
            WebRequest request) {
        HttpServletRequest httpServlet = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.error(String.format("MethodArgumentNotValidException caused from client with ip  %s. Error: %s", AppUtils.getRemoteIp(httpServlet),
                ExceptionUtils.getRootCauseMessage(ex)));

        return response(HttpStatus.BAD_REQUEST, new HttpHeaders(),
                buildGenericError(ex, ExceptionCode.ERROR_CODE, httpServlet, HttpStatus.BAD_REQUEST, LocaleContextHolder.getLocale()));
    }

    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status,
            WebRequest request) {
        HttpServletRequest httpServlet = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.error(String.format("HttpMediaTypeNotSupportedException caused from client with ip  %s. Error: %s", AppUtils.getRemoteIp(httpServlet),
                ExceptionUtils.getRootCauseMessage(ex)));

        return response(HttpStatus.BAD_REQUEST, new HttpHeaders(),
                buildGenericError(ex, ExceptionCode.ERROR_CODE, httpServlet, HttpStatus.BAD_REQUEST, LocaleContextHolder.getLocale()));
    }

    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status,
            WebRequest request) {
        HttpServletRequest httpServlet = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.error(String.format("HttpMessageNotReadableException caused from client with ip  %s. Error: %s", AppUtils.getRemoteIp(httpServlet),
                ExceptionUtils.getRootCauseMessage(ex)));

        if (ExceptionUtils.getRootCauseMessage(ex).contains("Duplicate entry")) {

            return response(HttpStatus.CONFLICT, new HttpHeaders(), buildIntegrityError(ex, httpServlet, HttpStatus.CONFLICT, LocaleContextHolder.getLocale()));
        } else {
            return response(HttpStatus.BAD_REQUEST, new HttpHeaders(),
                    buildGenericError(ex, ExceptionCode.ERROR_CODE, httpServlet, HttpStatus.BAD_REQUEST, LocaleContextHolder.getLocale()));
        }
    }


    @Override
    protected ResponseEntity<Object> handleHttpMessageNotWritable(HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        HttpServletRequest httpServlet = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.error("", ex);
        return response(HttpStatus.INTERNAL_SERVER_ERROR, new HttpHeaders(),
                buildGenericError(ex, ExceptionCode.INTERNAL_ERROR, httpServlet, HttpStatus.INTERNAL_SERVER_ERROR, LocaleContextHolder.getLocale()));
    }


    @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<?> handleConflictException(DataIntegrityViolationException ex, HttpServletRequest request, Locale locale) throws Exception {

        if (ex instanceof RepositoryConstraintViolationException) {
            return response(HttpStatus.BAD_REQUEST, new HttpHeaders(),
                    new RepositoryConstraintViolationExceptionMessage((RepositoryConstraintViolationException) ex, messageSourceAccessor));
        }


        return response(HttpStatus.CONFLICT, new HttpHeaders(), buildIntegrityError(ex, request, HttpStatus.CONFLICT, locale));
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<?> handleValidationException(ConstraintViolationException ex, HttpServletRequest request, Locale locale) throws Exception {
        try {
            ResponseEntity<ConstraintViolationException> response = new ResponseEntity<ConstraintViolationException>(ex, new HttpHeaders(),
                    HttpStatus.BAD_REQUEST);
            return response;
        } catch (Exception e) {
            log.error("", e);
        }
        return response(HttpStatus.BAD_REQUEST, new HttpHeaders(), "");
    }


    @ExceptionHandler(TransactionSystemException.class)
    public ResponseEntity<?> handleTransactionSystemException(TransactionSystemException ex, HttpServletRequest request, Locale locale) throws Exception {
        if (ex.getCause() instanceof RollbackException) {
            RollbackException rollbackException = (RollbackException) ex.getCause();
            if (rollbackException.getCause() instanceof ApplicationExceptionInterface) {
                ApplicationExceptionInterface finalException = (ApplicationExceptionInterface) rollbackException.getCause();
                return response(HttpStatus.BAD_REQUEST, new HttpHeaders(), buildGenericError(rollbackException.getCause(),
                        ExceptionCode.fromCode(finalException.getCode()), request, HttpStatus.BAD_REQUEST, LocaleContextHolder.getLocale()));
            }
        }

        return response(HttpStatus.INTERNAL_SERVER_ERROR, new HttpHeaders(),
                buildGenericError(ex, ExceptionCode.INTERNAL_ERROR, request, HttpStatus.INTERNAL_SERVER_ERROR, LocaleContextHolder.getLocale()));
    }

    @ExceptionHandler(InternalException.class)
    public ResponseEntity<?> handleInternalException(InternalException ex, HttpServletRequest request, Locale locale) throws Exception {
        return response(HttpStatus.BAD_REQUEST, new HttpHeaders(),
                buildGenericError(ex, ExceptionCode.fromCode(ex.getCode()), request, HttpStatus.BAD_REQUEST, LocaleContextHolder.getLocale()));
    }


    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<?> handleFileUpload(MaxUploadSizeExceededException ex, HttpServletRequest request, Locale locale) throws Exception {
        log.error(String.format("Received a file too big from %s. Error: %s", AppUtils.getRemoteIp(request), ExceptionUtils.getRootCauseMessage(ex)));
        return response(HttpStatus.BAD_REQUEST, new HttpHeaders(), buildIntegrityError(ex, request, HttpStatus.BAD_REQUEST, LocaleContextHolder.getLocale()));
    }


    private JsonException buildIntegrityError(final Throwable exception, final HttpServletRequest request, final HttpStatus httpStatus, Locale locale) {
        return buildIntegrityError(exception, request.getRequestURI(), httpStatus, locale);
    }


    private JsonException buildIntegrityError(final Throwable exception, String requestUri, final HttpStatus httpStatus, Locale locale) {
        String finalMessage = "";
        String rootMsg = ExceptionUtils.getRootCauseMessage(exception);
        Optional<Map.Entry<String, ExceptionCode>> entry = constraintCodeMap.entrySet().stream().filter((it) -> rootMsg.contains(it.getKey())).findAny();
        if (entry.isPresent()) {
            finalMessage = messageSource.getMessage(entry.get().getValue().getCode(), new Object[] {}, locale);
        } else {
            finalMessage = messageSource.getMessage(ExceptionCode.INTEGRITY_VIOLATION.getCode(), new Object[] { rootMsg }, locale);
        }
        JsonException jsonException = new JsonException();
        jsonException.setError(httpStatus.getReasonPhrase());
        jsonException.setStatus(httpStatus.value());
        jsonException.setException(exception.getClass().getName());
        jsonException.setMessage(finalMessage);
        jsonException.setPath(requestUri);

        return jsonException;
    }


    private JsonException buildGenericError(final Throwable exception, final ExceptionCode exceptionCode, final HttpServletRequest request,
                                            final HttpStatus httpStatus, Locale locale) {
        String rootMsg = ExceptionUtils.getRootCauseMessage(exception);
        String finalMessage = "";
        Object[] args = new Object[]{rootMsg};

        if (exception instanceof ApplicationExceptionInterface) {
            args = ((ApplicationExceptionInterface) exception).getArgs();
        }
        try {
            // Not storing in DB ValidationExceptions
            if (!(exception instanceof ValidationException)) {
                try {
                    ErrorLog errorLog = dbStoreException(exception);
                    String dbCode = messageSource.getMessage(ExceptionCode.ERROR_CODE.getCode(), new Object[]{errorLog.getCode()}, locale);

                    finalMessage = dbCode + " " + MessageUtils.getMessage(locale, exceptionCode.getCode(), args);
                } catch (Exception e) {
                    finalMessage = messageSource.getMessage(exceptionCode.getCode(), args, locale);
                }
            } else {
                finalMessage = messageSource.getMessage(exceptionCode.getCode(), args, locale);
            }
        } catch (Exception e) {
            finalMessage = messageSource.getMessage(exceptionCode.getCode(), args, locale);
        }

        JsonException jsonException = new JsonException();
        jsonException.setError(httpStatus.getReasonPhrase());
        jsonException.setStatus(httpStatus.value());
        jsonException.setException(exception.getClass().getName());
        jsonException.setMessage(finalMessage);
        jsonException.setPath(request.getRequestURI());
        if (exception instanceof ApplicationExceptionInterface) {
            jsonException.setErrorCode(((ApplicationExceptionInterface) exception).getCode());
        }

        return jsonException;
    }

    private static <T> ResponseEntity<T> response(HttpStatus status, HttpHeaders headers, T body) {

        Assert.notNull(headers, "Headers must not be null!");
        Assert.notNull(status, "HttpStatus must not be null!");

        return new ResponseEntity<T>(body, headers, status);
    }


    private ErrorLog dbStoreException(Throwable throwable) {
        HttpServletRequest httpServlet = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        ErrorLog errorLog = new ErrorLog();
        errorLog.setTitle(ExceptionUtils.getRootCauseMessage(throwable));
        errorLog.setText(ExceptionUtils.getStackTrace(throwable));
        errorLog.setLocation(AppUtils.getExceptionPosition(throwable));
        errorLog.setRemoteAddress(AppUtils.getRemoteIp(httpServlet));
        return errorLogRepository.save(errorLog);
    }
}

I'm using save/update methods exposed from the Repository:

@Transactional
public interface ContactRepository extends JpaRepository<Contact, Long>, RevisionRepository<Contact, Long, Integer> {
}

This is the controller that is not used though because I'm exposing endpoint via Repository using SDR:

    @RepositoryRestController
@Log4j2
public class ContactController extends RevisionController<Contact> {
@Autowired
private LocalValidatorFactoryBean validator;

@PersistenceContext
private EntityManager entityManager;

@Autowired
private ContactRepository contactRepository;

@Autowired
private ContactService contactService;

@Autowired
private NoteService noteService;

@Autowired
private MessageSource messageSource;

@Autowired
private MediaService mediaService;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@SuppressWarnings("rawtypes")
@Autowired
private PagedResourcesAssembler pagedResourcesAssembler;

@InitBinder
protected void initBinder(WebDataBinder binder) {
    binder.addValidators(validator);
}

@PreAuthorize("permitAll()")
@GetMapping(path = "/contacts/types")
public ResponseEntity<?> getContactTypes(Locale locale) {
    return ResponseEntity.ok(AppUtils.listToResourcesList(Arrays.asList(PersonType.values())));
}

@GetMapping(path = "/contacts/{id:[0-9]+}")
public ResponseEntity<?> findOne(@PathVariable("id") long id, Locale locale, PersistentEntityResourceAssembler resourceAssembler) {
    //
}

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_BACK_OFFICE','ROLE_ACCOUNTANT')")
@PostMapping(path = "/contacts/searches")
public ResponseEntity<?> search(@RequestBody(required = true) List<Filter> filters, Pageable pageable, Locale locale,
        PersistentEntityResourceAssembler resourceAssembler) {
    //
}

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_ACCOUNTANT')")
@PostMapping(path = "/contacts/{id}/enableWallet")
public ResponseEntity<?> enableWallet(@PathVariable("id") long contactId, HttpServletRequest request, Locale locale,
        PersistentEntityResourceAssembler resourceAssembler) {
    //

}


@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_ACCOUNTANT')")
@PostMapping(path = "/contacts/{id}/balanceThreshold")
public ResponseEntity<?> balanceThreshold(@PathVariable("id") long contactId, @RequestBody(required = true) BigDecimal balanceThreshold, Locale locale,
        PersistentEntityResourceAssembler resourceAssembler) {
    //
}


@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_ACCOUNTANT','ROLE_BACK_OFFICE')")
@SuppressWarnings("unchecked")
@GetMapping(path = "/contacts/{id}/movements")
public ResponseEntity<?> getMovements(@PathVariable("id") long contactId, @RequestParam(value = "from", required = false) Instant from,
        @RequestParam(value = "until", required = false) Instant until, @RequestParam(value = "description", required = false) String description,
        Pageable pageable, Locale locale, PersistentEntityResourceAssembler resourceAssembler) {
    //
}

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_BACK_OFFICE','ROLE_ACCOUNTANT')")
@GetMapping(path = "/contacts/{id}/notes")
public ResponseEntity<?> getNotes(@PathVariable(value = "id") long id, Pageable pageable, Locale locale,
        PersistentEntityResourceAssembler resourceAssembler) {
    //

}

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_BACK_OFFICE','ROLE_ACCOUNTANT')")
@GetMapping(path = "/contacts/{id}/auditLogs")
public ResponseEntity<?> getAuditLogs(@PathVariable(value = "id") long id, Pageable pageable, Locale locale,
        PersistentEntityResourceAssembler resourceAssembler) {
    //
}

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_BACK_OFFICE','ROLE_ACCOUNTANT')")
@GetMapping(path = "/contacts/{id}/media")
public ResponseEntity<?> getMedia(@PathVariable(value = "id") long id, Pageable pageable, Locale locale,
        PersistentEntityResourceAssembler resourceAssembler) {
    //
}


@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_BACK_OFFICE','ROLE_ACCOUNTANT')")
@PostMapping(path = "/contacts/{id}/notes")
public ResponseEntity<?> addNote(@PathVariable(value = "id") long id, @Valid @RequestBody(required = true) Note note, Locale locale,
        PersistentEntityResourceAssembler resourceAssembler) {
    //
}

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_BACK_OFFICE','ROLE_ACCOUNTANT')")
@GetMapping(path = "/contacts/{id}/revisions")
public ResponseEntity<?> findRevisions(@PathVariable(value = "id") Long id, Pageable pageable) {
    //
}


@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_BACK_OFFICE','ROLE_ACCOUNTANT')")
@GetMapping(path = "/contacts/{id}/revisions/{revid}")
public ResponseEntity<?> getChanges(@PathVariable(value = "id") Long id, @PathVariable(value = "revid") Integer revId, Pageable pageable) {
    //
}


@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_BACK_OFFICE','ROLE_ACCOUNTANT')")
@PostMapping(path = "/contacts/{id}/media", consumes = {"multipart/form-data"})
public ResponseEntity<?> addMedia(@PathVariable("id") long id, @RequestPart("files") List<MultipartFile> files, HttpServletRequest request, Locale locale,
        PersistentEntityResourceAssembler resourceAssembler) {
    //
}


@PreAuthorize("permitAll()")
@PostMapping(path = "/contacts/resetPassword")
public ResponseEntity<?> resetPassword(@RequestBody(required = true) String username, Locale locale, PersistentEntityResourceAssembler resourceAssembler) {
    //
}


@PreAuthorize("permitAll()")
@PostMapping(path = "/contacts/register")
public ResponseEntity<?> register(@Valid @RequestBody(required = true) Contact contact, HttpServletRequest request, PersistentEntityResourceAssembler resourceAssembler) {
   //
}

}

And this is the first part of the bean:

@Entity
@EntityListeners(ContactListener.class)
@Table(indexes = {@Index(name = "idx_enabled", columnList = "enabled"), @Index(name = "idx_name", columnList = "name")})
@ScriptAssert.List({
        //CHECK TAX CODE VALIDITY
        @ScriptAssert(lang = "javascript", script = "_.taxCode != null && _.taxCode != '' && _.personType=='NATURAL_PERSON'?_.isTaxCodeValid(_.taxCode,_.country):true", alias = "_", reportOn = "taxCode", message = "{contact.invalid.taxcode}"),

        //CHECK VAT NUMBER VALIDITY
        @ScriptAssert(lang = "javascript", script = "_.vatNumber != null && _.vatNumber != '' && _.personType=='LEGAL_PERSON'?_.isVatNumberValid(_.vatNumber,_.country):true", alias = "_", reportOn = "vatNumber", message = "{contact.invalid.vatNumber}")
})
@Data
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
@AllArgsConstructor
@Builder
@ToString(callSuper = true)
public class Contact extends AbstractEntity {

    @Builder.Default
    @Audited
    @NotNull
    @Column(nullable = false, columnDefinition = "VARCHAR(255) DEFAULT 'LEGAL_PERSON'")
    @Enumerated(EnumType.STRING)
    private PersonType personType = PersonType.LEGAL_PERSON;

    @Audited
    @NotEmpty
    @Size(min = 3, max = 255)
    @Column(nullable = false)
    private String name;

    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    private Account account;

    @Audited
    @NotBlank
    private String address;

Hovewer, when I do a POST saving the entity with some errors I got something like this:

{"errors":[{"entity":"Contact","property":"address","invalidValue":null,"message":"Il campo non può essere vuoto. Inserire un valore valido e ripetere l'operazione."},{"entity":"Contact","property":"personType","invalidValue":null,"message":"Il campo non può essere vuoto. Inserire un valore valido e ripetere l'operazione."},{"entity":"Contact","property":"city","invalidValue":null,"message":"Il campo non può essere vuoto. Inserire un valore valido e ripetere l'operazione."}]}

that is fine. When I do a PATCH with some errors in the form I've this reply:

{"timestamp":"2020-01-08T19:42:31.633+0000","status":400,"error":"Bad Request","exception":"org.springframework.http.converter.HttpMessageNotReadableException","message":"Cod. errore [494-577]. Cod. errore [ConstraintViolationException: Validation failed for classes [it.test.server.model.accounts.Contact] during update time for groups [javax.validation.groups.Default, ]\nList of constraint violations:[\n\tConstraintViolationImpl{interpolatedMessage='{contact.invalid.taxcode}', propertyPath=taxCode, rootBeanClass=class it.test.server.model.accounts.Contact, messageTemplate='{contact.invalid.taxcode}'}\n]].","path":"/api/v1/contacts/5752","errorCode":null}

that is wrong. In the first case the exception is caught from @ExceptionHandler(DataIntegrityViolationException.class) 's method, in the second one from handleHttpMessageNotReadable() methods.

Do you have any hint to point me in the right way to solve the issue?

You may use ProblemHandling which extends ValidationAdviceTrait which uses MethodArgumentNotValidAdviceTrait , ConstraintViolationAdviceTrait . You can custmize the message using ProblemBuilder with title, status code, details etc. You can also handle other exception using handleException(ExceptionClass ex, NativeWebRequest request) . It is working fine with both methods POST and PATCH .

@ControllerAdvice
public class ExceptionTranslator implements ProblemHandling, SecurityAdviceTrait {

    private static final String FIELD_ERRORS_KEY = "fieldErrors";
    private static final String MESSAGE_KEY = "message";
    private static final String PATH_KEY = "path";
    private static final String VIOLATIONS_KEY = "violations";

    @Value("${jhipster.clientApp.name}")
    private String applicationName;

    private final Logger log = LoggerFactory.getLogger(ExceptionTranslator.class);

    /**
     * Post-process the Problem payload to add the message key for the front-end if needed.
     */
    @Override
    public ResponseEntity<Problem> process(@Nullable ResponseEntity<Problem> entity, NativeWebRequest request) {
        log.debug("process invalid input(s) for entity: {}, request: {}",entity,request);

        List<String> messages = new ArrayList<>();
        String messageStr = "";

        if (entity == null) {
            return entity;
        }

        Problem problem = entity.getBody();
        try {
            ((ConstraintViolationProblem) problem).getViolations().forEach(m-> messages.add(m.getMessage()));
            messageStr = messages.toString();
            log.debug("Error message: {}",messageStr);
        } catch (ClassCastException e) {
            log.debug("Cannot cast Problem to ConstraintViolationProblem");
            messageStr = problem.getDetail();
            log.debug("Error message detail: {}",messageStr);
        }

        if (!(problem instanceof ConstraintViolationProblem || problem instanceof DefaultProblem)) {
            return entity;
        }
        ProblemBuilder builder = Problem.builder()
            .withType(Problem.DEFAULT_TYPE.equals(problem.getType()) ? ErrorConstants.DEFAULT_TYPE : problem.getType())
            .withStatus(problem.getStatus())
            .withTitle(problem.getTitle())
            .withDetail(messageStr)
            .with(PATH_KEY, request.getNativeRequest(HttpServletRequest.class).getRequestURI());

        if (problem instanceof ConstraintViolationProblem) {
            builder
                .with(VIOLATIONS_KEY, ((ConstraintViolationProblem) problem).getViolations())
                .with(MESSAGE_KEY, ErrorConstants.ERR_VALIDATION)
                .withDetail(messageStr);
        } else {
            builder
                .withCause(((DefaultProblem) problem).getCause())
                .withDetail(messageStr)
                .withInstance(problem.getInstance());
            problem.getParameters().forEach(builder::with);
            if (!problem.getParameters().containsKey(MESSAGE_KEY) && problem.getStatus() != null) {
                builder.with(MESSAGE_KEY, "error.http." + problem.getStatus().getStatusCode()).withDetail(messageStr);
            }
        }
        return new ResponseEntity<>(builder.build(), entity.getHeaders(), entity.getStatusCode());
    }

    @Override
    public ResponseEntity<Problem> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, @Nonnull NativeWebRequest request) {
        log.debug("Handle invalid method arguments: {}",ex.toString());

        BindingResult result = ex.getBindingResult();
        List<FieldErrorVM> fieldErrors = result.getFieldErrors().stream()
            .map(f -> new FieldErrorVM(f.getObjectName().replaceFirst("DTO$", ""), f.getField(), f.getCode()))
            .collect(Collectors.toList());

        List<String> messages = new ArrayList<>();
        fieldErrors.forEach(m -> messages.add("Please provide a valid value for " + m.getField()));
        log.debug("Error message: {}", messages);

        Problem problem = Problem.builder()
            .withType(ErrorConstants.CONSTRAINT_VIOLATION_TYPE)
            .withTitle("Method argument not valid")
            .withStatus(defaultConstraintViolationStatus())
            .with(MESSAGE_KEY, ErrorConstants.ERR_VALIDATION)
            .with(FIELD_ERRORS_KEY, fieldErrors)
            .withDetail(messages.toString())
            .build();
        return create(ex, problem, request);
    }
}

The response will be as follow for both POST, PATCH

{
  "type": "https://www.jhipster.tech/problem/constraint-violation",
  "title": "Method argument not valid",
  "status": 400,
  "detail": "[Please provide a valid value for category]",
  "path": "/api/categories",
  "message": "error.validation",
  "fieldErrors": [
    {
      "objectName": "botCategory",
      "field": "category",
      "message": "Pattern"
    }
  ]
}

@PatchMapping("/endpoint")
    public ResponseEntity<yourResponseDTO> create(@Valid @RequestBody yourRequestDTO requestDTO) 
{...}

@PostMapping("/endpoint")
    public ResponseEntity<yourResponseDTO> update(@Valid @RequestBody yourRequestDTO requestDTO) 
{...}

public class yourRequestDTO {

    private Long id;

    @NotNull(message = ErrorConstants.INVALID_NAME)
    @Size(max = 50, message = ErrorConstants.INVALID_SIZE_50)
    @Pattern(regexp = Constants.BOTCATEGORY_REGEX, message = ErrorConstants.INVALID_BOT_CATEGORY_PATTERN)
    private String category;

    @Size(max = 100, message = ErrorConstants.INVALID_SIZE_100)
    private String description;
}

Related imports

import io.github.jhipster.web.util.HeaderUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.NativeWebRequest;
import org.zalando.problem.DefaultProblem;
import org.zalando.problem.Problem;
import org.zalando.problem.ProblemBuilder;
import org.zalando.problem.Status;
import org.zalando.problem.spring.web.advice.ProblemHandling;
import org.zalando.problem.spring.web.advice.security.SecurityAdviceTrait;
import org.zalando.problem.violations.ConstraintViolationProblem;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.PersistenceException;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;

Finally I solved the problem. Indeed the configuration was right, but I had an exception that was visible only in DEBUG level (thanks to @Angelo Immediata):

17/01/2020 14:51:35,016 DEBUG http-nio-8081-exec-3 ServletInvocableHandlerMethod:174 - Could not resolve parameter [1] in public org.springframework.http.ResponseEntity<org.springframework.hateoas.ResourceSupport> org.springframework.data.rest.webmvc.RepositoryEntityController.patchItemResource(org.springframework.data.rest.webmvc.RootResourceInformation,org.springframework.data.rest.webmvc.PersistentEntityResource,java.io.Serializable,org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler,org.springframework.data.rest.webmvc.support.ETag,java.lang.String) throws org.springframework.web.HttpRequestMethodNotSupportedException,org.springframework.data.rest.webmvc.ResourceNotFoundException: Could not read payload!; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction (through reference chain: it.test.server.model.accounts.Contact["country"])

The problem was that I override this method in CountryRepository:

    @Override
    @PreAuthorize("permitAll()")
    Optional<Country> findById(Long id);

Removing this method solved the issue.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM