/* vim: set ts=2 et sw=2 cindent fo=qroca: */ package com.globant.katari.core.web; import java.io.IOException; import java.util.LinkedList; import java.util.Iterator; import java.util.Enumeration; import java.util.List; import java.util.Set; import java.util.HashSet; import java.util.Collections; import java.util.Comparator; import javax.servlet.ServletException; import javax.servlet.Filter; import javax.servlet.FilterConfig; import javax.servlet.FilterChain; import javax.servlet.ServletContext; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.Validate; import org.apache.commons.collections.IteratorUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** This class chains the filters provided by each module. * * Each module provides a regular expression that must match the request path, * a filter with its configuration and a priority. All filters are executed in * order of ascending priority (lowest numbers go first), for every matching * request path. * * The regular expression is checked against the servlet path and path info., * including the fragment where each module is mapped. For example, if the * request is made to http://server:port/module/user/remove/id/1, the string * /module/user/remove/id/1 is matched against each regex. */ public final class ModuleFilterProxy implements Filter { /** The serialization version number. * * This number must change every time a new serialization incompatible change * is introduced in the class. */ private static final long serialVersionUID = 20080226; /** The class logger. */ private static Logger log = LoggerFactory.getLogger(ModuleFilterProxy.class); /** A list of module names to module configuration. * * A module configuration is simply another map that maps a url fragment to a * servlet plus its configuration. It is never null. */ private List<FilterMapping> filterMaps = new LinkedList<FilterMapping>(); /** Builds a ModuleFilterProxy with no registered filters. */ public ModuleFilterProxy() { } /** Builds a ModuleFilterProxy with an initial list of registered filters. * * @param initialFilters The list of initial filters. It cannot be null. */ public ModuleFilterProxy(final List<FilterMapping> initialFilters) { Validate.notNull(initialFilters, "The list of initial filters cannot be" + " null"); filterMaps.addAll(initialFilters); sort(); } /** Adds a list of filters to the chain. * * Each module can have a list of filters that must be called before each * request. This operation is inteded for modules to add a list of filters to * the chain of module filters. * * @param additionalFilterMappings The list of filter mappings provided by a * module. It cannot be null. */ public void addFilters(final List<FilterMapping> additionalFilterMappings) { Validate.notNull(additionalFilterMappings, "The filter mappings cannot be" + " null"); filterMaps.addAll(additionalFilterMappings); sort(); } /** Comparator for filter priority, used in sort(). */ private static class PriorityComparator implements Comparator<FilterMapping> { /** {@inheritDoc} * * Lower priority comes first. */ public int compare(final FilterMapping o1, final FilterMapping o2) { return o1.getPriority() - o2.getPriority(); } } /** Sorts the list of filters according to their priority. */ private void sort() { // Sort the list of filters. Collections.sort(filterMaps, new PriorityComparator()); } /** Called by the servlet container to indicate to a servlet that it is being * placed into service. * * It calls init on all the registered filters. * * @param filterConfig The servlet's configuration and initialization * parameters. This object is created by the container. * * @throws ServletException if an unexpected exception occurs. * * TODO Validate that when a filter has been already initialized, the * parameters has not changed. This is to avoid having two * FilterAndParameters instance with the same filter and different * parameters. In such case, the filter will be initialized only once with an * undefined set of parameters. */ public void init(final FilterConfig filterConfig) throws ServletException { log.trace("Entering init"); Set<Filter> alreadyInitialized = new HashSet<Filter>(); for (final FilterMapping filterMapping : filterMaps) { if (!alreadyInitialized.contains(filterMapping.getFilter())) { // A servlet context wrapper that returns the init parameters of this // configured filter. This context is used in the filter config created // for the filter. final ServletContext context = new ServletContextWrapper(filterConfig.getServletContext()) { public String getInitParameter(final String name) { return filterMapping.getParameters().get(name); } @SuppressWarnings("unchecked") public Enumeration getInitParameterNames() { return IteratorUtils.asEnumeration( filterMapping.getParameters().values().iterator()); } }; FilterConfig config = new FilterConfig() { public String getFilterName() { return filterConfig.getFilterName(); } public ServletContext getServletContext() { return context; } public String getInitParameter(final String name) { return filterMapping.getParameters().get(name); } @SuppressWarnings("unchecked") public Enumeration getInitParameterNames() { return IteratorUtils.asEnumeration( filterMapping.getParameters().values().iterator()); } }; filterMapping.getFilter().init(config); alreadyInitialized.add(filterMapping.getFilter()); } } log.trace("Leaving init"); } /** Called by the servlet container to allow the servlet to respond to a * request. * * This gives a chance to every filter to filter the request. If a filter * does not call chain.doFilter, the request processing is stopped, including * further filter processing. * * @param request The ServletRequest object that contains the client's * request. * * @param response The ServletResponse object that contains the servlet's * response * * @param filterChain allows the Filter to pass on the request and response * to the next entity in the chain. * * @throws IOException if an input or output exception occurs. * * @throws ServletException if some other error occurs. */ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain filterChain) throws ServletException, IOException { log.trace("Entering doFilter"); Chain chain = new Chain(filterMaps, filterChain); chain.doFilter(request, response); log.trace("Leaving doFilter"); } /** Called by the web container to indicate to a filter that it is being * taken out of service. * * It calls destroy on all the registered filters. */ public void destroy() { log.trace("Entering destroy"); Set<Filter> alreadyDestroyed = new HashSet<Filter>(); for (FilterMapping mapping : filterMaps) { if (!alreadyDestroyed.contains(mapping.getFilter())) { mapping.getFilter().destroy(); alreadyDestroyed.add(mapping.getFilter()); } } log.trace("Leaving destroy"); } /** FilterChain implementation that keeps an iterator on the list of filters * and to call the next filter in the chain. */ private static class Chain implements FilterChain { /** The class logger. */ private static Logger log = LoggerFactory.getLogger(Chain.class); /** The list of filter that filters the requests. * * It is never null. */ private List<FilterMapping> filterMappings; /** The tail of the chain. * * It is never null. */ private FilterChain tailChain; /** The current filter in the chain. * * It is null until the iteration begins. The first call to doFilter * initializes this iterator. It iterates on elements of filters. */ private Iterator<FilterMapping> current = null; /** Creates a new chain. * * @param theFilterMappings The list of filters to be ran over the * request/response. * * @param theTailChain The original servlet chain that is logically * appended at the end of the filter chain. It cannot be null. */ public Chain(final List<FilterMapping> theFilterMappings, final FilterChain theTailChain) { log.trace("Entering Chain"); Validate.notNull(theFilterMappings, "The filter mappings cannot be null"); Validate.notNull(theTailChain, "The tail chain cannot be null"); filterMappings = theFilterMappings; tailChain = theTailChain; log.trace("Leaving Chain"); } /** Causes the next filter in the chain to be invoked, or if the calling * filter is the last filter in the chain, causes the resource at the end * of the chain to be invoked. * * @param request The ServletRequest object that contains the client's * request. It cannot be null. * * @param response The ServletResponse object that contains the servlet's * response * * @throws IOException if an input or output exception occurs. * * @throws ServletException if an error occurs. */ public void doFilter(final ServletRequest request, final ServletResponse response) throws java.io.IOException, ServletException { log.trace("Entering doFilter"); Validate.notNull(request, "The request cannot be null"); if (!(request instanceof HttpServletRequest)) { throw new RuntimeException("Calling doFilter on an non http request"); } HttpServletRequest httpRequest = (HttpServletRequest) request; if (current == null) { current = filterMappings.iterator(); } if (current.hasNext()) { FilterMapping filterMapping = current.next(); String url = httpRequest.getServletPath() + httpRequest.getPathInfo(); if (url.matches(filterMapping.getPattern())) { // Call the filter. filterMapping.getFilter().doFilter(request, response, this); } else { // Skip current filter. The recursive implementation is easier to // understand. doFilter(request, response); } } else { tailChain.doFilter(request, response); } log.trace("Leaving doFilter"); } } }