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.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 11:00:20 -0400 (Thu, 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
314
315
316 if (du == null) {
317
318 Dispatcher.setInstance(dispatcher);
319
320
321
322 dispatcher.prepare(request, response);
323 } else {
324 dispatcher = du;
325 }
326
327 try {
328
329
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
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
413 chain.doFilter(request, response);
414 }
415
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
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
460 response.setDateHeader("Expires", expires);
461 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
462 is.close();
463 return;
464 }
465
466
467 String contentType = getContentType(name);
468 if (contentType != null) {
469 response.setContentType(contentType);
470 }
471
472 if (serveStaticBrowserCache) {
473
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
506
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();
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 }