001: /*
002: * $Id: FilterDispatcher.java 530439 2007-04-19 15:00:20Z hermanns $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts2.dispatcher;
022:
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.io.OutputStream;
026: import java.net.URLDecoder;
027: import java.util.ArrayList;
028: import java.util.Calendar;
029: import java.util.Enumeration;
030: import java.util.HashMap;
031: import java.util.List;
032: import java.util.Map;
033: import java.util.StringTokenizer;
034:
035: import javax.servlet.Filter;
036: import javax.servlet.FilterChain;
037: import javax.servlet.FilterConfig;
038: import javax.servlet.ServletContext;
039: import javax.servlet.ServletException;
040: import javax.servlet.ServletRequest;
041: import javax.servlet.ServletResponse;
042: import javax.servlet.http.HttpServletRequest;
043: import javax.servlet.http.HttpServletResponse;
044:
045: import org.apache.commons.logging.Log;
046: import org.apache.commons.logging.LogFactory;
047: import org.apache.struts2.RequestUtils;
048: import org.apache.struts2.StrutsConstants;
049: import org.apache.struts2.StrutsStatics;
050: import org.apache.struts2.dispatcher.mapper.ActionMapper;
051: import org.apache.struts2.dispatcher.mapper.ActionMapping;
052:
053: import com.opensymphony.xwork2.inject.Inject;
054: import com.opensymphony.xwork2.util.ClassLoaderUtil;
055: import com.opensymphony.xwork2.util.profiling.UtilTimerStack;
056: import com.opensymphony.xwork2.ActionContext;
057:
058: /**
059: * Master filter for Struts that handles four distinct
060: * responsibilities:
061: *
062: * <ul>
063: *
064: * <li>Executing actions</li>
065: *
066: * <li>Cleaning up the {@link ActionContext} (see note)</li>
067: *
068: * <li>Serving static content</li>
069: *
070: * <li>Kicking off XWork's interceptor chain for the request lifecycle</li>
071: *
072: * </ul>
073: *
074: * <p/> <b>IMPORTANT</b>: this filter must be mapped to all requests. Unless you know exactly what you are doing, always
075: * map to this URL pattern: /*
076: *
077: * <p/> <b>Executing actions</b>
078: *
079: * <p/> This filter executes actions by consulting the {@link ActionMapper} and determining if the requested URL should
080: * invoke an action. If the mapper indicates it should, <b>the rest of the filter chain is stopped</b> and the action is
081: * invoked. This is important, as it means that filters like the SiteMesh filter must be placed <b>before</b> this
082: * filter or they will not be able to decorate the output of actions.
083: *
084: * <p/> <b>Cleaning up the {@link ActionContext}</b>
085: *
086: * <p/> This filter will also automatically clean up the {@link ActionContext} for you, ensuring that no memory leaks
087: * take place. However, this can sometimes cause problems integrating with other products like SiteMesh. See {@link
088: * ActionContextCleanUp} for more information on how to deal with this.
089: *
090: * <p/> <b>Serving static content</b>
091: *
092: * <p/> This filter also serves common static content needed when using various parts of Struts, such as JavaScript
093: * files, CSS files, etc. It works by looking for requests to /struts/*, and then mapping the value after "/struts/"
094: * to common packages in Struts and, optionally, in your class path. By default, the following packages are
095: * automatically searched:
096: *
097: * <ul>
098: *
099: * <li>org.apache.struts2.static</li>
100: *
101: * <li>template</li>
102: *
103: * </ul>
104: *
105: * <p/> This means that you can simply request /struts/xhtml/styles.css and the XHTML UI theme's default stylesheet
106: * will be returned. Likewise, many of the AJAX UI components require various JavaScript files, which are found in the
107: * org.apache.struts2.static package. If you wish to add additional packages to be searched, you can add a comma
108: * separated (space, tab and new line will do as well) list in the filter init parameter named "packages". <b>Be
109: * careful</b>, however, to expose any packages that may have sensitive information, such as properties file with
110: * database access credentials.
111: *
112: * <p/>
113: *
114: * <p>
115: *
116: * This filter supports the following init-params:
117: * <!-- START SNIPPET: params -->
118: *
119: * <ul>
120: *
121: * <li><b>config</b> - a comma-delimited list of XML configuration files to load.</li>
122: *
123: * <li><b>actionPackages</b> - a comma-delimited list of Java packages to scan for Actions.</li>
124: *
125: * <li><b>configProviders</b> - a comma-delimited list of Java classes that implement the
126: * {@link com.opensymphony.xwork2.config.ConfigurationProvider} interface that should be used for building the {@link com.opensymphony.xwork2.config.Configuration}.</li>
127: *
128: * <li><b>*</b> - any other parameters are treated as framework constants.</li>
129: *
130: * </ul>
131: *
132: * <!-- END SNIPPET: params -->
133: *
134: * </p>
135: *
136: * To use a custom {@link Dispatcher}, the <code>createDispatcher()</code> method could be overriden by
137: * the subclass.
138: *
139: * @see ActionMapper
140: * @see ActionContextCleanUp
141: *
142: * @version $Date: 2007-04-19 11:00:20 -0400 (Thu, 19 Apr 2007) $ $Id: FilterDispatcher.java 530439 2007-04-19 15:00:20Z hermanns $
143: */
144: public class FilterDispatcher implements StrutsStatics, Filter {
145:
146: /**
147: * Provide a logging instance.
148: */
149: private static final Log LOG = LogFactory
150: .getLog(FilterDispatcher.class);
151:
152: /**
153: * Store set of path prefixes to use with static resources.
154: */
155: private String[] pathPrefixes;
156:
157: /**
158: * Provide a formatted date for setting heading information when caching static content.
159: */
160: private final Calendar lastModifiedCal = Calendar.getInstance();
161:
162: /**
163: * Store state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
164: */
165: private static boolean serveStatic;
166:
167: /**
168: * Store state of StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE setting.
169: */
170: private static boolean serveStaticBrowserCache;
171:
172: /**
173: * Store state of StrutsConstants.STRUTS_I18N_ENCODING setting.
174: */
175: private static String encoding;
176:
177: /**
178: * Provide ActionMapper instance, set by injection.
179: */
180: private static ActionMapper actionMapper;
181:
182: /**
183: * Provide FilterConfig instance, set on init.
184: */
185: private FilterConfig filterConfig;
186:
187: /**
188: * Expose Dispatcher instance to subclass.
189: */
190: protected Dispatcher dispatcher;
191:
192: /**
193: * Initializes the filter by creating a default dispatcher
194: * and setting the default packages for static resources.
195: *
196: * @param filterConfig The filter configuration
197: */
198: public void init(FilterConfig filterConfig) throws ServletException {
199: this .filterConfig = filterConfig;
200:
201: dispatcher = createDispatcher(filterConfig);
202: dispatcher.init();
203:
204: String param = filterConfig.getInitParameter("packages");
205: String packages = "org.apache.struts2.static template org.apache.struts2.interceptor.debugging";
206: if (param != null) {
207: packages = param + " " + packages;
208: }
209: this .pathPrefixes = parse(packages);
210: }
211:
212: /**
213: * Calls dispatcher.cleanup,
214: * which in turn releases local threads and destroys any DispatchListeners.
215: *
216: * @see javax.servlet.Filter#destroy()
217: */
218: public void destroy() {
219: if (dispatcher == null) {
220: LOG
221: .warn("something is seriously wrong, Dispatcher is not initialized (null) ");
222: } else {
223: dispatcher.cleanup();
224: }
225: }
226:
227: /**
228: * Create a default {@link Dispatcher} that subclasses can override
229: * with a custom Dispatcher, if needed.
230: *
231: * @param filterConfig Our FilterConfig
232: * @return Initialized Dispatcher
233: */
234: protected Dispatcher createDispatcher(FilterConfig filterConfig) {
235: Map<String, String> params = new HashMap<String, String>();
236: for (Enumeration e = filterConfig.getInitParameterNames(); e
237: .hasMoreElements();) {
238: String name = (String) e.nextElement();
239: String value = filterConfig.getInitParameter(name);
240: params.put(name, value);
241: }
242: return new Dispatcher(filterConfig.getServletContext(), params);
243: }
244:
245: /**
246: * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
247: * @param val New setting
248: */
249: @Inject(StrutsConstants.STRUTS_SERVE_STATIC_CONTENT)
250: public static void setServeStaticContent(String val) {
251: serveStatic = "true".equals(val);
252: }
253:
254: /**
255: * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE setting.
256: * @param val New setting
257: */
258: @Inject(StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE)
259: public static void setServeStaticBrowserCache(String val) {
260: serveStaticBrowserCache = "true".equals(val);
261: }
262:
263: /**
264: * Modify state of StrutsConstants.STRUTS_I18N_ENCODING setting.
265: * @param val New setting
266: */
267: @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
268: public static void setEncoding(String val) {
269: encoding = val;
270: }
271:
272: /**
273: * Modify ActionMapper instance.
274: * @param mapper New instance
275: */
276: @Inject
277: public static void setActionMapper(ActionMapper mapper) {
278: actionMapper = mapper;
279: }
280:
281: /**
282: * Provide a workaround for some versions of WebLogic.
283: * <p/>
284: * Servlet 2.3 specifies that the servlet context can be retrieved from the session. Unfortunately, some versions of
285: * WebLogic can only retrieve the servlet context from the filter config. Hence, this method enables subclasses to
286: * retrieve the servlet context from other sources.
287: *
288: * @return the servlet context.
289: */
290: protected ServletContext getServletContext() {
291: return filterConfig.getServletContext();
292: }
293:
294: /**
295: * Expose the FilterConfig instance.
296: *
297: * @return Our FilterConfit instance
298: */
299: protected FilterConfig getFilterConfig() {
300: return filterConfig;
301: }
302:
303: /**
304: * Wrap and return the given request, if needed, so as to to transparently
305: * handle multipart data as a wrapped class around the given request.
306: *
307: * @param request Our ServletRequest object
308: * @param response Our ServerResponse object
309: * @return Wrapped HttpServletRequest object
310: * @throws ServletException on any error
311: */
312: protected HttpServletRequest prepareDispatcherAndWrapRequest(
313: HttpServletRequest request, HttpServletResponse response)
314: throws ServletException {
315:
316: Dispatcher du = Dispatcher.getInstance();
317:
318: // Prepare and wrap the request if the cleanup filter hasn't already, cleanup filter should be
319: // configured first before struts2 dispatcher filter, hence when its cleanup filter's turn,
320: // static instance of Dispatcher should be null.
321: if (du == null) {
322:
323: Dispatcher.setInstance(dispatcher);
324:
325: // prepare the request no matter what - this ensures that the proper character encoding
326: // is used before invoking the mapper (see WW-9127)
327: dispatcher.prepare(request, response);
328: } else {
329: dispatcher = du;
330: }
331:
332: try {
333: // Wrap request first, just in case it is multipart/form-data
334: // parameters might not be accessible through before encoding (ww-1278)
335: request = dispatcher.wrapRequest(request,
336: getServletContext());
337: } catch (IOException e) {
338: String message = "Could not wrap servlet request with MultipartRequestWrapper!";
339: LOG.error(message, e);
340: throw new ServletException(message, e);
341: }
342:
343: return request;
344: }
345:
346: /**
347: * Create a string array from a comma-delimited list of packages.
348: *
349: * @param packages A comma-delimited String listing packages
350: * @return A string array of packages
351: */
352: protected String[] parse(String packages) {
353: if (packages == null) {
354: return null;
355: }
356: List<String> pathPrefixes = new ArrayList<String>();
357:
358: StringTokenizer st = new StringTokenizer(packages, ", \n\t");
359: while (st.hasMoreTokens()) {
360: String pathPrefix = st.nextToken().replace('.', '/');
361: if (!pathPrefix.endsWith("/")) {
362: pathPrefix += "/";
363: }
364: pathPrefixes.add(pathPrefix);
365: }
366:
367: return pathPrefixes.toArray(new String[pathPrefixes.size()]);
368: }
369:
370: /**
371: * Process an action or handle a request a static resource.
372: * <p/>
373: * The filter tries to match the request to an action mapping.
374: * If mapping is found, the action processes is delegated to the dispatcher's serviceAction method.
375: * If action processing fails, doFilter will try to create an error page via the dispatcher.
376: * <p/>
377: * Otherwise, if the request is for a static resource,
378: * the resource is copied directly to the response, with the appropriate caching headers set.
379: * <p/>
380: * If the request does not match an action mapping, or a static resource page,
381: * then it passes through.
382: *
383: * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
384: */
385: public void doFilter(ServletRequest req, ServletResponse res,
386: FilterChain chain) throws IOException, ServletException {
387:
388: HttpServletRequest request = (HttpServletRequest) req;
389: HttpServletResponse response = (HttpServletResponse) res;
390: ServletContext servletContext = getServletContext();
391:
392: String timerKey = "FilterDispatcher_doFilter: ";
393: try {
394: UtilTimerStack.push(timerKey);
395: request = prepareDispatcherAndWrapRequest(request, response);
396: ActionMapping mapping;
397: try {
398: mapping = actionMapper.getMapping(request, dispatcher
399: .getConfigurationManager());
400: } catch (Exception ex) {
401: LOG.error("error getting ActionMapping", ex);
402: dispatcher.sendError(request, response, servletContext,
403: HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
404: ex);
405: return;
406: }
407:
408: if (mapping == null) {
409: // there is no action in this request, should we look for a static resource?
410: String resourcePath = RequestUtils
411: .getServletPath(request);
412:
413: if ("".equals(resourcePath)
414: && null != request.getPathInfo()) {
415: resourcePath = request.getPathInfo();
416: }
417:
418: if (serveStatic && resourcePath.startsWith("/struts")) {
419: String name = resourcePath.substring("/struts"
420: .length());
421: findStaticResource(name, request, response);
422: } else {
423: // this is a normal request, let it pass through
424: chain.doFilter(request, response);
425: }
426: // The framework did its job here
427: return;
428: }
429:
430: dispatcher.serviceAction(request, response, servletContext,
431: mapping);
432:
433: } finally {
434: try {
435: ActionContextCleanUp.cleanUp(req);
436: } finally {
437: UtilTimerStack.pop(timerKey);
438: }
439: }
440: }
441:
442: /**
443: * Locate a static resource and copy directly to the response,
444: * setting the appropriate caching headers.
445: *
446: * @param name The resource name
447: * @param request The request
448: * @param response The response
449: * @throws IOException If anything goes wrong
450: */
451: protected void findStaticResource(String name,
452: HttpServletRequest request, HttpServletResponse response)
453: throws IOException {
454: if (!name.endsWith(".class")) {
455: for (String pathPrefix : pathPrefixes) {
456: InputStream is = findInputStream(name, pathPrefix);
457: if (is != null) {
458: Calendar cal = Calendar.getInstance();
459:
460: // check for if-modified-since, prior to any other headers
461: long ifModifiedSince = 0;
462: try {
463: ifModifiedSince = request
464: .getDateHeader("If-Modified-Since");
465: } catch (Exception e) {
466: LOG
467: .warn("Invalid If-Modified-Since header value: '"
468: + request
469: .getHeader("If-Modified-Since")
470: + "', ignoring");
471: }
472: long lastModifiedMillis = lastModifiedCal
473: .getTimeInMillis();
474: long now = cal.getTimeInMillis();
475: cal.add(Calendar.DAY_OF_MONTH, 1);
476: long expires = cal.getTimeInMillis();
477:
478: if (ifModifiedSince > 0
479: && ifModifiedSince <= lastModifiedMillis) {
480: // not modified, content is not sent - only basic headers and status SC_NOT_MODIFIED
481: response.setDateHeader("Expires", expires);
482: response
483: .setStatus(HttpServletResponse.SC_NOT_MODIFIED);
484: is.close();
485: return;
486: }
487:
488: // set the content-type header
489: String contentType = getContentType(name);
490: if (contentType != null) {
491: response.setContentType(contentType);
492: }
493:
494: if (serveStaticBrowserCache) {
495: // set heading information for caching static content
496: response.setDateHeader("Date", now);
497: response.setDateHeader("Expires", expires);
498: response.setDateHeader("Retry-After", expires);
499: response.setHeader("Cache-Control", "public");
500: response.setDateHeader("Last-Modified",
501: lastModifiedMillis);
502: } else {
503: response.setHeader("Cache-Control", "no-cache");
504: response.setHeader("Pragma", "no-cache");
505: response.setHeader("Expires", "-1");
506: }
507:
508: try {
509: copy(is, response.getOutputStream());
510: } finally {
511: is.close();
512: }
513: return;
514: }
515: }
516: }
517:
518: response.sendError(HttpServletResponse.SC_NOT_FOUND);
519: }
520:
521: /**
522: * Determine the content type for the resource name.
523: *
524: * @param name The resource name
525: * @return The mime type
526: */
527: protected String getContentType(String name) {
528: // NOT using the code provided activation.jar to avoid adding yet another dependency
529: // this is generally OK, since these are the main files we server up
530: if (name.endsWith(".js")) {
531: return "text/javascript";
532: } else if (name.endsWith(".css")) {
533: return "text/css";
534: } else if (name.endsWith(".html")) {
535: return "text/html";
536: } else if (name.endsWith(".txt")) {
537: return "text/plain";
538: } else if (name.endsWith(".gif")) {
539: return "image/gif";
540: } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
541: return "image/jpeg";
542: } else if (name.endsWith(".png")) {
543: return "image/png";
544: } else {
545: return null;
546: }
547: }
548:
549: /**
550: * Copy bytes from the input stream to the output stream.
551: *
552: * @param input The input stream
553: * @param output The output stream
554: * @throws IOException If anything goes wrong
555: */
556: protected void copy(InputStream input, OutputStream output)
557: throws IOException {
558: final byte[] buffer = new byte[4096];
559: int n;
560: while (-1 != (n = input.read(buffer))) {
561: output.write(buffer, 0, n);
562: }
563: output.flush(); // WW-1526
564: }
565:
566: /**
567: * Look for a static resource in the classpath.
568: *
569: * @param name The resource name
570: * @param packagePrefix The package prefix to use to locate the resource
571: * @return The inputstream of the resource
572: * @throws IOException If there is a problem locating the resource
573: */
574: protected InputStream findInputStream(String name,
575: String packagePrefix) throws IOException {
576: String resourcePath;
577: if (packagePrefix.endsWith("/") && name.startsWith("/")) {
578: resourcePath = packagePrefix + name.substring(1);
579: } else {
580: resourcePath = packagePrefix + name;
581: }
582:
583: resourcePath = URLDecoder.decode(resourcePath, encoding);
584:
585: return ClassLoaderUtil.getResourceAsStream(resourcePath,
586: getClass());
587: }
588: }
|