View Javadoc

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