AbstractConcreteExceptionResolver.java

package cn.home1.oss.lib.errorhandle.api;

import static cn.home1.oss.lib.errorhandle.api.ResolvedErrorException.isResolvedError;
import static cn.home1.oss.lib.errorhandle.api.ResolvedErrorException.isResolvedErrorWrapByOther;
import static com.google.common.base.Throwables.getStackTraceAsString;
import static com.google.common.collect.Lists.newArrayList;
import static cn.home1.oss.lib.common.CurlUtils.curl;
import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST;
import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;

import com.google.common.collect.ImmutableList;

import cn.home1.oss.lib.errorhandle.api.ExceptionTranslator.Location;

import cn.home1.oss.lib.common.Defaults;

import lombok.extern.slf4j.Slf4j;

import org.joda.time.DateTime;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.List;
import java.util.Optional;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

/**
 * AbstractConcreteExceptionResolver.
 *
 * @param <T> exception type
 * @author zhanghaolun
 */
@Slf4j
public abstract class AbstractConcreteExceptionResolver<T extends Throwable> implements ConcreteExceptionResolver<T> {

  private static final String TRACE_OFF = "trace off";

  protected final Class<T> exceptionClass;

  protected ConversionService conversionService;
  protected ExceptionTranslator exceptionTranslator;
  protected StackTraceIndicator stackTraceIndicator;

  /**
   * This constructor determines the exception class from the generic class parameter {@code T}.
   */
  protected AbstractConcreteExceptionResolver() {
    this.exceptionClass = determineTargetType();
  }

  protected AbstractConcreteExceptionResolver(final Class<T> exceptionClass) {
    this.exceptionClass = exceptionClass;
  }

  @Deprecated
  static HttpStatus parseHttpStatus(final Object value) {
    Assert.notNull(value, "Values of the resolverMap map must not be null");

    final HttpStatus result;
    if (value instanceof HttpStatus) {
      result = (HttpStatus) value;
    } else if (value instanceof Integer) {
      result = HttpStatus.valueOf((int) value);
    } else if (value instanceof String) {
      result = HttpStatus.valueOf(Integer.parseInt((String) value));
    } else {
      throw new IllegalArgumentException(String.format( //
        "Values of the resolverMap maps must be instance of " //
          + "ErrorResponseFactory, HttpStatus, String, or int, " //
          + "but %s given",
        value.getClass()));
    }
    return result;
  }

  @Deprecated
  @SuppressWarnings("unused")
  private static <T extends Throwable> Throwable cause(final T exception) {
    Throwable cause = null;
    if (exception != null) {
      cause = exception;
      while (cause instanceof ServletException && cause.getCause() != null) {
        final ServletException servletException = ((ServletException) cause);
        cause = servletException.getCause();
      }
    }
    return cause;
  }

  private static String error(final Optional<Integer> statusOptional) {
    // TODO Optional<Integer>' used as type for parameter
    String result;
    if (statusOptional.isPresent()) {
      final Integer status = statusOptional.get();
      try {
        final HttpStatus httpStatus = HttpStatus.valueOf(status);
        result = httpStatus.getReasonPhrase();
      } catch (final IllegalArgumentException ignored) { // Unable to obtain a reason
        if (log.isDebugEnabled()) {
          log.debug("Unable to obtain a reason, status {}", status, ignored);
        }
        result = "Http Status " + status;
      }
    } else {
      result = "None";
    }
    return result;
  }

  @Override
  public final ResolvedError resolve( //
    final HttpServletRequest request, //
    final T throwable //
  ) {
    final DateTime now = Defaults.now();

    // See http://stackoverflow.com/a/12979543/2217862
    // This attribute is never set in MockMvc, so it's not covered in integration it.
    request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

    final RequestAttributes requestAttributes = new ServletRequestAttributes(request);

    final Boolean stackTrace = this.stackTraceIndicator.stackTrace(request, null);
    final String path = request.getQueryString() != null ? //
      request.getRequestURI() + request.getQueryString() : //
      request.getRequestURI();
    final String track = stackTrace ? curl(request) : TRACE_OFF;

    return this.resolve(requestAttributes, throwable, now, stackTrace, path, track);
  }

  /*
   * (non-Javadoc)
   * 
   * @see ExceptionResolver#resolve(RequestAttributes, Throwable)
   */
  @Override
  public final ResolvedError resolve( //
    final RequestAttributes requestAttributes, //
    final T throwable //
  ) {
    final DateTime now = Defaults.now();

    final Object requestUri = getAttribute( //
      requestAttributes, //
      "javax.servlet.error.request_uri" //
    );
    final Boolean stackTrace = this.stackTraceIndicator.stackTrace(null, null);
    final String path = requestUri != null ? requestUri.toString() : null;
    // TODO final String track = stackTrace ? curl(request) : TRACE_OFF; ? 忘记为什么这里简单使用requestUri
    final String track = stackTrace ? path : TRACE_OFF;

    return this.resolve(requestAttributes, throwable, now, stackTrace, path, track);
  }

