Revision: 1319 http://stripes.svn.sourceforge.net/stripes/?rev=1319&view=rev Author: bengunter Date: 2010-11-10 18:55:09 +0000 (Wed, 10 Nov 2010)
Log Message: ----------- Applied fix for STS-678 from 1.5.x to trunk. Modified Paths: -------------- trunk/stripes/src/net/sourceforge/stripes/controller/DynamicMappingFilter.java Modified: trunk/stripes/src/net/sourceforge/stripes/controller/DynamicMappingFilter.java =================================================================== --- trunk/stripes/src/net/sourceforge/stripes/controller/DynamicMappingFilter.java 2010-11-10 18:10:34 UTC (rev 1318) +++ trunk/stripes/src/net/sourceforge/stripes/controller/DynamicMappingFilter.java 2010-11-10 18:55:09 UTC (rev 1319) @@ -19,19 +19,38 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; +import javax.servlet.RequestDispatcher; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.config.Configuration; @@ -39,29 +58,29 @@ import net.sourceforge.stripes.util.HttpUtil; import net.sourceforge.stripes.util.Log; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + /** * <p> * A servlet filter that dynamically maps URLs to {...@link ActionBean}s. This filter can be used to - * allow Stripes to dispatch requests to {...@link ActionBean}s based on their URL binding, even if - * the URL to which they are bound is not explicitly mapped in {...@code web.xml}. + * allow Stripes to dispatch requests to {...@link ActionBean}s based on their URL binding, even if the + * URL to which they are bound is not explicitly mapped in {...@code web.xml}. * </p> * <p> - * There are a few caveats that must be observed when using this filter: - * <ul> - * <li>{...@link StripesFilter} <em>MUST</em> be defined in {...@code web.xml} so that it will be - * loaded and initialized.</li> - * <li>This filter <em>MUST</em> be mapped to {...@code /*} to work correctly.</li> - * <li>This filter <em>MUST</em> be the last filter in the filter chain. When it dynamically maps - * an {...@link ActionBean} to a URL, the filter chain is interrupted.</li> - * </ul> + * One caveat must be observed when using this filter. This filter <em>MUST</em> be the last filter + * in the filter chain. When it dynamically maps an {...@link ActionBean} to a URL, the filter chain is + * interrupted. * </p> * <p> - * {...@link StripesFilter} and {...@link DispatcherServlet} need not be mapped to any URL patterns in - * {...@code web.xml} since this filter will determine at runtime whether or not they need to be - * invoked. In fact, you don't even need to define {...@link DispatcherServlet} in {...@code web.xml} at - * all because this filter uses an instance it creates and manages internally. However, some - * resources, such as JSPs, may require access to the Stripes {...@link Configuration}. Thus, - * {...@link StripesFilter} should be mapped to {...@code *.jsp} if you intend to access JSPs directly. + * {...@link StripesFilter} and/or {...@link DispatcherServlet} may be declared in {...@code web.xml}, but + * neither is required for this filter to work. If you choose not to declare {...@link StripesFilter} + * in {...@code web.xml}, then this filter should be configured the way you would normally configure + * {...@link StripesFilter}. However, some resources, such as JSPs, may require access to the Stripes + * {...@link Configuration} through {...@link StripesFilter}. If you intend to access JSPs directly, then + * {...@link StripesFilter} should be explicitly mapped to {...@code *.jsp}. * </p> * <p> * This filter takes the following approach to determining when to dispatch an {...@link ActionBean}: @@ -76,15 +95,15 @@ * </p> * <p> * One benefit of this approach is that static resources can be delivered from the same namespace to - * which an {...@link ActionBean} is mapped using clean URLs. (Form more information on clean URLs, see + * which an {...@link ActionBean} is mapped using clean URLs. (For more information on clean URLs, see * {...@link UrlBinding}.) For example, if your {...@code UserActionBean} is mapped to * {...@code @UrlBinding("/user/{id}/{$event}")} and you have a static file at {...@code /user/icon.gif}, * then your icon will be delivered correctly because the initial request will not have returned a * {...@code 404} error. * </p> * <p> - * This filter accepts one init-param. {...@code IncludeBufferSize} (optional, default 1024) sets the - * number of characters to be buffered by {...@link TempBufferWriter} for include requests. See + * The {...@code IncludeBufferSize} initialization parameter (optional, default 1024) sets the number + * of characters to be buffered by {...@link TempBufferWriter} for include requests. See * {...@link TempBufferWriter} for more information. * <p> * This is the suggested mapping for this filter in {...@code web.xml}. @@ -98,6 +117,10 @@ * <filter-class> * net.sourceforge.stripes.controller.DynamicMappingFilter * </filter-class> + * <init-param> + * <param-name>ActionResolver.Packages</param-name> + * <param-value>com.yourcompany.stripes.action</param-value> + * </init-param> * </filter> * * <filter-mapping> @@ -279,36 +302,48 @@ * The name of the init-param that can be used to set the size of the buffer used by * {...@link TempBufferWriter} before it overflows. */ - private static final String INCLUDE_BUFFER_SIZE_PARAM = "IncludeBufferSize"; + public static final String INCLUDE_BUFFER_SIZE_PARAM = "IncludeBufferSize"; + /** + * The attribute name used to store a reference to {...@link StripesFilter} in the servlet context. + */ + public static final String CONTEXT_KEY_STRIPES_FILTER = StripesFilter.class.getName(); + + /** + * Request header that indicates that the current request is part of the process of trying to + * force initialization of {...@link StripesFilter}. If this header is present then + * {...@link DynamicMappingFilter} makes no attempt to map the request to an {...@link ActionBean}. + */ + private static final String REQ_HEADER_INIT_FLAG = "X-Dynamic-Mapping-Filter-Init"; + /** The size of the buffer used by {...@link TempBufferWriter} before it overflows. */ private static int includeBufferSize = 1024; /** Logger */ private static Log log = Log.getInstance(DynamicMappingFilter.class); - private boolean initialized = false; + private FilterConfig filterConfig; private ServletContext servletContext; private StripesFilter stripesFilter; private DispatcherServlet stripesDispatcher; + private boolean stripesFilterIsInternal, initializing; public void init(final FilterConfig config) throws ServletException { try { - includeBufferSize = Integer.valueOf(config.getInitParameter(INCLUDE_BUFFER_SIZE_PARAM) - .trim()); - log.info(DynamicMappingFilter.class.getName(), " include buffer size is ", - includeBufferSize); + String value = config.getInitParameter(INCLUDE_BUFFER_SIZE_PARAM); + if (value != null) { + includeBufferSize = Integer.valueOf(value.trim()); + log.info(getClass().getSimpleName(), " include buffer size is ", includeBufferSize); + } } - catch (NullPointerException e) { - // ignore it - } catch (Exception e) { log.warn(e, "Could not interpret '", config.getInitParameter(INCLUDE_BUFFER_SIZE_PARAM), "' as a number for init-param '", INCLUDE_BUFFER_SIZE_PARAM, - "'. Using default value of ", includeBufferSize, "."); + "'. Using default value ", includeBufferSize, "."); } + this.filterConfig = config; this.servletContext = config.getServletContext(); this.stripesDispatcher = new DispatcherServlet(); this.stripesDispatcher.init(new ServletConfig() { @@ -331,15 +366,25 @@ } public void destroy() { - stripesDispatcher.destroy(); + try { + if (stripesDispatcher != null) + stripesDispatcher.destroy(); + } + finally { + stripesDispatcher = null; + + try { + if (stripesFilterIsInternal && stripesFilter != null) + stripesFilter.destroy(); + } + finally { + stripesFilter = null; + } + } } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - // Initialize (only once) - if (!initialized) - doOneTimeConfiguration(); - // Wrap the response in a wrapper that catches errors (but not exceptions) final ErrorTrappingResponseWrapper wrapper = new ErrorTrappingResponseWrapper( (HttpServletResponse) response); @@ -349,22 +394,34 @@ boolean fileNotFoundExceptionThrown = false; try { - chain.doFilter(request, wrapper); + chain.doFilter(request, wrapper); } catch (FileNotFoundException exc) { - fileNotFoundExceptionThrown = true; + fileNotFoundExceptionThrown = true; } + // Check the instance field as well as request header for initialization request + boolean initializing = this.initializing + || ((HttpServletRequest) request).getHeader(REQ_HEADER_INIT_FLAG) != null; + // If a FileNotFoundException or SC_NOT_FOUND error occurred, then try to match an ActionBean to the URL Integer errorCode = wrapper.getErrorCode(); - if ((errorCode != null && errorCode == HttpServletResponse.SC_NOT_FOUND) || fileNotFoundExceptionThrown) { - stripesFilter.doFilter(request, response, new FilterChain() { + if (!initializing && (errorCode != null && errorCode == HttpServletResponse.SC_NOT_FOUND) + || fileNotFoundExceptionThrown) { + // Get a reference to a StripesFilter instance + StripesFilter sf = getStripesFilter(); + if (sf == null) { + initStripesFilter((HttpServletRequest) request, wrapper); + sf = getStripesFilter(); + } + + sf.doFilter(request, response, new FilterChain() { public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // Look for an ActionBean that is mapped to the URI String uri = HttpUtil.getRequestedPath((HttpServletRequest) request); - Class<? extends ActionBean> beanType = stripesFilter.getInstanceConfiguration() - .getActionResolver().getActionBeanType(uri); + Class<? extends ActionBean> beanType = getStripesFilter() + .getInstanceConfiguration().getActionResolver().getActionBeanType(uri); // If found then call the dispatcher directly. Otherwise, send the error. if (beanType == null) { @@ -382,16 +439,354 @@ } /** - * Perform initialization that can't be done in {...@code init(..)}. This is normally called only - * once, on the first invocation of {...@code doFilter(..)}. + * Get a reference to {...@link StripesFilter}. The first time this method is called, the reference + * will be looked up in the servlet context and cached in the {...@link #stripesFilter} field. */ - protected void doOneTimeConfiguration() throws ServletException { - stripesFilter = (StripesFilter) servletContext.getAttribute(StripesFilter.class.getName()); + protected StripesFilter getStripesFilter() { if (stripesFilter == null) { - throw new StripesServletException("Could not get a reference to StripesFilter from " - + "the servlet context. The dynamic mapping filter works in conjunction with " - + "StripesFilter and requires that it be defined in web.xml"); + stripesFilter = (StripesFilter) servletContext.getAttribute(CONTEXT_KEY_STRIPES_FILTER); + if (stripesFilter != null) { + log.debug("Found StripesFilter in the servlet context."); + } } - initialized = true; + + return stripesFilter; } + + /** + * The servlet spec allows a container to wait until a filter is required to process a request + * before it initializes the filter. Since we need to get a reference to {...@link StripesFilter} + * from the servlet context, we really need {...@link StripesFilter} to have been initialized at + * the time we process our first request. If that didn't happen automatically, this method does + * its best to force it to happen. + * + * @param request The current request + * @param response The current response + * @throws ServletException If anything goes wrong that simply can't be ignored. + */ + protected synchronized void initStripesFilter(HttpServletRequest request, + HttpServletResponse response) throws ServletException { + try { + log.info("StripesFilter not initialized. Checking the situation in web.xml ..."); + Document document = parseWebXml(); + NodeList filterNodes = eval("/web-app/filter/filter-class[text()='" + + StripesFilter.class.getName() + "']/..", document, XPathConstants.NODESET); + if (filterNodes == null || filterNodes.getLength() != 1) { + String msg; + if (filterNodes.getLength() < 1) { + msg = "StripesFilter is not declared in web.xml. "; + } + else { + msg = "StripesFilter is declared multiple times in web.xml; refusing to use either one. "; + } + + log.info(msg, "Initializing with \"", filterConfig.getFilterName(), + "\" configuration."); + createStripesFilter(filterConfig); + } + else { + Node filterNode = filterNodes.item(0); + final String name = eval("filter-name", filterNode, XPathConstants.STRING); + log.debug("Found StripesFilter declared as ", name, " in web.xml"); + + List<String> patterns = getFilterUrlPatterns(filterNode); + if (patterns.isEmpty()) { + log.info("StripesFilter is declared but not mapped in web.xml. ", + "Initializing with \"", name, "\" configuration from web.xml."); + + final Map<String, String> parameters = getFilterParameters(filterNode); + createStripesFilter(new FilterConfig() { + public ServletContext getServletContext() { + return servletContext; + } + + public Enumeration<String> getInitParameterNames() { + return Collections.enumeration(parameters.keySet()); + } + + public String getInitParameter(String name) { + return parameters.get(name); + } + + public String getFilterName() { + return name; + } + }); + } + else { + issueRequests(patterns, request, response); + } + } + } + catch (RuntimeException e) { + throw e; + } + catch (Exception e) { + throw new StripesServletException( + "Unhandled exception trying to force initialization of StripesFilter", e); + } + + // Blow up if no StripesFilter instance could be acquired or created + if (getStripesFilter() == null) { + String msg = "There is no StripesFilter instance available in the servlet context, " + + "and DynamicMappingFilter was unable to initialize one. See previous log " + + "messages for more information."; + log.error(msg); + throw new StripesServletException(msg); + } + } + + /** + * Parse the application's {...@code web.xml} file and return a DOM {...@link Document}. + * + * @throws ParserConfigurationException If thrown by the XML parser + * @throws IOException If thrown by the XML parser + * @throws SAXException If thrown by the XML parser + */ + protected Document parseWebXml() throws SAXException, IOException, ParserConfigurationException { + return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( + servletContext.getResourceAsStream("/WEB-INF/web.xml")); + } + + /** + * Evaluate an xpath expression against a DOM {...@link Node} and return the result. + * + * @param expression The expression to evaluate + * @param source The node against which the expression will be evaluated + * @param returnType One of the constants defined in {...@link XPathConstants} + * @return The result returned by {...@link XPath#evaluate(String, Object, QName)} + * @throws XPathExpressionException If the xpath expression is invalid + */ + @SuppressWarnings("unchecked") + protected <T> T eval(String expression, Node source, QName returnType) + throws XPathExpressionException { + XPath xpath = XPathFactory.newInstance().newXPath(); + return (T) xpath.evaluate(expression, source, returnType); + } + + /** + * Get all the URL patterns to which a filter is mapped in {...@code web.xml}. This includes direct + * mappings using {...@code filter-mapping/url-pattern} and indirect mappings using + * {...@code filter-mapping/servlet-name} and {...@code servlet-mapping/url-pattern}. + * + * @param filterNode The DOM ({...@code <filter>}) {...@link Node} containing the filter + * declaration from {...@code web.xml} + * @return A list of all the patterns to which the filter is mapped + * @throws XPathExpressionException In case of failure evaluating an xpath expression + */ + protected List<String> getFilterUrlPatterns(Node filterNode) throws XPathExpressionException { + String filterName = eval("filter-name", filterNode, XPathConstants.STRING); + Document document = filterNode.getOwnerDocument(); + + NodeList urlMappings = eval("/web-app/filter-mapping/filter-name[text()='" + filterName + + "']/../url-pattern", document, XPathConstants.NODESET); + NodeList servletMappings = eval("/web-app/filter-mapping/filter-name[text()='" + filterName + + "']/../servlet-name", document, XPathConstants.NODESET); + + List<String> patterns = new ArrayList<String>(); + if (urlMappings != null && urlMappings.getLength() > 0) { + for (int i = 0; i < urlMappings.getLength(); i++) { + patterns.add(urlMappings.item(i).getTextContent().trim()); + } + } + + if (servletMappings != null && servletMappings.getLength() > 0) { + for (int i = 0; i < servletMappings.getLength(); i++) { + String servletName = servletMappings.item(i).getTextContent().trim(); + urlMappings = eval("/web-app/servlet-mapping/servlet-name[text()='" + servletName + + "']/../url-pattern", document, XPathConstants.NODESET); + for (int j = 0; j < urlMappings.getLength(); j++) { + patterns.add(urlMappings.item(j).getTextContent().trim()); + } + } + } + + log.debug("Filter ", filterName, " maps to ", patterns); + return patterns; + } + + /** + * Get the initialization parameters for a filter declared in {...@code web.xml}. + * + * @param filterNode The DOM ({...@code <filter>}) {...@link Node} containing the filter + * declaration from {...@code web.xml} + * @return A map of parameter names to parameter values + * @throws XPathExpressionException In case of failure evaluation an xpath expression + */ + protected Map<String, String> getFilterParameters(Node filterNode) + throws XPathExpressionException { + Map<String, String> params = new LinkedHashMap<String, String>(); + NodeList paramNodes = eval("init-param", filterNode, XPathConstants.NODESET); + for (int i = 0; i < paramNodes.getLength(); i++) { + Node node = paramNodes.item(i); + String key = eval("param-name", node, XPathConstants.STRING); + String value = eval("param-value", node, XPathConstants.STRING); + params.put(key, value); + } + return params; + } + + /** + * Create and initialize an instance of {...@link StripesFilter} with the given configuration. + * + * @param config The filter configuration + * @throws ServletException If initialization of the filter fails + */ + protected void createStripesFilter(FilterConfig config) throws ServletException { + StripesFilter filter = new StripesFilter(); + filter.init(config); + this.stripesFilter = filter; + this.stripesFilterIsInternal = true; + } + + /** + * Issue a series of requests in an attempt to force an invocation (and initialization) of + * {...@link StripesFilter} in the application context. All patterns will be requested first with + * an internal forward, then an include and finally with a brand new request to the address and + * port returned by {...@link HttpServletRequest#getLocalAddr()} and + * {...@link HttpServletRequest#getLocalPort()}, respectively. + * + * @param patterns The list of patterns to request, as specified by {...@code url-pattern} elements + * in {...@code web.xml} + * @param request The current request, required to process a forward or include + * @param response The current response, required to process a forward or include + */ + protected void issueRequests(List<String> patterns, HttpServletRequest request, + HttpServletResponse response) { + // Replace globs in the patterns with a random string + String random = "stripes-dmf-request-" + UUID.randomUUID(); + List<String> uris = new ArrayList<String>(patterns.size()); + for (String pattern : patterns) { + String uri = pattern.replace("*", random); + if (!uri.startsWith("/")) + uri = "/" + uri; + uris.add(uri); + } + + // Set the HTTP method to something generally harmless + HttpServletRequestWrapper req = new HttpServletRequestWrapper(request) { + @Override + public String getMethod() { + return "OPTIONS"; + } + }; + + // Response swallows all output + HttpServletResponseWrapper rsp = new HttpServletResponseWrapper(response) { + @Override + public ServletOutputStream getOutputStream() throws IOException { + return new ServletOutputStream() { + @Override + public void write(int b) throws IOException { + // No output + } + }; + } + + @Override + public PrintWriter getWriter() throws IOException { + return new PrintWriter(getOutputStream()); + } + }; + + // Try forward first + log.info("Found StripesFilter declared and mapped in web.xml but not yet initialized."); + Iterator<String> iterator = uris.iterator(); + while (getStripesFilter() == null && iterator.hasNext()) { + String uri = iterator.next(); + log.info("Try to force initialization of StripesFilter with forward to ", uri); + try { + initializing = true; + RequestDispatcher dispatcher = servletContext.getRequestDispatcher(uri); + dispatcher.forward(req, rsp); + } + catch (Exception e) { + log.debug(e, "Ignored exception during forward"); + } + finally { + initializing = false; + response.reset(); + } + } + + // If forward failed, try include + iterator = uris.iterator(); + while (getStripesFilter() == null && iterator.hasNext()) { + String uri = iterator.next(); + log.info("Try to force initialization of StripesFilter with include of ", uri); + try { + initializing = true; + RequestDispatcher dispatcher = servletContext.getRequestDispatcher(uri); + dispatcher.forward(req, rsp); + } + catch (Exception e) { + log.debug(e, "Ignored exception during forward"); + } + finally { + initializing = false; + response.reset(); + } + } + + // If both forward and include failed, then do something truly abominable ... + iterator = uris.iterator(); + while (getStripesFilter() == null && iterator.hasNext()) { + try { + String uri = iterator.next(); + log.info("Try to force initialization of StripesFilter with request to ", uri); + requestRemotely(request, uri); + } + catch (Exception e) { + log.debug(e, "Ignored exception during request"); + } + } + } + + /** + * Issue a new request to a path relative to the request's context. The connection is made to + * the address and port returned by {...@link HttpServletRequest#getLocalAddr()} and + * {...@link HttpServletRequest#getLocalPort()}, respectively. + * + * @param request The current request + * @param relativePath The context-relative path to request + */ + @SuppressWarnings("unchecked") + public void requestRemotely(HttpServletRequest request, String relativePath) { + HttpURLConnection cxn = null; + try { + // Create a new URL using the current request's protocol, port and context + String protocol = new URL(request.getRequestURL().toString()).getProtocol(); + String file = request.getContextPath() + relativePath; + URL url = new URL(protocol, request.getLocalAddr(), request.getLocalPort(), file); + cxn = (HttpURLConnection) url.openConnection(); + + // Set the HTTP method to something generally harmless + cxn.setRequestMethod("OPTIONS"); + + // Copy all the request headers to the new request + Enumeration<String> headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String hdr = headerNames.nextElement(); + cxn.setRequestProperty(hdr, request.getHeader(hdr)); + } + + // Set a flag to let DMF know not to process the request + cxn.setRequestProperty(REQ_HEADER_INIT_FLAG, "true"); + + // Log the HTTP status + log.debug(cxn.getResponseCode(), " ", cxn.getResponseMessage(), " (", cxn + .getContentLength(), " bytes) from ", url); + } + catch (Exception e) { + log.debug(e, "Request failed trying to force initialization of StripesFilter"); + } + finally { + try { + cxn.disconnect(); + } + catch (Exception e) { + // Ignore + } + } + } } This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. ------------------------------------------------------------------------------ The Next 800 Companies to Lead America's Growth: New Video Whitepaper David G. Thomson, author of the best-selling book "Blueprint to a Billion" shares his insights and actions to help propel your business during the next growth cycle. Listen Now! http://p.sf.net/sfu/SAP-dev2dev _______________________________________________ Stripes-development mailing list Stripes-development@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/stripes-development