Resolving of ServletExceptions via the configured HandlerExceptionResolvers
authorKai Moritz <kai@juplo.de>
Mon, 20 Jun 2016 15:33:15 +0000 (17:33 +0200)
committerKai Moritz <kai@juplo.de>
Mon, 20 Jun 2016 15:33:15 +0000 (17:33 +0200)
Implemented the class ExceptionResolverErrorController, tries to resolve the
root-cause of a ServletException via the configured HandlerExceptionResolvers.

To do so, the class mimics the initialization of the configured
HandlerExceptionResolvers, that the DispatcherServlet executes, because,
unfortunatly, the DispatcherServlet does not make the configured resolvers
available.

src/main/java/de/juplo/thymeproxy/Application.java
src/main/java/de/juplo/thymeproxy/ExceptionResolverErrorController.java [new file with mode: 0644]

index 1c33ce2..9311cf8 100644 (file)
@@ -2,33 +2,25 @@ package de.juplo.thymeproxy;
 
 import de.juplo.thymeleaf.JuploDialect;
 import java.util.HashMap;
-import java.util.Locale;
 import java.util.Map;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.apache.http.HttpStatus;
+import java.util.Properties;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.web.BasicErrorController;
 import org.springframework.boot.autoconfigure.web.ErrorAttributes;
 import org.springframework.boot.autoconfigure.web.ErrorController;
-import org.springframework.boot.autoconfigure.web.ErrorProperties;
 import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.core.Ordered;
 import org.springframework.core.env.Environment;
-import org.springframework.http.MediaType;
-import org.springframework.web.servlet.ModelAndView;
-import org.springframework.web.servlet.View;
 import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
 import org.springframework.web.servlet.mvc.UrlFilenameViewController;
-import org.thymeleaf.exceptions.TemplateInputException;
 import org.thymeleaf.resourceresolver.IResourceResolver;
 import org.thymeleaf.resourceresolver.UrlResourceResolver;
-import org.thymeleaf.spring4.view.ThymeleafViewResolver;
 import org.thymeleaf.templateresolver.TemplateResolver;
 
 
@@ -121,15 +113,31 @@ public class Application extends WebMvcConfigurerAdapter
   }
 
   @Bean
-  public View error(ThymeleafViewResolver resolver) throws Exception
+  public SimpleMappingExceptionResolver simpleMappingExceptionResolver()
   {
-    return resolver.resolveViewName("templates/500", Locale.getDefault());
+    SimpleMappingExceptionResolver resolver =
+        new SimpleMappingExceptionResolver();
+
+    Properties mappings = new Properties();
+    mappings.setProperty("TemplateInputException", "templates/404");
+
+    resolver.setExceptionMappings(mappings);
+    resolver.setDefaultErrorView("templates/500");
+    resolver.setWarnLogCategory("exception");
+    return resolver;
   }
 
   @Bean
-  public ErrorController errorController(ErrorAttributes errorAttributes)
+  public ErrorController errorController(
+      ApplicationContext context,
+      ErrorAttributes errorAttributes
+      )
   {
-    return new CustomErrorController(errorAttributes, properties.getError());
+    return new ExceptionResolverErrorController(
+        context,
+        errorAttributes,
+        properties.getError()
+        );
   }
 
 
@@ -144,51 +152,4 @@ public class Application extends WebMvcConfigurerAdapter
   {
     SpringApplication.run(Application.class, args);
   }
-
-
-  static class CustomErrorController extends BasicErrorController
-  {
-    public final static String TEMPLATE_INPUT_EXCEPTION =
-        TemplateInputException.class.getCanonicalName();
-
-
-    CustomErrorController(
-        ErrorAttributes errorAttributes,
-        ErrorProperties errorProperties
-        )
-    {
-      super(errorAttributes, errorProperties);
-    }
-
-
-    @Override
-    public ModelAndView errorHtml(
-        HttpServletRequest request,
-        HttpServletResponse response
-        )
-    {
-      Map<String, Object> model =
-          getErrorAttributes(
-              request,
-              isIncludeStackTrace(request, MediaType.TEXT_HTML)
-              );
-
-      String view;
-
-      switch ((String)model.get("exception"))
-      {
-        case "org.thymeleaf.exceptions.TemplateInputException":
-          response.setStatus(HttpStatus.SC_NOT_FOUND);
-          view = "templates/404";
-          break;
-
-        default:
-          response.setStatus(getStatus(request).value());
-          view = "templates/500";
-          break;
-      }
-
-      return new ModelAndView(view, model);
-    }
-  }
 }
