View Javadoc

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