001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014package org.gbif.ws.server.mapper; 015 016import java.util.ArrayList; 017import java.util.Comparator; 018import java.util.List; 019import java.util.regex.Pattern; 020 021import javax.validation.ConstraintViolationException; 022import javax.validation.Path; 023 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026import org.springframework.http.HttpStatus; 027import org.springframework.http.MediaType; 028import org.springframework.http.ResponseEntity; 029import org.springframework.validation.FieldError; 030import org.springframework.web.bind.MethodArgumentNotValidException; 031import org.springframework.web.bind.annotation.ControllerAdvice; 032import org.springframework.web.bind.annotation.ExceptionHandler; 033 034/** 035 * Converts validation exceptions into a http 422 bad request and gives a meaningful messages on the 036 * issues. 037 */ 038@ControllerAdvice 039public class ValidationExceptionMapper { 040 041 private static final Logger LOG = LoggerFactory.getLogger(ValidationExceptionMapper.class); 042 043 private static final Pattern LIST_PATH = Pattern.compile("\\[0\\]\\.<[^<>]+>$"); 044 045 @ExceptionHandler(MethodArgumentNotValidException.class) 046 public ResponseEntity<Object> toResponse(MethodArgumentNotValidException exception) { 047 LOG.error("Validation error: {}", exception.getMessage()); 048 List<String> errors = new ArrayList<>(); 049 050 exception.getBindingResult().getAllErrors().stream() 051 .map(error -> ((FieldError) error)) 052 .sorted(Comparator.comparing(FieldError::getField, Comparator.naturalOrder())) 053 .forEach( 054 error -> { 055 LOG.debug( 056 "Validation of [{}] failed: {}", error.getField(), error.getDefaultMessage()); 057 errors.add( 058 String.format( 059 "Validation of [%s] failed: %s", 060 error.getField(), error.getDefaultMessage())); 061 }); 062 063 return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) 064 .contentType(MediaType.TEXT_PLAIN) 065 .body("<ul><li>" + String.join("</li><li>", errors) + "</li></ul>"); 066 } 067 068 @ExceptionHandler(ConstraintViolationException.class) 069 public ResponseEntity<Object> toResponse(ConstraintViolationException exception) { 070 LOG.error("Validation error: {}", exception.getMessage()); 071 List<String> errors = new ArrayList<>(); 072 073 exception.getConstraintViolations().stream() 074 .sorted( 075 Comparator.comparing( 076 cv -> getPropertyFromPropertyPath(cv.getPropertyPath()), Comparator.naturalOrder())) 077 .forEach( 078 cv -> { 079 LOG.debug("Validation of [{}] failed: {}", cv.getPropertyPath(), cv.getMessage()); 080 errors.add( 081 String.format( 082 "Validation of [%s] failed: %s", 083 getPropertyFromPropertyPath(cv.getPropertyPath()), cv.getMessage())); 084 }); 085 086 return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) 087 .contentType(MediaType.TEXT_PLAIN) 088 .body("<ul><li>" + String.join("</li><li>", errors) + "</li></ul>"); 089 } 090 091 private String getPropertyFromPropertyPath(Path propertyPath) { 092 String resultProperty = null; 093 094 if (propertyPath != null) { 095 resultProperty = propertyPath.toString(); 096 097 // remove the list elements to take the name of the field 098 resultProperty = LIST_PATH.matcher(resultProperty).replaceAll(""); 099 100 int lastDotIndex = resultProperty.lastIndexOf('.'); 101 if (lastDotIndex != -1) { 102 resultProperty = resultProperty.substring(lastDotIndex + 1); 103 } 104 } 105 106 return resultProperty; 107 } 108}