\ No newline at end of file
diff --git a/src/main/java/de/juplo/thymeproxy/ExceptionResolverErrorController.java b/src/main/java/de/juplo/thymeproxy/ExceptionResolverErrorController.java
new file mode 100644 (file)
index 0000000..6315100
--- /dev/null
@@ -0,0 +1,296 @@
+package de.juplo.thymeproxy;
+
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.beans.factory.BeanInitializationException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.boot.autoconfigure.web.BasicErrorController;
+import org.springframework.boot.autoconfigure.web.ErrorAttributes;
+import org.springframework.boot.autoconfigure.web.ErrorProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.support.PropertiesLoaderUtils;
+import org.springframework.http.MediaType;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.DispatcherServlet;
+import static org.springframework.web.servlet.DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME;
+import org.springframework.web.servlet.HandlerExceptionResolver;
+import org.springframework.web.servlet.ModelAndView;
+
+
+
+/**
+ *
+ * @author kai
+ */
+public class ExceptionResolverErrorController extends BasicErrorController
+{
+  private final static Logger LOG =
+      LoggerFactory.getLogger(ExceptionResolverErrorController.class);
+
+  public final static String EXCEPTION_ATTRIBUTE =
+      "javax.servlet.error.exception";
+
+
+  /**
+   * Name of the class path resource (relative to the DispatcherServlet class)
+   * that defines DispatcherServlet's default strategy names.
+   */
+  private static final String DEFAULT_STRATEGIES_PATH =
+      "DispatcherServlet.properties";
+  private static final Properties DEFAULT_STRATEGIES;
+
+  static
+  {
+    // Load default strategy implementations from properties file.
+    // This is currently strictly internal and not meant to be customized
+    // by application developers.
+    try
+    {
+      ClassPathResource resource =
+          new ClassPathResource(
+              DEFAULT_STRATEGIES_PATH,
+              DispatcherServlet.class
+              );
+      DEFAULT_STRATEGIES = PropertiesLoaderUtils.loadProperties(resource);
+    }
+    catch (IOException ex)
+    {
+      throw new IllegalStateException(
+          "Could not load 'DispatcherServlet.properties': " +
+          ex.getMessage()
+          );
+    }
+  }
+
+
+  /** List of HandlerExceptionResolvers used by this servlet */
+  private List<HandlerExceptionResolver> handlerExceptionResolvers;
+
+  /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean? */
+  private boolean detectAllHandlerExceptionResolvers = true;
+
+
+  ExceptionResolverErrorController(
+      ApplicationContext context,
+      ErrorAttributes errorAttributes,
+      ErrorProperties errorProperties
+      )
+  {
+    super(errorAttributes, errorProperties);
+    initHandlerExceptionResolvers(context);
+  }
+
+
+  @Override
+  public ModelAndView errorHtml(
+      HttpServletRequest request,
+      HttpServletResponse response
+      )
+  {
+    Map<String, Object> model =
+        getErrorAttributes(
+            request,
+            isIncludeStackTrace(request, MediaType.TEXT_HTML)
+            );
+
+    ModelAndView view = null;
+
+    Exception e = (Exception)request.getAttribute(EXCEPTION_ATTRIBUTE);
+    if (e != null)
+    {
+      if (e instanceof ServletException )
+      {
+        ServletException n = (ServletException)e;
+        Throwable t = n.getRootCause();
+        if (t != null && t instanceof Exception)
+          e = (Exception)t;
+      }
+
+      for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers)
+      {
+        view = resolver.resolveException(request, response, null, e);
+        if (view != null)
+          break;
+      }
+
+      if (view != null)
+      {
+        view.addAllObjects(model);
+        return view;
+      }
+    }
+
+    return new ModelAndView("error", model);
+  }
+
+
+  /**
+   * Initialize the HandlerExceptionResolver used by this class.
+   * <p>
+   * If no bean is defined with the given name in the BeanFactory for this
+   * namespace,
+   * we default to no exception resolver.
+   */
+  private void initHandlerExceptionResolvers(ApplicationContext context)
+  {
+    this.handlerExceptionResolvers = null;
+
+    if (this.detectAllHandlerExceptionResolvers)
+    {
+      // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
+      Map<String, HandlerExceptionResolver> matchingBeans =
+          BeanFactoryUtils.beansOfTypeIncludingAncestors(
+              context,
+              HandlerExceptionResolver.class,
+              true,
+              false
+              );
+      if (!matchingBeans.isEmpty())
+      {
+        this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
+        // We keep HandlerExceptionResolvers in sorted order.
+        AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
+      }
+    }
+    else
+    {
+      try
+      {
+        HandlerExceptionResolver her =
+            context.getBean(
+                HANDLER_EXCEPTION_RESOLVER_BEAN_NAME,
+                HandlerExceptionResolver.class
+                );
+        this.handlerExceptionResolvers = Collections.singletonList(her);
+      }
+      catch (NoSuchBeanDefinitionException e)
+      {
+        // Ignore, no HandlerExceptionResolver is fine too.
+      }
+    }
+
+    // Ensure we have at least some HandlerExceptionResolvers, by registering
+    // default HandlerExceptionResolvers if no other resolvers are found.
+    if (this.handlerExceptionResolvers == null)
+    {
+      this.handlerExceptionResolvers =
+          getDefaultStrategies(context, HandlerExceptionResolver.class);
+      LOG.debug("No HandlerExceptionResolvers found: using default");
+    }
+  }
+
+  /**
+   * Create a List of default strategy objects for the given strategy interface.
+   * <p>
+   * The default implementation uses the "DispatcherServlet.properties" file (in
+   * the same
+   * package as the DispatcherServlet class) to determine the class names. It
+   * instantiates
+   * the strategy objects through the context's BeanFactory.
+   *
+   * @param context           the current WebApplicationContext
+   * @param strategyInterface the strategy interface
+   * @return the List of corresponding strategy objects
+   */
+  @SuppressWarnings("unchecked")
+  protected <T> List<T> getDefaultStrategies(
+      ApplicationContext context,
+      Class<T> strategyInterface
+      )
+  {
+    String key = strategyInterface.getName();
+    String value = DEFAULT_STRATEGIES.getProperty(key);
+    if (value != null)
+    {
+      String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
+      List<T> strategies = new ArrayList<>(classNames.length);
+      for (String className : classNames)
+      {
+        try
+        {
+          Class<?> clazz =
+              ClassUtils.forName(
+                  className,
+                  DispatcherServlet.class.getClassLoader()
+                  );
+          Object strategy = createDefaultStrategy(context, clazz);
+          strategies.add((T) strategy);
+        }
+        catch (ClassNotFoundException ex)
+        {
+          throw new BeanInitializationException(
+              "Could not find DispatcherServlet's default strategy class [" +
+              className + "] for interface [" + key + "]",
+              ex
+              );
+        }
+        catch (LinkageError err)
+        {
+          throw new BeanInitializationException(
+              "Error loading DispatcherServlet's default strategy class [" +
+              className + "] for interface [" + key +
+              "]: problem with class file or dependent class",
+              err
+              );
+        }
+      }
+      return strategies;
+    }
+    else
+    {
+      return new LinkedList<>();
+    }
+  }
+
+  /**
+   * Create a default strategy.
+   * <p>
+   * The default implementation uses
+   * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}.
+   *
+   * @param context the current WebApplicationContext
+   * @param clazz   the strategy implementation class to instantiate
+   * @return the fully configured strategy instance
+   * @see
+   * org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory()
+   * @see
+   * org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean
+   */
+  protected Object createDefaultStrategy(
+      ApplicationContext context,
+      Class<?> clazz
+      )
+  {
+    return context.getAutowireCapableBeanFactory().createBean(clazz);
+  }
+
+
+  /**
+   * Set whether to detect all HandlerExceptionResolver beans in this servlet's
+   * context. Otherwise,
+   * just a single bean with name "handlerExceptionResolver" will be expected.
+   * <p>
+   * Default is "true". Turn this off if you want this servlet to use a single
+   * HandlerExceptionResolver, despite multiple HandlerExceptionResolver beans
+   * being defined in the context.
+   */
+  public void setDetectAllHandlerExceptionResolvers(boolean resolvers)
+  {
+    detectAllHandlerExceptionResolvers = resolvers;
+  }
+}