View Javadoc

1   /*
2    * $Id: FilterDispatcher.java 454720 2006-10-10 12:31:52Z tmjee $
3    *
4    * Copyright 2006 The Apache Software Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.struts2.dispatcher;
19  
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.OutputStream;
23  import java.net.URLDecoder;
24  import java.text.SimpleDateFormat;
25  import java.util.ArrayList;
26  import java.util.Calendar;
27  import java.util.List;
28  import java.util.StringTokenizer;
29  import java.util.TimeZone;
30  
31  import javax.servlet.Filter;
32  import javax.servlet.FilterChain;
33  import javax.servlet.FilterConfig;
34  import javax.servlet.ServletContext;
35  import javax.servlet.ServletException;
36  import javax.servlet.ServletRequest;
37  import javax.servlet.ServletResponse;
38  import javax.servlet.http.HttpServletRequest;
39  import javax.servlet.http.HttpServletResponse;
40  import javax.servlet.http.HttpSession;
41  
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  import org.apache.struts2.RequestUtils;
45  import org.apache.struts2.StrutsConstants;
46  import org.apache.struts2.StrutsStatics;
47  import org.apache.struts2.config.Settings;
48  import org.apache.struts2.dispatcher.mapper.ActionMapper;
49  import org.apache.struts2.dispatcher.mapper.ActionMapperFactory;
50  import org.apache.struts2.dispatcher.mapper.ActionMapping;
51  
52  import com.opensymphony.xwork2.util.ClassLoaderUtil;
53  import com.opensymphony.xwork2.util.profiling.UtilTimerStack;
54  import com.opensymphony.xwork2.ActionContext;
55  
56  /***
57   * Master filter for Struts that handles four distinct 
58   * responsibilities:
59   *
60   * <ul>
61   *
62   * <li>Executing actions</li>
63   *
64   * <li>Cleaning up the {@link ActionContext} (see note)</li>
65   *
66   * <li>Serving static content</li>
67   *
68   * <li>Kicking off XWork's interceptor chain for the request lifecycle</li>
69   *
70   * </ul>
71   *
72   * <p/> <b>IMPORTANT</b>: this filter must be mapped to all requests. Unless you know exactly what you are doing, always
73   * map to this URL pattern: /*
74   *
75   * <p/> <b>Executing actions</b>
76   *
77   * <p/> This filter executes actions by consulting the {@link ActionMapper} and determining if the requested URL should
78   * invoke an action. If the mapper indicates it should, <b>the rest of the filter chain is stopped</b> and the action is
79   * invoked. This is important, as it means that filters like the SiteMesh filter must be placed <b>before</b> this
80   * filter or they will not be able to decorate the output of actions.
81   *
82   * <p/> <b>Cleaning up the {@link ActionContext}</b>
83   *
84   * <p/> This filter will also automatically clean up the {@link ActionContext} for you, ensuring that no memory leaks
85   * take place. However, this can sometimes cause problems integrating with other products like SiteMesh. See {@link
86   * ActionContextCleanUp} for more information on how to deal with this.
87   *
88   * <p/> <b>Serving static content</b>
89   *
90   * <p/> This filter also serves common static content needed when using various parts of Struts, such as JavaScript
91   * files, CSS files, etc. It works by looking for requests to /struts/*, and then mapping the value after "/struts/"
92   * to common packages in Struts and, optionally, in your class path. By default, the following packages are
93   * automatically searched:
94   *
95   * <ul>
96   *
97   * <li>org.apache.struts2.static</li>
98   *
99   * <li>template</li>
100  *
101  * </ul>
102  *
103  * <p/> This means that you can simply request /struts/xhtml/styles.css and the XHTML UI theme's default stylesheet
104  * will be returned. Likewise, many of the AJAX UI components require various JavaScript files, which are found in the
105  * org.apache.struts2.static package. If you wish to add additional packages to be searched, you can add a comma
106  * separated (space, tab and new line will do as well) list in the filter init parameter named "packages". <b>Be
107  * careful</b>, however, to expose any packages that may have sensitive information, such as properties file with
108  * database access credentials.
109  *
110  * <p/>
111  * 
112  * To use a custom {@link Dispatcher}, the <code>createDispatcher()</code> method could be overriden by 
113  * the subclass.
114  *
115  * @see org.apache.struts2.lifecycle.LifecycleListener
116  * @see ActionMapper
117  * @see ActionContextCleanUp
118  * 
119  * @version $Date: 2006-10-10 07:31:52 -0500 (Tue, 10 Oct 2006) $ $Id: FilterDispatcher.java 454720 2006-10-10 12:31:52Z tmjee $
120  */
121 public class FilterDispatcher implements Filter, StrutsStatics {
122     private static final Log LOG = LogFactory.getLog(FilterDispatcher.class);
123 
124     private FilterConfig filterConfig;
125     private String[] pathPrefixes;
126     private Dispatcher dispatcher;
127 
128     private SimpleDateFormat df = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss");
129     private final Calendar lastModifiedCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
130     private final String lastModified = df.format(lastModifiedCal.getTime());
131 
132     /*** 
133      * Gets this filter's configuration
134      * 
135      * @return The filter config
136      */
137     protected FilterConfig getFilterConfig() {
138         return filterConfig;
139     }
140 
141     /***
142      * Cleans up the dispatcher
143      */
144     public void destroy() {
145         	if (dispatcher == null) {
146         		LOG.warn("something is seriously wrong, DispatcherUtil is not initialized (null) ");
147         	} else {
148         	    dispatcher.cleanup();
149         }
150     }
151 
152     /***
153      * Initializes the dispatcher and filter
154      */
155     public void init(FilterConfig filterConfig) throws ServletException {
156         this.filterConfig = filterConfig;
157         String param = filterConfig.getInitParameter("packages");
158         String packages = "org.apache.struts2.static template org.apache.struts2.interceptor.debugging";
159         if (param != null) {
160             packages = param + " " + packages;
161         }
162         this.pathPrefixes = parse(packages);
163         dispatcher = createDispatcher();
164     }
165     
166     /***
167      * Parses the list of packages
168      * 
169      * @param packages A comma-delimited String 
170      * @return A string array of packages
171      */
172     protected String[] parse(String packages) {
173         if (packages == null) {
174             return null;
175         }
176         List<String> pathPrefixes = new ArrayList<String>();
177 
178         StringTokenizer st = new StringTokenizer(packages, ", \n\t");
179         while (st.hasMoreTokens()) {
180             String pathPrefix = st.nextToken().replace('.', '/');
181             if (!pathPrefix.endsWith("/")) {
182                 pathPrefix += "/";
183             }
184             pathPrefixes.add(pathPrefix);
185         }
186 
187         return (String[]) pathPrefixes.toArray(new String[pathPrefixes.size()]);
188     }
189 
190 
191     /* (non-Javadoc)
192      * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
193      */
194     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
195         HttpServletRequest request = (HttpServletRequest) req;
196         HttpServletResponse response = (HttpServletResponse) res;
197         ServletContext servletContext = filterConfig.getServletContext();
198 
199         String timerKey = "FilterDispatcher_doFilter: ";
200         try {
201         	UtilTimerStack.push(timerKey);
202         	Dispatcher du = Dispatcher.getInstance();
203         
204         	// Prepare and wrap the request if the cleanup filter hasn't already
205         	if (du == null) {
206         		du = dispatcher;
207         		// prepare the request no matter what - this ensures that the proper character encoding
208         		// is used before invoking the mapper (see WW-9127)
209         		du.prepare(request, response);
210 
211         		try {
212         			// Wrap request first, just in case it is multipart/form-data 
213         			// parameters might not be accessible through before encoding (ww-1278)
214         			request = du.wrapRequest(request, servletContext);
215         		} catch (IOException e) {
216         			String message = "Could not wrap servlet request with MultipartRequestWrapper!";
217         			LOG.error(message, e);
218         			throw new ServletException(message, e);
219         		}
220         		Dispatcher.setInstance(du);
221         	}
222 
223         	ActionMapper mapper = null;
224         	ActionMapping mapping = null;
225         	try {
226         		mapper = ActionMapperFactory.getMapper();
227         		mapping = mapper.getMapping(request, du.getConfigurationManager());
228         	} catch (Exception ex) {
229         		du.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
230         		ActionContextCleanUp.cleanUp(req);
231         		return;
232         	}
233 
234         	if (mapping == null) {
235         		// there is no action in this request, should we look for a static resource?
236         		String resourcePath = RequestUtils.getServletPath(request);
237 
238         		if ("".equals(resourcePath) && null != request.getPathInfo()) {
239         			resourcePath = request.getPathInfo();
240         		}
241 
242         		if ("true".equals(Settings.get(StrutsConstants.STRUTS_SERVE_STATIC_CONTENT)) 
243                     && resourcePath.startsWith("/struts")) {
244         			String name = resourcePath.substring("/struts".length());
245         			findStaticResource(name, response);
246         		} else {
247         			// this is a normal request, let it pass through
248         			chain.doFilter(request, response);
249         		}
250         		// The framework did its job here
251         		return;
252         	}
253 
254 
255         	try {
256         		dispatcher.serviceAction(request, response, servletContext, mapping);
257         	} finally {
258         		ActionContextCleanUp.cleanUp(req);
259         	}
260         }
261         finally {
262         	UtilTimerStack.pop(timerKey);
263         }
264     }
265 
266     /***
267      * Servlet 2.3 specifies that the servlet context can be retrieved from the session. Unfortunately, some versions of
268      * WebLogic can only retrieve the servlet context from the filter config. Hence, this method enables subclasses to
269      * retrieve the servlet context from other sources.
270      *
271      * @param session the HTTP session where, in Servlet 2.3, the servlet context can be retrieved
272      * @return the servlet context.
273      */
274     protected ServletContext getServletContext(HttpSession session) {
275         return filterConfig.getServletContext();
276     }
277 
278     /***
279      * Fins a static resource
280      * 
281      * @param name The resource name
282      * @param response The request
283      * @throws IOException If anything goes wrong
284      */
285     protected void findStaticResource(String name, HttpServletResponse response) throws IOException {
286         if (!name.endsWith(".class")) {
287             for (int i = 0; i < pathPrefixes.length; i++) {
288                 InputStream is = findInputStream(name, pathPrefixes[i]);
289                 if (is != null) {
290                     // set the content-type header
291                     String contentType = getContentType(name);
292                     if (contentType != null) {
293                         response.setContentType(contentType);
294                     }
295                     
296                     if ("true".equals(Settings.get(StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE))) {
297                     	// set heading information for caching static content
298                         Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
299                         response.setHeader("Date",df.format(cal.getTime())+" GMT");
300                         cal.add(Calendar.DAY_OF_MONTH,1);
301                         response.setHeader("Expires",df.format(cal.getTime())+" GMT");
302                         response.setHeader("Retry-After",df.format(cal.getTime())+" GMT");
303                         response.setHeader("Cache-Control","public");
304                         response.setHeader("Last-Modified",lastModified+" GMT");
305                     }
306                     else {
307                     	response.setHeader("Cache-Control","no-cache");
308                         response.setHeader("Pragma","no-cache");
309                         response.setHeader("Expires","-1");
310                     }
311 
312                     try {
313                         copy(is, response.getOutputStream());
314                     } finally {
315                         is.close();
316                     }
317                     return;
318                 }
319             }
320         }
321 
322         response.sendError(HttpServletResponse.SC_NOT_FOUND);
323     }
324 
325     /***
326      * Determines the content type for the resource name
327      * 
328      * @param name The resource name
329      * @return The mime type
330      */
331     protected String getContentType(String name) {
332         // NOT using the code provided activation.jar to avoid adding yet another dependency
333         // this is generally OK, since these are the main files we server up
334         if (name.endsWith(".js")) {
335             return "text/javascript";
336         } else if (name.endsWith(".css")) {
337             return "text/css";
338         } else if (name.endsWith(".html")) {
339             return "text/html";
340         } else if (name.endsWith(".txt")) {
341             return "text/plain";
342         } else if (name.endsWith(".gif")) {
343             return "image/gif";
344         } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
345             return "image/jpeg";
346         } else if (name.endsWith(".png")) {
347             return "image/png";
348         } else {
349             return null;
350         }
351     }
352 
353     /***
354      * Copies the from the input stream to the output stream
355      * 
356      * @param input The input stream
357      * @param output The output stream
358      * @throws IOException If anything goes wrong
359      */
360     protected void copy(InputStream input, OutputStream output) throws IOException {
361         final byte[] buffer = new byte[4096];
362         int n;
363         while (-1 != (n = input.read(buffer))) {
364             output.write(buffer, 0, n);
365         }
366     }
367 
368     /***
369      * Looks for a static resource in the classpath
370      * 
371      * @param name The resource name
372      * @param packagePrefix The package prefix to use to locate the resource
373      * @return The inputstream of the resource
374      * @throws IOException If there is a problem locating the resource
375      */
376     protected InputStream findInputStream(String name, String packagePrefix) throws IOException {
377         String resourcePath;
378         if (packagePrefix.endsWith("/") && name.startsWith("/")) {
379             resourcePath = packagePrefix + name.substring(1);
380         } else {
381             resourcePath = packagePrefix + name;
382         }
383 
384         String enc = (String) Settings.get(StrutsConstants.STRUTS_I18N_ENCODING);
385         resourcePath = URLDecoder.decode(resourcePath, enc);
386 
387         return ClassLoaderUtil.getResourceAsStream(resourcePath, getClass());
388     }
389     
390     /***
391      * Create a {@link Dispatcher}, this serves as a hook for subclass to overried
392      * such that a custom {@link Dispatcher} could be created. 
393      * 
394      * @return Dispatcher
395      */
396     protected Dispatcher createDispatcher() {
397     	return new Dispatcher(filterConfig.getServletContext());
398     }
399 }