View Javadoc

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