AbstractRestExceptionHandler.java

package org.imageconverter.infra;

import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCause;
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;
import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
import static org.apache.commons.text.StringEscapeUtils.escapeJava;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.commons.collections4.CollectionUtils;
import org.imageconverter.infra.exception.BaseApplicationException;
import org.slf4j.MDC;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

/**
 * Defaults handlers methods
 * 
 * @author Fernando Romulo da Silva
 */
abstract class AbstractRestExceptionHandler extends ResponseEntityExceptionHandler {

    protected final MessageSource messageSource;

    /**
     * Default constructor.
     * 
     * @param messageSource Object used to translate messages
     */
    protected AbstractRestExceptionHandler(final MessageSource messageSource) {
	super();
	this.messageSource = messageSource;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ResponseEntity<Object> handleMissingServletRequestParameter(final MissingServletRequestParameterException ex, final HttpHeaders headers, final HttpStatus status,
		    final WebRequest request) {

	final var locale = request.getLocale();
	final Object[] params = { ex.getParameterName() };

	final var msg = messageSource.getMessage("exception.missingServletRequestParameter", params, locale);

	return handleObjectException(msg, List.of(), ex, request, HttpStatus.BAD_REQUEST);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(final HttpMessageNotReadableException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {

	return handleObjectException(ex, request, HttpStatus.BAD_REQUEST);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {

	final var subErrors = new ArrayList<Map<String, String>>();

	final var locale = request.getLocale();

	ex.getBindingResult().getFieldErrors().forEach((error) -> {

	    final var errors = new HashMap<String, String>();

	    errors.put("object", error.getObjectName());
	    errors.put("field", error.getField());
	    errors.put("error", messageSource.getMessage(error, locale));

	    subErrors.add(errors);
	});

	final var msg = messageSource.getMessage("exception.handleMethodArgumentNotValid", null, locale);

	return handleObjectException(msg, subErrors, ex, request, HttpStatus.BAD_REQUEST);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(final NoHandlerFoundException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {
	final var locale = request.getLocale();

	final var msg = messageSource.getMessage("exception.handleNoHandlerFound", null, locale);

	return handleObjectException(msg, List.of(), ex, request, status);
    }

    /**
     * Handle exceptions, especially {@link BaseApplicationException} and its descendents and use the exception's message.
     * 
     * @param ex      The exception
     * @param request Request object, to create response body.
     * @param status  The error status
     * @return A {@link ResponseEntity} object that's the response
     */
    protected ResponseEntity<Object> handleObjectException(final Throwable ex, final WebRequest request, final HttpStatus status) {

	final var msg = ex instanceof BaseApplicationException ? escapeJava(getMessage(ex)) : escapeJava(getRootCauseMessage(ex));

	final var body = buildResponseBody(msg, List.of(), status, ex, request);

	if (logger.isErrorEnabled()) {
	    logger.error(msg, getRootCause(ex));
	}

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

    /**
     * Handle exceptions using the message on response.
     * 
     * @param msg     The message that it'll be used
     * @param ex      The exception
     * @param request Request object, to create response body.
     * @param status  The error status
     * @return A {@link ResponseEntity} object that's the response
     */
    protected ResponseEntity<Object> handleObjectException(final String msg, final Collection<Map<String, String>> subErrors, final Throwable ex, final WebRequest request, final HttpStatus status) {

	final var body = buildResponseBody(msg, subErrors, status, ex, request);

	if (logger.isErrorEnabled()) {
	    logger.error(escapeJava(getRootCauseMessage(ex)), getRootCause(ex));
	}

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

    private Map<String, Object> buildResponseBody(final String message, final Collection<Map<String, String>> subErrors, final HttpStatus status, final Throwable ex, final WebRequest request) {
	final var body = new LinkedHashMap<String, Object>();

	body.put("timestamp", LocalDateTime.now().format(ISO_DATE_TIME));
	body.put("status", status.value());
	body.put("error", status.getReasonPhrase());
	body.put("message", message);

	if (CollectionUtils.isNotEmpty(subErrors)) {
	    body.put("subErrors", subErrors);
	}

	body.put("traceId", MDC.get("traceId"));
	body.put("spanId", MDC.get("spanId"));

	if (isTraceOn(request)) {
	    body.put("stackTrace", getStackTrace(ex));
	}

	return body;
    }

    private boolean isTraceOn(final WebRequest request) {

	final var value = request.getParameterValues("trace");

	return Objects.nonNull(value) //
			&& value.length > 0 //
			&& "true".contentEquals(value[0]);
    }

}