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-08-21 23:56:46 +0200 (Do, 21. Aug 2008) $ $Id: FilterDispatcher.java 687874 2008-08-21 21:56:46Z 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 /***
154 * Store set of path prefixes to use with static resources.
155 */
156 private String[] pathPrefixes;
157
158 /***
159 * Provide a formatted date for setting heading information when caching static content.
160 */
161 private final Calendar lastModifiedCal = Calendar.getInstance();
162
163 /***
164 * Store state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
165 */
166 private static boolean serveStatic;
167
168 /***
169 * Store state of StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE setting.
170 */
171 private static boolean serveStaticBrowserCache;
172
173 /***
174 * Store state of StrutsConstants.STRUTS_I18N_ENCODING setting.
175 */
176 private static String encoding;
177
178 /***
179 * Provide ActionMapper instance, set by injection.
180 */
181 private static ActionMapper actionMapper;
182
183 /***
184 * Provide FilterConfig instance, set on init.
185 */
186 private FilterConfig filterConfig;
187
188 /***
189 * Expose Dispatcher instance to subclass.
190 */
191 protected Dispatcher dispatcher;
192
193 /***
194 * Initializes the filter by creating a default dispatcher
195 * and setting the default packages for static resources.
196 *
197 * @param filterConfig The filter configuration
198 */
199 public void init(FilterConfig filterConfig) throws ServletException {
200 this.filterConfig = filterConfig;
201
202 dispatcher = createDispatcher(filterConfig);
203 dispatcher.init();
204
205 String param = filterConfig.getInitParameter("packages");
206 String packages = "org.apache.struts2.static template org.apache.struts2.interceptor.debugging";
207 if (param != null) {
208 packages = param + " " + packages;
209 }
210 this.pathPrefixes = parse(packages);
211 }
212
213 /***
214 * Calls dispatcher.cleanup,
215 * which in turn releases local threads and destroys any DispatchListeners.
216 *
217 * @see javax.servlet.Filter#destroy()
218 */
219 public void destroy() {
220 if (dispatcher == null) {
221 LOG.warn("something is seriously wrong, Dispatcher is not initialized (null) ");
222 } else {
223 dispatcher.cleanup();
224 }
225 }
226
227 /***
228 * Create a default {@link Dispatcher} that subclasses can override
229 * with a custom Dispatcher, if needed.
230 *
231 * @param filterConfig Our FilterConfig
232 * @return Initialized Dispatcher
233 */
234 protected Dispatcher createDispatcher(FilterConfig filterConfig) {
235 Map<String,String> params = new HashMap<String,String>();
236 for (Enumeration e = filterConfig.getInitParameterNames(); e.hasMoreElements(); ) {
237 String name = (String) e.nextElement();
238 String value = filterConfig.getInitParameter(name);
239 params.put(name, value);
240 }
241 return new Dispatcher(filterConfig.getServletContext(), params);
242 }
243
244 /***
245 * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
246 * @param val New setting
247 */
248 @Inject(StrutsConstants.STRUTS_SERVE_STATIC_CONTENT)
249 public static void setServeStaticContent(String val) {
250 serveStatic = "true".equals(val);
251 }
252
253 /***
254 * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE setting.
255 * @param val New setting
256 */
257 @Inject(StrutsConstants.STRUTS_SERVE_STATIC_BROWSER_CACHE)
258 public static void setServeStaticBrowserCache(String val) {
259 serveStaticBrowserCache = "true".equals(val);
260 }
261
262 /***
263 * Modify state of StrutsConstants.STRUTS_I18N_ENCODING setting.
264 * @param val New setting
265 */
266 @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
267 public static void setEncoding(String val) {
268 encoding = val;
269 }
270
271 /***
272 * Modify ActionMapper instance.
273 * @param mapper New instance
274 */
275 @Inject
276 public static void setActionMapper(ActionMapper mapper) {
277 actionMapper = mapper;
278 }
279
280 /***
281 * Provide a workaround for some versions of WebLogic.
282 * <p/>
283 * Servlet 2.3 specifies that the servlet context can be retrieved from the session. Unfortunately, some versions of
284 * WebLogic can only retrieve the servlet context from the filter config. Hence, this method enables subclasses to
285 * retrieve the servlet context from other sources.
286 *
287 * @return the servlet context.
288 */
289 protected ServletContext getServletContext() {
290 return filterConfig.getServletContext();
291 }
292
293 /***
294 * Expose the FilterConfig instance.
295 *
296 * @return Our FilterConfit instance
297 */
298 protected FilterConfig getFilterConfig() {
299 return filterConfig;
300 }
301
302 /***
303 * Wrap and return the given request, if needed, so as to to transparently
304 * handle multipart data as a wrapped class around the given request.
305 *
306 * @param request Our ServletRequest object
307 * @param response Our ServerResponse object
308 * @return Wrapped HttpServletRequest object
309 * @throws ServletException on any error
310 */
311 protected HttpServletRequest prepareDispatcherAndWrapRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException {
312
313 Dispatcher du = Dispatcher.getInstance();
314
315
316
317
318 if (du == null) {
319
320 Dispatcher.setInstance(dispatcher);
321
322
323
324 dispatcher.prepare(request, response);
325 } else {
326 dispatcher = du;
327 }
328
329 try {
330
331
332 request = dispatcher.wrapRequest(request, getServletContext());
333 } catch (IOException e) {
334 String message = "Could not wrap servlet request with MultipartRequestWrapper!";
335 LOG.error(message, e);
336 throw new ServletException(message, e);
337 }
338
339 return request;
340 }
341
342 /***
343 * Create a string array from a comma-delimited list of packages.
344 *
345 * @param packages A comma-delimited String listing packages
346 * @return A string array of packages
347 */
348 protected String[] parse(String packages) {
349 if (packages == null) {
350 return null;
351 }
352 List<String> pathPrefixes = new ArrayList<String>();
353
354 StringTokenizer st = new StringTokenizer(packages, ", \n\t");
355 while (st.hasMoreTokens()) {
356 String pathPrefix = st.nextToken().replace('.', '/');
357 if (!pathPrefix.endsWith("/")) {
358 pathPrefix += "/";
359 }
360 pathPrefixes.add(pathPrefix);
361 }
362
363 return pathPrefixes.toArray(new String[pathPrefixes.size()]);
364 }
365
366
367 /***
368 * Process an action or handle a request a static resource.
369 * <p/>
370 * The filter tries to match the request to an action mapping.
371 * If mapping is found, the action processes is delegated to the dispatcher's serviceAction method.
372 * If action processing fails, doFilter will try to create an error page via the dispatcher.
373 * <p/>
374 * Otherwise, if the request is for a static resource,
375 * the resource is copied directly to the response, with the appropriate caching headers set.
376 * <p/>
377 * If the request does not match an action mapping, or a static resource page,
378 * then it passes through.
379 *
380 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
381 */
382 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
383
384
385 HttpServletRequest request = (HttpServletRequest) req;
386 HttpServletResponse response = (HttpServletResponse) res;
387 ServletContext servletContext = getServletContext();
388
389 String timerKey = "FilterDispatcher_doFilter: ";
390 try {
391 UtilTimerStack.push(timerKey);
392 request = prepareDispatcherAndWrapRequest(request, response);
393 ActionMapping mapping;
394 try {
395 mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager());
396 } catch (Exception ex) {
397 LOG.error("error getting ActionMapping", ex);
398 dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
399 return;
400 }
401
402 if (mapping == null) {
403
404 String resourcePath = RequestUtils.getServletPath(request);
405
406 if ("".equals(resourcePath) && null != request.getPathInfo()) {
407 resourcePath = request.getPathInfo();
408 }
409
410 if (serveStatic && resourcePath.startsWith("/struts")) {
411 String name = resourcePath.substring("/struts".length());
412 findStaticResource(name, request, response);
413 } else {
414
415 chain.doFilter(request, response);
416 }
417
418 return;
419 }
420
421 dispatcher.serviceAction(request, response, servletContext, mapping);
422
423 } finally {
424 try {
425 ActionContextCleanUp.cleanUp(req);
426 } finally {
427 UtilTimerStack.pop(timerKey);
428 }
429 }
430 }
431
432 /***
433 * Locate a static resource and copy directly to the response,
434 * setting the appropriate caching headers.
435 *
436 * @param path The resource path
437 * @param request The request
438 * @param response The response
439 * @throws IOException If anything goes wrong
440 */
441 public void findStaticResource(String path, HttpServletRequest request, HttpServletResponse response)
442 throws IOException {
443 String name = cleanupPath(path);
444 for (String pathPrefix : pathPrefixes) {
445 URL resourceUrl = findResource(buildPath(name, pathPrefix));
446 if (resourceUrl != null) {
447 InputStream is = null;
448 try {
449
450 String pathEnding = buildPath(name, pathPrefix);
451 if (resourceUrl.getFile().endsWith(pathEnding))
452 is = resourceUrl.openStream();
453 } catch (Exception ex) {
454
455 continue;
456 }
457
458
459 if (is != null) {
460 process(is, path, request, response);
461 return;
462 }
463 }
464 }
465
466 response.sendError(HttpServletResponse.SC_NOT_FOUND);
467 }
468
469 /***
470 * Look for a static resource in the classpath.
471 *
472 * @param path The resource path
473 * @return The inputstream of the resource
474 * @throws IOException If there is a problem locating the resource
475 */
476 protected URL findResource(String path) throws IOException {
477 return ClassLoaderUtil.getResource(path, getClass());
478 }
479
480 /***
481 * @param name resource name
482 * @param packagePrefix The package prefix to use to locate the resource
483 * @return full path
484 * @throws java.io.UnsupportedEncodingException
485 * @throws IOException
486 */
487 protected String buildPath(String name, String packagePrefix) throws UnsupportedEncodingException {
488 String resourcePath;
489 if (packagePrefix.endsWith("/") && name.startsWith("/")) {
490 resourcePath = packagePrefix + name.substring(1);
491 } else {
492 resourcePath = packagePrefix + name;
493 }
494
495 return URLDecoder.decode(resourcePath, encoding);
496 }
497
498 protected void process(InputStream is, String path, HttpServletRequest request, HttpServletResponse response) throws IOException {
499 if (is != null) {
500 Calendar cal = Calendar.getInstance();
501
502
503 long ifModifiedSince = 0;
504 try {
505 ifModifiedSince = request.getDateHeader("If-Modified-Since");
506 } catch (Exception e) {
507 LOG.warn("Invalid If-Modified-Since header value: '"
508 + request.getHeader("If-Modified-Since") + "', ignoring");
509 }
510 long lastModifiedMillis = lastModifiedCal.getTimeInMillis();
511 long now = cal.getTimeInMillis();
512 cal.add(Calendar.DAY_OF_MONTH, 1);
513 long expires = cal.getTimeInMillis();
514
515 if (ifModifiedSince > 0 && ifModifiedSince <= lastModifiedMillis) {
516
517
518 response.setDateHeader("Expires", expires);
519 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
520 is.close();
521 return;
522 }
523
524
525 String contentType = getContentType(path);
526 if (contentType != null) {
527 response.setContentType(contentType);
528 }
529
530 if (serveStaticBrowserCache) {
531
532 response.setDateHeader("Date", now);
533 response.setDateHeader("Expires", expires);
534 response.setDateHeader("Retry-After", expires);
535 response.setHeader("Cache-Control", "public");
536 response.setDateHeader("Last-Modified", lastModifiedMillis);
537 } else {
538 response.setHeader("Cache-Control", "no-cache");
539 response.setHeader("Pragma", "no-cache");
540 response.setHeader("Expires", "-1");
541 }
542
543 try {
544 copy(is, response.getOutputStream());
545 } finally {
546 is.close();
547 }
548 }
549 }
550
551 /***
552 * Determine the content type for the resource name.
553 *
554 * @param name The resource name
555 * @return The mime type
556 */
557 protected String getContentType(String name) {
558
559
560 if (name.endsWith(".js")) {
561 return "text/javascript";
562 } else if (name.endsWith(".css")) {
563 return "text/css";
564 } else if (name.endsWith(".html")) {
565 return "text/html";
566 } else if (name.endsWith(".txt")) {
567 return "text/plain";
568 } else if (name.endsWith(".gif")) {
569 return "image/gif";
570 } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
571 return "image/jpeg";
572 } else if (name.endsWith(".png")) {
573 return "image/png";
574 } else {
575 return null;
576 }
577 }
578
579 /***
580 * Copy bytes from the input stream to the output stream.
581 *
582 * @param input The input stream
583 * @param output The output stream
584 * @throws IOException If anything goes wrong
585 */
586 protected void copy(InputStream input, OutputStream output) throws IOException {
587 final byte[] buffer = new byte[4096];
588 int n;
589 while (-1 != (n = input.read(buffer))) {
590 output.write(buffer, 0, n);
591 }
592 output.flush();
593 }
594
595 /***
596 * Look for a static resource in the classpath.
597 *
598 * @param name The resource name
599 * @param packagePrefix The package prefix to use to locate the resource
600 * @return The inputstream of the resource
601 * @throws IOException If there is a problem locating the resource
602 */
603 protected InputStream findInputStream(String name, String packagePrefix) throws IOException {
604 String resourcePath;
605 if (packagePrefix.endsWith("/") && name.startsWith("/")) {
606 resourcePath = packagePrefix + name.substring(1);
607 } else {
608 resourcePath = packagePrefix + name;
609 }
610
611 resourcePath = URLDecoder.decode(resourcePath, encoding);
612
613 return ClassLoaderUtil.getResourceAsStream(resourcePath, getClass());
614 }
615
616 /***
617 * @param path requested path
618 * @return path without leading "/struts" or "/static"
619 */
620 protected String cleanupPath(String path) {
621
622 return path.substring(7);
623 }
624 }