  protected ResolvedError resolve( //
    final RequestAttributes requestAttributes, //
    final T throwable, //
    final DateTime now, //
    final Boolean stackTrace, //
    final String path, //
    final String track //
  ) {
    final ResolvedError resolvedError;
    if (isResolvedError(throwable)) {
      resolvedError = ((ResolvedErrorException) throwable).getError();
      resolvedError.trackPrepend(track);
    } else if (isResolvedErrorWrapByOther(throwable)) {
      // 注意顺序
      // 1.判断是不是ResolvedErrorException
      // 2.判断是不是被封装了的ResolvedErrorException(如HystrixException)
      log.warn("这里将被封装过ResolvedErrorException提取出来,原异常信息为:", throwable);
      resolvedError = ((ResolvedErrorException) throwable.getCause()).getError();
      resolvedError.trackPrepend(track);
    } else {
      final Location location = find(throwable).orElse(null);
      // basic
      final Optional<List<ValidationError>> validationErrorsOptional = this.validationErrors(throwable);
      final Optional<Integer> statusOptional = this.status(requestAttributes, location, throwable);
      final String error = error(statusOptional);
      final List<ValidationError> validationErrors = validationErrorsOptional.orElse(null);
      final String exception = throwable != null ? throwable.getClass().getName() : null;
      final String message = message(requestAttributes, throwable, validationErrorsOptional);
      final Integer status = statusOptional.orElse(500);
      final Long timestamp = now.getMillis();
      final String trace = stackTrace && throwable != null ? getStackTraceAsString(throwable) : null;
      // extended
      final String datetime = now.toString(Defaults.ISO8601);
      final String localizedMessage = this.localizedMessage(requestAttributes, location, throwable).orElse("null");
      final HttpHeaders headers = this.createHeaders(requestAttributes, throwable).orElse(null);
      final List<String> tracks = ImmutableList.of(track);

      resolvedError = ResolvedError.resolvedErrorBuilder() //
        // basic
        .error(error) //
        .validationErrors( //
          validationErrors != null ? validationErrors.toArray(new ValidationError[validationErrors.size()]) : null //
        ) //
        .exception(exception) //
        .message(message) //
        .path(path) //
        .status(status) //
        .timestamp(timestamp) //
        .trace(trace) //
        // extended
        .datetime(datetime) //
        .localizedMessage(localizedMessage) //
        .headers(HttpHeader.fromHttpHeaders(headers)) //
        .tracks(tracks.toArray(new String[tracks.size()])) //
        .build();
    }

    logError(requestAttributes, throwable, resolvedError);

    return resolvedError;
  }

  /**
   * Logs the exception; on ERROR level when status is 5xx, otherwise on INFO level without stack trace, or DEBUG level
   * with stack trace. The logger name is {@code ExceptionResolver}.
   *
   * @param requestAttributes requestAttributes
   * @param throwable         throwable
   * @param resolvedError     The exception to log.
   */
  protected void logError( //
    final RequestAttributes requestAttributes, //
    final T throwable, //
    final ResolvedError resolvedError //
  ) {
    if (resolvedError.getStatus() >= HttpStatus.INTERNAL_SERVER_ERROR.value()) {
      final Marker marker = MarkerFactory.getMarker("error");
      final String msg = String.format( //
        "%s ~> %d", //
        resolvedError.getPath(), //
        resolvedError.getStatus() //
      );

      if (log.isTraceEnabled()) {
        log.trace("attributes in request scope: {}", //
          newArrayList(requestAttributes.getAttributeNames(SCOPE_REQUEST)));
      }
      log.warn(marker, msg, new Object[]{throwable});
    }
  }

  @SuppressWarnings("unchecked")
  private Class<T> determineTargetType() {
    return (Class<T>) GenericTypeResolver.resolveTypeArguments( //
      this.getClass(), //
      AbstractConcreteExceptionResolver.class //
    )[0];
  }

  @Override
  public Class<T> getExceptionClass() {
    return this.exceptionClass;
  }

  @Override
  public void setConversionService(final ConversionService conversionService) {
    this.conversionService = conversionService;
  }

  @Override
  public ExceptionTranslator getExceptionTranslator() {
    return this.exceptionTranslator;
  }

  @Override
  public void setExceptionTranslator(final ExceptionTranslator exceptionTranslator) {
    this.exceptionTranslator = exceptionTranslator;
  }

  @Override
  public void setStackTraceIndicator(final StackTraceIndicator stackTraceIndicator) {
    this.stackTraceIndicator = stackTraceIndicator;
  }
}