001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.filter;
018
019import java.util.Enumeration;
020import java.util.HashSet;
021import java.util.Set;
022
023import javax.servlet.Filter;
024import javax.servlet.FilterConfig;
025import javax.servlet.ServletContext;
026import javax.servlet.ServletException;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.beans.BeanWrapper;
032import org.springframework.beans.BeansException;
033import org.springframework.beans.MutablePropertyValues;
034import org.springframework.beans.PropertyAccessorFactory;
035import org.springframework.beans.PropertyValue;
036import org.springframework.beans.PropertyValues;
037import org.springframework.beans.factory.BeanNameAware;
038import org.springframework.beans.factory.DisposableBean;
039import org.springframework.beans.factory.InitializingBean;
040import org.springframework.context.EnvironmentAware;
041import org.springframework.core.env.Environment;
042import org.springframework.core.env.EnvironmentCapable;
043import org.springframework.core.io.Resource;
044import org.springframework.core.io.ResourceEditor;
045import org.springframework.core.io.ResourceLoader;
046import org.springframework.lang.Nullable;
047import org.springframework.util.Assert;
048import org.springframework.util.CollectionUtils;
049import org.springframework.util.StringUtils;
050import org.springframework.web.context.ServletContextAware;
051import org.springframework.web.context.support.ServletContextResourceLoader;
052import org.springframework.web.context.support.StandardServletEnvironment;
053import org.springframework.web.util.NestedServletException;
054
055/**
056 * Simple base implementation of {@link javax.servlet.Filter} which treats
057 * its config parameters ({@code init-param} entries within the
058 * {@code filter} tag in {@code web.xml}) as bean properties.
059 *
060 * <p>A handy superclass for any type of filter. Type conversion of config
061 * parameters is automatic, with the corresponding setter method getting
062 * invoked with the converted value. It is also possible for subclasses to
063 * specify required properties. Parameters without matching bean property
064 * setter will simply be ignored.
065 *
066 * <p>This filter leaves actual filtering to subclasses, which have to
067 * implement the {@link javax.servlet.Filter#doFilter} method.
068 *
069 * <p>This generic filter base class has no dependency on the Spring
070 * {@link org.springframework.context.ApplicationContext} concept.
071 * Filters usually don't load their own context but rather access service
072 * beans from the Spring root application context, accessible via the
073 * filter's {@link #getServletContext() ServletContext} (see
074 * {@link org.springframework.web.context.support.WebApplicationContextUtils}).
075 *
076 * @author Juergen Hoeller
077 * @since 06.12.2003
078 * @see #addRequiredProperty
079 * @see #initFilterBean
080 * @see #doFilter
081 */
082public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,
083                EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {
084
085        /** Logger available to subclasses. */
086        protected final Log logger = LogFactory.getLog(getClass());
087
088        @Nullable
089        private String beanName;
090
091        @Nullable
092        private Environment environment;
093
094        @Nullable
095        private ServletContext servletContext;
096
097        @Nullable
098        private FilterConfig filterConfig;
099
100        private final Set<String> requiredProperties = new HashSet<>(4);
101
102
103        /**
104         * Stores the bean name as defined in the Spring bean factory.
105         * <p>Only relevant in case of initialization as bean, to have a name as
106         * fallback to the filter name usually provided by a FilterConfig instance.
107         * @see org.springframework.beans.factory.BeanNameAware
108         * @see #getFilterName()
109         */
110        @Override
111        public void setBeanName(String beanName) {
112                this.beanName = beanName;
113        }
114
115        /**
116         * Set the {@code Environment} that this filter runs in.
117         * <p>Any environment set here overrides the {@link StandardServletEnvironment}
118         * provided by default.
119         * <p>This {@code Environment} object is used only for resolving placeholders in
120         * resource paths passed into init-parameters for this filter. If no init-params are
121         * used, this {@code Environment} can be essentially ignored.
122         */
123        @Override
124        public void setEnvironment(Environment environment) {
125                this.environment = environment;
126        }
127
128        /**
129         * Return the {@link Environment} associated with this filter.
130         * <p>If none specified, a default environment will be initialized via
131         * {@link #createEnvironment()}.
132         * @since 4.3.9
133         */
134        @Override
135        public Environment getEnvironment() {
136                if (this.environment == null) {
137                        this.environment = createEnvironment();
138                }
139                return this.environment;
140        }
141
142        /**
143         * Create and return a new {@link StandardServletEnvironment}.
144         * <p>Subclasses may override this in order to configure the environment or
145         * specialize the environment type returned.
146         * @since 4.3.9
147         */
148        protected Environment createEnvironment() {
149                return new StandardServletEnvironment();
150        }
151
152        /**
153         * Stores the ServletContext that the bean factory runs in.
154         * <p>Only relevant in case of initialization as bean, to have a ServletContext
155         * as fallback to the context usually provided by a FilterConfig instance.
156         * @see org.springframework.web.context.ServletContextAware
157         * @see #getServletContext()
158         */
159        @Override
160        public void setServletContext(ServletContext servletContext) {
161                this.servletContext = servletContext;
162        }
163
164        /**
165         * Calls the {@code initFilterBean()} method that might
166         * contain custom initialization of a subclass.
167         * <p>Only relevant in case of initialization as bean, where the
168         * standard {@code init(FilterConfig)} method won't be called.
169         * @see #initFilterBean()
170         * @see #init(javax.servlet.FilterConfig)
171         */
172        @Override
173        public void afterPropertiesSet() throws ServletException {
174                initFilterBean();
175        }
176
177        /**
178         * Subclasses may override this to perform custom filter shutdown.
179         * <p>Note: This method will be called from standard filter destruction
180         * as well as filter bean destruction in a Spring application context.
181         * <p>This default implementation is empty.
182         */
183        @Override
184        public void destroy() {
185        }
186
187
188        /**
189         * Subclasses can invoke this method to specify that this property
190         * (which must match a JavaBean property they expose) is mandatory,
191         * and must be supplied as a config parameter. This should be called
192         * from the constructor of a subclass.
193         * <p>This method is only relevant in case of traditional initialization
194         * driven by a FilterConfig instance.
195         * @param property name of the required property
196         */
197        protected final void addRequiredProperty(String property) {
198                this.requiredProperties.add(property);
199        }
200
201        /**
202         * Standard way of initializing this filter.
203         * Map config parameters onto bean properties of this filter, and
204         * invoke subclass initialization.
205         * @param filterConfig the configuration for this filter
206         * @throws ServletException if bean properties are invalid (or required
207         * properties are missing), or if subclass initialization fails.
208         * @see #initFilterBean
209         */
210        @Override
211        public final void init(FilterConfig filterConfig) throws ServletException {
212                Assert.notNull(filterConfig, "FilterConfig must not be null");
213
214                this.filterConfig = filterConfig;
215
216                // Set bean properties from init parameters.
217                PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
218                if (!pvs.isEmpty()) {
219                        try {
220                                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
221                                ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
222                                Environment env = this.environment;
223                                if (env == null) {
224                                        env = new StandardServletEnvironment();
225                                }
226                                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, env));
227                                initBeanWrapper(bw);
228                                bw.setPropertyValues(pvs, true);
229                        }
230                        catch (BeansException ex) {
231                                String msg = "Failed to set bean properties on filter '" +
232                                        filterConfig.getFilterName() + "': " + ex.getMessage();
233                                logger.error(msg, ex);
234                                throw new NestedServletException(msg, ex);
235                        }
236                }
237
238                // Let subclasses do whatever initialization they like.
239                initFilterBean();
240
241                if (logger.isDebugEnabled()) {
242                        logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use");
243                }
244        }
245
246        /**
247         * Initialize the BeanWrapper for this GenericFilterBean,
248         * possibly with custom editors.
249         * <p>This default implementation is empty.
250         * @param bw the BeanWrapper to initialize
251         * @throws BeansException if thrown by BeanWrapper methods
252         * @see org.springframework.beans.BeanWrapper#registerCustomEditor
253         */
254        protected void initBeanWrapper(BeanWrapper bw) throws BeansException {
255        }
256
257        /**
258         * Subclasses may override this to perform custom initialization.
259         * All bean properties of this filter will have been set before this
260         * method is invoked.
261         * <p>Note: This method will be called from standard filter initialization
262         * as well as filter bean initialization in a Spring application context.
263         * Filter name and ServletContext will be available in both cases.
264         * <p>This default implementation is empty.
265         * @throws ServletException if subclass initialization fails
266         * @see #getFilterName()
267         * @see #getServletContext()
268         */
269        protected void initFilterBean() throws ServletException {
270        }
271
272        /**
273         * Make the FilterConfig of this filter available, if any.
274         * Analogous to GenericServlet's {@code getServletConfig()}.
275         * <p>Public to resemble the {@code getFilterConfig()} method
276         * of the Servlet Filter version that shipped with WebLogic 6.1.
277         * @return the FilterConfig instance, or {@code null} if none available
278         * @see javax.servlet.GenericServlet#getServletConfig()
279         */
280        @Nullable
281        public FilterConfig getFilterConfig() {
282                return this.filterConfig;
283        }
284
285        /**
286         * Make the name of this filter available to subclasses.
287         * Analogous to GenericServlet's {@code getServletName()}.
288         * <p>Takes the FilterConfig's filter name by default.
289         * If initialized as bean in a Spring application context,
290         * it falls back to the bean name as defined in the bean factory.
291         * @return the filter name, or {@code null} if none available
292         * @see javax.servlet.GenericServlet#getServletName()
293         * @see javax.servlet.FilterConfig#getFilterName()
294         * @see #setBeanName
295         */
296        @Nullable
297        protected String getFilterName() {
298                return (this.filterConfig != null ? this.filterConfig.getFilterName() : this.beanName);
299        }
300
301        /**
302         * Make the ServletContext of this filter available to subclasses.
303         * Analogous to GenericServlet's {@code getServletContext()}.
304         * <p>Takes the FilterConfig's ServletContext by default.
305         * If initialized as bean in a Spring application context,
306         * it falls back to the ServletContext that the bean factory runs in.
307         * @return the ServletContext instance
308         * @throws IllegalStateException if no ServletContext is available
309         * @see javax.servlet.GenericServlet#getServletContext()
310         * @see javax.servlet.FilterConfig#getServletContext()
311         * @see #setServletContext
312         */
313        protected ServletContext getServletContext() {
314                if (this.filterConfig != null) {
315                        return this.filterConfig.getServletContext();
316                }
317                else if (this.servletContext != null) {
318                        return this.servletContext;
319                }
320                else {
321                        throw new IllegalStateException("No ServletContext");
322                }
323        }
324
325
326        /**
327         * PropertyValues implementation created from FilterConfig init parameters.
328         */
329        @SuppressWarnings("serial")
330        private static class FilterConfigPropertyValues extends MutablePropertyValues {
331
332                /**
333                 * Create new FilterConfigPropertyValues.
334                 * @param config the FilterConfig we'll use to take PropertyValues from
335                 * @param requiredProperties set of property names we need, where
336                 * we can't accept default values
337                 * @throws ServletException if any required properties are missing
338                 */
339                public FilterConfigPropertyValues(FilterConfig config, Set<String> requiredProperties)
340                                throws ServletException {
341
342                        Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ?
343                                        new HashSet<>(requiredProperties) : null);
344
345                        Enumeration<String> paramNames = config.getInitParameterNames();
346                        while (paramNames.hasMoreElements()) {
347                                String property = paramNames.nextElement();
348                                Object value = config.getInitParameter(property);
349                                addPropertyValue(new PropertyValue(property, value));
350                                if (missingProps != null) {
351                                        missingProps.remove(property);
352                                }
353                        }
354
355                        // Fail if we are still missing properties.
356                        if (!CollectionUtils.isEmpty(missingProps)) {
357                                throw new ServletException(
358                                                "Initialization from FilterConfig for filter '" + config.getFilterName() +
359                                                "' failed; the following required properties were missing: " +
360                                                StringUtils.collectionToDelimitedString(missingProps, ", "));
361                        }
362                }
363        }
364
365}