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