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}