Listing of Source sesspool/SessionPoolingData.java
package se.entra.phantom.server;

import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.NoSuchElementException;
import javax.swing.JDesktopPane;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Element;
import se.entra.phantom.common.Utilities;

/**
 * This class holds the data used by all session
 * instances of a pool. It can be extended to provide
 * more pool data or functionality. The class is then
 * instanciated by the implementor class extending
 * from <code>DefaultSessionPoolingHandler</code>
 * in the static method <code>createSessionPoolingData</code>.
 */
public class SessionPoolingData implements Runnable
{
  //////////////////////////////////////////////////////////
  /// The script names, internal functions and "defines" ///
  //////////////////////////////////////////////////////////

  /**
   * The array contains the script names in the index order of the
   * table of <code>Element</code> for the scripts. The names are
   * case sensitive, as well as the parameters.
   */
  public final String [] scriptNames =
    {
    "start",
    "ping",
    "check",
    "reclaim",
    "dispose"
    };

  /**
   * The names of the internal functions.
   * The method names are preceeded with "script".
   */
  public static final String [] functions =
    {
    "SCREEN"    ,
    "CONDITIONS",
    "IF"        ,
    "WHILE"     ,
    "BREAK"     ,
    "SET"       ,
    "SEND"      ,
    "RESET"     ,
    "WAIT"      ,
    "HOSTERROR" ,
    "ONERROR"   ,
    "LOG"       ,
    "TRACE"     ,
    "RETURN"    ,
    "DISPOSE"  
    };

  /**
   * The script method indexes: start.
   */
  public static final int SCRIPT_START = 0;

  /**
   * The script method indexes: ping.
   */
  public static final int SCRIPT_PING = 1;

  /**
   * The script method indexes: .
   */
  public static final int SCRIPT_CHECK = 2;

  /**
   * The script method indexes: reclaim.
   */
  public static final int SCRIPT_RECLAIM = 3;

  /**
   * The script method indexes: dispose.
   */
  public static final int SCRIPT_DISPOSE = 4;

  //////////////////////////////////////////
  /// The "final" class instance members ///
  //////////////////////////////////////////

  /**
   * The runtime ID used for this pool.
   */
  public final String runtimeID;

  /**
   * The host ID used (zero is 'A', i.e. (int)(char-'A')).
   */
  public final int hostID;

  /**
   * The minimum amount of sessions in the pool.
   */
  public final int minSessions;

  /**
   * The maximum amount of sessions in the pool.
   */
  public final int maxSessions;

  /**
   * The ping time in seconds (<=0 for none).
   */
  public final int pingTime;

  /**
   * The name of the XML script file.
   */
  public final String scriptFile;

  /**
   * The runtime application data (null if none is used).
   */
  public final PhantomRuntime runtime;

  /**
   * The pool implementing class.
   */
  public final Class<?> implClass;

  /**
   * The child nodes in the XML file data.
   */
  public final Node firstChild;

  /**
   * The JDesktopPane used for the Server GUI (null
   * if no GUI is displayed).
   */
  public final JDesktopPane desktopPane;

  /**
   * The pool name.
   */
  public final String poolName;

  //////////////////////////////
  /// Class instance members ///
  //////////////////////////////

  /**
   * This is the vector of all the sessions in the pool.
   *
   * <p>If is to be modified in some way, use it as a synchronization
   * object for thread safety.
   */
  private final Vector<DefaultSessionPoolingHandler> sessions = new Vector<DefaultSessionPoolingHandler>();

  /**
   * The enabled state of the pool (initially disabled).
   */
  private boolean isEnabled;

  /**
   * This flag is true if the pool is already disposed of.
   */
  private boolean isDisposed;

  /**
   * This flag is true if the runtime application must be
   * enabled at first start of the pool.
   */
  private boolean isEnableAppRequired;

  /**
   * The registered methods of the script. Each method
   * has the argument <code>SessionPoolingScriptData</code>.
   *
   * <p>The table contains names of the methods as the keys as an instance
   * of <code>Method</code> as the element.
   */
  private final Hashtable<String,Method> scriptMethods = new Hashtable<String,Method>();

