package io.kaif.web.support;
import static java.util.stream.Collectors.*;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.servlet.support.RequestContextUtils;
import io.kaif.config.SpringProfile;
/**
* common exception handler for restful controllers, allow convert common spring data access
* exception and part of spring mvc exceptions(*) to json, include english error message
* <p>
* subclass should implements {@link #createErrorResponse(HttpStatus, String)} to create error json
* <p>
* (*) note that not all spring mvc internal exception could be catched and translate to json, this
* seems due to spring 4.0 bug. when @ControllerAdvice specify selectors (in our case,
* anontations=RestController), it could not catch all spring mvc exceptions, such as
* {@link org.springframework.web.HttpRequestMethodNotSupportedException}
*
* @author ingram
*/
@Order(Ordered.LOWEST_PRECEDENCE)
public abstract class AbstractRestExceptionHandler<E extends ErrorResponse>
extends ResponseEntityExceptionHandler {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private MessageSource messageSource;
@Autowired
private Environment environment;
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public ResponseEntity<E> handleAccessDeniedException(final AccessDeniedException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.UNAUTHORIZED;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.RestAccessDeniedException"));
if (environment.acceptsProfiles(SpringProfile.DEV)) {
//only dev server log detail access denied
logException(ex, errorResponse, request);
} else {
logger.warn("{} {}", guessUri(request), ex.getClass().getSimpleName());
}
return new ResponseEntity<>(errorResponse, status);
}
protected final String guessUri(WebRequest request) {
String uri = "non uri";
if (request instanceof ServletWebRequest) {
uri = ((ServletWebRequest) request).getRequest().getRequestURI();
}
return uri;
}
@ExceptionHandler(DataAccessException.class)
@ResponseBody
public ResponseEntity<E> handleDataAccessException(final DataAccessException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.DataAccessException", ex.getClass().getSimpleName()));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
protected abstract E createErrorResponse(HttpStatus status, String reason);
@ExceptionHandler(EmptyResultDataAccessException.class)
@ResponseBody
public ResponseEntity<E> handleEmptyResultDataAccessException(final EmptyResultDataAccessException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.NOT_FOUND;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.EmptyResultDataAccessException", ex.getClass().getSimpleName()));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
protected final String i18n(WebRequest request, String key, Object... args) {
return messageSource.getMessage(key, args, "!" + key + "!", resolveLocale(request));
}
private Locale resolveLocale(WebRequest request) {
if (!(request instanceof ServletWebRequest)) {
return request.getLocale();
}
ServletWebRequest servletWebRequest = (ServletWebRequest) request;
return RequestContextUtils.getLocale(servletWebRequest.getRequest());
}
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseBody
public ResponseEntity<E> handleDataIntegrityViolationException(final DataIntegrityViolationException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.CONFLICT;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.DataIntegrityViolationException"));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@ExceptionHandler(DuplicateKeyException.class)
@ResponseBody
public ResponseEntity<E> handleDuplicateKeyException(final DuplicateKeyException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.CONFLICT;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.DuplicateKeyException"));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@Override
protected ResponseEntity<Object> handleExceptionInternal(final Exception ex,
final Object body,
final HttpHeaders headers,
final HttpStatus status,
final WebRequest request) {
final E errorResponse = createErrorResponse(status, status.getReasonPhrase());
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
//TODO detail i18n and missing parameter name
final String detail = ex.getBindingResult()
.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(joining(", "));
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.MethodArgumentNotValidException", detail));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
/**
* Customize the response for MissingServletRequestParameterException.
* This method delegates to
* {@link #handleExceptionInternal(Exception, Object, org.springframework.http.HttpHeaders,
* org.springframework.http.HttpStatus, org.springframework.web.context.request.WebRequest)}.
*
* @param ex
* the exception
* @param headers
* the headers to be written to the response
* @param status
* the selected response status
* @param request
* the current request
* @return a {@code ResponseEntity} instance
*/
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
final E errorResponse = createErrorResponse(status, ex.getMessage());
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@ExceptionHandler({ OptimisticLockingFailureException.class })
@ResponseBody
public ResponseEntity<E> handleOptimisticLockingFailureException(final OptimisticLockingFailureException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.LOCKED;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.OptimisticLockingFailureException"));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<E> handleOtherException(final Exception ex, final WebRequest request) {
// IOException ...etc
final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.Exception", ex.getClass().getSimpleName()));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@ExceptionHandler({ PermissionDeniedDataAccessException.class })
@ResponseBody
public ResponseEntity<E> handlePermissionDeniedDataAccessException(final PermissionDeniedDataAccessException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.UNAUTHORIZED;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.PermissionDeniedDataAccessException"));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@ExceptionHandler({ PessimisticLockingFailureException.class })
@ResponseBody
public ResponseEntity<E> handlePessimisticLockingFailureException(final PessimisticLockingFailureException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.LOCKED;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.PessimisticLockingFailureException"));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@ExceptionHandler({ QueryTimeoutException.class })
@ResponseBody
public ResponseEntity<E> handleQueryTimeoutException(final QueryTimeoutException ex,
final WebRequest request) {
final HttpStatus status = HttpStatus.REQUEST_TIMEOUT;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.QueryTimeoutException"));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public ResponseEntity<E> handleRuntimeException(final RuntimeException ex,
final WebRequest request) {
// Runtime Exception always hidden, we should not leak internal Exception stacktrace
final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
final E errorResponse = createErrorResponse(status,
i18n(request, "rest-error.RuntimeException", ex.getClass().getSimpleName()));
logException(ex, errorResponse, request);
return new ResponseEntity<>(errorResponse, status);
}
protected final void logException(final Exception ex,
final E errorResponse,
final WebRequest request) {
final StringBuilder sb = new StringBuilder();
sb.append(errorResponse);
sb.append("\n");
sb.append(request.getDescription(true));
sb.append("\nparameters -- ");
for (final Iterator<String> iter = request.getParameterNames(); iter.hasNext(); ) {
final String name = iter.next();
sb.append(name);
sb.append(":");
final String[] values = request.getParameterValues(name);
if (values == null) {
sb.append("null");
} else if (values.length == 0) {
sb.append("");
} else if (values.length == 1) {
sb.append(values[0]);
} else {
sb.append(Arrays.toString(values));
}
sb.append(" ");
}
logger.error(sb.toString(), ex);
}
}