View Javadoc

1   package com.atlassian.johnson.spring.web;
2   
3   import com.atlassian.johnson.Initable;
4   import com.atlassian.johnson.event.Event;
5   import com.atlassian.johnson.event.EventLevel;
6   import com.atlassian.johnson.event.EventType;
7   import com.atlassian.johnson.support.EventExceptionTranslator;
8   import org.slf4j.Logger;
9   import org.slf4j.LoggerFactory;
10  import org.springframework.beans.BeanInstantiationException;
11  import org.springframework.beans.BeanUtils;
12  import org.springframework.util.ClassUtils;
13  import org.springframework.util.StringUtils;
14  
15  import javax.annotation.Nonnull;
16  import javax.annotation.Nullable;
17  import javax.servlet.ServletConfig;
18  import javax.servlet.ServletContext;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.Enumeration;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  
26  import static com.google.common.base.Preconditions.checkNotNull;
27  
28  /**
29   * Constants related to determining which {@link com.atlassian.johnson.event.EventType EventType} to use for Spring-
30   * related {@link com.atlassian.johnson.event.Event Event}s.
31   *
32   * @since 2.0
33   */
34  public class SpringEventType {
35  
36      /**
37       * Defines the {@code init-param} which may be used for controlling whether an event is added when a portion of
38       * Spring initialisation is bypassed due to previous errors.
39       * <p>
40       * Note: This flag does not control whether an event is added when Spring initialisation is not bypassed and fails.
41       *
42       * @see #addEventOnBypass(javax.servlet.ServletContext)
43       * @since 3.0
44       */
45      public static final String PARAM_ADD_EVENT_ON_BYPASS = "johnson.spring.addEventOnBypass";
46  
47      /**
48       * Defines the {@code init-param} which may be used for controlling the event type added when Spring events occur.
49       * Where the value must be set depends on the type being initialised.
50       *
51       * @see #getContextEventType(javax.servlet.ServletContext)
52       * @see #getServletEventType(javax.servlet.ServletConfig)
53       * @since 3.0
54       */
55      public static final String PARAM_EVENT_TYPE = "johnson.spring.eventType";
56  
57      /**
58       * An {@code init-param} which can be applied to the {@code ServletContext} or {@code ServletConfig} to register
59       * one or more {@link EventExceptionTranslator} types to be applied to exceptions thrown from Spring or SpringMVC
60       * startup.
61       *
62       * @see #translateThrowable(ServletConfig, Throwable)
63       * @since 3.0
64       */
65      public static final String PARAM_EXCEPTION_TRANSLATOR_CLASS = "exceptionTranslatorClass";
66  
67      /**
68       * Defines the separator characters which can be used between {@link #PARAM_EXCEPTION_TRANSLATOR_CLASS exception
69       * translator classes}.
70       */
71      public static final String SEPARATORS = ",; \t\n";
72  
73      /**
74       * Defines the default context event type which will be used if one is not explicitly set.
75       */
76      public static final String SPRING_CONTEXT_EVENT_TYPE = "spring";
77  
78      /**
79       * Defines the default servlet event type which will be used if one is not explicitly set.
80       */
81      public static final String SPRING_SERVLET_EVENT_TYPE = "spring-mvc";
82  
83      private static final Logger LOG = LoggerFactory.getLogger(SpringEventType.class);
84  
85      private SpringEventType() {
86          throw new UnsupportedOperationException(getClass().getName() + " should not be instantiated");
87      }
88  
89      /**
90       * Retrieves a flag indicating whether a Johnson event should be added when Spring initialisation is bypassed
91       * due to previous fatal errors.
92       * <p>
93       * By default, an event is <i>not</i> added. If a {@code context-param} named {@link #PARAM_ADD_EVENT_ON_BYPASS}
94       * exists with the value {@code true}, then an {@link #getContextEventType(javax.servlet.ServletContext) event}
95       * will be added when initialisation is bypassed.
96       * <p>
97       * To set this value, add the following to {@code web.xml}:
98       * <pre><code>
99       *     &lt;context-param&gt;
100      *         &lt;param-name&gt;johnson.spring.addEventOnBypass&lt;/param-name&gt;
101      *         &lt;param-value&gt;true&lt;/param-value&gt;
102      *     &lt;/context-param&gt;
103      * </code></pre>
104      * Note: If initialisation is not bypassed and fails, this flag <i>does not</i> control whether an event will be
105      * added at that time.
106      *
107      * @param context the servlet context
108      * @return {@code true} if an event has been explicitly requested; otherwise, {@code false}
109      */
110     public static boolean addEventOnBypass(@Nonnull ServletContext context) {
111         return "true".equals(checkNotNull(context, "context").getInitParameter(PARAM_ADD_EVENT_ON_BYPASS));
112     }
113 
114     /**
115      * Retrieves a flag indicating whether a Johnson event should be added when SpringMVC initialisation is bypassed
116      * due to previous fatal Spring errors.
117      * <p>
118      * By default, an event is <i>not</i> added. If an {@code init-param} named {@link #PARAM_ADD_EVENT_ON_BYPASS}
119      * exists, its value ({@code true} or {@code false}) controls whether an event is added. Otherwise, a fallback check
120      * is made {@link #addEventOnBypass(javax.servlet.ServletContext) to the context} for a {@code context-param}.
121      * This means if an event is explicitly requested at the context level, by default it will also be requested at
122      * the servlet level. However, individual servlets can explicitly disable that by setting their {@code init-param}
123      * to {@code false}.
124      * <p>
125      * To set this value, add the following to the declaration for the servlet in {@code web.xml}:
126      * <pre><code>
127      *     &lt;init-param&gt;
128      *         &lt;param-name&gt;johnson.spring.addEventOnBypass&lt;/param-name&gt;
129      *         &lt;param-value&gt;true&lt;/param-value&gt;
130      *     &lt;/init-param&gt;
131      * </code></pre>
132      * Note: If initialisation is not bypassed and fails, this flag <i>does not</i> control whether an event will be
133      * added at that time.
134      *
135      * @param config the servlet configuration
136      * @return {@code true} if an event has been specifically requested, either at the servlet level or at the context
137      * level; otherwise, {@code false}
138      */
139     public static boolean addEventOnBypass(@Nonnull ServletConfig config) {
140         String value = checkNotNull(config, "config").getInitParameter(PARAM_ADD_EVENT_ON_BYPASS);
141         if (value == null) {
142             //If no parameter was found at the servlet level, look at the context level.
143             return addEventOnBypass(config.getServletContext());
144         }
145         //If any value is found at the servlet level, be it true or false, that value always overrides any value set
146         //at the context level.
147         return "true".equals(value);
148     }
149 
150     /**
151      * A fail-safe event creator with reliable semantics to fall back on when a more specific {@link Event event} is
152      * not available.
153      *
154      * @param eventType the event type to use
155      * @param message   the message to use
156      * @param t         the exception thrown while attempting to initialise the WebApplicationContext
157      * @return the event to add to Johnson, which will never be {@code null}
158      */
159     @Nonnull
160     public static Event createDefaultEvent(@Nonnull String eventType, @Nonnull String message, @Nonnull Throwable t) {
161         return new Event(EventType.get(eventType), message, Event.toString(t), EventLevel.get(EventLevel.FATAL));
162     }
163 
164     /**
165      * Examines the provided {@code ServletContext} for a {@code context-param} named {@link #PARAM_EVENT_TYPE} and, if
166      * one is found, returns its value; otherwise the default {@link #SPRING_CONTEXT_EVENT_TYPE} is returned.
167      * <p>
168      * To set this value, add the following to {@code web.xml}:
169      * <pre><code>
170      *     &lt;context-param&gt;
171      *         &lt;param-name&gt;johnson.spring.eventType&lt;/param-name&gt;
172      *         &lt;param-value&gt;my-spring-context-event-type&lt;/param-value&gt;
173      *     &lt;/context-param&gt;
174      * </code></pre>
175      *
176      * @param context the servlet context
177      * @return the context event type
178      */
179     @Nonnull
180     public static String getContextEventType(@Nonnull ServletContext context) {
181         String value = checkNotNull(context, "context").getInitParameter(PARAM_EVENT_TYPE);
182         if (!StringUtils.hasText(value)) {
183             value = SPRING_CONTEXT_EVENT_TYPE;
184         }
185         return value;
186     }
187 
188     /**
189      * Examines the provided {@code ServletConfig} for an {@code init-param} named {@link #PARAM_EVENT_TYPE} and, if
190      * one is found, returns its value; otherwise, the default {@link #SPRING_SERVLET_EVENT_TYPE} is returned.
191      * <p>
192      * To set this value, add the following to the declaration for the servlet in {@code web.xml}:
193      * <pre><code>
194      *     &lt;init-param&gt;
195      *         &lt;param-name&gt;johnson.spring.eventType&lt;/param-name&gt;
196      *         &lt;param-value&gt;my-spring-servlet-event-type&lt;/param-value&gt;
197      *     &lt;/init-param&gt;
198      * </code></pre>
199      *
200      * @param config the servlet configuration
201      * @return the servlet event type
202      */
203     @Nonnull
204     public static String getServletEventType(@Nonnull ServletConfig config) {
205         String value = checkNotNull(config, "config").getInitParameter(PARAM_EVENT_TYPE);
206         if (!StringUtils.hasText(value)) {
207             value = SPRING_SERVLET_EVENT_TYPE;
208         }
209         return value;
210     }
211 
212     @Nullable
213     public static Event translateThrowable(@Nonnull ServletConfig config, @Nonnull Throwable t) {
214         return translateThrowable(new ServletConfigMapSupplier(config), t);
215     }
216 
217     @Nullable
218     public static Event translateThrowable(@Nonnull ServletContext servletContext, @Nonnull Throwable t) {
219         return translateThrowable(new ServletContextMapSupplier(servletContext), t);
220     }
221 
222     @Nullable
223     @SuppressWarnings("unchecked")
224     private static Class<EventExceptionTranslator> loadTranslatorClass(@Nonnull String className) {
225         try {
226             Class<?> clazz = ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
227             if (EventExceptionTranslator.class.isAssignableFrom(clazz)) {
228                 return (Class) clazz;
229             }
230             LOG.warn("Translator class {} does not implement {}", className, EventExceptionTranslator.class.getName());
231         } catch (ClassNotFoundException e) {
232             LOG.warn("Translator class {} could not be loaded", className);
233         }
234 
235         return null;
236     }
237 
238     private static Event translateThrowable(@Nonnull MapSupplier supplier, @Nonnull Throwable t) {
239         String param = supplier.getValue(PARAM_EXCEPTION_TRANSLATOR_CLASS);
240         if (!StringUtils.hasText(param)) {
241             return null;
242         }
243 
244         List<Class<EventExceptionTranslator>> translatorClasses = new ArrayList<Class<EventExceptionTranslator>>();
245         for (String className : StringUtils.tokenizeToStringArray(param, SEPARATORS)) {
246             Class<EventExceptionTranslator> clazz = loadTranslatorClass(className);
247             if (clazz != null) {
248                 translatorClasses.add(clazz);
249             }
250         }
251 
252         if (translatorClasses.isEmpty()) {
253             LOG.warn("None of the configured translator classes could be loaded");
254             return null;
255         }
256 
257         for (Class<EventExceptionTranslator> clazz : translatorClasses) {
258             try {
259                 EventExceptionTranslator translator = BeanUtils.instantiateClass(clazz, EventExceptionTranslator.class);
260                 if (translator instanceof Initable) {
261                     ((Initable) translator).init(supplier.get());
262                 }
263 
264                 Event event = translator.translate(t);
265                 if (event != null) {
266                     return event;
267                 }
268             } catch (BeanInstantiationException e) {
269                 LOG.warn("{} could not be instantiated", clazz.getName(), e);
270             }
271         }
272         return null;
273     }
274 
275     //If you're wondering, everything that happens below here is because ServletConfig and ServletContext have a
276     //set of _completely identical methods_ but no shared interface between them. Because Java.
277 
278     private abstract static class AbstractMapSupplier implements MapSupplier {
279 
280         private Map<String, String> map;
281 
282         @Nonnull
283         @Override
284         public Map<String, String> get() {
285             if (map == null) {
286                 map = buildMap();
287             }
288 
289             return map;
290         }
291 
292         protected Map<String, String> buildMap() {
293             List<String> names = Collections.list(getNames());
294             if (names.isEmpty()) {
295                 return Collections.emptyMap();
296             }
297 
298             Map<String, String> map = new HashMap<String, String>(names.size(), 1.0f);
299             for (String name : names) {
300                 map.put(name, getValue(name));
301             }
302             return Collections.unmodifiableMap(map);
303         }
304 
305         protected abstract Enumeration<String> getNames();
306     }
307 
308     private interface MapSupplier {
309 
310         @Nonnull
311         Map<String, String> get();
312 
313         @Nullable
314         String getValue(@Nonnull String name);
315     }
316 
317     private static class ServletConfigMapSupplier extends AbstractMapSupplier {
318 
319         private final ServletConfig config;
320 
321         private ServletConfigMapSupplier(ServletConfig config) {
322             this.config = config;
323         }
324 
325         @Override
326         public String getValue(@Nonnull String name) {
327             return config.getInitParameter(name);
328         }
329 
330         @Nonnull
331         @Override
332         protected Enumeration<String> getNames() {
333             return config.getInitParameterNames();
334         }
335     }
336 
337     private static class ServletContextMapSupplier extends AbstractMapSupplier {
338 
339         private final ServletContext servletContext;
340 
341         private ServletContextMapSupplier(ServletContext servletContext) {
342             this.servletContext = servletContext;
343         }
344 
345         @Override
346         public String getValue(@Nonnull String name) {
347             return servletContext.getInitParameter(name);
348         }
349 
350         @Override
351         protected Enumeration<String> getNames() {
352             return servletContext.getInitParameterNames();
353         }
354     }
355 }