  /**
   * The different script start nodes.
   * null values indicates no script assigned.
   *
   * <br> 0=start,
   * <br> 1=ping,
   * <br> 2=check,
   * <br> 3=reclaim,
   * <br> 4=dispose.
   */
  private Element scripts [] = new Element [5];

  /**
   * The different script maximum times in seconds.
   * -1 indicates no maximum time assigned.
   *
   * <br> 0=start,
   * <br> 1=ping,
   * <br> 2=check,
   * <br> 3=reclaim,
   * <br> 4=dispose.
   */
  private int maxTimes [] = new int [5];
  
  /**
   * The ping checker thread.
   */
  private Thread pingThread;

  ///

  /**
   * Creates the session pooling data instance.
   *
   * <p>This method cannot be overridden. For an extending class
   * containing extra data members, the <code>final</code> variables
   * <code>runtimeID</code>, <code>hostID</code>, 
   * <code>minSessions</code>, <code>maxSessions</code>, 
   * <code>pingTime</code>, <code>runtime</code>, 
   * <code>implClass</code>, <code>firstChild</code>,
   * <code>desktopPane</code> and <code>poolName</code>
   * must be initialized in the new constructor.
   */
  public SessionPoolingData(String runtimeID,
                            int hostID,
                            int minSessions,int maxSessions,
                            int pingTime,
                            String scriptFile,
                            PhantomRuntime runtime,
                            Class<?> implClass,
                            Node firstChild,
                            JDesktopPane desktopPane)
    {
    // Initialize final variables.
    this.runtimeID  =runtimeID;
    this.hostID     =hostID;
    this.minSessions=minSessions;
    this.maxSessions=maxSessions;
    this.pingTime   =pingTime;
    this.scriptFile =scriptFile;
    this.runtime    =runtime;
    this.implClass  =implClass;
    this.firstChild =firstChild;
    this.desktopPane=desktopPane;

    // Set the pool name.
    poolName="Pool: "+runtimeID+"."+((char)(hostID+'A'));

    // Find the scripts.
    if ( !getScriptTags() )
      logWarning("No script actions found in script file "+scriptFile);

    // Register all internal methods.
    registerInternalMethods();

    // Register all external methods.
    registerXMLScriptMethods();
    }

  ///////////////
  /// Helpers ///
  ///////////////

  /**
   * Adds a trace output for session pooling if client verbose trace is turned on.
   */
  protected void trace(String txt)
    {
    //System.out.println(" >> SESSION-POOLING: "+poolName+": "+txt);
    if ( ClientSessionManager.doClientVerboseTrace() )
      BinaryTrace.dump(null,"SESSION-POOLING: "+poolName+": "+txt);
    }

  /**
   * Logs an session pooling error event in the event log.
   */
  protected void logError(String txt)
    {
    EventManager.logEvent(EventID.EVENT_E_SessionPooling,poolName+": "+txt);
    }

  /**
   * Logs an session pooling warning event in the event log.
   */
  protected void logWarning(String txt)
    {
    EventManager.logEvent(EventID.EVENT_W_SessionPooling,poolName+": "+txt);
    }

  /**
   * Logs an session pooling informational event in the event log.
   */
  protected void logInfo(String txt)
    {
    EventManager.logEvent(EventID.EVENT_I_SessionPooling,poolName+": "+txt);
    }

  /////////////////////////
  /// Session functions ///
  /////////////////////////

  /**
   * Closes "nicely" or not all the started sessions in
   * the pool. The "nicely" flag indicates if the Dispose script
   * should run or not.
   *
   * <p>There is an inactivity timer that will kill the server
   * if the method <code>aliveNotification</code> in the
   * <code>ClientSessionManager csm</code> parameter that should
   * be called at intervals e.g. when a session is about to be
   * disposed.
   *
   * <p>This call blocks the caller thread until all sessions
   * are stopped.
   */
  @SuppressWarnings("unchecked")
  public final void closeAllSessions(ClientSessionManager csm,boolean nicely)
    {
    synchronized(sessions)
      {
      if ( isDisposed )
        return;

      trace("Close all sessions, nicely = "+nicely);

      Vector<DefaultSessionPoolingHandler> v=(Vector<DefaultSessionPoolingHandler>)sessions.clone();
      for ( Enumeration<DefaultSessionPoolingHandler> e=v.elements(); e.hasMoreElements(); )
        {
        DefaultSessionPoolingHandler handler=e.nextElement();
        if ( nicely && !isDisposed )
          handler.startDisposeThread(handler);
        else
          handler.dispose();
        }

      // If nicely, wait for all sessions to be closed.
      if ( nicely && !isDisposed )
        {
        if ( sessions.size()>0 )
          logInfo("Closing all sessions (running dispose script)");

        while ( sessions.size()>0 )
          {
          try { sessions.wait(); }
          catch(InterruptedException e) {}
          }
        }

      trace("Closed all sessions");
      }
    }

