1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
192
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
205 if (du == null) {
206 du = dispatcher;
207
208
209 du.prepare(request, response);
210
211 try {
212
213
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
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
248 chain.doFilter(request, response);
249 }
250
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
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
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
333
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 }