
me.alidg.errors.WebErrorHandlers Maven / Gradle / Ivy
Show all versions of errors-spring-boot-starter Show documentation
package me.alidg.errors;
import me.alidg.errors.HttpError.CodedMessage;
import me.alidg.errors.conf.ErrorsProperties;
import me.alidg.errors.fingerprint.UuidFingerprintProvider;
import me.alidg.errors.handlers.LastResortWebErrorHandler;
import me.alidg.errors.message.TemplateAwareMessageSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
/**
* A factory over {@link WebErrorHandler} implementations. The factory would query all the
* implementations to find an appropriate exception handler to handle a particular exception.
*
* Code to Message Translation
* The {@link WebErrorHandlers} is also responsible for Error Code to Error Message translation
* and i18n (RIP SRP!). In order to fulfill this requirement, we need an instance of {@link MessageSource}
* to translate error codes to error messages.
*
* Default Exception Handler
* By default, when we couldn't find any {@link WebErrorHandler} implementation to handle the
* exception, we would use the {@link LastResortWebErrorHandler} as the default exception handler.
* If you don't like its exception handling approach, consider passing a valid non-null
* {@link #defaultWebErrorHandler} to the constructor.
*
* @author Ali Dehghani
* @see WebErrorHandler
* @see HandledException
* @see LastResortWebErrorHandler
*/
public class WebErrorHandlers {
/**
* Plain old logger.
*/
private static final Logger log = LoggerFactory.getLogger(WebErrorHandlers.class);
/**
* Collection of {@link WebErrorHandler} implementations. The {@link WebErrorHandlers}
* would choose at most one implementation from this collection to delegate the exception
* handling task. This collection can't be null or empty.
*/
@NonNull
private final List webErrorHandlers;
/**
* To refine exceptions before handling the them.
*/
@NonNull
private final ExceptionRefiner exceptionRefiner;
/**
* To log the to-be-handled exceptions.
*/
@NonNull
private final ExceptionLogger exceptionLogger;
/**
* To execute additional actions using HttpErrors (e.g. logging or messaging).
*/
@NonNull
private final List webErrorHandlerPostProcessors;
/**
* To generate unique fingerprint of error message.
*/
@NonNull
private final FingerprintProvider fingerprintProvider;
/**
* Encapsulates the configuration properties to configure the error starter.
*/
@NonNull
private final ErrorsProperties errorsProperties;
/**
* Responsible for resolving error messages from error codes.
*/
@NonNull
private final TemplateAwareMessageSource messageSource;
/**
* This is the fallback error handler which will be used when all other {@link WebErrorHandler}
* implementations refuse to handle the exception. By default, the {@link LastResortWebErrorHandler}
* would be used in such circumstances but you have the option to provide your own custom error
* handler as the fallback handler.
*/
@NonNull
private WebErrorHandler defaultWebErrorHandler = LastResortWebErrorHandler.INSTANCE;
/**
* Backward-compatible constructor with defaults for {@link #webErrorHandlerPostProcessors}
*
* @param messageSource The code to message translator.
* @param webErrorHandlers Collection of {@link WebErrorHandler} implementations.
* @param defaultWebErrorHandler Fallback web error handler.
* @param exceptionRefiner Possibly can refine exceptions before handling them.
* @param exceptionLogger Logs exceptions.
* @deprecated Use {@link #builder(MessageSource)} instead.
*/
@Deprecated
public WebErrorHandlers(@NonNull MessageSource messageSource,
@NonNull List webErrorHandlers,
@Nullable WebErrorHandler defaultWebErrorHandler,
@Nullable ExceptionRefiner exceptionRefiner,
@Nullable ExceptionLogger exceptionLogger) {
this(messageSource, webErrorHandlers, defaultWebErrorHandler,
exceptionRefiner != null ? exceptionRefiner : ExceptionRefiner.NoOp.INSTANCE,
exceptionLogger != null ? exceptionLogger : ExceptionLogger.NoOp.INSTANCE,
Collections.emptyList(), new UuidFingerprintProvider(), new ErrorsProperties());
}
/**
* To initialize the {@link WebErrorHandlers} instance with a code-to-message translator, a
* non-empty collection of {@link WebErrorHandler} implementations and an optional fallback
* error handler.
*
* This constructor is meant to be called by {@link WebErrorHandlersBuilder#build()} method.
*
* @param messageSource The code to message translator.
* @param webErrorHandlers Collection of {@link WebErrorHandler} implementations.
* @param defaultWebErrorHandler Fallback web error handler.
* @param exceptionRefiner Possibly can refine exceptions before handling them.
* @param exceptionLogger Logs exceptions.
* @param webErrorHandlerPostProcessors Executes additional actions on HttpError.
* @param fingerprintProvider Calculates fingerprint of error message.
* @param errorsProperties Configuration properties bean.
* @throws NullPointerException When one of the required parameters is null.
* @throws IllegalArgumentException When the collection of implementations is empty.
*/
WebErrorHandlers(@NonNull MessageSource messageSource,
@NonNull List webErrorHandlers,
@Nullable WebErrorHandler defaultWebErrorHandler,
@NonNull ExceptionRefiner exceptionRefiner,
@NonNull ExceptionLogger exceptionLogger,
@NonNull List webErrorHandlerPostProcessors,
@NonNull FingerprintProvider fingerprintProvider,
@NonNull ErrorsProperties errorsProperties) {
this.errorsProperties = requireNonNull(errorsProperties);
this.messageSource = new TemplateAwareMessageSource(
requireNonNull(messageSource, "We need a MessageSource implementation to message translation"));
this.webErrorHandlers = requireAtLeastOneHandler(webErrorHandlers);
if (defaultWebErrorHandler != null) this.defaultWebErrorHandler = defaultWebErrorHandler;
this.exceptionRefiner = requireNonNull(exceptionRefiner);
this.exceptionLogger = requireNonNull(exceptionLogger);
this.webErrorHandlerPostProcessors = requireNonNull(webErrorHandlerPostProcessors);
this.fingerprintProvider = requireNonNull(fingerprintProvider);
}
/**
* A nexus to the {@link WebErrorHandlersBuilder} API.
*
* @param messageSource The abstraction responsible for converting error codes to messages.
* @return A {@link WebErrorHandlersBuilder} in its initial state.
* @throws NullPointerException When the given message source is missing.
*/
public static WebErrorHandlersBuilder builder(@NonNull MessageSource messageSource) {
return new WebErrorHandlersBuilder(messageSource);
}
private static List requireAtLeastOneHandler(List handlers) {
if (requireNonNull(handlers, "Collection of error handlers is required").isEmpty())
throw new IllegalArgumentException("We need at least one error handler");
return handlers;
}
/**
* Given any {@code exception}, first it would select an appropriate exception handler or
* falls back to a default handler and then tries to handle the exception using the chosen
* handler. Then would convert the {@link HandledException} to its corresponding {@link HttpError}.
*
* @param originalException The originalException to handle.
* @param httpRequest The current HTTP request.
* @param locale Will be used to target a specific locale while translating the codes to error
* messages.
* @return An {@link HttpError} instance containing both error and message combinations and also,
* the intended HTTP Status Code.
*/
@NonNull
public HttpError handle(@Nullable Throwable originalException, @Nullable Object httpRequest, @Nullable Locale locale) {
if (locale == null) locale = Locale.ROOT;
exceptionLogger.log(originalException);
log.debug("About to handle an exception", originalException);
Throwable exception = refineIfNeeded(originalException);
WebErrorHandler handler = findHandler(exception);
log.debug("The '{}' is going to handle the '{}' exception", className(handler), className(exception));
HandledException handled = handler.handle(exception);
List codeWithMessages = translateErrors(handled, locale);
HttpError httpError = new HttpError(codeWithMessages, handled.getStatusCode());
httpError.setOriginalException(originalException);
httpError.setRefinedException(exception);
httpError.setRequest(httpRequest);
if (errorsProperties.isAddFingerprint()) {
String fingerprint = fingerprintProvider.generate(httpError);
log.debug("Generated fingerprint: {}", fingerprint);
httpError.setFingerprint(fingerprint);
}
log.debug("About to execute {} error handler post processors", webErrorHandlerPostProcessors.size());
webErrorHandlerPostProcessors.forEach(p -> p.process(httpError));
return httpError;
}
private Throwable refineIfNeeded(Throwable exception) {
Throwable refined = exceptionRefiner.refine(exception);
if (refined != null) {
log.debug("The caught exception got refined", refined);
return refined;
}
return exception;
}
private List translateErrors(HandledException handled, Locale locale) {
return handled
.getErrorCodes()
.stream()
.map(code -> withMessage(code, getArgumentsFor(handled, code), locale))
.collect(toList());
}
private CodedMessage withMessage(String code, List arguments, Locale locale) {
try {
String message = messageSource.interpolate(code, arguments, locale);
return new CodedMessage(code, message, arguments);
} catch (Exception e) {
return new CodedMessage(code, null, arguments);
}
}
private WebErrorHandler findHandler(Throwable exception) {
if (exception == null) return defaultWebErrorHandler;
return webErrorHandlers
.stream()
.filter(p -> p.canHandle(exception))
.findFirst()
.orElse(defaultWebErrorHandler);
}
private String className(Object toInspect) {
if (toInspect == null) return "null";
return toInspect.getClass().getName();
}
private List getArgumentsFor(HandledException handled, String errorCode) {
return handled.getArguments().getOrDefault(errorCode, emptyList());
}
}