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}