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