  /**
   * Disposes "nicely" or not all the started sessions in
   * the pool. The "nicely" flag indicates if the Dispose script
   * should run or not.
   *
   * <p>There is an inactivety timer that will kill the server
   * if the method <code>aliveNotification</code> in the
   * <code>ClientSessionManager csm</code> parameter that should
   * be called at intervals e.g. when a session is about to be
   * disposed.
   *
   * <p>This call blocks the caller thread until all sessions
   * are stopped.
   */
  public final void dispose(ClientSessionManager csm,boolean nicely)
    {
    synchronized(sessions)
      {
      if ( isDisposed )
        return;

      trace("Dispose of pool");

      isEnabled=false;
      closeAllSessions(csm,nicely);
      isDisposed=true;

      // Log event.
      logInfo("Disposed of all sessions");
    
      // Re-enable the application is required.
      if ( isEnableAppRequired )
        {
        isEnableAppRequired=false;
        PhantomRuntime rt=csm.getRuntimeApplication(runtimeID);
        if ( rt!=null && !rt.isEnabled() )
          {
          rt.setEnabled(true);
          trace("Enabled application "+runtimeID+" after session pool disposal");
          }
        }
      }
    }

  /**
   * Checks if a session pool is enabled or not.
   */
  public boolean isEnabled()
    {
    return isEnabled;
    }

  /**
   * Changes the enabled state of this pool.
   *
   * <p>When enabled, this includes starting the minimum
   * amount of sessions.
   */
  public final void setEnabled(boolean enable)
    {
    Thread thread;
    synchronized(sessions)
      {
      if ( enable==isEnabled )
        return;

      isEnabled=enable;
      thread=pingThread;
      pingThread=null;
      }

    String s="Pool enabled = "+enable;
    trace(s);
    logInfo(s);
   
    if ( enable )
      {
      // Create the minimum amount of sessions and start the ping thread.
      createMinimumSessions(true);
      if ( pingTime>0 )
        {
        pingThread=new ServerThread(this,"SPPinger");
        pingThread.start();
        }
      }
    else
      {
      // Re-enable the application is required.
      ClientSessionManager csm=ClientSessionManager.getServerAdminInterface();
      synchronized(sessions)
        {
        if ( isEnableAppRequired )
          {
          isEnableAppRequired=false;
          PhantomRuntime rt=csm.getRuntimeApplication(runtimeID);
          if ( rt!=null && !rt.isEnabled() )
            {
            rt.setEnabled(true);
            trace("Enabled application "+runtimeID+" due to session pool disabled state");
            }
          }
        }

      closeAllSessions(csm,true);
      if ( thread!=null )
        {
        // Wait until thread has exited.
        if ( thread.isAlive() )
          {
          trace("Waiting for Pinger thread to exit");
          try { thread.join(); }
          catch(InterruptedException e) {}
          trace("Pinger thread successfully stopped");
          }
        }
      }
    }

