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 * <context-param>
100 * <param-name>johnson.spring.addEventOnBypass</param-name>
101 * <param-value>true</param-value>
102 * </context-param>
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 * <init-param>
128 * <param-name>johnson.spring.addEventOnBypass</param-name>
129 * <param-value>true</param-value>
130 * </init-param>
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 * <context-param>
171 * <param-name>johnson.spring.eventType</param-name>
172 * <param-value>my-spring-context-event-type</param-value>
173 * </context-param>
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 * <init-param>
195 * <param-name>johnson.spring.eventType</param-name>
196 * <param-value>my-spring-servlet-event-type</param-value>
197 * </init-param>
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 }