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