  /**
   * Creates the required minimum amount of sessions in a pool.
   *
   * <p>During creation of this minimum amount of sessions,
   * the runtime application will be disabled.
   *
   * <p>When the count of started sessions is reached, the
   * application will be release. This can be overridden
   * by an administrator to activate the application.
   */
  private void createMinimumSessions(boolean doDisableRT)
    {
    try
      {
      // Check if new sessions are required.
      int cc=sessions.size();
      if ( !isDisposed && isEnabled && cc<minSessions )
        {
        // Display progress.
        logInfo("Starting new sessions, pool must contain at least "+minSessions+" sessions, current amount "+cc);

        // Get the runtime application that should be used,
        // this might return null, thus no application is configured.
        ClientSessionManager csm=ClientSessionManager.getServerAdminInterface();
        if ( doDisableRT && cc==0 )
          {
          // Only check runtime enabling if no sessions in the
          // pool exist.
          PhantomRuntime rt=csm.getRuntimeApplication(runtimeID);
          if ( rt!=null )
            {
            // Save state of application, then disable it. When all
            // sessions (minimum amount) has been started, the application
            // is enabled.
            synchronized(sessions)
              {
              if ( !isEnableAppRequired )
                {
                isEnableAppRequired=rt.isEnabled();
                if ( isEnableAppRequired )
                  {
                  rt.setEnabled(false);
                  String s="Disabled application "+runtimeID+" while minimum amount of sessions are started";
                  trace(s);
                  logInfo(s);
                  }
                }
              }
            }
          }

        // Create all required instances.
        while ( sessions.size()<minSessions && !isDisposed && isEnabled )
          createSession(csm,false);
        }
      }
    catch(Exception e)
      {
      String s="Error creating new sessions in the pool: "+e;
      trace(s);
      logError(s);
      }
    }

  /**
   * Creates a single session.
   * 
   * @return the instance of the session pooling handler found (or created),
   *          or null if not possible.
   *
   * @throws  InstantiationException  for errors when creating the instance.
   * @throws  IllegalAccessException  for errors when creating the instance.
   */
  private DefaultSessionPoolingHandler createSession(ClientSessionManager csm,boolean inSameThread) throws InstantiationException, IllegalAccessException
    {
    trace("Create session - begin");

    DefaultSessionPoolingHandler handler=(DefaultSessionPoolingHandler)implClass.newInstance();
    sessions.addElement(handler);

    ClientConnectionData ccd=csm.createPooledSessionData(poolName);

    trace("Create session - initialize");
    handler.initialize(this,ccd);
    
    if ( inSameThread )
      {
      // Do the start in the same thread by calling the run method
      // directly.
      SessionPoolingScriptRunner runner=new SessionPoolingScriptRunner(handler,SCRIPT_START);
      runner.run();
      if ( !handler.isValidAndCheckSession() )
        {
        // Log error.
        trace("Failure starting new session");
        logError("Failure starting new session");
        
        // Make sure to dispose of it.
        if ( !handler.isDisposed() )
          handler.dispose();
        
        // In hopeless cases, make sure it's gone in the array.
        sessions.removeElement(handler);
        handler=null;
        }
      }
    else    
      startSessionThread(handler,ccd);

    trace("Create session - end (OK = "+(handler!=null)+")");
    return handler;
    }

  /**
   * Notifies that a session has started correctly.
   * This call will enable the application once all sessions
   * have started correctly.
   */
  public void sessionStarted()
    {
    synchronized(sessions)
      {
      if ( isEnabled && !isDisposed )
        {
        trace("Session has been started");
        if ( sessions.size()==minSessions )
          {
          // Display progress.
          String s="Pool reached required amount of minimum sessions "+minSessions;
          trace(s);
          logInfo(s);
          
          if ( isEnableAppRequired )
            {
            // Clear enable-app flag.
            isEnableAppRequired=false;

            // Get the runtime application that should be used,
            // this might return null, thus no application is configured.
            ClientSessionManager csm=ClientSessionManager.getServerAdminInterface();
            PhantomRuntime rt=csm.getRuntimeApplication(runtimeID);
            if ( rt!=null && !rt.isEnabled() )
              {
              rt.setEnabled(true);
              s="Enabled application "+runtimeID+" after starting minimum amount of sessions";
              logInfo(s);
              trace(s);
              }
            }
          }
        }
      }
    }

  /**
   * Notifies that a session has been disposed of. A waiting dispose-session
   * call will be notified when an element has been removed.
   */
  public final void sessionDisposed(DefaultSessionPoolingHandler handler)
    {
    synchronized(sessions)
      {
      // Remove the element and notify waiting threads.
      sessions.removeElement(handler);
      sessions.notifyAll();
      }
    
    // Check for minimum amount, but don't disable the runtime.
    createMinimumSessions(false);
    }
  
