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