Save This Page
Home » struts-2.0.11.2-src » org.apache » struts2 » dispatcher » [javadoc | source]
    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   }

Save This Page
Home » struts-2.0.11.2-src » org.apache » struts2 » dispatcher » [javadoc | source]