View Javadoc

1   /*
2    * $Id: FilterDispatcher.java 530439 2007-04-19 15:00:20Z hermanns $
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.net.URLDecoder;
27  import java.util.ArrayList;
28  import java.util.Calendar;
29  import java.util.Enumeration;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.StringTokenizer;
34  
35  import javax.servlet.Filter;
36  import javax.servlet.FilterChain;
37  import javax.servlet.FilterConfig;
38  import javax.servlet.ServletContext;
39  import javax.servlet.ServletException;
40  import javax.servlet.ServletRequest;
41  import javax.servlet.ServletResponse;
42  import javax.servlet.http.HttpServletRequest;
43  import javax.servlet.http.HttpServletResponse;
44  
45  import org.apache.commons.logging.Log;
46  import org.apache.commons.logging.LogFactory;
47  import org.apache.struts2.RequestUtils;
48  import org.apache.struts2.StrutsConstants;
49  import org.apache.struts2.StrutsStatics;
50  import org.apache.struts2.dispatcher.mapper.ActionMapper;
51  import org.apache.struts2.dispatcher.mapper.ActionMapping;
52  
53  import com.opensymphony.xwork2.inject.Inject;
54  import com.opensymphony.xwork2.util.ClassLoaderUtil;
55  import com.opensymphony.xwork2.util.profiling.UtilTimerStack;
56  import com.opensymphony.xwork2.ActionContext;
57  
58  /***
59   * Master filter for Struts that handles four distinct
60   * responsibilities:
61   *
62   * <ul>
63   *
64   * <li>Executing actions</li>
65   *
66   * <li>Cleaning up the {@link ActionContext} (see note)</li>
67   *
68   * <li>Serving static content</li>
69   *
70   * <li>Kicking off XWork's interceptor chain for the request lifecycle</li>
71   *
72   * </ul>
73   *
74   * <p/> <b>IMPORTANT</b>: this filter must be mapped to all requests. Unless you know exactly what you are doing, always
75   * map to this URL pattern: /*
76   *
77   * <p/> <b>Executing actions</b>
78   *
79   * <p/> This filter executes actions by consulting the {@link ActionMapper} and determining if the requested URL should
80   * invoke an action. If the mapper indicates it should, <b>the rest of the filter chain is stopped</b> and the action is
81   * invoked. This is important, as it means that filters like the SiteMesh filter must be placed <b>before</b> this
82   * filter or they will not be able to decorate the output of actions.
83   *
84   * <p/> <b>Cleaning up the {@link ActionContext}</b>
85   *
86   * <p/> This filter will also automatically clean up the {@link ActionContext} for you, ensuring that no memory leaks
87   * take place. However, this can sometimes cause problems integrating with other products like SiteMesh. See {@link
88   * ActionContextCleanUp} for more information on how to deal with this.
89   *
90   * <p/> <b>Serving static content</b>
91   *
92   * <p/> This filter also serves common static content needed when using various parts of Struts, such as JavaScript
93   * files, CSS files, etc. It works by looking for requests to /struts/*, and then mapping the value after "/struts/"
94   * to common packages in Struts and, optionally, in your class path. By default, the following packages are
95   * automatically searched:
96   *
97   * <ul>
98   *
99   * <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 17:00:20 +0200 (Do, 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.getLog(FilterDispatcher.class);
150 
151     /***
152      * Store set of path prefixes to use with static resources.
153      */
154     private String[] pathPrefixes;
155 
156     /***
157      * Provide a formatted date for setting heading information when caching static content.
158      */
159     private final Calendar lastModifiedCal = Calendar.getInstance();
160 
161     /***
162      * Store state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
163      */
164     private static boolean serveStatic;
165 
166     /***
167      * Store state of StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE setting.
168      */
169     private static boolean serveStaticBrowserCache;
170 
171     /***
172      * Store state of StrutsConstants.STRUTS_I18N_ENCODING setting.
173      */
174     private static String encoding;
175 
176     /***
177      * Provide ActionMapper instance, set by injection.
178      */
179     private static ActionMapper actionMapper;
180 
181     /***
182      * Provide FilterConfig instance, set on init.
183      */
184     private FilterConfig filterConfig;
185 
186     /***
187      * Expose Dispatcher instance to subclass.
188      */
189     protected Dispatcher dispatcher;
190 
191     /***
192      * Initializes the filter by creating a default dispatcher
193      * and setting the default packages for static resources.
194      *
195      * @param filterConfig The filter configuration
196      */
197     public void init(FilterConfig filterConfig) throws ServletException {
198     	 this.filterConfig = filterConfig;
199     	 
200         dispatcher = createDispatcher(filterConfig);
201         dispatcher.init();
202        
203         String param = filterConfig.getInitParameter("packages");
204         String packages = "org.apache.struts2.static template org.apache.struts2.interceptor.debugging";
205         if (param != null) {
206             packages = param + " " + packages;
207         }
208         this.pathPrefixes = parse(packages);
209     }
210 
211     /***
212      * Calls dispatcher.cleanup,
213      * which in turn releases local threads and destroys any DispatchListeners.
214      *
215      * @see javax.servlet.Filter#destroy()
216      */
217     public void destroy() {
218         if (dispatcher == null) {
219             LOG.warn("something is seriously wrong, Dispatcher is not initialized (null) ");
220         } else {
221             dispatcher.cleanup();
222         }
223     }
224     
225     /***
226      * Create a default {@link Dispatcher} that subclasses can override
227      * with a custom Dispatcher, if needed.
228      *
229      * @param filterConfig Our FilterConfig
230      * @return Initialized Dispatcher 
231      */
232     protected Dispatcher createDispatcher(FilterConfig filterConfig) {
233         Map<String,String> params = new HashMap<String,String>();
234         for (Enumeration e = filterConfig.getInitParameterNames(); e.hasMoreElements(); ) {
235             String name = (String) e.nextElement();
236             String value = filterConfig.getInitParameter(name);
237             params.put(name, value);
238         }
239         return new Dispatcher(filterConfig.getServletContext(), params);
240     }
241 
242     /***
243      * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
244      * @param val New setting
245      */
246     @Inject(StrutsConstants.STRUTS_SERVE_STATIC_CONTENT)
247     public static void setServeStaticContent(String val) {
248         serveStatic = "true".equals(val);
249     }
250     
251     /***
252      * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE setting.
253      * @param val New setting
254      */
255     @Inject(StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE)
256     public static void setServeStaticBrowserCache(String val) {
257         serveStaticBrowserCache = "true".equals(val);
258     }
259     
260     /***
261      * Modify state of StrutsConstants.STRUTS_I18N_ENCODING setting.
262      * @param val New setting
263      */
264     @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
265     public static void setEncoding(String val) {
266         encoding = val;
267     }
268     
269     /***
270      * Modify ActionMapper instance.
271      * @param mapper New instance
272      */
273     @Inject
274     public static void setActionMapper(ActionMapper mapper) {
275         actionMapper = mapper;
276     }
277     
278     /***
279      * Provide a workaround for some versions of WebLogic.
280      * <p/>
281      * Servlet 2.3 specifies that the servlet context can be retrieved from the session. Unfortunately, some versions of
282      * WebLogic can only retrieve the servlet context from the filter config. Hence, this method enables subclasses to
283      * retrieve the servlet context from other sources.
284      *
285      * @return the servlet context.
286      */
287     protected ServletContext getServletContext() {
288         return filterConfig.getServletContext();
289     }
290 
291     /***
292      * Expose the FilterConfig instance.
293      *
294      * @return Our FilterConfit instance
295      */
296     protected FilterConfig getFilterConfig() {
297         return filterConfig;
298     }
299 
300     /***
301      * Wrap and return the given request, if needed, so as to to transparently
302      * handle multipart data as a wrapped class around the given request.
303      *
304      * @param request Our ServletRequest object
305      * @param response Our ServerResponse object
306      * @return Wrapped HttpServletRequest object
307      * @throws ServletException on any error
308      */
309     protected HttpServletRequest prepareDispatcherAndWrapRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException {
310 
311         Dispatcher du = Dispatcher.getInstance();
312 
313         // Prepare and wrap the request if the cleanup filter hasn't already, cleanup filter should be
314         // configured first before struts2 dispatcher filter, hence when its cleanup filter's turn,
315         // static instance of Dispatcher should be null.
316         if (du == null) {
317 
318             Dispatcher.setInstance(dispatcher);
319 
320             // prepare the request no matter what - this ensures that the proper character encoding
321             // is used before invoking the mapper (see WW-9127)
322             dispatcher.prepare(request, response);
323         } else {
324             dispatcher = du;
325         }
326         
327         try {
328             // Wrap request first, just in case it is multipart/form-data
329             // parameters might not be accessible through before encoding (ww-1278)
330             request = dispatcher.wrapRequest(request, getServletContext());
331         } catch (IOException e) {
332             String message = "Could not wrap servlet request with MultipartRequestWrapper!";
333             LOG.error(message, e);
334             throw new ServletException(message, e);
335         }
336 
337         return request;
338     }
339 
340     /***
341      * Create a string array from a comma-delimited list of packages.
342      *
343      * @param packages A comma-delimited String listing packages
344      * @return A string array of packages
345      */
346     protected String[] parse(String packages) {
347         if (packages == null) {
348             return null;
349         }
350         List<String> pathPrefixes = new ArrayList<String>();
351 
352         StringTokenizer st = new StringTokenizer(packages, ", \n\t");
353         while (st.hasMoreTokens()) {
354             String pathPrefix = st.nextToken().replace('.', '/');
355             if (!pathPrefix.endsWith("/")) {
356                 pathPrefix += "/";
357             }
358             pathPrefixes.add(pathPrefix);
359         }
360 
361         return pathPrefixes.toArray(new String[pathPrefixes.size()]);
362     }
363 
364 
365     /***
366      * Process an action or handle a request a static resource.
367      * <p/>
368      * The filter tries to match the request to an action mapping.
369      * If mapping is found, the action processes is delegated to the dispatcher's serviceAction method.
370      * If action processing fails, doFilter will try to create an error page via the dispatcher.
371      * <p/>
372      * Otherwise, if the request is for a static resource,
373      * the resource is copied directly to the response, with the appropriate caching headers set.
374      * <p/>
375      * If the request does not match an action mapping, or a static resource page, 
376      * then it passes through.
377      *
378      * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
379      */
380     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
381 
382 
383         HttpServletRequest request = (HttpServletRequest) req;
384         HttpServletResponse response = (HttpServletResponse) res;
385         ServletContext servletContext = getServletContext();
386 
387         String timerKey = "FilterDispatcher_doFilter: ";
388         try {
389             UtilTimerStack.push(timerKey);
390             request = prepareDispatcherAndWrapRequest(request, response);
391             ActionMapping mapping;
392             try {
393                 mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager());
394             } catch (Exception ex) {
395                 LOG.error("error getting ActionMapping", ex);
396                 dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
397                 return;
398             }
399 
400             if (mapping == null) {
401                 // there is no action in this request, should we look for a static resource?
402                 String resourcePath = RequestUtils.getServletPath(request);
403 
404                 if ("".equals(resourcePath) && null != request.getPathInfo()) {
405                     resourcePath = request.getPathInfo();
406                 }
407 
408                 if (serveStatic && resourcePath.startsWith("/struts")) {
409                     String name = resourcePath.substring("/struts".length());
410                     findStaticResource(name, request, response);
411                 } else {
412                     // this is a normal request, let it pass through
413                     chain.doFilter(request, response);
414                 }
415                 // The framework did its job here
416                 return;
417             }
418 
419             dispatcher.serviceAction(request, response, servletContext, mapping);
420 
421         } finally {
422             try {
423                 ActionContextCleanUp.cleanUp(req);
424             } finally {
425                 UtilTimerStack.pop(timerKey);
426             }
427         }
428     }
429 
430     /***
431      * Locate a static resource and copy directly to the response,
432      * setting the appropriate caching headers. 
433      *
434      * @param name The resource name
435      * @param request The request
436      * @param response The response
437      * @throws IOException If anything goes wrong
438      */
439     protected void findStaticResource(String name, HttpServletRequest request, HttpServletResponse response) throws IOException {
440         if (!name.endsWith(".class")) {
441             for (String pathPrefix : pathPrefixes) {
442                 InputStream is = findInputStream(name, pathPrefix);
443                 if (is != null) {
444                     Calendar cal = Calendar.getInstance();
445                     
446                     // check for if-modified-since, prior to any other headers
447                     long ifModifiedSince = 0;
448                     try {
449                     	ifModifiedSince = request.getDateHeader("If-Modified-Since");
450                     } catch (Exception e) {
451                     	LOG.warn("Invalid If-Modified-Since header value: '" + request.getHeader("If-Modified-Since") + "', ignoring");
452                     }
453     				long lastModifiedMillis = lastModifiedCal.getTimeInMillis();
454     				long now = cal.getTimeInMillis();
455                     cal.add(Calendar.DAY_OF_MONTH, 1);
456                     long expires = cal.getTimeInMillis();
457                     
458     				if (ifModifiedSince > 0 && ifModifiedSince <= lastModifiedMillis) {
459     					// not modified, content is not sent - only basic headers and status SC_NOT_MODIFIED
460                         response.setDateHeader("Expires", expires);
461     					response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
462     					is.close();
463     					return;
464     				}
465                 	
466                 	// set the content-type header
467                     String contentType = getContentType(name);
468                     if (contentType != null) {
469                         response.setContentType(contentType);
470                     }
471 
472                     if (serveStaticBrowserCache) {
473                     	// set heading information for caching static content
474                         response.setDateHeader("Date", now);
475                         response.setDateHeader("Expires", expires);
476                         response.setDateHeader("Retry-After", expires);
477                         response.setHeader("Cache-Control", "public");
478                         response.setDateHeader("Last-Modified", lastModifiedMillis);
479                     } else {
480                         response.setHeader("Cache-Control", "no-cache");
481                         response.setHeader("Pragma", "no-cache");
482                         response.setHeader("Expires", "-1");
483                     }
484 
485                     try {
486                         copy(is, response.getOutputStream());
487                     } finally {
488                         is.close();
489                     }
490                     return;
491                 }
492             }
493         }
494 
495         response.sendError(HttpServletResponse.SC_NOT_FOUND);
496     }
497 
498     /***
499      * Determine the content type for the resource name.
500      *
501      * @param name The resource name
502      * @return The mime type
503      */
504     protected String getContentType(String name) {
505         // NOT using the code provided activation.jar to avoid adding yet another dependency
506         // this is generally OK, since these are the main files we server up
507         if (name.endsWith(".js")) {
508             return "text/javascript";
509         } else if (name.endsWith(".css")) {
510             return "text/css";
511         } else if (name.endsWith(".html")) {
512             return "text/html";
513         } else if (name.endsWith(".txt")) {
514             return "text/plain";
515         } else if (name.endsWith(".gif")) {
516             return "image/gif";
517         } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
518             return "image/jpeg";
519         } else if (name.endsWith(".png")) {
520             return "image/png";
521         } else {
522             return null;
523         }
524     }
525 
526     /***
527      * Copy bytes from the input stream to the output stream.
528      *
529      * @param input The input stream
530      * @param output The output stream
531      * @throws IOException If anything goes wrong
532      */
533     protected void copy(InputStream input, OutputStream output) throws IOException {
534         final byte[] buffer = new byte[4096];
535         int n;
536         while (-1 != (n = input.read(buffer))) {
537             output.write(buffer, 0, n);
538         }
539         output.flush(); // WW-1526
540     }
541 
542     /***
543      * Look for a static resource in the classpath.
544      *
545      * @param name The resource name
546      * @param packagePrefix The package prefix to use to locate the resource
547      * @return The inputstream of the resource
548      * @throws IOException If there is a problem locating the resource
549      */
550     protected InputStream findInputStream(String name, String packagePrefix) throws IOException {
551         String resourcePath;
552         if (packagePrefix.endsWith("/") && name.startsWith("/")) {
553             resourcePath = packagePrefix + name.substring(1);
554         } else {
555             resourcePath = packagePrefix + name;
556         }
557 
558         resourcePath = URLDecoder.decode(resourcePath, encoding);
559 
560         return ClassLoaderUtil.getResourceAsStream(resourcePath, getClass());
561     }
562 }