001: /*
002: * Copyright (c) 2002-2003 by OpenSymphony
003: * All rights reserved.
004: */
005: package com.opensymphony.webwork.dispatcher;
006:
007: import com.opensymphony.util.ClassLoaderUtil;
008: import com.opensymphony.webwork.RequestUtils;
009: import com.opensymphony.webwork.WebWorkConstants;
010: import com.opensymphony.webwork.WebWorkStatics;
011: import com.opensymphony.webwork.config.Configuration;
012: import com.opensymphony.webwork.dispatcher.mapper.ActionMapper;
013: import com.opensymphony.webwork.dispatcher.mapper.ActionMapperFactory;
014: import com.opensymphony.webwork.dispatcher.mapper.ActionMapping;
015: import com.opensymphony.xwork.ActionContext;
016: import com.opensymphony.xwork.interceptor.component.ComponentConfiguration;
017: import com.opensymphony.xwork.interceptor.component.ComponentManager;
018: import com.opensymphony.xwork.interceptor.component.DefaultComponentManager;
019: import org.apache.commons.logging.Log;
020: import org.apache.commons.logging.LogFactory;
021:
022: import javax.servlet.*;
023: import javax.servlet.http.HttpServletRequest;
024: import javax.servlet.http.HttpServletResponse;
025: import javax.servlet.http.HttpSession;
026: import java.io.IOException;
027: import java.io.InputStream;
028: import java.io.OutputStream;
029: import java.net.URL;
030: import java.net.URLDecoder;
031: import java.util.*;
032: import java.text.SimpleDateFormat;
033:
034: /**
035: * Master filter for WebWork that handles four distinct responsibilities:
036: *
037: * <ul>
038: *
039: * <li>Executing actions</li>
040: *
041: * <li>Cleaning up the {@link ActionContext} (see note)</li>
042: *
043: * <li>Serving static content</li>
044: *
045: * <li>Kicking off XWork's IoC for the request lifecycle</li>
046: *
047: * </ul>
048: *
049: * <p/> <b>IMPORTANT</b>: this filter must be mapped to all requests. Unless you know exactly what you are doing, always
050: * map to this URL pattern: /*
051: *
052: * <p/> <b>Executing actions</b>
053: *
054: * <p/> This filter executes actions by consulting the {@link ActionMapper} and determining if the requested URL should
055: * invoke an action. If the mapper indicates it should, <b>the rest of the filter chain is stopped</b> and the action is
056: * invoked. This is important, as it means that filters like the SiteMesh filter must be placed <b>before</b> this
057: * filter or they will not be able to decorate the output of actions.
058: *
059: * <p/> <b>Cleaning up the {@link ActionContext}</b>
060: *
061: * <p/> This filter will also automatically clean up the {@link ActionContext} for you, ensuring that no memory leaks
062: * take place. However, this can sometimes cause problems integrating with other products like SiteMesh. See {@link
063: * ActionContextCleanUp} for more information on how to deal with this.
064: *
065: * <p/> <b>Serving static content</b>
066: *
067: * <p/> This filter also serves common static content needed when using various parts of WebWork, such as JavaScript
068: * files, CSS files, etc. It works by looking for requests to /webwork/*, and then mapping the value after "/webwork/"
069: * to common packages in WebWork and, optionally, in your class path. By default, the following packages are
070: * automatically searched:
071: *
072: * <ul>
073: *
074: * <li>com.opensymphony.webwork.static</li>
075: *
076: * <li>template</li>
077: *
078: * </ul>
079: *
080: * <p/> This means that you can simply request /webwork/xhtml/styles.css and the XHTML UI theme's default stylesheet
081: * will be returned. Likewise, many of the AJAX UI components require various JavaScript files, which are found in the
082: * com.opensymphony.webwork.static package. If you wish to add additional packages to be searched, you can add a comma
083: * separated (space, tab and new line will do as well) list in the filter init parameter named "packages". <b>Be
084: * careful</b>, however, to expose any packages that may have sensitive information, such as properties file with
085: * database access credentials.
086: *
087: * <p/> <b>Kicking off XWork's IoC for the request lifecycle</b>
088: *
089: * <p/> This filter also kicks off the XWork IoC request scope, provided that you are using XWork's IoC. All you have to
090: * do to get started with XWork's IoC is add a components.xml file to WEB-INF/classes and properly set up the {@link
091: * com.opensymphony.webwork.lifecycle.LifecycleListener} in web.xml. See the IoC docs for more information. <p/>
092: *
093: * @author Patrick Lightbody
094: * @author tm_jee
095: * @see com.opensymphony.webwork.lifecycle.LifecycleListener
096: * @see ActionMapper
097: * @see ActionContextCleanUp
098: * @since 2.2
099: */
100: public class FilterDispatcher implements Filter, WebWorkStatics {
101: private static final Log LOG = LogFactory
102: .getLog(FilterDispatcher.class);
103:
104: protected FilterConfig filterConfig;
105: protected String[] pathPrefixes;
106:
107: private SimpleDateFormat df = new SimpleDateFormat(
108: "E, d MMM yyyy HH:mm:ss");
109: private final Calendar lastModifiedCal = Calendar
110: .getInstance(TimeZone.getTimeZone("GMT"));
111: private final String lastModified = df.format(lastModifiedCal
112: .getTime());
113:
114: public FilterConfig getFilterConfig() {
115: return filterConfig;
116: }
117:
118: public void destroy() {
119: DispatcherUtils du = DispatcherUtils.getInstance(); // should not be null as it is initialized in init(FilterConfig)
120: if (du == null) {
121: LOG
122: .warn("something is seriously wrong, DispatcherUtil is not initialized (null) ");
123: }
124: du.cleanup();
125: }
126:
127: public void init(FilterConfig filterConfig) throws ServletException {
128: this .filterConfig = filterConfig;
129: String param = filterConfig.getInitParameter("packages");
130: String packages = "com.opensymphony.webwork.static template com.opensymphony.webwork.interceptor.debugging";
131: if (param != null) {
132: packages = param + " " + packages;
133: }
134: this .pathPrefixes = parse(packages);
135: DispatcherUtils.initialize(filterConfig.getServletContext());
136: }
137:
138: protected String[] parse(String packages) {
139: if (packages == null) {
140: return null;
141: }
142: List pathPrefixes = new ArrayList();
143:
144: StringTokenizer st = new StringTokenizer(packages, ", \n\t");
145: while (st.hasMoreTokens()) {
146: String pathPrefix = st.nextToken().replace('.', '/');
147: if (!pathPrefix.endsWith("/")) {
148: pathPrefix += "/";
149: }
150: pathPrefixes.add(pathPrefix);
151: }
152:
153: return (String[]) pathPrefixes.toArray(new String[pathPrefixes
154: .size()]);
155: }
156:
157: public void doFilter(ServletRequest req, ServletResponse res,
158: FilterChain chain) throws IOException, ServletException {
159: HttpServletRequest request = (HttpServletRequest) req;
160: HttpServletResponse response = (HttpServletResponse) res;
161: ServletContext servletContext = filterConfig
162: .getServletContext();
163:
164: // prepare the request no matter what - this ensures that the proper character encoding
165: // is used before invoking the mapper (see WW-9127)
166: DispatcherUtils du = DispatcherUtils.getInstance();
167: du.prepare(request, response);
168: try {
169: // Wrap request first, just in case it is multipart/form-data
170: // parameters might not be accessible through before encoding (ww-1278)
171: request = du.wrapRequest(request, servletContext);
172: } catch (IOException e) {
173: String message = "Could not wrap servlet request with MultipartRequestWrapper!";
174: LOG.error(message, e);
175: throw new ServletException(message, e);
176: }
177:
178: ActionMapper mapper = ActionMapperFactory.getMapper();
179: ActionMapping mapping = mapper.getMapping(request);
180:
181: if (mapping == null) {
182: // there is no action in this request, should we look for a static resource?
183: String resourcePath = RequestUtils.getServletPath(request);
184:
185: if ("".equals(resourcePath)
186: && null != request.getPathInfo()) {
187: resourcePath = request.getPathInfo();
188: }
189:
190: if ("true"
191: .equals(Configuration
192: .get(WebWorkConstants.WEBWORK_SERVE_STATIC_CONTENT))
193: && resourcePath.startsWith("/webwork")) {
194: String name = resourcePath.substring("/webwork"
195: .length());
196: findStaticResource(name, response);
197: } else {
198: // this is a normal request, let it pass through
199: chain.doFilter(request, response);
200: }
201: // WW did its job here
202: return;
203: }
204:
205: Object o = null;
206: try {
207:
208: setupContainer(request);
209: o = beforeActionInvocation(request, servletContext);
210:
211: du
212: .serviceAction(request, response, servletContext,
213: mapping);
214: } finally {
215: afterActionInvocation(request, servletContext, o);
216: ActionContextCleanUp.cleanUp(req);
217: }
218: }
219:
220: protected void afterActionInvocation(HttpServletRequest request,
221: Object o, Object o1) {
222: // nothing by default, but a good hook for scoped ioc integration
223: }
224:
225: protected Object beforeActionInvocation(HttpServletRequest request,
226: ServletContext servletContext) {
227: // nothing by default, but a good hook for scoped ioc integration
228: return null;
229: }
230:
231: protected void setupContainer(HttpServletRequest request) {
232: ComponentManager container = null;
233: HttpSession session = request.getSession(false);
234: ComponentManager fallback = null;
235: if (session != null) {
236: fallback = (ComponentManager) session
237: .getAttribute(ComponentManager.COMPONENT_MANAGER_KEY);
238: }
239:
240: ServletContext servletContext = getServletContext(session);
241: if (fallback == null) {
242: fallback = (ComponentManager) servletContext
243: .getAttribute(ComponentManager.COMPONENT_MANAGER_KEY);
244: }
245:
246: if (fallback != null) {
247: container = createComponentManager();
248: container.setFallback(fallback);
249: }
250:
251: ComponentConfiguration config = (ComponentConfiguration) servletContext
252: .getAttribute("ComponentConfiguration");
253: if (config != null) {
254: if (container == null) {
255: container = createComponentManager();
256: }
257:
258: config.configure(container, "request");
259: request.setAttribute(
260: ComponentManager.COMPONENT_MANAGER_KEY, container);
261: }
262: }
263:
264: /**
265: * Servlet 2.3 specifies that the servlet context can be retrieved from the session. Unfortunately, some versions of
266: * WebLogic can only retrieve the servlet context from the filter config. Hence, this method enables subclasses to
267: * retrieve the servlet context from other sources.
268: *
269: * @param session the HTTP session where, in Servlet 2.3, the servlet context can be retrieved
270: * @return the servlet context.
271: */
272: protected ServletContext getServletContext(HttpSession session) {
273: return filterConfig.getServletContext();
274: }
275:
276: protected void findStaticResource(String name,
277: HttpServletResponse response) throws IOException {
278: if (!name.endsWith(".class")) {
279: for (int i = 0; i < pathPrefixes.length; i++) {
280: InputStream is = findInputStream(name, pathPrefixes[i]);
281: if (is != null) {
282: // set the content-type header
283: String contentType = getContentType(name);
284: if (contentType != null) {
285: response.setContentType(contentType);
286: }
287:
288: // set heading information for caching static content
289: Calendar cal = Calendar.getInstance(TimeZone
290: .getTimeZone("GMT"));
291: response.setHeader("Date", df.format(cal.getTime())
292: + " GMT");
293: cal.add(Calendar.DAY_OF_MONTH, 1);
294: response.setHeader("Expires", df.format(cal
295: .getTime())
296: + " GMT");
297: response.setHeader("Retry-After", df.format(cal
298: .getTime())
299: + " GMT");
300: response.setHeader("Cache-Control", "public");
301: response.setHeader("Last-Modified", lastModified
302: + " GMT");
303:
304: try {
305: copy(is, response.getOutputStream());
306: } finally {
307: is.close();
308: }
309: return;
310: }
311: }
312: }
313:
314: response.sendError(HttpServletResponse.SC_NOT_FOUND);
315: }
316:
317: private String getContentType(String name) {
318: // NOT using the code provided activation.jar to avoid adding yet another dependency
319: // this is generally OK, since these are the main files we server up
320: if (name.endsWith(".js")) {
321: return "text/javascript";
322: } else if (name.endsWith(".css")) {
323: return "text/css";
324: } else if (name.endsWith(".html")) {
325: return "text/html";
326: } else if (name.endsWith(".txt")) {
327: return "text/plain";
328: } else if (name.endsWith(".gif")) {
329: return "image/gif";
330: } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
331: return "image/jpeg";
332: } else if (name.endsWith(".png")) {
333: return "image/png";
334: } else {
335: return null;
336: }
337: }
338:
339: protected void copy(InputStream input, OutputStream output)
340: throws IOException {
341: final byte[] buffer = new byte[4096];
342: int n;
343: while (-1 != (n = input.read(buffer))) {
344: output.write(buffer, 0, n);
345: }
346: // some app server eg WebSphere6 doesn't like it if didn't flush (WW-1384)
347: output.flush();
348: }
349:
350: protected InputStream findInputStream(String name,
351: String packagePrefix) throws IOException {
352: String resourcePath;
353: if (packagePrefix.endsWith("/") && name.startsWith("/")) {
354: resourcePath = packagePrefix + name.substring(1);
355: } else {
356: resourcePath = packagePrefix + name;
357: }
358:
359: String enc = (String) Configuration
360: .get(WebWorkConstants.WEBWORK_I18N_ENCODING);
361: resourcePath = URLDecoder.decode(resourcePath, enc);
362:
363: return ClassLoaderUtil.getResourceAsStream(resourcePath,
364: getClass());
365: }
366:
367: /**
368: * handle .. chars here and other URL hacks
369: */
370: protected boolean checkUrl(URL url, String rawResourcePath) {
371:
372: // ignore folder resources - they provide streams too ! dunno why :)
373: if (url.getPath().endsWith("/")) {
374: return false;
375: }
376:
377: // check for parent path access
378: // NOTE : most servlet containers shoudl resolve .. chars in the request url anyway
379: if (url.toExternalForm().indexOf(rawResourcePath) == -1) {
380: return false;
381: }
382:
383: return true;
384: }
385:
386: /**
387: * Returns a new <tt>DefaultComponentManager</tt> instance. This method is useful for developers wishing to subclass
388: * this class and provide a different implementation of <tt>DefaultComponentManager</tt>.
389: *
390: * @return a new <tt>DefaultComponentManager</tt> instance.
391: */
392: protected DefaultComponentManager createComponentManager() {
393: return new DefaultComponentManager();
394: }
395: }
|