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