  /**
   * The session pooling ping thread.
   */
  @Override
  public void run()
    {
    for ( ;; )
      {
      // Wait 2 seconds, then check for enabled or disposed.
      try { Thread.sleep(2000); }
      catch(InterruptedException e) {}
      if ( !isEnabled || isDisposed )
        break;
      
      // Scan all sessions that could need a ping.
      try
        {
        for ( Enumeration<DefaultSessionPoolingHandler> e=sessions.elements(); e.hasMoreElements(); )
          {
          DefaultSessionPoolingHandler handler=e.nextElement();
          handler.checkPingRequired();
          }
        }
      catch(NoSuchElementException e) {}
      }
    }

  /**
   * Starts the execution of the Start Session thread.
   */
  private void startSessionThread(DefaultSessionPoolingHandler handler,ClientConnectionData ccd)
    {
    SessionPoolingScriptRunner runner=new SessionPoolingScriptRunner(handler,SCRIPT_START);
    new ServerThread(ccd,runner,"SPStart").start();
    }

  /**
   * Gets all the started sessions. The enumeration
   * contains <code>DefaultSessionPoolingHandler</code>
   * elements.
   *
   * @return  null  if the pool is disposed, otherwise
   *                 and enumeration of <code>DefaultSessionPoolingHandler</code>
   *                 elements.
   */
  public Enumeration<DefaultSessionPoolingHandler> getSessions()
    {
    if ( isDisposed )
      return null;

    return sessions.elements();
    }
  
  /**
   * Checks if a session should be reclaimed to the pool.
   */
  public boolean isReclaimRequired()
    {
    if ( isDisposed || !isEnabled )
      return false;
    
    return (sessions.size()<=maxSessions);
    }

  /**
   * Checks if there is a session pooling handler that can be used for this
   * client session.
   * 
   * @return  null if none is found.
   */
  public DefaultSessionPoolingHandler grabSessionFromPool()
    {
    // Disposed or disabled means no available sessions.
    if ( isDisposed || !isEnabled )
      return null;
    
    // Locate a session that can be used.
    try
      {
      for ( Enumeration<DefaultSessionPoolingHandler> e=sessions.elements(); e.hasMoreElements(); )
        {
        DefaultSessionPoolingHandler handler=e.nextElement();
        if ( handler.isValidAndCheckSession() )
          return handler;
        }
      }
    catch(NoSuchElementException e) {}
    
    // No session was available. Start a new one.
    String s="No pooled session is available of the "+sessions.size()+" created sessions, starting a new one";
    trace(s);
    logWarning(s);

    try
      {
      return createSession(ClientSessionManager.getServerAdminInterface(),true);
      }
    catch(Exception e)
      {
      s="Error starting new session: "+e;
      trace(s);
      logError(s);
      }

    // Return no one found!
    return null;
    }
  
  /**
   * Terminates a single session.
   * 
   * @return  false  if the session is not found or already disposed,
   *          true   if terminated.
   */
  public boolean terminateSession(long connectionID)
    {
    DefaultSessionPoolingHandler client=null;
    synchronized(sessions)
      {
      try
        {
        for ( Enumeration<DefaultSessionPoolingHandler> e=sessions.elements(); e.hasMoreElements(); )
          {
          DefaultSessionPoolingHandler handler=e.nextElement();
          if ( handler.getClientConnectionData().getConnectionID()==connectionID )
            {
            client=handler;
            break;
            }
          }
        }
      catch(NoSuchElementException e)
        {
        return false;
        }
      }
    
    // Check if found.
    if ( client==null )
      return false;
    
    // Do dispose of this client.
    if ( client.isDisposed() )
      return false;
    
    client.dispose();
    return true;
    }

  /////////////////////////////////////
  /// Method registration & look-up ///
  /////////////////////////////////////

