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 }