1 package com.atlassian.johnson.spring.web.servlet;
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 com.atlassian.johnson.spring.web.context.JohnsonContextLoaderListener;
10 import org.slf4j.Logger;
11 import org.slf4j.LoggerFactory;
12 import org.springframework.core.Conventions;
13 import org.springframework.web.context.WebApplicationContext;
14 import org.springframework.web.servlet.DispatcherServlet;
15
16 import javax.annotation.Nonnull;
17 import javax.servlet.ServletConfig;
18 import javax.servlet.ServletContext;
19
20 import static com.atlassian.johnson.spring.web.SpringEventType.createDefaultEvent;
21 import static com.atlassian.johnson.spring.web.SpringEventType.translateThrowable;
22
23 /**
24 * Extends the standard Spring {@code DispatcherServlet} to make it Johnson-aware. When using this class, if the root
25 * Spring context fails to start the dispatcher will not attempt to create/start its child context.
26 * <p>
27 * The goal of this class is to prevent the web application from being shutdown if SpringMVC cannot be started. By
28 * default, if the dispatcher's {@code WebApplicationContext} cannot be started for any reason, an exception is thrown
29 * which is propagated up to the container. When this happens, the entire web application is terminated. This precludes
30 * the use of Johnson, which requires that the web application be up so that it can serve its status pages.
31 */
32 public class JohnsonDispatcherServlet extends DispatcherServlet {
33
34 /**
35 * The prefix for the attribute added to the {@code ServletContext} when Spring MVC initialization is bypassed
36 * because a previous {@link Event} indicates the application has already failed. The MVC dispatcher's name is
37 * appended to this prefix, separated by a colon, to form the attribute.
38 *
39 * @see #getBypassedAttributeName()
40 * @since 3.0
41 */
42 public static final String PREFIX_BYPASSED = Conventions.getQualifiedAttributeName(JohnsonDispatcherServlet.class, "bypassed");
43
44 private static final Logger LOG = LoggerFactory.getLogger(JohnsonDispatcherServlet.class);
45
46 public JohnsonDispatcherServlet() {
47 }
48
49 public JohnsonDispatcherServlet(WebApplicationContext webApplicationContext) {
50 super(webApplicationContext);
51 }
52
53 /**
54 * Performs standard SpringMVC {@code DispatcherServlet} teardown and ensures any attributes added to the servlet
55 * context for this dispatcher are removed.
56 */
57 @Override
58 public void destroy() {
59 try {
60 super.destroy();
61 } finally {
62 getServletContext().removeAttribute(getBypassedAttributeName());
63 }
64 }
65
66 /**
67 * Overrides the standard SpringMVC {@code DispatcherServlet} initialisation to make it Johnson-aware, allowing it
68 * to be automatically bypassed, when paired with a {@link JohnsonContextLoaderListener}, or add an {@link Event}
69 * if initialisation fails.
70 * <p>
71 * This implementation will never throw an exception. Unlike the base implementation, though, it may return
72 * {@code null} if initialisation is bypassed (due to the main {@code WebApplicationContext} being bypassed
73 * or failing to initialise) or if initialisation fails.
74 *
75 * @return the initialised context, or {@code null} if initialisation is bypassed or fails
76 */
77 @Override
78 protected WebApplicationContext initWebApplicationContext() {
79 ServletConfig servletConfig = getServletConfig();
80 ServletContext servletContext = getServletContext();
81 JohnsonEventContainer container = Johnson.getEventContainer(servletContext);
82 String eventType = SpringEventType.getServletEventType(servletConfig);
83
84 Object attribute = servletContext.getAttribute(JohnsonContextLoaderListener.ATTR_BYPASSED);
85 //First, check to see if the WebApplicationContext was bypassed. If it was, it's possible, based on
86 //configuration, that no event was added. However, we must bypass SpringMVC initialisation as well,
87 //because no parent context will be available.
88 if (Boolean.TRUE == attribute) //Fully bypassed, without even trying to start
89 {
90 LOG.error("Bypassing SpringMVC dispatcher [{}] initialisation; Spring initialisation was bypassed",
91 getServletName());
92 servletContext.setAttribute(getBypassedAttributeName(), Boolean.TRUE);
93
94 if (SpringEventType.addEventOnBypass(servletConfig)) {
95 String message = "SpringMVC dispatcher [" + getServletName() +
96 "] will not be started because the Spring WebApplicationContext was not started";
97 container.addEvent(new Event(EventType.get(eventType), message, EventLevel.get(EventLevel.FATAL)));
98 }
99
100 return null;
101 }
102 //If WebApplicationContext initialisation wasn't bypassed, check to see if it failed. SpringMVC initialisation
103 //is guaranteed to fail if the primary WebApplicationContext failed, so we'll want to bypass it.
104 if (attribute instanceof Event) {
105 Event event = (Event) attribute;
106
107 LOG.error("Bypassing SpringMVC dispatcher [{}] initialisation; Spring initialisation failed: {}",
108 getServletName(), event.getDesc());
109 servletContext.setAttribute(getBypassedAttributeName(), Boolean.TRUE);
110
111 if (SpringEventType.addEventOnBypass(servletConfig)) {
112 String message = "SpringMVC dispatcher [" + getServletName() +
113 "] will not be started because the Spring WebApplicationContext failed to start";
114 container.addEvent(new Event(EventType.get(eventType), message, event.getLevel()));
115 }
116
117 return null;
118 }
119
120 //If we make it here, the Spring WebApplicationContext should have started successfully. That means it's safe
121 //to try and start this SpringMVC dispatcher.
122 WebApplicationContext context = null;
123 try {
124 LOG.debug("Attempting to initialise the Spring ApplicationContext");
125 context = super.initWebApplicationContext();
126 } catch (Throwable t) {
127 String message = "SpringMVC dispatcher [" + getServletName() + "] could not be started";
128 LOG.error(message, t);
129
130 Event event = translateThrowable(getServletConfig(), t); //First apply EventExceptionTranslators, if set
131 if (event == null) {
132 event = createEvent(eventType, message, t); //For 2.x compatibility, try createEvent
133 //noinspection ConstantConditions
134 if (event == null) {
135 //When derived classes misbehave creating the event, apply a default
136 event = createDefaultEvent(eventType, message, t);
137 }
138 }
139 servletContext.setAttribute(getBypassedAttributeName(), event);
140
141 container.addEvent(event);
142 }
143 return context;
144 }
145
146 /**
147 * May be overridden in derived classes to allow them to override the default event type or message based on
148 * application-specific understanding of the exception that was thrown.
149 * <p>
150 * For cases where derived classes are not able to offer a more specific event type or message, they are
151 * encouraged to fall back on the behaviour of this superclass method.
152 *
153 * @param defaultEventType the default event type to use if no more specific type is appropriate
154 * @param defaultMessage the default message to use if no more specific message is available
155 * @param t the exception thrown while attempting to initialise the WebApplicationContext
156 * @return the event to add to Johnson, which may not be {@code null}
157 */
158 @Nonnull
159 protected Event createEvent(@Nonnull String defaultEventType, @Nonnull String defaultMessage,
160 @Nonnull Throwable t) {
161 return createDefaultEvent(defaultEventType, defaultMessage, t);
162 }
163
164 /**
165 * Allows derived classes to override the name of the attribute which is added to the {@code ServletContext} when
166 * this dispatcher is bypassed (whether due to the {@link JohnsonContextLoaderListener} being bypassed or due to
167 * the initialisation of the dispatcher failing).
168 * <p>
169 * The default attribute name is: "{@link #PREFIX_BYPASSED}:{@code getServletName()}"
170 *
171 * @return the name for the bypassed context attribute
172 */
173 @Nonnull
174 protected String getBypassedAttributeName() {
175 return PREFIX_BYPASSED + ":" + getServletName();
176 }
177 }