We have used a filter and synchronizing on the session as others have
suggested.  Here is the code which we found sometime ago.  I believe
we made minor modifications to the author's original code but its been
so long ...

HTH

sean

/*
 * (c) 2004, Kevin Chipalowsky ([EMAIL PROTECTED]) and
 * Ivelin Ivanov ([EMAIL PROTECTED])
 *
 * Released under terms of the Artistic License
 * http://www.opensource.org/licenses/artistic-license.php
 */

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;

import org.apache.log4j.Logger;

/**
 * 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>
 *   <li>Requests wait a maximum of 5 seconds, which can be overridden
 *       per URI pattern in the filter's configuration.</li>
 * </ul>
 *
 * @author Kevin Chipalowsky and Ivelin Ivanov
 */
public class RequestControlFilter implements Filter
{

  // initialize logging
  private static final Logger log =
Logger.getLogger(RequestControlFilter.class);

  /**
   * Initialize this filter by reading its configuration parameters
   *
   * @param config  Configuration from web.xml file
   * @throws ServletException
   */
  public void init( FilterConfig config ) throws ServletException
  {
    log.debug("filter initializing");

    // parse all of the initialization parameters, collecting the exclude
    // patterns and the max wait parameters
    Enumeration enum = config.getInitParameterNames();
    excludePatterns = new LinkedList();
    maxWaitDurations = new HashMap();
    while( enum.hasMoreElements() )
    {
      String paramName = ( String )enum.nextElement();
      String paramValue = config.getInitParameter( paramName );
      if( paramName.startsWith( "excludePattern" ) )
      {
        // compile the pattern only this once
        Pattern excludePattern = Pattern.compile( paramValue );
        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 );
        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.
   *
   * @param request ServletRequest
   * @param response ServletResponse
   * @param chain FilterChain
   * @throws IOException
   * @throws ServletException
   */
  public void doFilter(
    ServletRequest request,
    ServletResponse response,
    FilterChain chain )
    throws IOException, ServletException
  {
    HttpServletRequest httpRequest = (HttpServletRequest)request;
    HttpSession session = httpRequest.getSession();

    // 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 HttpSession -
   * @return Object
   */
  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 HttpServletRequest
   */
  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 HttpServletRequest -
   * @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 = 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)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 request HttpServletRequest
   * @return boolean
   */
  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 = 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;
  }

  /** 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;
}

Reply via email to