// Copyright © 2015 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.front; import com.fasterxml.jackson.databind.JsonMappingException; import com.google.common.collect.ImmutableList; import fi.hsl.parkandride.MDCFilter; import fi.hsl.parkandride.core.domain.NotFoundException; import fi.hsl.parkandride.core.domain.Violation; import fi.hsl.parkandride.core.service.AccessDeniedException; import fi.hsl.parkandride.core.service.AuthenticationRequiredException; import fi.hsl.parkandride.core.service.ValidationException; import org.apache.catalina.connector.ClientAbortException; import org.joda.time.DateTime; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; import org.springframework.web.HttpMediaTypeException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import static fi.hsl.parkandride.MDCFilter.LIIPI_APPLICATION_ID; import static java.util.stream.Collectors.toList; import static org.springframework.http.HttpStatus.*; import static org.springframework.http.MediaType.APPLICATION_JSON; @ControllerAdvice public class ExceptionHandlers { @InitBinder public void validateApplicationId(HttpServletRequest request) { final String appId = request.getHeader(LIIPI_APPLICATION_ID); Optional.ofNullable(appId).ifPresent(MDCFilter::validateAppId); } @ExceptionHandler(NotFoundException.class) @ResponseStatus(value= NOT_FOUND) public void notFound(HttpServletRequest req, NotFoundException ex) { // status: 404 } @ExceptionHandler(ValidationException.class) public ResponseEntity<Map<String, Object>> validationException(HttpServletRequest request, ValidationException ex) { return handleError(request, BAD_REQUEST, ex, ex.getMessage(), ex.violations); } @ExceptionHandler(IllegalHeaderException.class) public ResponseEntity<Map<String, Object>> validationException(HttpServletRequest request, IllegalHeaderException ex) { return handleError(request, BAD_REQUEST, ex, ex.getMessage(), null); } @ExceptionHandler(ClientAbortException.class) public void clientAbortException(ClientAbortException e) { // Nothing to respond here as client has terminated connection } @ExceptionHandler(BindException.class) public ResponseEntity<Map<String, Object>> bindException(HttpServletRequest request, BindException ex) { List<Violation> violations = ex.getFieldErrors().stream() .map(fieldError -> new Violation(fieldError.getCode(), fieldError.getField(), fieldError.getDefaultMessage())) .collect(toList()); return handleError(request, BAD_REQUEST, ex, "Invalid request parameters", violations); } @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<Map<String, Object>> jsonException(HttpServletRequest request, HttpMessageNotReadableException ex) { if (ex.getCause() instanceof JsonMappingException) { JsonMappingException jsonEx = (JsonMappingException) ex.getCause(); String path = getPath(jsonEx); Violation violation = new Violation("TypeMismatch", path, jsonEx.getMessage()); return handleError(request, BAD_REQUEST, ex, "Invalid input", ImmutableList.of(violation)); } return handleError(request, BAD_REQUEST, ex); } @ExceptionHandler(AuthenticationRequiredException.class) @ResponseBody public ResponseEntity<Void> authenticationRequiredException(AuthenticationRequiredException ex) { HttpHeaders headers = new HttpHeaders(); return new ResponseEntity<Void>(null, headers, UNAUTHORIZED); } @ExceptionHandler(AccessDeniedException.class) @ResponseBody public ResponseEntity<Void> accessDeniedException(AccessDeniedException ex) { return new ResponseEntity<Void>((Void) null, FORBIDDEN); } private String getPath(JsonMappingException jsonEx) { StringBuilder path = new StringBuilder(); for (JsonMappingException.Reference ref : jsonEx.getPath()) { String field = ref.getFieldName(); int index = ref.getIndex(); if (field != null) { if (path.length() > 0) { path.append('.'); } path.append(field); } if (index >= 0) { path.append('[').append(index).append(']'); } } return path.toString(); } @ExceptionHandler({ HttpRequestMethodNotSupportedException.class, HttpMediaTypeException.class }) public ResponseEntity<Map<String, Object>> methodNotSupportedException(HttpServletRequest request, ServletException ex) { return handleError(request, BAD_REQUEST, ex); } @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, Object>> exception(HttpServletRequest request, Exception ex) { return handleError(request, INTERNAL_SERVER_ERROR, ex); } private ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request, HttpStatus status, Throwable ex) { return handleError(request, status, ex, ex.getMessage(), null); } private ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request, HttpStatus status, Throwable ex, String message, List<Violation> violations) { ex = resolveError(ex); Map<String, Object> errorAttributes = new LinkedHashMap<>(); errorAttributes.put("status", status.value()); errorAttributes.put("message", message); errorAttributes.put("timestamp", DateTime.now()); errorAttributes.put("exception", resolveError(ex).getClass().getName()); if (violations != null && !violations.isEmpty()) { errorAttributes.put("violations", violations); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(APPLICATION_JSON); return new ResponseEntity<>(errorAttributes, headers, status); } private Throwable resolveError(Throwable ex) { while (ex instanceof ServletException && ex.getCause() != null) { ex = ((ServletException) ex).getCause(); } return ex; } }