  /**
   * Registers all internal methods in the implementing script class.
   */
  protected void registerInternalMethods()
    {
    trace("Registering internal methods - begin");
    for ( int ii=functions.length-1; ii>=0; --ii )
      {
      String name=functions[ii];
      registerScriptMethod(implClass,"script"+name,name);
      }
    trace("Registering internal methods - end");
    }

  /**
   * This function registers all script methods defined in the XML file.
   * Each method has the argument <code>SessionPoolingScriptData</code>.
   * The name of the method is case insensitive in the script, but not when
   * it is registered (the name must match a method in the declaring class).
   *
   * <p>If the class registering the method is not an extended class from
   * <code>DefaultSessionPoolingHandler</code>, then it must implement the
   * interface <code>SessionPoolingScriptMethodInterface</code> that
   * defines methods that will be called when the object is instantiated
   * for each individual session in the pool.
   *
   * @return  true  for successful registration, false otherwise (and
   *                 an event in the server log is created).
   */
  protected boolean registerXMLScriptMethods()
    {
    Node node=firstChild;
    while ( node!=null )
      {
      // Check if Element and "extensions".
      if ( node.getNodeName().equals("extensions") && (node instanceof Element) )
        {
        NodeList nodeList=node.getChildNodes();
        for ( int ii=0, cc=nodeList.getLength(); ii<cc; ++ii )
          {
          Node extensionNode=nodeList.item(ii);
          if ( extensionNode.getNodeName().equals("function") && (extensionNode instanceof Element) )
            {
            // Find the script name that matches, skip if already
            // defined.
            Element element=(Element)extensionNode;
            String name  =element.getAttribute("name"  );
            String clazz =element.getAttribute("class" );
            String method=element.getAttribute("method");
            try
              {
              Class<?> c;
              if ( clazz.length()==0 )
                {
                trace("Registering external function "+name+", method "+method);
                c=implClass;
                clazz=implClass.getName();
                }
              else
                {
                trace("Registering external function "+name+" in class "+clazz+" method "+method);
                c=Class.forName(clazz);
                }
              registerScriptMethod(c,method,name);
              }
            catch(Throwable e)
              {
              String s="Register method "+method+" failure: class "+clazz+", method name "+method+", function name "+name+": "+e;
              trace(s);
              logError(s);
              continue;
              }
            }
          }
        }

      // Try next one.
      node=node.getNextSibling();
      }

    return true;
    }

  /**
   * Gets the action scripts in a script file. These are the
   * "action name=nnn" tags under the "actions" main tag.
   *
   * @return  true  if all scripts are successfully registered.
   */
  protected boolean getScriptTags()
    {
    // First locate the "actions" tag.
    Node node=firstChild;
    while ( node!=null )
      {
      // Check if Element and "actions".
      if ( node.getNodeName().equals("actions") && (node instanceof Element) )
        {
        // Found the actions. Now find the "action" tags that
        // belongs to the "actions" tag.
        int found=0;
        NodeList nodeList=node.getChildNodes();
        for ( int ii=0, cc=nodeList.getLength(); ii<cc; ++ii )
          {
          Node actionNode=nodeList.item(ii);
          if ( actionNode.getNodeName().equals("action") && (actionNode instanceof Element) )
            {
            // Find the script name that matches, skip if already
            // defined.
            Element element=(Element)actionNode;
            String name=element.getAttribute("name");
            for ( int jj=scriptNames.length-1; jj>=0; --jj )
              {
              if ( name.equalsIgnoreCase(scriptNames[jj]) )
                {
                // Already taken? Skip it...
                if ( scripts[jj]!=null )
                  {
                  trace("Action "+name+" already assigned, skipping");
                  break;
                  }

                // Get the maximum time.
                int mt=-1;
                try { mt=Integer.parseInt(element.getAttribute("maxtime")); }
                catch(Exception e) {}

                // Check if there is a script element child node. If so,
                // use it...
                NodeList elementScripts=element.getElementsByTagName("script");
                if ( elementScripts.getLength()>0 )
                  {
                  Node scriptStart=elementScripts.item(0);
                  if ( scriptStart instanceof Element )
                    {
                    // Save script element node.
                    trace("Action "+name+" script "+scriptStart+" maxtime = "+mt);
                    scripts[jj]=(Element)scriptStart;
                    maxTimes[jj]=mt;

                    // Set script found...
                    ++found;
                    break;
                    }
                  }

                // No script found.
                trace("Action "+name+" didn't have a valid <script>");
                break;
                }
              }
            }
          }

        // Return OK if all "action" tags are found.
        return (found==scriptNames.length);
        }
      node=node.getNextSibling();
      }

    // Log event and return failure...
    logError("Script "+scriptFile+": <actions> tag not found");
    trace("Script "+scriptFile+": <actions> tag not found");
    return false;
    }

