Resolving of ServletExceptions via the configured HandlerExceptionResolvers
[maven-thymeleaf-skin] / src / main / java / de / juplo / thymeproxy / ExceptionResolverErrorController.java
1 package de.juplo.thymeproxy;
2
3
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.Collections;
7 import java.util.LinkedList;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.Properties;
11 import javax.servlet.ServletException;
12 import javax.servlet.http.HttpServletRequest;
13 import javax.servlet.http.HttpServletResponse;
14 import org.slf4j.Logger;
15 import org.slf4j.LoggerFactory;
16 import org.springframework.beans.factory.BeanFactoryUtils;
17 import org.springframework.beans.factory.BeanInitializationException;
18 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
19 import org.springframework.boot.autoconfigure.web.BasicErrorController;
20 import org.springframework.boot.autoconfigure.web.ErrorAttributes;
21 import org.springframework.boot.autoconfigure.web.ErrorProperties;
22 import org.springframework.context.ApplicationContext;
23 import org.springframework.core.annotation.AnnotationAwareOrderComparator;
24 import org.springframework.core.io.ClassPathResource;
25 import org.springframework.core.io.support.PropertiesLoaderUtils;
26 import org.springframework.http.MediaType;
27 import org.springframework.util.ClassUtils;
28 import org.springframework.util.StringUtils;
29 import org.springframework.web.servlet.DispatcherServlet;
30 import static org.springframework.web.servlet.DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME;
31 import org.springframework.web.servlet.HandlerExceptionResolver;
32 import org.springframework.web.servlet.ModelAndView;
33
34
35
36 /**
37  *
38  * @author kai
39  */
40 public class ExceptionResolverErrorController extends BasicErrorController
41 {
42   private final static Logger LOG =
43       LoggerFactory.getLogger(ExceptionResolverErrorController.class);
44
45   public final static String EXCEPTION_ATTRIBUTE =
46       "javax.servlet.error.exception";
47
48
49   /**
50    * Name of the class path resource (relative to the DispatcherServlet class)
51    * that defines DispatcherServlet's default strategy names.
52    */
53   private static final String DEFAULT_STRATEGIES_PATH =
54       "DispatcherServlet.properties";
55   private static final Properties DEFAULT_STRATEGIES;
56
57   static
58   {
59     // Load default strategy implementations from properties file.
60     // This is currently strictly internal and not meant to be customized
61     // by application developers.
62     try
63     {
64       ClassPathResource resource =
65           new ClassPathResource(
66               DEFAULT_STRATEGIES_PATH,
67               DispatcherServlet.class
68               );
69       DEFAULT_STRATEGIES = PropertiesLoaderUtils.loadProperties(resource);
70     }
71     catch (IOException ex)
72     {
73       throw new IllegalStateException(
74           "Could not load 'DispatcherServlet.properties': " +
75           ex.getMessage()
76           );
77     }
78   }
79
80
81   /** List of HandlerExceptionResolvers used by this servlet */
82   private List<HandlerExceptionResolver> handlerExceptionResolvers;
83
84   /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean? */
85   private boolean detectAllHandlerExceptionResolvers = true;
86
87
88   ExceptionResolverErrorController(
89       ApplicationContext context,
90       ErrorAttributes errorAttributes,
91       ErrorProperties errorProperties
92       )
93   {
94     super(errorAttributes, errorProperties);
95     initHandlerExceptionResolvers(context);
96   }
97
98
99   @Override
100   public ModelAndView errorHtml(
101       HttpServletRequest request,
102       HttpServletResponse response
103       )
104   {
105     Map<String, Object> model =
106         getErrorAttributes(
107             request,
108             isIncludeStackTrace(request, MediaType.TEXT_HTML)
109             );
110
111     ModelAndView view = null;
112
113     Exception e = (Exception)request.getAttribute(EXCEPTION_ATTRIBUTE);
114     if (e != null)
115     {
116       if (e instanceof ServletException )
117       {
118         ServletException n = (ServletException)e;
119         Throwable t = n.getRootCause();
120         if (t != null && t instanceof Exception)
121           e = (Exception)t;
122       }
123
124       for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers)
125       {
126         view = resolver.resolveException(request, response, null, e);
127         if (view != null)
128           break;
129       }
130
131       if (view != null)
132       {
133         view.addAllObjects(model);
134         return view;
135       }
136     }
137
138     return new ModelAndView("error", model);
139   }
140
141
142   /**
143    * Initialize the HandlerExceptionResolver used by this class.
144    * <p>
145    * If no bean is defined with the given name in the BeanFactory for this
146    * namespace,
147    * we default to no exception resolver.
148    */
149   private void initHandlerExceptionResolvers(ApplicationContext context)
150   {
151     this.handlerExceptionResolvers = null;
152
153     if (this.detectAllHandlerExceptionResolvers)
154     {
155       // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
156       Map<String, HandlerExceptionResolver> matchingBeans =
157           BeanFactoryUtils.beansOfTypeIncludingAncestors(
158               context,
159               HandlerExceptionResolver.class,
160               true,
161               false
162               );
163       if (!matchingBeans.isEmpty())
164       {
165         this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
166         // We keep HandlerExceptionResolvers in sorted order.
167         AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
168       }
169     }
170     else
171     {
172       try
173       {
174         HandlerExceptionResolver her =
175             context.getBean(
176                 HANDLER_EXCEPTION_RESOLVER_BEAN_NAME,
177                 HandlerExceptionResolver.class
178                 );
179         this.handlerExceptionResolvers = Collections.singletonList(her);
180       }
181       catch (NoSuchBeanDefinitionException e)
182       {
183         // Ignore, no HandlerExceptionResolver is fine too.
184       }
185     }
186
187     // Ensure we have at least some HandlerExceptionResolvers, by registering
188     // default HandlerExceptionResolvers if no other resolvers are found.
189     if (this.handlerExceptionResolvers == null)
190     {
191       this.handlerExceptionResolvers =
192           getDefaultStrategies(context, HandlerExceptionResolver.class);
193       LOG.debug("No HandlerExceptionResolvers found: using default");
194     }
195   }
196
197   /**
198    * Create a List of default strategy objects for the given strategy interface.
199    * <p>
200    * The default implementation uses the "DispatcherServlet.properties" file (in
201    * the same
202    * package as the DispatcherServlet class) to determine the class names. It
203    * instantiates
204    * the strategy objects through the context's BeanFactory.
205    *
206    * @param context           the current WebApplicationContext
207    * @param strategyInterface the strategy interface
208    * @return the List of corresponding strategy objects
209    */
210   @SuppressWarnings("unchecked")
211   protected <T> List<T> getDefaultStrategies(
212       ApplicationContext context,
213       Class<T> strategyInterface
214       )
215   {
216     String key = strategyInterface.getName();
217     String value = DEFAULT_STRATEGIES.getProperty(key);
218     if (value != null)
219     {
220       String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
221       List<T> strategies = new ArrayList<>(classNames.length);
222       for (String className : classNames)
223       {
224         try
225         {
226           Class<?> clazz =
227               ClassUtils.forName(
228                   className,
229                   DispatcherServlet.class.getClassLoader()
230                   );
231           Object strategy = createDefaultStrategy(context, clazz);
232           strategies.add((T) strategy);
233         }
234         catch (ClassNotFoundException ex)
235         {
236           throw new BeanInitializationException(
237               "Could not find DispatcherServlet's default strategy class [" +
238               className + "] for interface [" + key + "]",
239               ex
240               );
241         }
242         catch (LinkageError err)
243         {
244           throw new BeanInitializationException(
245               "Error loading DispatcherServlet's default strategy class [" +
246               className + "] for interface [" + key +
247               "]: problem with class file or dependent class",
248               err
249               );
250         }
251       }
252       return strategies;
253     }
254     else
255     {
256       return new LinkedList<>();
257     }
258   }
259
260   /**
261    * Create a default strategy.
262    * <p>
263    * The default implementation uses
264    * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}.
265    *
266    * @param context the current WebApplicationContext
267    * @param clazz   the strategy implementation class to instantiate
268    * @return the fully configured strategy instance
269    * @see
270    * org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory()
271    * @see
272    * org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean
273    */
274   protected Object createDefaultStrategy(
275       ApplicationContext context,
276       Class<?> clazz
277       )
278   {
279     return context.getAutowireCapableBeanFactory().createBean(clazz);
280   }
281
282
283   /**
284    * Set whether to detect all HandlerExceptionResolver beans in this servlet's
285    * context. Otherwise,
286    * just a single bean with name "handlerExceptionResolver" will be expected.
287    * <p>
288    * Default is "true". Turn this off if you want this servlet to use a single
289    * HandlerExceptionResolver, despite multiple HandlerExceptionResolver beans
290    * being defined in the context.
291    */
292   public void setDetectAllHandlerExceptionResolvers(boolean resolvers)
293   {
294     detectAllHandlerExceptionResolvers = resolvers;
295   }
296 }