1 package com.atlassian.johnson.spring.web.context;
2
3 import com.atlassian.johnson.Johnson;
4 import com.atlassian.johnson.JohnsonEventContainer;
5 import com.atlassian.johnson.event.Event;
6 import com.atlassian.johnson.event.EventLevel;
7 import com.atlassian.johnson.event.EventType;
8 import com.atlassian.johnson.spring.web.SpringEventType;
9 import org.slf4j.Logger;
10 import org.slf4j.LoggerFactory;
11 import org.springframework.core.Conventions;
12 import org.springframework.web.context.ContextLoaderListener;
13 import org.springframework.web.context.WebApplicationContext;
14
15 import javax.servlet.ServletContext;
16 import javax.servlet.ServletContextEvent;
17
18 /**
19 * Extends the standard Spring {@code ContextLoaderListener} to make it Johnson-aware. When using this class, if the
20 * Spring context fails to start an event will added to Johnson rather than propagated to the servlet container.
21 * <p/>
22 * The goal of this class is to prevent the web application from being shutdown if Spring cannot be started. By default,
23 * if the {@code WebApplicationContext} cannot be started for any reason, an exception is thrown which is propagated up
24 * to the container. When this happens, the entire web application is terminated. This precludes the use of Johnson,
25 * which requires that the web application be up so that it can serve its status pages.
26 */
27 public class JohnsonContextLoaderListener extends ContextLoaderListener
28 {
29 public static final String BYPASSED_ATTRIBUTE = Conventions.getQualifiedAttributeName(JohnsonContextLoaderListener.class, "bypassed");
30
31 private static final Logger LOG = LoggerFactory.getLogger(JohnsonContextLoaderListener.class);
32
33 /**
34 * Performs standard Spring {@code ContextLoaderListener} teardown and ensures any attributes added to the servlet
35 * context for this dispatcher are removed.
36 *
37 * @param event the context event
38 */
39 @Override
40 public void contextDestroyed(ServletContextEvent event)
41 {
42 try
43 {
44 super.contextDestroyed(event);
45 }
46 finally
47 {
48 event.getServletContext().removeAttribute(BYPASSED_ATTRIBUTE);
49 }
50 }
51
52 /**
53 * Overrides the standard Spring {@code ContextLoaderListener} initialisation to make it Johnson-aware, allowing it
54 * to be automatically bypassed, when {@link com.atlassian.johnson.event.ApplicationEventCheck application checks}
55 * produce {@link EventLevel#FATAL fatal} events, or add an {@link Event} if initialisation fails.
56 * <p/>
57 * This implementation will never throw an exception. Unlike the base implementation, though, it may return
58 * {@code null} if initialisation is bypassed (due to previous fatal events or failing to initialise) or if
59 * initialisation fails.
60 *
61 * @param servletContext the servlet context
62 * @return the initialised context, or {@code null} if initialisation is bypassed or fails
63 */
64 @Override
65 public WebApplicationContext initWebApplicationContext(ServletContext servletContext)
66 {
67 String eventType = SpringEventType.getContextEventType(servletContext);
68
69 //Search for previous FATAL errors and, if any are found, add another indicating Spring startup has been
70 //canceled. The presence of other error types in the container (warnings, errors) will not prevent this
71 //implementation from attempting to start Spring.
72 JohnsonEventContainer container = Johnson.getEventContainer(servletContext);
73 if (container.hasEvents())
74 {
75 LOG.debug("Searching Johnson for previous {} errors", EventLevel.FATAL);
76 for (Event event : container.getEvents())
77 {
78 EventLevel level = event.getLevel();
79 if (EventLevel.FATAL.equals(level.getLevel()))
80 {
81 LOG.error("Bypassing Spring ApplicationContext initialisation; a previous {} error was found: {}",
82 level.getLevel(), event.getDesc());
83 servletContext.setAttribute(BYPASSED_ATTRIBUTE, Boolean.TRUE);
84
85 if (SpringEventType.addEventOnBypass(servletContext))
86 {
87 String message = "The Spring WebApplicationContext will not be started due to a previous " +
88 level.getLevel() + " error";
89 container.addEvent(new Event(EventType.get(eventType), message, level));
90 }
91
92 //The base class actually ignores the return, so null is safe.
93 return null;
94 }
95 }
96 }
97
98 WebApplicationContext context = null;
99 try
100 {
101 LOG.debug("Attempting to initialise the Spring ApplicationContext");
102 context = super.initWebApplicationContext(servletContext);
103 }
104 catch (Throwable t)
105 {
106 String message = "The Spring WebApplicationContext could not be started";
107 LOG.error(message, t);
108
109 //The Spring ContextLoader class sets the exception that was thrown during initialisation on the servlet
110 //context under this constant. Whenever things attempt to retrieve the WebApplicationContext from the
111 //servlet context, if the property value is an exception, it is rethrown. This makes other parts of the
112 //web application, like DelegatingFilterProxies, fail to start (which, in turn, brings down the entire
113 //web application and prevents access to Johnson).
114 //Because we need the web application to be able to come up even if Spring fails, that behaviour is not
115 //desirable. So before we add the event to Johnson, the first thing we do is clear that attribute back
116 //off of the context. That way, when things attempt to retrieve the context, they'll just get a null back
117 //(which matches what happens if we bypass Spring startup completely, above)
118 servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
119
120 //After we remove Spring's attribute, we need to set our own. This allows other Johnson-aware constructs
121 //to know we've bypassed Spring initialisation (or, more exactly, that it failed)
122 Event event = createEvent(eventType, message, t);
123 if (event == null)
124 {
125 //When derived classes misbehave on creating the event, fallback internally on our own implementation
126 event = createDefaultEvent(eventType, message, t);
127 }
128 servletContext.setAttribute(BYPASSED_ATTRIBUTE, event);
129
130 //Add the event to Johnson
131 container.addEvent(event);
132 }
133 return context;
134 }
135
136 /**
137 * May be overridden in derived classes to allow them to override the default event type or message based on
138 * application-specific understanding of the exception that was thrown.
139 * <p/>
140 * For cases where derived classes are not able to offer a more specific event type or message, they are
141 * encouraged to fall back on the behaviour of this superclass method.
142 *
143 * @param defaultEventType the default event type to use if no more specific type is appropriate
144 * @param defaultMessage the default message to use if no more specific message is available
145 * @param t the exception thrown while attempting to initialise the WebApplicationContext
146 * @return the event to add to Johnson, which may not be {@code null}
147 */
148 protected Event createEvent(String defaultEventType, String defaultMessage, Throwable t)
149 {
150 return createDefaultEvent(defaultEventType, defaultMessage, t);
151 }
152
153 /**
154 * A fail-safe event creator which cannot be overridden, allowing
155 * {@link #initWebApplicationContext(ServletContext)}} something with reliable semantics to fall back on when
156 * {@link #createEvent(String, String, Throwable)} overrides return {@code null} against its contract.
157 *
158 * @param eventType the event type to use
159 * @param message the message to use
160 * @param t the exception thrown while attempting to initialise the WebApplicationContext
161 * @return the event to add to Johnson, which will never be {@code null}
162 */
163 private Event createDefaultEvent(String eventType, String message, Throwable t)
164 {
165 return new Event(EventType.get(eventType), message,
166 Event.toString(t), EventLevel.get(EventLevel.FATAL));
167 }
168 }