001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community 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 * http://www.opensource.org/licenses/ecl2.php
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 */
016package org.kuali.rice.kew.web;
017
018import org.apache.log4j.Logger;
019import org.kuali.rice.core.api.config.property.Config;
020import org.kuali.rice.core.api.config.property.ConfigContext;
021import org.kuali.rice.core.api.reflect.ObjectDefinition;
022import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
023import org.kuali.rice.core.api.util.ClassLoaderUtils;
024
025import javax.servlet.Filter;
026import javax.servlet.FilterChain;
027import javax.servlet.FilterConfig;
028import javax.servlet.ServletContext;
029import javax.servlet.ServletException;
030import javax.servlet.ServletRequest;
031import javax.servlet.ServletResponse;
032import javax.servlet.http.HttpServletRequest;
033import java.io.IOException;
034import java.util.Collections;
035import java.util.Enumeration;
036import java.util.HashMap;
037import java.util.Iterator;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Map;
041import java.util.SortedSet;
042import java.util.TreeSet;
043
044/**
045 * A filter which at runtime reads a series of filter configurations, constructs
046 * and initializes those filters, and invokes them when it is invoked. This
047 * allows runtime user configuration of arbitrary filters in the webapp context.
048 *
049 * @author Kuali Rice Team (rice.collab@kuali.org)
050 */
051public class BootstrapFilter implements Filter {
052        private static final Logger LOG = Logger.getLogger(BootstrapFilter.class);
053
054        private static final String FILTER_PREFIX = "filter.";
055
056        private static final String CLASS_SUFFIX = ".class";
057
058        private static final String FILTER_MAPPING_PREFIX = "filtermapping.";
059
060        private FilterConfig config;
061
062        private final Map<String, Filter> filters = new HashMap<String, Filter>();
063
064        private final SortedSet<FilterMapping> filterMappings = new TreeSet<FilterMapping>();
065
066        private boolean initted = false;
067
068        public void init(FilterConfig cfg) throws ServletException {
069                this.config = cfg;
070        }
071
072        private void addFilter(String name, String classname, Map<String, String> props) throws ServletException {
073                LOG.debug("Adding filter: " + name + "=" + classname);
074                Object filterObject = GlobalResourceLoader.getResourceLoader().getObject(new ObjectDefinition(classname));
075                if (filterObject == null) {
076                        throw new ServletException("Filter '" + name + "' class not found: " + classname);
077
078                }
079                if (!(filterObject instanceof Filter)) {
080                        LOG.error("Class '" + filterObject.getClass() + "' does not implement servlet javax.servlet.Filter");
081                        return;
082                }
083                Filter filter = (Filter) filterObject;
084                BootstrapFilterConfig fc = new BootstrapFilterConfig(config.getServletContext(), name);
085                for (Map.Entry<String, String> entry : props.entrySet()) {
086                        String key = entry.getKey().toString();
087                        final String prefix = FILTER_PREFIX + name + ".";
088                        if (!key.startsWith(prefix) || key.equals(FILTER_PREFIX + name + CLASS_SUFFIX)) {
089                                continue;
090                        }
091                        String paramName = key.substring(prefix.length());
092                        fc.addInitParameter(paramName, entry.getValue());
093                }
094                try {
095                        filter.init(fc);
096                        filters.put(name, filter);
097                } catch (ServletException se) {
098                        LOG.error("Error initializing filter: " + name + " [" + classname + "]", se);
099                }
100        }
101
102        private void addFilterMapping(String filterName, String orderNumber, String value) {
103                filterMappings.add(new FilterMapping(filterName, orderNumber, value));
104        }
105
106        private synchronized void init() throws ServletException {
107                if (initted) {
108                        return;
109                }
110                LOG.debug("initializing...");
111                Config cfg = ConfigContext.getCurrentContextConfig();
112                
113                @SuppressWarnings({ "unchecked", "rawtypes" })
114                final Map<String, String> p = new HashMap<String, String>((Map) cfg.getProperties());
115                
116                for (Map.Entry<String, String> entry : p.entrySet()) {
117                        String key = entry.getKey().toString();
118                        if (key.startsWith(FILTER_MAPPING_PREFIX)) {
119                                String[] values = key.split("\\.");
120                                if (values.length != 2 && values.length != 3) {
121                                        throw new ServletException("Invalid filter mapping defined.  Should contain 2 or 3 pieces in the form of filtermapping.<<filter name>>.<<order number>> with the last piece optional.");
122                                }
123                                String filterName = values[1];
124                                String orderNumber = (values.length == 2 ? "0" : values[2]);
125                                String value = entry.getValue();
126                                addFilterMapping(filterName, orderNumber, value);
127                        } else if (key.startsWith(FILTER_PREFIX) && key.endsWith(CLASS_SUFFIX)) {
128                                String name = key.substring(FILTER_PREFIX.length(), key.length() - CLASS_SUFFIX.length());
129                                String value = entry.getValue();
130                                // ClassLoader cl =
131                                // SpringServiceLocator.getPluginRegistry().getInstitutionPlugin().getClassLoader();
132                                // addFilter(name, value, cl, p);
133                                addFilter(name, value, p);
134                        }
135                }
136                // do a diff log a warn if any filter has no mappings
137                for (String filterName : filters.keySet()) {
138                        if (!hasFilterMapping(filterName)) {
139                                LOG.warn("NO FILTER MAPPING DETECTED.  Filter " + filterName + " has no mapping and will not be called.");
140                        }
141                }
142                initted = true;
143        }
144
145        private boolean hasFilterMapping(String filterName) {
146                for (FilterMapping filterMapping : filterMappings) {
147                        if (filterMapping.getFilterName().equals(filterName)) {
148                                return true;
149                        }
150                }
151                return false;
152        }
153
154        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
155            LOG.debug("Begin BootstrapFilter...");
156                init();
157                // build the filter chain and execute it
158                if (!filterMappings.isEmpty() && request instanceof HttpServletRequest) {
159                        chain = buildChain((HttpServletRequest) request, chain);
160                }
161                LOG.debug("...ending BootstrapFilter preperation, executing BootstrapFilter Chain.");
162                chain.doFilter(request, response);
163
164        }
165
166        private FilterChain buildChain(HttpServletRequest request, FilterChain targetChain) {
167                BootstrapFilterChain chain = new BootstrapFilterChain(targetChain, ClassLoaderUtils.getDefaultClassLoader());
168                String requestPath = request.getServletPath();
169                for (FilterMapping mapping : filterMappings) {
170                        Filter filter = filters.get(mapping.getFilterName());
171                        if (!chain.containsFilter(filter) && matchFiltersURL(mapping.getUrlPattern(), requestPath)) {
172                                chain.addFilter(filter);
173                        }
174                }
175                return chain;
176        }
177
178        public void destroy() {
179                for (Filter filter : filters.values()) {
180                        try {
181                                filter.destroy();
182                        } catch (Exception e) {
183                                LOG.error("Error destroying filter: " + filter, e);
184                        }
185                }
186        }
187
188        /**
189         * This method was borrowed from the Tomcat codebase.
190         */
191        private boolean matchFiltersURL(String urlPattern, String requestPath) {
192
193                if (requestPath == null) {
194                        return (false);
195                }
196
197                // Match on context relative request path
198                if (urlPattern == null) {
199                        return (false);
200                }
201
202                // Case 1 - Exact Match
203                if (urlPattern.equals(requestPath)) {
204                        return (true);
205                }
206
207                // Case 2 - Path Match ("/.../*")
208                if (urlPattern.equals("/*") || urlPattern.equals("*")) {
209                        return (true);
210                }
211                if (urlPattern.endsWith("/*")) {
212                        if (urlPattern.regionMatches(0, requestPath, 0, urlPattern.length() - 2)) {
213                                if (requestPath.length() == (urlPattern.length() - 2)) {
214                                        return (true);
215                                } else if ('/' == requestPath.charAt(urlPattern.length() - 2)) {
216                                        return (true);
217                                }
218                        }
219                        return (false);
220                }
221
222                // Case 3 - Extension Match
223                if (urlPattern.startsWith("*.")) {
224                        int slash = requestPath.lastIndexOf('/');
225                        int period = requestPath.lastIndexOf('.');
226                        if ((slash >= 0) && (period > slash) && (period != requestPath.length() - 1) && ((requestPath.length() - period) == (urlPattern.length() - 1))) {
227                                return (urlPattern.regionMatches(2, requestPath, period + 1, urlPattern.length() - 2));
228                        }
229                }
230
231                // Case 4 - "Default" Match
232                return (false); // NOTE - Not relevant for selecting filters
233
234        }
235
236}
237
238/**
239 * A filter chain that invokes a series of filters with which it was
240 * initialized, and then delegates to a target filterchain.
241 *
242 * @author Kuali Rice Team (rice.collab@kuali.org)
243 */
244class BootstrapFilterChain implements FilterChain {
245
246        private final List<Filter> filters = new LinkedList<Filter>();
247
248        private final FilterChain target;
249
250        private Iterator<Filter> filterIterator;
251
252        private ClassLoader originalClassLoader;
253
254        public BootstrapFilterChain(FilterChain target, ClassLoader originalClassLoader) {
255                this.target = target;
256                this.originalClassLoader = originalClassLoader;
257        }
258
259        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
260                if (filterIterator == null) {
261                        filterIterator = filters.iterator();
262                }
263                if (filterIterator.hasNext()) {
264                        (filterIterator.next()).doFilter(request, response, this);
265                } else {
266                        // reset the CCL to the original classloader before calling the non
267                        // workflow configured filter - this makes it so our
268                        // CCL is the webapp classloader in workflow action classes and the
269                        // code they call
270                        Thread.currentThread().setContextClassLoader(originalClassLoader);
271                        target.doFilter(request, response);
272                }
273        }
274
275        public void addFilter(Filter filter) {
276                filters.add(filter);
277        }
278
279        public boolean containsFilter(Filter filter) {
280                return filters.contains(filter);
281        }
282
283        public boolean isEmpty() {
284                return filters.isEmpty();
285        }
286
287}
288
289/**
290 * Borrowed from spring-mock.
291 *
292 * @author Kuali Rice Team (rice.collab@kuali.org)
293 */
294class BootstrapFilterConfig implements FilterConfig {
295
296        private final ServletContext servletContext;
297
298        private final String filterName;
299
300        private final Map<String, String> initParameters = new HashMap<String, String>();
301
302        public BootstrapFilterConfig() {
303                this(null, "");
304        }
305
306        public BootstrapFilterConfig(String filterName) {
307                this(null, filterName);
308        }
309
310        public BootstrapFilterConfig(ServletContext servletContext) {
311                this(servletContext, "");
312        }
313
314        public BootstrapFilterConfig(ServletContext servletContext, String filterName) {
315                this.servletContext = servletContext;
316                this.filterName = filterName;
317        }
318
319        public String getFilterName() {
320                return filterName;
321        }
322
323        public ServletContext getServletContext() {
324                return servletContext;
325        }
326
327        public void addInitParameter(String name, String value) {
328                this.initParameters.put(name, value);
329        }
330
331        public String getInitParameter(String name) {
332                return this.initParameters.get(name);
333        }
334
335        public Enumeration<String> getInitParameterNames() {
336                return Collections.enumeration(this.initParameters.keySet());
337        }
338
339}
340
341class FilterMapping implements Comparable<FilterMapping> {
342
343        private String filterName;
344
345        private String orderValue;
346
347        private String urlPattern;
348
349        public FilterMapping(String filterName, String orderValue, String urlPattern) {
350                this.filterName = filterName;
351                this.orderValue = orderValue;
352                this.urlPattern = urlPattern;
353        }
354
355        public int compareTo(FilterMapping object) {
356                return orderValue.compareTo(object.orderValue);
357        }
358
359        public String getFilterName() {
360                return filterName;
361        }
362
363        public String getOrderValue() {
364                return orderValue;
365        }
366
367        public String getUrlPattern() {
368                return urlPattern;
369        }
370
371}