View Javadoc

    1   /*
    2    * $Id: FilterDispatcher.java 492272 2007-01-03 19:53:44Z mrdon $
    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 ConfigurationProvider} interface that should be used for building the {@link 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-01-03 14:53:44 -0500 (Wed, 03 Jan 2007) $ $Id: FilterDispatcher.java 492272 2007-01-03 19:53:44Z mrdon $
    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
    324             try {
    325                 // Wrap request first, just in case it is multipart/form-data
    326                 // parameters might not be accessible through before encoding (ww-1278)
    327                 request = dispatcher.wrapRequest(request, getServletContext());
    328             } catch (IOException e) {
    329                 String message = "Could not wrap servlet request with MultipartRequestWrapper!";
    330                 LOG.error(message, e);
    331                 throw new ServletException(message, e);
    332             }
    333         }
    334         else {
    335             dispatcher = du;
    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                 ActionContextCleanUp.cleanUp(req);
    398                 return;
    399             }
    400
    401             if (mapping == null) {
    402                 // there is no action in this request, should we look for a static resource?
    403                 String resourcePath = RequestUtils.getServletPath(request);
    404
    405                 if ("".equals(resourcePath) && null != request.getPathInfo()) {
    406                     resourcePath = request.getPathInfo();
    407                 }
    408
    409                 if (serveStatic && resourcePath.startsWith("/struts")) {
    410                     String name = resourcePath.substring("/struts".length());
    411                     findStaticResource(name, request, response);
    412                 } else {
    413                     // this is a normal request, let it pass through
    414                     chain.doFilter(request, response);
    415                 }
    416                 // The framework did its job here
    417                 return;
    418             }
    419
    420             try {
    421                 dispatcher.serviceAction(request, response, servletContext, mapping);
    422             } finally {
    423                 ActionContextCleanUp.cleanUp(req);
    424             }
    425         }
    426         finally {
    427             UtilTimerStack.pop(timerKey);
    428         }
    429     }
    430
    431     /***
    432      * Locate a static resource and copy directly to the response,
    433      * setting the appropriate caching headers.
    434      *
    435      * @param name The resource name
    436      * @param request The request
    437      * @param response The response
    438      * @throws IOException If anything goes wrong
    439      */
    440     protected void findStaticResource(String name, HttpServletRequest request, HttpServletResponse response) throws IOException {
    441         if (!name.endsWith(".class")) {
    442             for (String pathPrefix : pathPrefixes) {
    443                 InputStream is = findInputStream(name, pathPrefix);
    444                 if (is != null) {
    445                     Calendar cal = Calendar.getInstance();
    446
    447                     // check for if-modified-since, prior to any other headers
    448                     long ifModifiedSince = 0;
    449                     try {
    450                     	ifModifiedSince = request.getDateHeader("If-Modified-Since");
    451                     } catch (Exception e) {
    452                     	LOG.warn("Invalid If-Modified-Since header value: '" + request.getHeader("If-Modified-Since") + "', ignoring");
    453                     }
    454     				long lastModifiedMillis = lastModifiedCal.getTimeInMillis();
    455     				long now = cal.getTimeInMillis();
    456                     cal.add(Calendar.DAY_OF_MONTH, 1);
    457                     long expires = cal.getTimeInMillis();
    458
    459     				if (ifModifiedSince > 0 && ifModifiedSince <= lastModifiedMillis) {
    460     					// not modified, content is not sent - only basic headers and status SC_NOT_MODIFIED
    461                         response.setDateHeader("Expires", expires);
    462     					response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
    463     					is.close();
    464     					return;
    465     				}
    466
    467                 	// set the content-type header
    468                     String contentType = getContentType(name);
    469                     if (contentType != null) {
    470                         response.setContentType(contentType);
    471                     }
    472
    473                     if (serveStaticBrowserCache) {
    474                     	// set heading information for caching static content
    475                         response.setDateHeader("Date", now);
    476                         response.setDateHeader("Expires", expires);
    477                         response.setDateHeader("Retry-After", expires);
    478                         response.setHeader("Cache-Control", "public");
    479                         response.setDateHeader("Last-Modified", lastModifiedMillis);
    480                     } else {
    481                         response.setHeader("Cache-Control", "no-cache");
    482                         response.setHeader("Pragma", "no-cache");
    483                         response.setHeader("Expires", "-1");
    484                     }
    485
    486                     try {
    487                         copy(is, response.getOutputStream());
    488                     } finally {
    489                         is.close();
    490                     }
    491                     return;
    492                 }
    493             }
    494         }
    495
    496         response.sendError(HttpServletResponse.SC_NOT_FOUND);
    497     }
    498
    499     /***
    500      * Determine the content type for the resource name.
    501      *
    502      * @param name The resource name
    503      * @return The mime type
    504      */
    505     protected String getContentType(String name) {
    506         // NOT using the code provided activation.jar to avoid adding yet another dependency
    507         // this is generally OK, since these are the main files we server up
    508         if (name.endsWith(".js")) {
    509             return "text/javascript";
    510         } else if (name.endsWith(".css")) {
    511             return "text/css";
    512         } else if (name.endsWith(".html")) {
    513             return "text/html";
    514         } else if (name.endsWith(".txt")) {
    515             return "text/plain";
    516         } else if (name.endsWith(".gif")) {
    517             return "image/gif";
    518         } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
    519             return "image/jpeg";
    520         } else if (name.endsWith(".png")) {
    521             return "image/png";
    522         } else {
    523             return null;
    524         }
    525     }
    526
    527     /***
    528      * Copy bytes from the input stream to the output stream.
    529      *
    530      * @param input The input stream
    531      * @param output The output stream
    532      * @throws IOException If anything goes wrong
    533      */
    534     protected void copy(InputStream input, OutputStream output) throws IOException {
    535         final byte[] buffer = new byte[4096];
    536         int n;
    537         while (-1 != (n = input.read(buffer))) {
    538             output.write(buffer, 0, n);
    539         }
    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 }