001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.web.servlet;
018:
019: import java.io.IOException;
020: import java.security.Principal;
021:
022: import javax.servlet.ServletException;
023: import javax.servlet.http.HttpServletRequest;
024: import javax.servlet.http.HttpServletResponse;
025:
026: import org.springframework.beans.BeanUtils;
027: import org.springframework.beans.BeansException;
028: import org.springframework.context.ApplicationContext;
029: import org.springframework.context.ApplicationContextException;
030: import org.springframework.context.ApplicationEvent;
031: import org.springframework.context.ApplicationListener;
032: import org.springframework.context.ConfigurableApplicationContext;
033: import org.springframework.context.event.ContextRefreshedEvent;
034: import org.springframework.util.StringUtils;
035: import org.springframework.web.context.ConfigurableWebApplicationContext;
036: import org.springframework.web.context.WebApplicationContext;
037: import org.springframework.web.context.support.ServletRequestHandledEvent;
038: import org.springframework.web.context.support.WebApplicationContextUtils;
039: import org.springframework.web.context.support.XmlWebApplicationContext;
040: import org.springframework.web.util.NestedServletException;
041: import org.springframework.web.util.WebUtils;
042:
043: /**
044: * Base servlet for Spring's web framework. Provides integration with
045: * a Spring application context, in a JavaBean-based overall solution.
046: *
047: * <p>This class offers the following functionality:
048: * <ul>
049: * <li>Manages a {@link org.springframework.web.context.WebApplicationContext}
050: * instance per servlet. The servlet's configuration is determined by beans
051: * in the servlet's namespace.
052: * <li>Publishes events on request processing, whether or not a request is
053: * successfully handled.
054: * </ul>
055: *
056: * <p>Subclasses must implement {@link #doService} to handle requests. Because this extends
057: * {@link HttpServletBean} rather than HttpServlet directly, bean properties are
058: * automatically mapped onto it. Subclasses can override {@link #initFrameworkServlet()}
059: * for custom initialization.
060: *
061: * <p>Detects a "contextClass" parameter at the servlet init-param level,
062: * falling back to the default context class,
063: * {@link org.springframework.web.context.support.XmlWebApplicationContext},
064: * if not found. Note that, with the default FrameworkServlet,
065: * a custom context class needs to implement the
066: * {@link org.springframework.web.context.ConfigurableWebApplicationContext} SPI.
067: *
068: * <p>Passes a "contextConfigLocation" servlet init-param to the context instance,
069: * parsing it into potentially multiple file paths which can be separated by any
070: * number of commas and spaces, like "test-servlet.xml, myServlet.xml".
071: * If not explicitly specified, the context implementation is supposed to build a
072: * default location from the namespace of the servlet.
073: *
074: * <p>Note: In case of multiple config locations, later bean definitions will
075: * override ones defined in earlier loaded files, at least when using Spring's
076: * default ApplicationContext implementation. This can be leveraged to
077: * deliberately override certain bean definitions via an extra XML file.
078: *
079: * <p>The default namespace is "'servlet-name'-servlet", e.g. "test-servlet" for a
080: * servlet-name "test" (leading to a "/WEB-INF/test-servlet.xml" default location
081: * with XmlWebApplicationContext). The namespace can also be set explicitly via
082: * the "namespace" servlet init-param.
083: *
084: * @author Rod Johnson
085: * @author Juergen Hoeller
086: * @see #doService
087: * @see #setContextClass
088: * @see #setContextConfigLocation
089: * @see #setNamespace
090: */
091: public abstract class FrameworkServlet extends HttpServletBean
092: implements ApplicationListener {
093:
094: /**
095: * Suffix for WebApplicationContext namespaces. If a servlet of this class is
096: * given the name "test" in a context, the namespace used by the servlet will
097: * resolve to "test-servlet".
098: */
099: public static final String DEFAULT_NAMESPACE_SUFFIX = "-servlet";
100:
101: /**
102: * Default context class for FrameworkServlet.
103: * @see org.springframework.web.context.support.XmlWebApplicationContext
104: */
105: public static final Class DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;
106:
107: /**
108: * Prefix for the ServletContext attribute for the WebApplicationContext.
109: * The completion is the servlet name.
110: */
111: public static final String SERVLET_CONTEXT_PREFIX = FrameworkServlet.class
112: .getName()
113: + ".CONTEXT.";
114:
115: /** WebApplicationContext implementation class to use */
116: private Class contextClass = DEFAULT_CONTEXT_CLASS;
117:
118: /** Namespace for this servlet */
119: private String namespace;
120:
121: /** Explicit context config location */
122: private String contextConfigLocation;
123:
124: /** Should we publish the context as a ServletContext attribute? */
125: private boolean publishContext = true;
126:
127: /** Should we publish a ServletRequestHandledEvent at the end of each request? */
128: private boolean publishEvents = true;
129:
130: /** WebApplicationContext for this servlet */
131: private WebApplicationContext webApplicationContext;
132:
133: /** Flag used to detect whether onRefresh has already been called */
134: private boolean refreshEventReceived = false;
135:
136: /**
137: * Set a custom context class. This class must be of type
138: * {@link org.springframework.web.context.WebApplicationContext}.
139: * <p>When using the default FrameworkServlet implementation,
140: * the context class must also implement the
141: * {@link org.springframework.web.context.ConfigurableWebApplicationContext}
142: * interface.
143: * @see #createWebApplicationContext
144: */
145: public void setContextClass(Class contextClass) {
146: this .contextClass = contextClass;
147: }
148:
149: /**
150: * Return the custom context class.
151: */
152: public Class getContextClass() {
153: return this .contextClass;
154: }
155:
156: /**
157: * Set a custom namespace for this servlet,
158: * to be used for building a default context config location.
159: */
160: public void setNamespace(String namespace) {
161: this .namespace = namespace;
162: }
163:
164: /**
165: * Return the namespace for this servlet, falling back to default scheme if
166: * no custom namespace was set: e.g. "test-servlet" for a servlet named "test".
167: */
168: public String getNamespace() {
169: return (this .namespace != null) ? this .namespace
170: : getServletName() + DEFAULT_NAMESPACE_SUFFIX;
171: }
172:
173: /**
174: * Set the context config location explicitly, instead of relying on the default
175: * location built from the namespace. This location string can consist of
176: * multiple locations separated by any number of commas and spaces.
177: */
178: public void setContextConfigLocation(String contextConfigLocation) {
179: this .contextConfigLocation = contextConfigLocation;
180: }
181:
182: /**
183: * Return the explicit context config location, if any.
184: */
185: public String getContextConfigLocation() {
186: return this .contextConfigLocation;
187: }
188:
189: /**
190: * Set whether to publish this servlet's context as a ServletContext attribute,
191: * available to all objects in the web container. Default is "true".
192: * <p>This is especially handy during testing, although it is debatable whether
193: * it's good practice to let other application objects access the context this way.
194: */
195: public void setPublishContext(boolean publishContext) {
196: this .publishContext = publishContext;
197: }
198:
199: /**
200: * Return whether to publish this servlet's context as a ServletContext attribute.
201: */
202: public boolean isPublishContext() {
203: return this .publishContext;
204: }
205:
206: /**
207: * Set whether this servlet should publish a ServletRequestHandledEvent at the end
208: * of each request. Default is "true"; can be turned off for a slight performance
209: * improvement, provided that no ApplicationListeners rely on such events.
210: * @see org.springframework.web.context.support.ServletRequestHandledEvent
211: */
212: public void setPublishEvents(boolean publishEvents) {
213: this .publishEvents = publishEvents;
214: }
215:
216: /**
217: * Return whether this servlet should publish a ServletRequestHandledEvent
218: * at the end of each request.
219: */
220: public boolean isPublishEvents() {
221: return this .publishEvents;
222: }
223:
224: /**
225: * Overridden method of {@link HttpServletBean}, invoked after any bean properties
226: * have been set. Creates this servlet's WebApplicationContext.
227: */
228: protected final void initServletBean() throws ServletException,
229: BeansException {
230: getServletContext().log(
231: "Initializing Spring FrameworkServlet '"
232: + getServletName() + "'");
233: if (logger.isInfoEnabled()) {
234: logger.info("FrameworkServlet '" + getServletName()
235: + "': initialization started");
236: }
237: long startTime = System.currentTimeMillis();
238:
239: try {
240: this .webApplicationContext = initWebApplicationContext();
241: initFrameworkServlet();
242: } catch (ServletException ex) {
243: logger.error("Context initialization failed", ex);
244: throw ex;
245: } catch (BeansException ex) {
246: logger.error("Context initialization failed", ex);
247: throw ex;
248: }
249:
250: if (logger.isInfoEnabled()) {
251: long elapsedTime = System.currentTimeMillis() - startTime;
252: logger.info("FrameworkServlet '" + getServletName()
253: + "': initialization completed in " + elapsedTime
254: + " ms");
255: }
256: }
257:
258: /**
259: * Initialize and publish the WebApplicationContext for this servlet.
260: * <p>Delegates to {@link #createWebApplicationContext} for actual creation
261: * of the context. Can be overridden in subclasses.
262: * @return the WebApplicationContext instance
263: * @throws BeansException if the context couldn't be initialized
264: * @see #setContextClass
265: * @see #setContextConfigLocation
266: */
267: protected WebApplicationContext initWebApplicationContext()
268: throws BeansException {
269: WebApplicationContext parent = WebApplicationContextUtils
270: .getWebApplicationContext(getServletContext());
271: WebApplicationContext wac = createWebApplicationContext(parent);
272:
273: if (!this .refreshEventReceived) {
274: // Apparently not a ConfigurableApplicationContext with refresh support:
275: // triggering initial onRefresh manually here.
276: onRefresh(wac);
277: }
278:
279: if (isPublishContext()) {
280: // Publish the context as a servlet context attribute.
281: String attrName = getServletContextAttributeName();
282: getServletContext().setAttribute(attrName, wac);
283: if (logger.isDebugEnabled()) {
284: logger
285: .debug("Published WebApplicationContext of servlet '"
286: + getServletName()
287: + "' as ServletContext attribute with name ["
288: + attrName + "]");
289: }
290: }
291:
292: return wac;
293: }
294:
295: /**
296: * Instantiate the WebApplicationContext for this servlet, either a default
297: * {@link org.springframework.web.context.support.XmlWebApplicationContext}
298: * or a {@link #setContextClass custom context class}, if set.
299: * <p>This implementation expects custom contexts to implement the
300: * {@link org.springframework.web.context.ConfigurableWebApplicationContext}
301: * interface. Can be overridden in subclasses.
302: * <p>Do not forget to register this servlet instance as application listener on the
303: * created context (for triggering its {@link #onRefresh callback}, and to call
304: * {@link org.springframework.context.ConfigurableApplicationContext#refresh()}
305: * before returning the context instance.
306: * @param parent the parent ApplicationContext to use, or <code>null</code> if none
307: * @return the WebApplicationContext for this servlet
308: * @throws BeansException if the context couldn't be initialized
309: * @see org.springframework.web.context.support.XmlWebApplicationContext
310: */
311: protected WebApplicationContext createWebApplicationContext(
312: WebApplicationContext parent) throws BeansException {
313:
314: if (logger.isDebugEnabled()) {
315: logger
316: .debug("Servlet with name '"
317: + getServletName()
318: + "' will try to create custom WebApplicationContext context of class '"
319: + getContextClass().getName() + "'"
320: + ", using parent context [" + parent + "]");
321: }
322: if (!ConfigurableWebApplicationContext.class
323: .isAssignableFrom(getContextClass())) {
324: throw new ApplicationContextException(
325: "Fatal initialization error in servlet with name '"
326: + getServletName()
327: + "': custom WebApplicationContext class ["
328: + getContextClass().getName()
329: + "] is not of type ConfigurableWebApplicationContext");
330: }
331:
332: ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils
333: .instantiateClass(getContextClass());
334: wac.setParent(parent);
335: wac.setServletContext(getServletContext());
336: wac.setServletConfig(getServletConfig());
337: wac.setNamespace(getNamespace());
338: if (getContextConfigLocation() != null) {
339: wac
340: .setConfigLocations(StringUtils
341: .tokenizeToStringArray(
342: getContextConfigLocation(),
343: ConfigurableWebApplicationContext.CONFIG_LOCATION_DELIMITERS));
344: }
345: wac.addApplicationListener(this );
346:
347: postProcessWebApplicationContext(wac);
348: wac.refresh();
349:
350: return wac;
351: }
352:
353: /**
354: * Post-process the given WebApplicationContext before it is refreshed
355: * and activated as context for this servlet.
356: * <p>The default implementation is empty. <code>refresh()</code> will
357: * be called automatically after this method returns.
358: * @param wac the configured WebApplicationContext (not refreshed yet)
359: * @see #createWebApplicationContext
360: * @see ConfigurableWebApplicationContext#refresh()
361: */
362: protected void postProcessWebApplicationContext(
363: ConfigurableWebApplicationContext wac) {
364: }
365:
366: /**
367: * Return the ServletContext attribute name for this servlet's WebApplicationContext.
368: * <p>The default implementation returns
369: * <code>SERVLET_CONTEXT_PREFIX + servlet name</code>.
370: * @see #SERVLET_CONTEXT_PREFIX
371: * @see #getServletName
372: */
373: public String getServletContextAttributeName() {
374: return SERVLET_CONTEXT_PREFIX + getServletName();
375: }
376:
377: /**
378: * Return this servlet's WebApplicationContext.
379: */
380: public final WebApplicationContext getWebApplicationContext() {
381: return this .webApplicationContext;
382: }
383:
384: /**
385: * This method will be invoked after any bean properties have been set and
386: * the WebApplicationContext has been loaded. The default implementation is empty;
387: * subclasses may override this method to perform any initialization they require.
388: * @throws ServletException in case of an initialization exception
389: * @throws BeansException if thrown by ApplicationContext methods
390: */
391: protected void initFrameworkServlet() throws ServletException,
392: BeansException {
393: }
394:
395: /**
396: * Refresh this servlet's application context, as well as the
397: * dependent state of the servlet.
398: * @throws BeansException in case of errors
399: * @see #getWebApplicationContext()
400: * @see org.springframework.context.ConfigurableApplicationContext#refresh()
401: */
402: public void refresh() throws BeansException {
403: WebApplicationContext wac = getWebApplicationContext();
404: if (!(wac instanceof ConfigurableApplicationContext)) {
405: throw new IllegalStateException(
406: "WebApplicationContext does not support refresh: "
407: + wac);
408: }
409: ((ConfigurableApplicationContext) wac).refresh();
410: }
411:
412: /**
413: * ApplicationListener endpoint that receives events from this servlet's
414: * WebApplicationContext.
415: * <p>The default implementation calls {@link #onRefresh} in case of a
416: * {@link org.springframework.context.event.ContextRefreshedEvent},
417: * triggering a refresh of this servlet's context-dependent state.
418: * @param event the incoming ApplicationContext event
419: */
420: public void onApplicationEvent(ApplicationEvent event) {
421: if (event instanceof ContextRefreshedEvent) {
422: this .refreshEventReceived = true;
423: onRefresh(((ContextRefreshedEvent) event)
424: .getApplicationContext());
425: }
426: }
427:
428: /**
429: * Template method which can be overridden to add servlet-specific refresh work.
430: * Called after successful context refresh.
431: * <p>This implementation is empty.
432: * @param context the current WebApplicationContext
433: * @throws BeansException in case of errors
434: * @see #refresh()
435: */
436: protected void onRefresh(ApplicationContext context)
437: throws BeansException {
438: // For subclasses: do nothing by default.
439: }
440:
441: /**
442: * Delegate GET requests to processRequest/doService.
443: * <p>Will also be invoked by HttpServlet's default implementation of <code>doHead</code>,
444: * with a <code>NoBodyResponse</code> that just captures the content length.
445: * @see #doService
446: * @see #doHead
447: */
448: protected final void doGet(HttpServletRequest request,
449: HttpServletResponse response) throws ServletException,
450: IOException {
451:
452: processRequest(request, response);
453: }
454:
455: /**
456: * Delegate POST requests to {@link #processRequest}.
457: * @see #doService
458: */
459: protected final void doPost(HttpServletRequest request,
460: HttpServletResponse response) throws ServletException,
461: IOException {
462:
463: processRequest(request, response);
464: }
465:
466: /**
467: * Delegate PUT requests to {@link #processRequest}.
468: * @see #doService
469: */
470: protected final void doPut(HttpServletRequest request,
471: HttpServletResponse response) throws ServletException,
472: IOException {
473:
474: processRequest(request, response);
475: }
476:
477: /**
478: * Delegate DELETE requests to {@link #processRequest}.
479: * @see #doService
480: */
481: protected final void doDelete(HttpServletRequest request,
482: HttpServletResponse response) throws ServletException,
483: IOException {
484:
485: processRequest(request, response);
486: }
487:
488: /**
489: * Process this request, publishing an event regardless of the outcome.
490: * <p>The actual event handling is performed by the abstract
491: * {@link #doService} template method.
492: */
493: protected final void processRequest(HttpServletRequest request,
494: HttpServletResponse response) throws ServletException,
495: IOException {
496:
497: long startTime = System.currentTimeMillis();
498: Throwable failureCause = null;
499:
500: try {
501: doService(request, response);
502: } catch (ServletException ex) {
503: failureCause = ex;
504: throw ex;
505: } catch (IOException ex) {
506: failureCause = ex;
507: throw ex;
508: } catch (Throwable ex) {
509: failureCause = ex;
510: throw new NestedServletException(
511: "Request processing failed", ex);
512: }
513:
514: finally {
515: if (failureCause != null) {
516: logger
517: .debug("Could not complete request",
518: failureCause);
519: } else {
520: logger.debug("Successfully completed request");
521: }
522: if (isPublishEvents()) {
523: // Whether or not we succeeded, publish an event.
524: long processingTime = System.currentTimeMillis()
525: - startTime;
526: this .webApplicationContext
527: .publishEvent(new ServletRequestHandledEvent(
528: this , request.getRequestURI(), request
529: .getRemoteAddr(), request
530: .getMethod(),
531: getServletConfig().getServletName(),
532: WebUtils.getSessionId(request),
533: getUsernameForRequest(request),
534: processingTime, failureCause));
535: }
536: }
537: }
538:
539: /**
540: * Determine the username for the given request.
541: * <p>The default implementation takes the name of the UserPrincipal, if any.
542: * Can be overridden in subclasses.
543: * @param request current HTTP request
544: * @return the username, or <code>null</code> if none found
545: * @see javax.servlet.http.HttpServletRequest#getUserPrincipal()
546: */
547: protected String getUsernameForRequest(HttpServletRequest request) {
548: Principal userPrincipal = request.getUserPrincipal();
549: return (userPrincipal != null ? userPrincipal.getName() : null);
550: }
551:
552: /**
553: * Subclasses must implement this method to do the work of request handling,
554: * receiving a centralized callback for GET, POST, PUT and DELETE.
555: * <p>The contract is essentially the same as that for the commonly overridden
556: * <code>doGet</code> or <code>doPost</code> methods of HttpServlet.
557: * <p>This class intercepts calls to ensure that exception handling and
558: * event publication takes place.
559: * @param request current HTTP request
560: * @param response current HTTP response
561: * @throws Exception in case of any kind of processing failure
562: * @see javax.servlet.http.HttpServlet#doGet
563: * @see javax.servlet.http.HttpServlet#doPost
564: */
565: protected abstract void doService(HttpServletRequest request,
566: HttpServletResponse response) throws Exception;
567:
568: /**
569: * Close the WebApplicationContext of this servlet.
570: * @see org.springframework.context.ConfigurableApplicationContext#close()
571: */
572: public void destroy() {
573: getServletContext().log(
574: "Destroying Spring FrameworkServlet '"
575: + getServletName() + "'");
576: if (this .webApplicationContext instanceof ConfigurableApplicationContext) {
577: ((ConfigurableApplicationContext) this.webApplicationContext)
578: .close();
579: }
580: }
581:
582: }
|