View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.acegisecurity.util;
17  
18  import org.acegisecurity.ConfigAttribute;
19  import org.acegisecurity.ConfigAttributeDefinition;
20  
21  import org.acegisecurity.intercept.web.FilterInvocation;
22  import org.acegisecurity.intercept.web.FilterInvocationDefinitionSource;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  
27  import org.springframework.beans.BeansException;
28  import org.springframework.beans.factory.InitializingBean;
29  
30  import org.springframework.context.ApplicationContext;
31  import org.springframework.context.ApplicationContextAware;
32  
33  import org.springframework.util.Assert;
34  
35  import java.io.IOException;
36  
37  import java.util.Iterator;
38  import java.util.LinkedHashSet;
39  import java.util.List;
40  import java.util.Set;
41  import java.util.Vector;
42  
43  import javax.servlet.Filter;
44  import javax.servlet.FilterChain;
45  import javax.servlet.FilterConfig;
46  import javax.servlet.ServletException;
47  import javax.servlet.ServletRequest;
48  import javax.servlet.ServletResponse;
49  
50  
51  /**
52   * Delegates <code>Filter</code> requests to a list of Spring-managed beans.<p>The <code>FilterChainProxy</code> is
53   * loaded via a standard {@link org.acegisecurity.util.FilterToBeanProxy} declaration in <code>web.xml</code>.
54   * <code>FilterChainProxy</code> will then pass {@link #init(FilterConfig)}, {@link #destroy()} and {@link
55   * #doFilter(ServletRequest, ServletResponse, FilterChain)} invocations through to each <code>Filter</code> defined
56   * against <code>FilterChainProxy</code>.</p>
57   *  <p><code>FilterChainProxy</code> is configured using a standard {@link
58   * org.acegisecurity.intercept.web.FilterInvocationDefinitionSource}. Each possible URI pattern that
59   * <code>FilterChainProxy</code> should service must be entered. The first matching URI pattern located by
60   * <code>FilterInvocationDefinitionSource</code> for a given request will be used to define all of the
61   * <code>Filter</code>s that apply to that request. NB: This means you must put most specific URI patterns at the top
62   * of the list, and ensure all <code>Filter</code>s that should apply for a given URI pattern are entered against the
63   * respective entry. The <code>FilterChainProxy</code> will not iterate the remainder of the URI patterns to locate
64   * additional <code>Filter</code>s.  The <code>FilterInvocationDefinitionSource</code> described the applicable URI
65   * pattern to fire the filter chain, followed by a list of configuration attributes. Each configuration attribute's
66   * {@link org.acegisecurity.ConfigAttribute#getAttribute()} corresponds to a bean name that is available from the
67   * application context.</p>
68   *  <p><code>FilterChainProxy</code> respects normal handling of <code>Filter</code>s that elect not to call {@link
69   * javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse,
70   * javax.servlet.FilterChain)}, in that the remainder of the origial or <code>FilterChainProxy</code>-declared filter
71   * chain will not be called.</p>
72   *  <p>It is particularly noted the <code>Filter</code> lifecycle mismatch between the servlet container and IoC
73   * container. As per {@link org.acegisecurity.util.FilterToBeanProxy} JavaDocs, we recommend you allow the IoC
74   * container to manage lifecycle instead of the servlet container. By default the <code>FilterToBeanProxy</code> will
75   * never call this class' {@link #init(FilterConfig)} and {@link #destroy()} methods, meaning each of the filters
76   * defined against <code>FilterInvocationDefinitionSource</code> will not be called. If you do need your filters to be
77   * initialized and destroyed, please set the <code>lifecycle</code> initialization parameter against the
78   * <code>FilterToBeanProxy</code> to specify servlet container lifecycle management.</p>
79   *  <p>If a filter name of {@link #TOKEN_NONE} is used, this allows specification of a filter pattern which should
80   * never cause any filters to fire.</p>
81   *
82   * @author Carlos Sanchez
83   * @author Ben Alex
84   * @version $Id: FilterChainProxy.java 1864 2007-05-25 02:03:12Z benalex $
85   */
86  public class FilterChainProxy implements Filter, InitializingBean, ApplicationContextAware {
87      //~ Static fields/initializers =====================================================================================
88  
89      private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
90      public static final String TOKEN_NONE = "#NONE#";
91  
92      //~ Instance fields ================================================================================================
93  
94      private ApplicationContext applicationContext;
95      private FilterInvocationDefinitionSource filterInvocationDefinitionSource;
96  
97      //~ Methods ========================================================================================================
98  
99      public void afterPropertiesSet() throws Exception {
100         Assert.notNull(filterInvocationDefinitionSource, "filterInvocationDefinitionSource must be specified");
101         Assert.notNull(this.filterInvocationDefinitionSource.getConfigAttributeDefinitions(),
102             "FilterChainProxy requires the FilterInvocationDefinitionSource to return a non-null response to "
103                     + "getConfigAttributeDefinitions()");
104     }
105 
106     public void destroy() {
107         Filter[] filters = obtainAllDefinedFilters();
108 
109         for (int i = 0; i < filters.length; i++) {
110             if (filters[i] != null) {
111                 if (logger.isDebugEnabled()) {
112                     logger.debug("Destroying Filter defined in ApplicationContext: '" + filters[i].toString() + "'");
113                 }
114 
115                 filters[i].destroy();
116             }
117         }
118     }
119 
120     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
121         throws IOException, ServletException {
122         FilterInvocation fi = new FilterInvocation(request, response, chain);
123 
124         ConfigAttributeDefinition cad = this.filterInvocationDefinitionSource.getAttributes(fi);
125 
126         if (cad == null) {
127             if (logger.isDebugEnabled()) {
128                 logger.debug(fi.getRequestUrl() + " has no matching filters");
129             }
130 
131             chain.doFilter(request, response);
132 
133             return;
134         }
135 
136         Filter[] filters = obtainAllDefinedFilters(cad);
137 
138         if (filters.length == 0) {
139             if (logger.isDebugEnabled()) {
140                 logger.debug(fi.getRequestUrl() + " has an empty filter list");
141             }
142 
143             chain.doFilter(request, response);
144 
145             return;
146         }
147 
148         VirtualFilterChain virtualFilterChain = new VirtualFilterChain(fi, filters);
149         virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse());
150     }
151 
152     public FilterInvocationDefinitionSource getFilterInvocationDefinitionSource() {
153         return filterInvocationDefinitionSource;
154     }
155 
156     public void init(FilterConfig filterConfig) throws ServletException {
157         Filter[] filters = obtainAllDefinedFilters();
158 
159         for (int i = 0; i < filters.length; i++) {
160             if (filters[i] != null) {
161                 if (logger.isDebugEnabled()) {
162                     logger.debug("Initializing Filter defined in ApplicationContext: '" + filters[i].toString() + "'");
163                 }
164 
165                 filters[i].init(filterConfig);
166             }
167         }
168     }
169 
170     /**
171      * Obtains all of the <b>unique</b><code>Filter</code> instances registered against the
172      * <code>FilterInvocationDefinitionSource</code>.<p>This is useful in ensuring a <code>Filter</code> is not
173      * initialized or destroyed twice.</p>
174      *
175      * @return all of the <code>Filter</code> instances in the application context for which there has been an entry
176      *         against the <code>FilterInvocationDefinitionSource</code> (only one entry is included in the array for
177      *         each <code>Filter</code> that actually exists in application context, even if a given
178      *         <code>Filter</code> is defined multiples times by the <code>FilterInvocationDefinitionSource</code>)
179      */
180     protected Filter[] obtainAllDefinedFilters() {
181         Iterator cads = this.filterInvocationDefinitionSource.getConfigAttributeDefinitions();
182         Set list = new LinkedHashSet();
183 
184         while (cads.hasNext()) {
185             ConfigAttributeDefinition attribDef = (ConfigAttributeDefinition) cads.next();
186             Filter[] filters = obtainAllDefinedFilters(attribDef);
187 
188             for (int i = 0; i < filters.length; i++) {
189                 list.add(filters[i]);
190             }
191         }
192 
193         return (Filter[]) list.toArray(new Filter[0]);
194     }
195 
196     /**
197      * Obtains all of the <code>Filter</code> instances registered against the specified
198      * <code>ConfigAttributeDefinition</code>.
199      *
200      * @param configAttributeDefinition for which we want to obtain associated <code>Filter</code>s
201      *
202      * @return the <code>Filter</code>s against the specified <code>ConfigAttributeDefinition</code> (never
203      *         <code>null</code>)
204      *
205      * @throws IllegalArgumentException DOCUMENT ME!
206      */
207     private Filter[] obtainAllDefinedFilters(ConfigAttributeDefinition configAttributeDefinition) {
208         List list = new Vector();
209         Iterator attributes = configAttributeDefinition.getConfigAttributes();
210 
211         while (attributes.hasNext()) {
212             ConfigAttribute attr = (ConfigAttribute) attributes.next();
213             String filterName = attr.getAttribute();
214 
215             if (filterName == null) {
216                 throw new IllegalArgumentException("Configuration attribute: '" + attr
217                     + "' returned null to the getAttribute() method, which is invalid when used with FilterChainProxy");
218             }
219 
220             if (!filterName.equals(TOKEN_NONE)) {
221                 list.add(this.applicationContext.getBean(filterName, Filter.class));
222             }
223         }
224 
225         return (Filter[]) list.toArray(new Filter[list.size()]);
226     }
227 
228     public void setApplicationContext(ApplicationContext applicationContext)
229         throws BeansException {
230         this.applicationContext = applicationContext;
231     }
232 
233     public void setFilterInvocationDefinitionSource(FilterInvocationDefinitionSource filterInvocationDefinitionSource) {
234         this.filterInvocationDefinitionSource = filterInvocationDefinitionSource;
235     }
236 
237     //~ Inner Classes ==================================================================================================
238 
239     /**
240      * A <code>FilterChain</code> that records whether or not {@link
241      * FilterChain#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} is called.<p>This
242      * <code>FilterChain</code> is used by <code>FilterChainProxy</code> to determine if the next <code>Filter</code>
243      * should be called or not.</p>
244      */
245     private class VirtualFilterChain implements FilterChain {
246         private FilterInvocation fi;
247         private Filter[] additionalFilters;
248         private int currentPosition = 0;
249 
250         public VirtualFilterChain(FilterInvocation filterInvocation, Filter[] additionalFilters) {
251             this.fi = filterInvocation;
252             this.additionalFilters = additionalFilters;
253         }
254 
255         private VirtualFilterChain() {}
256 
257         public void doFilter(ServletRequest request, ServletResponse response)
258             throws IOException, ServletException {
259             if (currentPosition == additionalFilters.length) {
260                 if (logger.isDebugEnabled()) {
261                     logger.debug(fi.getRequestUrl()
262                         + " reached end of additional filter chain; proceeding with original chain");
263                 }
264 
265                 fi.getChain().doFilter(request, response);
266             } else {
267                 currentPosition++;
268 
269                 if (logger.isDebugEnabled()) {
270                     logger.debug(fi.getRequestUrl() + " at position " + currentPosition + " of "
271                         + additionalFilters.length + " in additional filter chain; firing Filter: '"
272                         + additionalFilters[currentPosition - 1] + "'");
273                 }
274 
275                 additionalFilters[currentPosition - 1].doFilter(request, response, this);
276             }
277         }
278     }
279 }