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 }