/*
* (c) 2004, Kevin Chipalowsky (kevin@farwestsoftware.com) and Ivelin Ivanov
* (ivelin@apache.org) Released under terms of the Artistic License
* http://www.opensource.org/licenses/artistic-license.php
*/
package com.idega.servlet.filter;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* USAGE:
* Add the following to your web.xml (under /WEB-INF):
* <filter>
* <filter-name>RequestControlFilter</filter-name>
* <filter-class>com.idega.servlet.filter.RequestControlFilter</filter-class>
* </filter>
*
* And then you have to specify where to control requests:
*
* <filter-mapping>
* <filter-name>RequestControlFilter</filter-name>
* <url-pattern>*.jsp</url-pattern>
* </filter-mapping>
*
* <filter-mapping>
* <filter-name>RequestControlFilter</filter-name>
* <url-pattern>*.html</url-pattern>
* </filter-mapping>
*
* <filter-mapping>
* <filter-name>RequestControlFilter</filter-name>
* <url-pattern>/servlet/*</url-pattern>
* </filter-mapping>
*
* Use this filter to synchronize requests to your web application and
* reduce the maximum load that each individual user can put on your web
* application. Requests will be synchronized per session. When more than one
* additional requests are made while a request is in process, only the most
* recent of the additional requests will actually be processed.
* <p>
* If a user makes two requests, A and B, then A will be processed first while B
* waits. When A finishes, B will be processed.
* <p>
* If a user makes three or more requests (e.g. A, B, and C), then the first
* will be processed (A), and then after it finishes the last will be processed
* (C), and any intermediate requests will be skipped (B).
* <p>
* There are two additional limitiations:
* <ul>
* <li>Requests will be excluded from filtering if their URI matches one of the
* exclusion patterns. There will be no synchronization performed if a request
* matches one of those patterns.</li>
*
* Example:
* <init-param>
* <param-name>excludePattern.1 </param-name>
* <param-value>/images/.* </param-value>
* </init-param>
*
* <li>Requests wait a maximum of 5 seconds, which can be overridden per URI
* pattern in the filter's configuration.</li>
*
* Example:
* <init-param>
* <param-name>maxWaitMilliseconds.4000.1 </param-name>
* <param-value>/test/.* </param-value>
* </init-param>
* </ul>
*
* @author Kevin Chipalowsky and Ivelin Ivanov
*/
public class RequestControlFilter implements Filter {
/** A list of Pattern objects that match paths to exclude */
private LinkedList excludePatterns;
/** A map from Pattern to max wait duration (Long objects) */
private HashMap maxWaitDurations;
/** The session attribute key for the request currently being processed */
private final static String REQUEST_IN_PROCESS = "RequestControlFilter.requestInProcess";
/** The session attribute key for the request currently waiting in the queue */
private final static String REQUEST_QUEUE = "RequestControlFilter.requestQueue";
/** The session attribute key for the synchronization object */
private final static String SYNC_OBJECT_KEY = "RequestControlFilter.sessionSync";
/** The default maximum number of milliseconds to wait for a request */
private final static long DEFAULT_DURATION = 5000;
/**
* Initialize this filter by reading its configuration parameters
*
* @param config
* Configuration from web.xml file
*/
public void init(FilterConfig config) throws ServletException {
System.out.println("[idegaWebApp] : Starting RequestControlFilter");
// parse all of the initialization parameters, collecting the exclude
// patterns and the max wait parameters
Enumeration enumer = config.getInitParameterNames();
this.excludePatterns = new LinkedList();
this.maxWaitDurations = new HashMap();
while (enumer.hasMoreElements()) {
String paramName = (String) enumer.nextElement();
String paramValue = config.getInitParameter(paramName);
if (paramName.startsWith("excludePattern")) {
// compile the pattern only this once
Pattern excludePattern = Pattern.compile(paramValue);
this.excludePatterns.add(excludePattern);
}
else if (paramName.startsWith("maxWaitMilliseconds.")) {
// the delay gets parsed from the parameter name
String durationString = paramName.substring("maxWaitMilliseconds.".length());
int endDuration = durationString.indexOf('.');
if (endDuration != -1) {
durationString = durationString.substring(0, endDuration);
}
Long duration = new Long(durationString);
// compile the corresponding pattern, and store it with this delay in
// the map
Pattern waitPattern = Pattern.compile(paramValue);
this.maxWaitDurations.put(waitPattern, duration);
}
}
}
/**
* Called with the filter is no longer needed.
*/
public void destroy() {
// there is nothing to do
}
/**
* Synchronize the request and then either process it or skip it, depending on
* what other requests current exist for this session. See the description of
* this class for more details.
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpSession session = httpRequest.getSession();
//System.out.println("Doing stuff...");
// if this request is excluded from the filter, then just process it
if (!isFilteredRequest(httpRequest)) {
chain.doFilter(request, response);
return;
}
synchronized (getSynchronizationObject(session)) {
// if another request is being processed, then wait
if (isRequestInProcess(session)) {
// Put this request in the queue and wait
enqueueRequest(httpRequest);
if (!waitForRelease(httpRequest)) {
// this request was replaced in the queue by another request,
// so it need not be processed
return;
}
}
// lock the session, so that no other requests are processed until this
// one finishes
setRequestInProgress(httpRequest);
}
// process this request, and then release the session lock regardless of
// any exceptions thrown farther down the chain.
try {
chain.doFilter(request, response);
}
finally {
releaseQueuedRequest(httpRequest);
}
}
/**
* Get a synchronization object for this session
*
* @param session
*/
private static synchronized Object getSynchronizationObject(HttpSession session) {
// get the object from the session. If it does not yet exist,
// then create one.
Object syncObj = session.getAttribute(SYNC_OBJECT_KEY);
if (syncObj == null) {
syncObj = new Object();
session.setAttribute(SYNC_OBJECT_KEY, syncObj);
}
return syncObj;
}
/**
* Record that a request is in process so that the filter blocks additional
* requests until this one finishes.
*
* @param request
*/
private void setRequestInProgress(HttpServletRequest request) {
HttpSession session = request.getSession();
session.setAttribute(REQUEST_IN_PROCESS, request);
}
/**
* Release the next waiting request, because the current request has just
* finished.
*
* @param request
* The request that just finished
*/
private void releaseQueuedRequest(HttpServletRequest request) {
HttpSession session = request.getSession();
synchronized (getSynchronizationObject(session)) {
// if this request is still the current one (i.e., it didn't run for too
// long and result in another request being processed), then clear it
// and thus release the lock
if (session.getAttribute(REQUEST_IN_PROCESS) == request) {
session.removeAttribute(REQUEST_IN_PROCESS);
getSynchronizationObject(session).notify();
}
}
}
/**
* Is this server currently processing another request for this session?
*
* @param session
* The request's session
* @return true if the server is handling another request for this session
*/
private boolean isRequestInProcess(HttpSession session) {
return session.getAttribute(REQUEST_IN_PROCESS) != null;
}
/**
* Wait for this server to finish with its current request so that it can
* begin processing our next request. This method also detects if its request
* is replaced by another request in the queue.
*
* @param request
* Wait for this request to be ready to run
* @return true if this request may be processed, or false if this request was
* replaced by another in the queue.
*/
private boolean waitForRelease(HttpServletRequest request) {
HttpSession session = request.getSession();
// wait for the currently running request to finish, or until this
// thread has waited the maximum amount of time
try {
getSynchronizationObject(session).wait(getMaxWaitTime(request));
}
catch (InterruptedException ie) {
return false;
}
// This request can be processed now if it hasn't been replaced
// in the queue
return request == session.getAttribute(REQUEST_QUEUE);
}
/**
* Put a new request in the queue. This new request will replace any other
* requests that were waiting.
*
* @param request
* The request to queue
*/
private void enqueueRequest(HttpServletRequest request) {
HttpSession session = request.getSession();
// Put this request in the queue, replacing whoever was there before
session.setAttribute(REQUEST_QUEUE, request);
// if another request was waiting, notify it so it can discover that
// it was replaced
getSynchronizationObject(session).notify();
}
/**
* What is the maximum wait time (in milliseconds) for this request
*
* @param request
* @return Maximum number of milliseconds to hold this request in the queue
*/
private long getMaxWaitTime(HttpServletRequest request) {
// look for a Pattern that matches the request's path
String path = request.getRequestURI();
Iterator patternIter = this.maxWaitDurations.keySet().iterator();
while (patternIter.hasNext()) {
Pattern p = (Pattern) patternIter.next();
Matcher m = p.matcher(path);
if (m.matches()) {
// this pattern matches. At most, how long can this request wait?
Long maxDuration = (Long) this.maxWaitDurations.get(p);
return maxDuration.longValue();
}
}
// If no pattern matches the path, return the default value
return DEFAULT_DURATION;
}
/**
* Look through the filter's configuration, and determine whether or not it
* should synchronize this request with others.
*
* @param httpRequest
* @return
*/
private boolean isFilteredRequest(HttpServletRequest request) {
// iterate through the exclude patterns. If one matches this path,
// then the request is excluded.
String path = request.getRequestURI();
Iterator patternIter = this.excludePatterns.iterator();
while (patternIter.hasNext()) {
Pattern p = (Pattern) patternIter.next();
Matcher m = p.matcher(path);
if (m.matches()) {
// at least one of the patterns excludes this request
return false;
}
}
// this path is not excluded
return true;
}
}