View Javadoc

1   /*
2    * Copyright 2009-2010 Capgemini Licensed under the Apache License, Version 2.0
3    * (the "License"); you may not use this file except in compliance with the
4    * License. You may obtain a copy of the License at
5    * 
6    * http://www.apache.org/licenses/LICENSE-2.0
7    * 
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11   * License for the specific language governing permissions and limitations under
12   * the License.
13   */
14  package net.sourceforge.statelessfilter.filter;
15  
16  import java.io.IOException;
17  import java.io.InputStream;
18  import java.net.URI;
19  import java.net.URISyntaxException;
20  import java.net.URL;
21  import java.util.ArrayList;
22  import java.util.Enumeration;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Properties;
26  import java.util.regex.Pattern;
27  
28  import javax.servlet.Filter;
29  import javax.servlet.FilterChain;
30  import javax.servlet.FilterConfig;
31  import javax.servlet.ServletContext;
32  import javax.servlet.ServletException;
33  import javax.servlet.ServletRequest;
34  import javax.servlet.ServletResponse;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  
38  import net.sourceforge.statelessfilter.backend.ISessionBackend;
39  import net.sourceforge.statelessfilter.processor.IRequestProcessor;
40  import net.sourceforge.statelessfilter.spring.SpringContextChecker;
41  import net.sourceforge.statelessfilter.spring.SpringObjectInstantiationListener;
42  import net.sourceforge.statelessfilter.wrappers.BufferedHttpResponseWrapper;
43  import net.sourceforge.statelessfilter.wrappers.StatelessRequestWrapper;
44  import net.sourceforge.statelessfilter.wrappers.headers.HeaderBufferedHttpResponseWrapper;
45  
46  import org.apache.commons.lang.StringUtils;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  /**
51   * The filter overrides the default session management and use multiple and
52   * configurable backends instead.
53   * 
54   * @author Nicolas Richeton - Capgemini
55   * @author Guillaume Mary - Capgemini
56   * 
57   */
58  public class StatelessFilter implements Filter {
59      private static final String CONFIG_ATTRIBUTE_PREFIX = "attribute."; //$NON-NLS-1$
60      private static final String CONFIG_DEFAULT_BACKEND = "default"; //$NON-NLS-1$
61      private static final String CONFIG_DIRTY = "dirtycheck"; //$NON-NLS-1$
62      private static final String CONFIG_INSTANTIATION_LISTENER = "instantiationListener"; //$NON-NLS-1$
63      private static final String CONFIG_LOCATION = "configurationLocation"; //$NON-NLS-1$
64      private static final String CONFIG_LOCATION_DEFAULT = "/stateless.properties"; //$NON-NLS-1$
65      /**
66       * Location of backend configuration file.
67       */
68      private static final String CONFIG_PLUGIN_BACKEND = "stateless-backend.properties"; //$NON-NLS-1$
69      private static final String CONFIG_PLUGIN_BACKEND_IMPL = "backendImpl"; //$NON-NLS-1$
70      /**
71       * Location of request processor configuration file.
72       */
73      private static final String CONFIG_PLUGIN_PROCESSOR = "stateless-processor.properties"; //$NON-NLS-1$
74      private static final String CONFIG_PLUGIN_PROCESSOR_IMPL = "processorImpl"; //$NON-NLS-1$
75      private static final String DEBUG_INIT = "Stateless filter init..."; //$NON-NLS-1$
76      private static final String DEBUG_PROCESSING = "Processing "; //$NON-NLS-1$
77      private static final String DOT = "."; //$NON-NLS-1$
78      private static final String EXCLUDE_PATTERN_SEPARATOR = ","; //$NON-NLS-1$
79      private static final String INFO_BACKEND = "Backend "; //$NON-NLS-1$
80      private static final String INFO_BUFFERING = " enables output buffering."; //$NON-NLS-1$
81      private static final String INFO_DEFAULT_BACKEND = "Default Session backend is "; //$NON-NLS-1$
82      private static final String INFO_READY = " ready."; //$NON-NLS-1$
83      private static final String INFO_REQUEST_PROCESSOR = "Request processor "; //$NON-NLS-1$
84  
85      private static Logger logger = LoggerFactory.getLogger(StatelessFilter.class);
86  
87      private static final String WARN_BACKEND_NOT_FOUND1 = "Specified backend '"; //$NON-NLS-1$
88      private static final String WARN_BACKEND_NOT_FOUND2 = "' is not installed. Missing jar ? "; //$NON-NLS-1$
89      private static final String WARN_LOAD_CONF = "Cannot load global configuration /stateless.properties. Using defaults"; //$NON-NLS-1$
90      private IObjectInstantiationListener instantiationListener = null;
91  
92      private List<Pattern> excludePatterns = null;
93      private ServletContext servletContext = null;
94      protected Configuration configuration = new Configuration();
95  
96      /**
97       * Servlet config parameter name used to configure the list of the excluded
98       * uri patterns.
99       */
100     public static final String PARAM_EXCLUDE_PATTERN_LIST = "excludePatternList"; //$NON-NLS-1$
101 
102     /**
103      * @see javax.servlet.Filter#destroy()
104      */
105     public void destroy() {
106         // Destroy all backends
107         for (ISessionBackend backend : configuration.backends.values()) {
108             backend.destroy();
109         }
110         configuration.backends.clear();
111     }
112 
113     /**
114      * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
115      *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
116      */
117     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
118             ServletException {
119         HttpServletResponse httpResponse = (HttpServletResponse) response;
120 
121         // Test if the request is excluded
122         HttpServletRequest httpRequest = (HttpServletRequest) request;
123         if (isExcluded(httpRequest)) {
124             chain.doFilter(request, response);
125             return;
126         }
127 
128         // Wrap request
129         StatelessRequestWrapper statelessRequest = new StatelessRequestWrapper((HttpServletRequest) request,
130                 configuration);
131 
132         // Wrap response if necessary
133         HttpServletResponse targetResponse = httpResponse;
134 
135         BufferedHttpResponseWrapper bufferedResponse = null;
136         if (Configuration.BUFFERING_FULL.equals(configuration.isBufferingRequired)) {
137             bufferedResponse = new BufferedHttpResponseWrapper(httpResponse);
138             targetResponse = bufferedResponse;
139         }else 
140             if (Configuration.BUFFERING_HEADERS.equals(configuration.isBufferingRequired)) {
141                 targetResponse = new HeaderBufferedHttpResponseWrapper(statelessRequest,httpResponse);
142             }
143 
144         // Request processors
145         if (configuration.requestProcessors != null && !configuration.requestProcessors.isEmpty()) {
146             IRequestProcessor rp = null;
147             for (int i = 0; i < configuration.requestProcessors.size(); i++) {
148                 rp = configuration.requestProcessors.get(i);
149                 try {
150                     rp.preRequest(statelessRequest, targetResponse);
151                 } catch (Exception e) {
152                     throw new IOException(e.getMessage());
153                 }
154             }
155         }
156 
157         // Proceed with request
158         chain.doFilter(statelessRequest, targetResponse);
159 
160         // Write session
161         if (!statelessRequest.isSessionWritten()) {
162             statelessRequest.writeSession(statelessRequest, httpResponse);
163         }
164 
165         // Request processors : post process.
166         if (configuration.requestProcessors != null && !configuration.requestProcessors.isEmpty()) {
167             IRequestProcessor rp = null;
168             for (int i = configuration.requestProcessors.size() - 1; i >= 0; i--) {
169                 rp = configuration.requestProcessors.get(i);
170                 try {
171                     rp.postProcess(statelessRequest, targetResponse);
172                 } catch (Exception e) {
173                     throw new IOException(e.getMessage());
174                 }
175             }
176         }
177 
178         // Flush buffer if necessary
179         if (bufferedResponse != null) {
180             if (!bufferedResponse.performSend()) {
181                 bufferedResponse.flushBuffer();
182                 response.getOutputStream().write(bufferedResponse.getBuffer());
183                 response.flushBuffer();
184             }
185         }
186     }
187 
188     /**
189      * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
190      */
191     public void init(FilterConfig filterConfig) throws ServletException {
192         if (logger.isDebugEnabled()) {
193             logger.debug(DEBUG_INIT);
194         }
195 
196         // Get Servlet Context
197         this.servletContext = filterConfig.getServletContext();
198 
199         // Configuration
200         Properties globalProp = new Properties();
201 
202         // Try to load global configuration.
203         String configLocation = filterConfig.getInitParameter(CONFIG_LOCATION);
204         if (configLocation == null) {
205             configLocation = CONFIG_LOCATION_DEFAULT;
206         }
207         try {
208             globalProp.load(StatelessFilter.class.getResourceAsStream(configLocation));
209         } catch (Exception e) {
210             logger.warn(WARN_LOAD_CONF);
211             // This is not fatal, will just use defaults.
212         }
213 
214         // Enables Spring instantiation listener if Spring is available and
215         // configured.
216         if (SpringContextChecker.checkForSpring(servletContext)) {
217 
218             logger.info("Enabling Spring instantiation listener"); //$NON-NLS-1$
219 
220             instantiationListener = new SpringObjectInstantiationListener();
221             instantiationListener.setServletContext(servletContext);
222         }
223 
224         // Load instantiation listener
225         try {
226             initInstantiationListener(globalProp);
227         } catch (Exception e) {
228             throw new ServletException("Failed to load instantiation listener from /stateless.properties", //$NON-NLS-1$
229                     e);
230         }
231 
232         // Load and init all plugins
233 
234         try {
235             detectAndInitPlugins(CONFIG_PLUGIN_BACKEND, globalProp, IPlugin.TYPE_BACKEND);
236             detectAndInitPlugins(CONFIG_PLUGIN_PROCESSOR, globalProp, IPlugin.TYPE_REQUEST_PROCESSOR);
237 
238         } catch (Exception e) {
239             throw new ServletException(e);
240         }
241 
242         // Process attribute mapping
243         initAttributeMapping(globalProp);
244 
245         // Read default backend
246         initDefaultBackend(globalProp);
247 
248         // Use dirty state
249         initDirtyState(globalProp);
250 
251         // Init Buffering mode
252         initBuffering(globalProp);
253 
254         // init the excluded pattern from the servlet config
255         initExcludedPattern(filterConfig);
256 
257         checkConfiguration();
258     }
259 
260     private void initBuffering(Properties globalProp) {
261         String buffering = globalProp.getProperty("buffering", Configuration.BUFFERING_FALSE);
262         applyBuffering(buffering, "Configuration");
263     }
264 
265     /**
266      * Change buffering mode if current mode is lower than requested.
267      * 
268      * @param mode
269      */
270     private void applyBuffering(String mode, String source) {
271         if (Configuration.BUFFERING_FULL.equals(mode)
272                 && !Configuration.BUFFERING_FULL.equals(configuration.isBufferingRequired)) {
273             configuration.isBufferingRequired = Configuration.BUFFERING_FULL;
274             if (logger.isInfoEnabled())
275                 logger.info("Switching to buffering mode " + configuration.isBufferingRequired + " (" + source + ")");
276         } else if (Configuration.BUFFERING_HEADERS.equals(mode)
277                 && Configuration.BUFFERING_FALSE.equals(configuration.isBufferingRequired)) {
278             configuration.isBufferingRequired = Configuration.BUFFERING_HEADERS;
279             if (logger.isInfoEnabled())
280                 logger.info("Switching to buffering mode " + configuration.isBufferingRequired + " (" + source + ")");
281         }
282 
283     }
284 
285     /**
286      * Does some self tests to ensure configuration is valid
287      * 
288      * @throws ServletException
289      */
290     private void checkConfiguration() throws ServletException {
291         if (this.configuration.backends.size() == 0) {
292             throw new ServletException(
293                     "No backend installed. Please add one (stateless-session for instance) in the classpath"); //$NON-NLS-1$
294         }
295     }
296 
297     private void initExcludedPattern(FilterConfig filterConfig) {
298         String excludedPatternList = filterConfig.getInitParameter(PARAM_EXCLUDE_PATTERN_LIST);
299         if (excludedPatternList != null) {
300             String[] splittedExcludedPatternList = excludedPatternList.split(EXCLUDE_PATTERN_SEPARATOR);
301             List<Pattern> patterns = new ArrayList<Pattern>();
302             Pattern pattern = null;
303             for (String element : splittedExcludedPatternList) {
304                 pattern = Pattern.compile(element);
305                 patterns.add(pattern);
306             }
307             this.excludePatterns = patterns;
308         }
309     }
310 
311     private void detectAndInitPlugins(String propertyFile, Properties filterConfiguration, String type)
312             throws Exception {
313         Enumeration<URL> configurationURLs;
314 
315         configurationURLs = StatelessFilter.class.getClassLoader().getResources(propertyFile);
316 
317         URL url = null;
318         Properties pluginConfiguration = null;
319         InputStream is = null;
320         while (configurationURLs.hasMoreElements()) {
321             url = configurationURLs.nextElement();
322             if (logger.isDebugEnabled()) {
323                 logger.debug(DEBUG_PROCESSING + url.toString());
324             }
325 
326             // Load plugin configuration
327             pluginConfiguration = new Properties();
328             is = url.openStream();
329             pluginConfiguration.load(is);
330             is.close();
331 
332             // Init plugin
333             initPlugin(pluginConfiguration, filterConfiguration, type);
334         }
335 
336     }
337 
338     private void initAttributeMapping(Properties globalProp) throws ServletException {
339         for (Object key : globalProp.keySet()) {
340             String paramName = (String) key;
341             if (paramName.startsWith(CONFIG_ATTRIBUTE_PREFIX)) {
342                 String attrName = paramName.substring(CONFIG_ATTRIBUTE_PREFIX.length());
343                 String backend = globalProp.getProperty(paramName);
344                 configuration.backendsAttributeMapping.put(attrName, backend);
345 
346                 // Ensure backend is available
347                 if (!configuration.backends.containsKey(backend)) {
348                     throw new ServletException("Attributes are mapped on backend " + backend //$NON-NLS-1$
349                             + " but it is not installed."); //$NON-NLS-1$
350                 }
351             }
352         }
353     }
354 
355     private void initDefaultBackend(Properties globalProp) {
356         String defaultBack = globalProp.getProperty(CONFIG_DEFAULT_BACKEND);
357         if (defaultBack != null) {
358             if (configuration.backends.containsKey(defaultBack)) {
359                 configuration.defaultBackend = defaultBack;
360             } else {
361                 if (logger.isWarnEnabled()) {
362                     logger.warn(WARN_BACKEND_NOT_FOUND1 + defaultBack + WARN_BACKEND_NOT_FOUND2);
363                 }
364                 // This is not fatal will use first backend.
365             }
366         }
367     }
368 
369     private void initDirtyState(Properties globalProp) {
370         String useDirty = globalProp.getProperty(CONFIG_DIRTY);
371         if (Boolean.parseBoolean(useDirty)) {
372             configuration.useDirty = true;
373         }
374 
375         if (logger.isInfoEnabled()) {
376             logger.info("Use dirty state: " + configuration.useDirty); //$NON-NLS-1$
377             logger.info(INFO_DEFAULT_BACKEND + configuration.defaultBackend);
378         }
379     }
380 
381     /**
382      * Get and create instantiation listener from configuration file.
383      * 
384      * @param globalProp
385      * @throws ClassNotFoundException
386      * @throws InstantiationException
387      * @throws IllegalAccessException
388      */
389 
390     private void initInstantiationListener(Properties globalProp) throws ClassNotFoundException,
391             InstantiationException, IllegalAccessException {
392         String clazz = globalProp.getProperty(CONFIG_INSTANTIATION_LISTENER);
393         if (!StringUtils.isEmpty(clazz)) {
394             @SuppressWarnings("unchecked")
395             Class<IObjectInstantiationListener> backClazz = (Class<IObjectInstantiationListener>) Class.forName(clazz);
396 
397             // Create new instance
398             instantiationListener = backClazz.newInstance();
399             instantiationListener.setServletContext(servletContext);
400             logger.info("Using instantiation listener {}", clazz); //$NON-NLS-1$
401         }
402     }
403 
404     /**
405      * Does backend initialization.
406      * 
407      * @param backendProperties
408      * @param globalProperties
409      * @throws Exception
410      */
411     @SuppressWarnings("unchecked")
412     private void initPlugin(Properties backendProperties, Properties globalProperties, String type) throws Exception {
413 
414         IPlugin plugin = null;
415 
416         String location = CONFIG_PLUGIN_BACKEND_IMPL;
417         if (IPlugin.TYPE_REQUEST_PROCESSOR.equals(type)) {
418             location = CONFIG_PLUGIN_PROCESSOR_IMPL;
419         }
420 
421         if (instantiationListener != null) {
422             // Try to create class using listener
423             plugin = (IPlugin) instantiationListener.getInstance((String) backendProperties.get(location));
424         }
425 
426         if (plugin == null) {
427             // backend is still null, try to create it.
428 
429             // Get class
430             String clazz = (String) backendProperties.get(location);
431             Class<IPlugin> backClazz = (Class<IPlugin>) Class.forName(clazz);
432 
433             // Create new instance
434             plugin = backClazz.newInstance();
435         }
436 
437         // Load configuration from global.
438         HashMap<String, String> conf = new HashMap<String, String>();
439         String paramName = null;
440         String prefix = null;
441         String attrName = null;
442         for (Object key : globalProperties.keySet()) {
443             paramName = (String) key;
444             prefix = plugin.getId() + DOT;
445             if (paramName.startsWith(prefix)) {
446                 attrName = paramName.substring(prefix.length());
447                 conf.put(attrName, globalProperties.getProperty(paramName));
448             }
449         }
450 
451         // Init backend
452         plugin.init(conf);
453 
454         // Toggle buffering on if necessary
455         applyBuffering(plugin.isBufferingRequired(), plugin.getId());
456 
457         // Specific handling for request processors
458         if (IPlugin.TYPE_REQUEST_PROCESSOR.equals(type)) {
459             configuration.requestProcessors.add((IRequestProcessor) plugin);
460 
461             // Some info output.
462             if (logger.isInfoEnabled()) {
463                 logger.info(INFO_REQUEST_PROCESSOR + plugin.getId() + INFO_READY);
464             }
465         }
466 
467         // Specific handling for backends
468         if (IPlugin.TYPE_BACKEND.equals(type)) {
469             configuration.backends.put(plugin.getId(), (ISessionBackend) plugin);
470 
471             // Some info output.
472             if (logger.isInfoEnabled()) {
473                 logger.info(INFO_BACKEND + plugin.getId() + INFO_READY);
474             }
475 
476             // First backend is default.
477             if (StringUtils.isEmpty(configuration.defaultBackend)) {
478                 configuration.defaultBackend = plugin.getId();
479             }
480         }
481 
482     }
483 
484     /**
485      * Check if this request is excluded from the process.
486      * 
487      * @param httpRequest
488      *            HTTP request
489      * @return true if the URI requested match an excluded pattern
490      */
491     private boolean isExcluded(HttpServletRequest httpRequest) {
492         if (this.excludePatterns == null) {
493             return false;
494         }
495 
496         String uri = httpRequest.getRequestURI();
497         if (logger.isDebugEnabled()) {
498             logger.debug("Check URI : " + uri);
499         }
500 
501         try {
502             uri = new URI(uri).normalize().toString();
503 
504             for (Pattern pattern : this.excludePatterns) {
505                 if (pattern.matcher(uri).matches()) {
506                     if (logger.isInfoEnabled()) {
507                         logger.info("URI excluded : " + uri);
508                     }
509                     return true;
510                 }
511             }
512 
513         } catch (URISyntaxException e) {
514             logger.warn(
515                     "The following URI has a bad syntax. The request will be processed by the filter. URI : " + uri, e);
516         }
517 
518         return false;
519     }
520 }