  /**
   * Gets a method from a script tag in the global pool data.
   * The name of the method is case insensitive.
   *
   * @return  null  if no method is found, otherwise the
   *                 <code>Method</code> instance.
   */
  public Method getScriptTagMethod(String name)
    {
    return scriptMethods.get(name.toUpperCase());
    }

  ////////////////////////
  /// Script execution ///
  ////////////////////////

  /**
   * Executes the specified script index (SCRIPT_nnn value).
   *
   * <p>If the script is not defined in the XML file, true
   * is returned.
   *
   * @return  true or false depending on the script.
   *
   * @throws  SessionPoolingDisposed  if the session is disposed.
   */
  public boolean executeScript(DefaultSessionPoolingHandler handler,int scriptIndex) throws SessionPoolingDisposed
    {
    // Log start event.
    String name=scriptNames[scriptIndex];
    trace("Script "+name+" started");

    boolean returnCode=true;
    try
      {
      Node parent=scripts[scriptIndex];
      if ( parent!=null )
        {
        // Set the maximum execution time.
        long maxTime=maxTimes[scriptIndex]*1000L;
        if ( maxTime>0 )
          maxTime+=System.currentTimeMillis();

        // Execute the script.
        SessionPoolingScriptData data=new SessionPoolingScriptData(handler,name,parent,maxTime);
        try
          {
          data.currentElement=data.getFirstElement(parent);
          data.executeRemainingElements();
          }
        catch(SessionPoolingScriptBreak e) {}
        catch(SessionPoolingScriptReturn e) {}

        // Set return code.
        returnCode=data.returnCode;
        }
      }
    catch(SessionPoolingScriptHostError e)
      {
      // Host error.
      trace("Script "+name+": unhandled host error (no onerror tag)");
      handler.disposeNow();
      }
    catch(SessionPoolingDisposed e)
      {
      // Log disposed event.
      trace("Script "+name+" disposed");
      handler.disposeNow();
      }
    catch(Throwable e)
      {
      // Log internal error event.
      trace("Script "+name+" internal error: "+e+":\n"+Utilities.getStackTrace(e));
      return false;
      }

    // Log stopped event.
    trace("Script "+name+" stopped, return code "+returnCode);
    return returnCode;
    }

  /**
   * This function registers a script method in the table. Each method
   * has the argument <code>SessionPoolingScriptData</code>.
   * The name of the method is case insensitive in the script, but not when
   * it is registered (the name must match a method in the declaring class).
   *
   * <p>If the class registering the method is not an extended class from
   * <code>DefaultSessionPoolingHandler</code>, then it must implement the
   * interface <code>SessionPoolingScriptMethodInterface</code> that
   * defines methods that will be called when the object is instantiated
   * for each individual session in the pool.
   *
   * @return  true  for successful registration, false otherwise (and
   *                 an event in the server log is created).
   */
  public boolean registerScriptMethod(Class<?> clazz,String methodName,String scriptName)
    {
    try
      {
      Class<?> [] params={ SessionPoolingScriptData.class };
      Method method=clazz.getDeclaredMethod(methodName,params);
      scriptName=scriptName.toUpperCase();
      if ( scriptMethods.get(scriptName)!=null )
        {
        String s="Register duplicate method "+methodName+": class "+clazz.getName()+", function name "+scriptName;
        trace(s);
        logError(s);
        return false;
        }
      scriptMethods.put(scriptName,method);
      return true;
      }
    catch(Exception e)
      {
      String s="Register method "+methodName+" failure: class "+clazz.getName()+", function name "+scriptName+": "+e;
      trace(s);
      logError(s);
      return false;
      }
    }
}