/* Copyright 2005-2006 Tim Fennell * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.sourceforge.stripes.controller; import net.sourceforge.stripes.config.BootstrapPropertyResolver; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.config.RuntimeConfiguration; import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.exception.StripesServletException; import net.sourceforge.stripes.util.HttpUtil; import net.sourceforge.stripes.util.Log; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.beans.Introspector; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Set; /** * The Stripes filter is used to ensure that all requests coming to a Stripes application * are handled in the same way. It detects and wraps any requests that contain multipart/form * data, so that they may be treated much like any other request. Also ensures that * all downstream components have access to essential configuration and services whether * the request goes through the dispatcher, or straight to a JSP. * * @author Tim Fennell */ public class StripesFilter implements Filter { /** Key used to lookup the name of the Configuration class used to configure Stripes. */ public static final String CONFIG_CLASS = "Configuration.Class"; /** Log used throughout the class. */ private static final Log log = Log.getInstance(StripesFilter.class); /** The configuration instance for Stripes. */ private static Configuration configuration; /** The servlet context */ private ServletContext servletContext; /** * Some operations should only be done if the current invocation of * {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} is the * first in the filter chain. This {@link ThreadLocal} keeps track of * whether such operations should be done or not. */ private static final ThreadLocal<Boolean> initialInvocation = new ThreadLocal<Boolean>() { @Override protected Boolean initialValue() { return true; } }; /** * Performs the necessary initialization for the StripesFilter. Mainly this involves deciding * what configuration class to use, and then instantiating and initializing the chosen * Configuration. * * @throws ServletException thrown if a problem is encountered initializing Stripes */ public void init(FilterConfig filterConfig) throws ServletException { configuration = createConfiguration(filterConfig); this.servletContext = filterConfig.getServletContext(); this.servletContext.setAttribute(StripesFilter.class.getName(), this); Package pkg = getClass().getPackage(); log.info("Stripes Initialization Complete. Version: ", pkg.getSpecificationVersion(), ", Build: ", pkg.getImplementationVersion()); } /** * Create and configure a new {@link Configuration} instance using the suppied * {@link FilterConfig}. * * @param filterConfig The filter configuration supplied by the container. * @return The new configuration instance. * @throws ServletException If the configuration cannot be created. */ protected static Configuration createConfiguration(FilterConfig filterConfig) throws ServletException { BootstrapPropertyResolver bootstrap = new BootstrapPropertyResolver(filterConfig); // Set up the Configuration - if one isn't found by the bootstrapper then // we'll just use the default: RuntimeConfiguration Class<? extends Configuration> clazz = bootstrap.getClassProperty(CONFIG_CLASS, Configuration.class); if (clazz == null) clazz = RuntimeConfiguration.class; try { Configuration configuration = clazz.newInstance(); configuration.setBootstrapPropertyResolver(bootstrap); configuration.init(); return configuration; } catch (Exception e) { log.fatal(e, "Could not instantiate specified Configuration. Class name specified was ", "[", clazz.getName(), "]."); throw new StripesServletException("Could not instantiate specified Configuration. " + "Class name specified was [" + clazz.getName() + "].", e); } } /** * Returns the Configuration for the webapp. */ public static Configuration getConfiguration() { return configuration; } /** * Returns the configuration for this instance of the StripesFilter for any class * that has a reference to the filter. For normal runtime access to the configuration * during a request cycle, call getConfiguration() instead. * * @return the Configuration of this instance of the StripesFilter */ public Configuration getInstanceConfiguration() { return configuration; } /** * Performs the primary work of the filter, including constructing a StripesRequestWrapper to * wrap the HttpServletRequest, and using the configured LocalePicker to decide which * Locale will be used to process the request. */ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; // check the flag that indicates if this is the initial invocation boolean initial = initialInvocation.get(); if (initial) { initialInvocation.set(false); } // Wrap pretty much everything in a try/catch so that we can funnel even the most // bizarre or unexpected exceptions into the exception handler try { log.trace("Intercepting request to URL: ", HttpUtil.getRequestedPath(httpRequest)); if (initial) { // Figure out the locale and character encoding to use. The ordering of things here // is very important!! We pick the locale first since picking the encoding is // locale dependent, but the encoding *must* be set on the request before any // parameters or parts are accessed, and wrapping the request accesses stuff. Locale locale = configuration.getLocalePicker().pickLocale(httpRequest); log.debug("LocalePicker selected locale: ", locale); String encoding = configuration.getLocalePicker().pickCharacterEncoding( httpRequest, locale); if (encoding != null) { httpRequest.setCharacterEncoding(encoding); log.debug("LocalePicker selected character encoding: ", encoding); } else { log.debug("LocalePicker did not pick a character encoding, using default: ", httpRequest.getCharacterEncoding()); } StripesRequestWrapper request = wrapRequest(httpRequest); request.setLocale(locale); httpResponse.setLocale(locale); if (encoding != null) { httpResponse.setCharacterEncoding(encoding); } httpRequest = request; } else { // process URI parameters on subsequent invocations StripesRequestWrapper.findStripesWrapper(httpRequest).pushUriParameters( (HttpServletRequestWrapper) httpRequest); } // Execute the rest of the chain flashInbound(httpRequest); filterChain.doFilter(httpRequest, servletResponse); } catch (Throwable t) { this.configuration.getExceptionHandler().handle(t, httpRequest, httpResponse); } finally { // reset the flag that indicates if this is the initial invocation if (initial) { // Once the request is processed, clean up thread locals StripesFilter.initialInvocation.remove(); flashOutbound(httpRequest); } else { // restore URI parameters to their previous state StripesRequestWrapper.findStripesWrapper(httpRequest).popUriParameters(); } } } /** * Wraps the HttpServletRequest with a StripesServletRequest. This is done to ensure that any * form posts that contain file uploads get handled appropriately. * * @param servletRequest the HttpServletRequest handed to the dispatcher by the container * @return an instance of StripesRequestWrapper, which is an HttpServletRequestWrapper * @throws StripesServletException if the wrapper cannot be constructed */ protected StripesRequestWrapper wrapRequest(HttpServletRequest servletRequest) throws StripesServletException { try { return StripesRequestWrapper.findStripesWrapper(servletRequest); } catch (IllegalStateException e) { return new StripesRequestWrapper(servletRequest); } } /** * <p>Checks to see if there is a flash scope identified by a parameter to the current * request, and if there is, retrieves items from the flash scope and moves them * back to request attributes.</p> */ protected void flashInbound(HttpServletRequest req) { // Copy the attributes from the previous flash scope to the new request FlashScope flash = FlashScope.getPrevious(req); if (flash != null) { flash.beginRequest(req); } } /** * Manages the work that ensures that flash scopes get cleaned up properly when * requests go missing. Firstly timestamps the current flash scope (if one exists) * to record the time that the request exited the container. Then checks all * flash scopes to make sure none have been hanging out for more than a minute. */ protected void flashOutbound(HttpServletRequest req) { // Start the timer on the current flash scope FlashScope flash = FlashScope.getCurrent(req, false); if (flash != null) { flash.completeRequest(); } } /** Calls the cleanup() method on the log to release resources held by commons logging. */ public void destroy() { this.servletContext.removeAttribute(StripesFilter.class.getName()); Log.cleanup(); Introspector.flushCaches(); // Not 100% sure this is necessary, but it doesn't hurt } }