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

import java.awt.EventQueue;
import java.awt.Point;
import java.io.IOException;
import java.lang.reflect.Method;
import javax.swing.JDesktopPane;
import javax.swing.JInternalFrame;
import javax.swing.JLayeredPane;
import javax.swing.WindowConstants;
import javax.swing.event.InternalFrameAdapter;
import javax.swing.event.InternalFrameEvent;
import javax.swing.event.InternalFrameListener;
import org.w3c.dom.Element;

import se.entra.phantom.common.TerminalKeys;
import se.entra.phantom.common.Utilities;
import se.entra.phantom.server.rconsole.AdminConfigUser;
import se.entra.phantom.server.rconsole.AdminConfigUsers;

/**
 * When a session is started, "pinged", checked for validity, reclaimed or is about
 * to be disposed, the DefaultSessionPoolingHandler reads what to do for the action
 * in question. This is done in a script definition file. The script definition file
 * is written in XML as described below.
 *
 * <p>Each session pool can have a Phantom Runtime file associated with it. This eases
 * the interaction with the host system, to check for matching screens, use host fields,
 * etc. The Runtime file is only used for screen identification and host field processing,
 * i.e. no panels or objects (REXX or other) are used.
 *
 * <p><i>Note: In the JavaDoc of the class, tags are written as (tagname [params]/)
 * or (/tagname) for better readability in the code, instead of &lt;tagname [params]/&gt;
 * or &lt;/tagname&gt;.</i>
 *
 * <p><b>File Format</b>
 *
 * <p>The file must have the following format (note that XML is case sensitive as opposed
 * to HTML):
 * <pre>
 *   (?xml version="1.0"?)
 *
 *   (SessionPoolingScript
 *     version="1.00"
 *     implclass="se.entra.phantom.server.DefaultSessionPoolingHandler"
 *     runtimefile="samples/tutor/TUTOR.NPR")
 *
 *   (extensions)
 *     (function name="SaveGlobalHostData"
 *               method="saveGlobalHostData"
 *               class="com.acme.SessionPool"/)
 *
 *     (function name="YourHostCheck" method="yourHostCheck"/)
 *   (/extensions)
 *
 *   (actions)
 *     (action name="START" maxtime="seconds")
 *       (script)
 *         ... the script here ...
 *       (/script)
 *     (/action)
 *
 *     (action name="PING" maxtime="seconds")
 *       (script)
 *         ... the script here ...
 *       (/script)
 *     (/action)
 *
 *     (action name="CHECK" maxtime="seconds")
 *       (script)
 *         ... the script here ...
 *       (/script)
 *     (/action)
 *
 *     (action name="RECLAIM" maxtime="seconds")
 *       (script)
 *         ... the script here ...
 *       (/script)
 *     (/action)
 *
 *     (action name="DISPOSE" maxtime="seconds")
 *       (script)
 *         ... the script here ...
 *       (/script)
 *     (/action)
 *   (/actions)
 *   (/SessionPoolingScript)
 * </pre>
 *
 * Any main tags not recognized are not processed. The action tag must contain a
 * script tag describing what "instructions" are to be processed. For the action tag,
 * a parameter maxtime can be specified in seconds. If the script has not completed
 * within this time limit, the session is considered invalid and is disposed of (not
 * calling the script of the dispose action).
 *
 * <p>The <code>DefaultSessionPoolingHandler</code> is a class provided as a default handler
 * for the session pooling. It provides by default the option to add external
 * functions in new classes (using the extensions section in the XML file). This is
 * done using <i>Custom Method Calls</i> (see the documentation in PDF form).
 *
 * <p>An extending Java class can also override it in order to provide a higher or more
 * specialized functionality. For more advanced processing, the script tag handling can
 * be overridden.
 */
public class DefaultSessionPoolingHandler extends HostSessionManagerAdapter
{
  ////////////////////////
  /// States constants ///
  ////////////////////////

  /**
   * State indicating that the thread that will perform the Start script
   * is about to start executing.
   */
  public static final int STATE_STARTING   =  0;

  /**
   * State indicating that the Start script is executing.
   */
  public static final int STATE_START      =  1;

  /**
   * State indicating that the session is started and ready (since last
   * Start, Ping or Reclaim).
   */
  public static final int STATE_READY      =  2;

  /**
   * State indicating that the thread that will perform the Ping script
   * is about to start executing.
   */
  public static final int STATE_PINGING    =  3;

  /**
   * State indicating that the Ping script is executing.
   */
  public static final int STATE_PING       =  4;

  /**
   * State indicating that the Check script is executing.
   */
  public static final int STATE_CHECK      =  5;

  /**
   * State indicating that this session is in use by an application.
   */
  public static final int STATE_INUSE      =  6;

  /**
   * State indicating that the thread that will perform the Reclaim script
   * is about to start executing.
   */
  public static final int STATE_RECLAIMING =  7;

  /**
   * State indicating that the Reclaim script is executing.
   */
  public static final int STATE_RECLAIM    =  8;

  /**
   * State indicating that the session is about to be disposed
   * (i.e. the thread that will perform the Disposing script
   * is about to start executing or is executing).
   */
  public static final int STATE_DISPOSING  =  9;

  /**
   * State indicating that the session is disposed.
   */
  public static final int STATE_DISPOSED   = 10;

  /**
   * The string represenation of the numerical STATE_nnn in english.
   * The array is not made "final" if it needs to contain another language.
   */
  public static String [] stringStates =
    {
    "Starting",
    "Start",
    "Ready",
    "Ping",
    "Pinging",
    "Check",
    "In use",
    "Reclaiming",
    "Reclaim",
    "Disposing",
    "Disposed"
    };

  /**
   * To cascade the internal frames.
   */
  private static int windowCount;

  //////////////////////
  /// Static methods ///
  //////////////////////

  /**
   * Creates and initializes a pool.
   *
   * <p>This method is overridable in order to create an instance
   * of an extended class of <code>SessionPoolingData</code>.
   *
   * <p>If an error occurs in e.g. the script file, the calling
   * party is responsible for logging the event.
   *
   * @return  the original data, must be overridden in order
   *           to return an instance of an extended 
   *           <code>SessionPoolingData</code> class instance.
   */
  public static SessionPoolingData createSessionPoolingData(SessionPoolingData original)
    {
    // Create the session data.
    return original;
    }

  /////////////////////
  /// Instance data ///
  /////////////////////

  /**
   * Flag set if session is disposed by the system and should
   * abort any scripting thread executing.
   */
  protected boolean isDisposed;

  /**
   * The pool data shared by all sessions in the same pool.
   */
  protected SessionPoolingData globalPoolData;

  /**
   * The client connection data for this session.
   */
  protected ClientConnectionData clientConnectionData;

  /**
   * The state variable, by default STATE_STARTING.
   */
  protected int currentState;

  /**
   * The internal frame window.
   */
  protected JInternalFrame iframe;

  /**
   * The time in milliseconds when an action completed,
   * i.e. finished starting or pinging.
   */
  protected long completeTime;

  /**
   * The host session manager can handle multiple host sessions,
   * but only one is the current one that notifies the listener
   * of events.
   */
  protected HostSessionManager hostSessionManager;

  /**
   * The screen that currently matches (null when none single-matches).
   */
  protected PhantomHostScreen currentScreen;

  /**
   * Flag indicating that the host screen has changed.
   */
  protected boolean hasHostScreenChanged;

  /**
   * Flag indicating that the host cursor position has changed.
   */
  protected boolean hasHostCursorChanged;

  /**
   * Flag indicating that the host fields has changed.
   */
  protected boolean hasHostFieldsChanged;

  /**
   * Flag indicating that any host change has occured.
   */
  protected boolean hasHostChanged;

  /**
   * The current timeout in milliseconds.
   */
  protected int timeoutValue = 60000;


  ///////////////////////////////////////////////////
  /// Constructor, initializer & access functions ///
  ///////////////////////////////////////////////////

  /**
   * The constructor does nothing at all.
   */
  public DefaultSessionPoolingHandler()
    {
    // Nothing!
    }

  /**
   * Called to initialize this instance with the global
   * pool data.
   */
  public void initialize(SessionPoolingData globalPoolData,ClientConnectionData clientConnectionData)
    {
    this.globalPoolData=globalPoolData;
    this.clientConnectionData=clientConnectionData;
    }

  /**
   * Gets the client connection data for this session.
   */
  public ClientConnectionData getClientConnectionData()
    {
    return clientConnectionData;
    }

  /**
   * Gets the global pooling data.
   */
  public SessionPoolingData getGlobalPoolData()
    {
    return globalPoolData;
    }

  /**
   * 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 globalPoolData.getScriptTagMethod(name);
    }

  ///////////////////////////
  /// Disposing functions ///
  ///////////////////////////

  /**
   * Sets the disposed flag and throws the SessionPoolingDisposed exception.
   *
   * @throws  SessionPoolingDisposed  if the session has been disposed of.
   */
  public final void checkDisposed() throws SessionPoolingDisposed
    {
    if ( isDisposed )
      {
      trace("Thread exception thrown");
      throw new SessionPoolingDisposed("Script disposed");
      }
    }

  /**
   * Checks if the session is disposed of.
   *
   * @return  true if the session has been disposed of.
   */
  public final boolean isDisposed()
    {
    return isDisposed;
    }

  /**
   * Disposes of the session due to a fatal error.
   * This will disable the current pool and thus cause it
   * not to start further sessions.
   *
   * @throws  SessionPoolingDisposed  always.
   */
  public void disposeFatal() throws SessionPoolingDisposed
    {
    globalPoolData.setEnabled(false);
    disposeNow("Fatal error dispose");
    }

  /**
   * Disposes of the session (e.g. after an error) and throws
   * the <code>SessionPoolingDisposed</code> exception.
   *
   * @throws  SessionPoolingDisposed  always.
   */
  public void disposeNow() throws SessionPoolingDisposed
    {
    disposeNow(null);
    }

  /**
   * Disposes of the session (e.g. after an error) and throws
   * the <code>SessionPoolingDisposed</code> exception with the
   * message <code>exceptionMsg</code>.
   *
   * @throws  SessionPoolingDisposed  always.
   */
  public void disposeNow(String exceptionMsg) throws SessionPoolingDisposed
    {
    // Check if session is disposed of and if so, just throw
    // the exception.
    synchronized(this)
      {
      if ( isDisposed )
        throw new SessionPoolingDisposed();
      }

    // Do the dispose.
    dispose();
    throw new SessionPoolingDisposed();
    }

  /**
   * Call to dispose promptly of the session, without executing scripts, etc.
   * It sets the disposed flag. This should terminate any scripting threads
   * as soon as possible. The host session(s) started for this pool session
   * are also disconnected.
   *
   * <p>This method MUST call the <code>SessionPoolingData
   * sessionDisposed</code> method.
   *
   * <p>If the session already is disposed of, nothing will happen.
   */
  public void dispose()
    {
    synchronized(this)
      {
      if ( isDisposed )
        {
        trace("Dispose called, but session already disposed");
        return;
        }
      isDisposed=true;
      currentState=STATE_DISPOSED;
      }

    // Close all host sessions.
    HostSessionManager manager=hostSessionManager;
    if ( manager!=null )
      {
      manager.bindHostSessionManagerListener(null);
      manager.disconnectAll();
      }

    // Remove the GUI window if present.
    removeWindow();

    // Let the pool know...
    globalPoolData.sessionDisposed(this);
    logInfo("Session disposed");
    trace("Session disposed");
    }

  ////////////////////////
  /// States functions ///
  ////////////////////////

  /**
   * Gets the current state. See the STATE_nnn values.
   */
  public int getCurrentState()
    {
    return currentState;
    }

  /**
   * Gets the string representation of the current state as set
   * by the <code>stringStates</code> string array.
   */
  public String getCurrentStringState()
    {
    return stringStates[currentState];
    }

  /**
   * Checks if this session is valid and the check script has just completed successfully.
   */
  public boolean isValidAndCheckSession()
    {
    synchronized(this)
      {
      // Check for ready state and check session.
      if ( currentState==STATE_READY && runCheckScript() )
        {
        // Set session in use...
        currentState=STATE_INUSE;
        }
      else
        return false;
      }

    // Session is available.
    logInfo("Client session will use pooled session ID "+getClientConnectionData().getConnectionID());
    return true;
    }
  
  /**
   * Attaches this session to a started client session.
   * This will hide the window of the session pool until
   * the client session is disposed.
   */
  public void attachSession(ClientSession cs)
    {
    // Hide any GUI window is displayed.
    JInternalFrame iframe=this.iframe;
    if ( iframe!=null )
      iframe.setVisible(false);
    
    // Transfer host sessions between the managers.
    cs.getHostSessionManager().transferHostSessions(hostSessionManager);
    }
  
  /**
   * Detaches a client session from this pooled session.
   * This will restore the host sessions and start running
   * the reclaim or dispose scripts.
   */
  public void detachSession(ClientSession cs)
    {
    // Check for ready state and check session.
    synchronized(this)
      {
      if ( currentState!=STATE_INUSE )
        {
        logError("detachSession was called, but the session was not in use!");
        return;
        }
      
      // Transfer the host sessions back to us...
      hostSessionManager.transferHostSessions(cs.getHostSessionManager());
      }
    
    // Show any GUI window is displayed.
    JInternalFrame iframe=this.iframe;
    if ( iframe!=null )
      iframe.setVisible(true);
    
    // Check if session should be returned to pool with reclaim or disposed.
    synchronized(this)
      {
      if ( globalPoolData.isReclaimRequired() )
        {
        currentState=STATE_RECLAIMING;
        logInfo("Client session detaching from pooled session "+getClientConnectionData().getConnectionID()+" with action: reclaim to pool");
        startReclaimThread();
        }
      else
        {
        logInfo("Client session detaching from pooled session "+getClientConnectionData().getConnectionID()+" with action: discard from pool");
        if ( !isDisposed )
          dispose();
        }
      }
    }

  /**
   * Starts execution of the Reclaim thread. Before calling this method, the <code>currentState</code>
   * variable should be set to STATE_RECLAIMING.
   */
  public void startReclaimThread()
    {
    SessionPoolingScriptRunner runner=new SessionPoolingScriptRunner(this,SessionPoolingScriptRunner.SCRIPT_RECLAIM);
    new ServerThread(getClientConnectionData(),runner,"SPReclm").start();
    }

  /**
   * Starts execution of the Dispose thread.
   */
  public void startDisposeThread(DefaultSessionPoolingHandler handler)
    {
    synchronized(this)
      {
      // Only dispose when doing nothing...
      if ( currentState==STATE_READY )
        {
        currentState=STATE_DISPOSING;
        SessionPoolingScriptRunner runner=new SessionPoolingScriptRunner(this,SessionPoolingScriptRunner.SCRIPT_DISPOSE);
        new ServerThread(getClientConnectionData(),runner,"SPDisp").start();
        return;
        }
      }
    
    // Otherwise dispose.
    dispose();
    }

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

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

  /**
   * Adds a trace output for session pooling if client verbose trace is turned on.
   */
  public void trace(String txt,String [] moreTxt)
    {
    if ( clientConnectionData.doClientVerboseTrace() )
      {
      // Build new array...
      int cc=moreTxt.length;
      String [] s=new String [cc+1];
      s[0]="SESSION-POOLING: "+globalPoolData.poolName+", script "+globalPoolData.scriptFile+": "+txt;
      for ( ; cc>0; --cc )
        s[cc]="  "+moreTxt[cc-1];

      BinaryTrace.dump(clientConnectionData,s);
      }
    }

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

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

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

  //////////////////////
  /// Ping functions ///
  //////////////////////

  /**
   * Sets the time for completion of a start or ping action.
   */
  public void setCompleteTime()
    {
    completeTime=System.currentTimeMillis();
    }

  /**
   * Checks if a session ping is required for this session
   * and if so this operation is initiated.
   *
   * <p>This will not be performed if there is no Ping script
   * defined in the XML file.
   *
   * <p>If the session is busy or in use by an application,
   * false is returned.
   *
   * @return  true  if a ping operation has begun, false otherwise.
   */
  public boolean checkPingRequired()
    {
    synchronized(this)
      {
      // Skip if not ready (not in use also).
      if ( currentState!=STATE_READY )
        return false;

      // Check last ping time.
      long interval=globalPoolData.pingTime*1000L;
      if ( interval==0L )
        return false;
      if ( System.currentTimeMillis()-completeTime<interval )
        return false;

      // Initiate the ping operation.
      currentState=STATE_PINGING;
      SessionPoolingScriptRunner runner=new SessionPoolingScriptRunner(this,SessionPoolingScriptRunner.SCRIPT_PING);
      new ServerThread(clientConnectionData,runner,"SPPing").start();
      }
    return true;
    }

  /////////////////////////////////////////////
  /// Host initialization and GUI functions ///
  /////////////////////////////////////////////

  /**
   * Removes the GUI window if present.
   */
  private void removeWindow()
    {
    // Dispose of the window (if any).
    JInternalFrame f=this.iframe;
    iframe=null;
    if ( f!=null )
      {
      trace("Disposing of GUI window");

      // The desktop pane can be null if not yet added to
      // the desktop pane, e.g. when the host session cannot
      // be created.
      Utilities.disposeInternalFrame(f);
      trace("GUI window disposed");
      }
    }

  /**
   * Creates the host session manager and a GUI window (if server
   * runs with GUI). The host session is also started.
   */
  @SuppressWarnings("deprecation")
  protected void createHostSessionManager()
    {
    trace("Create session - BEGIN");

    // Create the child window if required.
    final JDesktopPane desktopPane=globalPoolData.desktopPane;
    if ( desktopPane!=null )
      {
      trace("Creating GUI window");

      // Call dispose of server when closing the window.
      InternalFrameListener onClose=new InternalFrameAdapter()
        {
        @Override
        public void internalFrameClosing(InternalFrameEvent e)
          {
          if ( !isDisposed )
            {
            logInfo("GUI window closed by operator");
            trace("GUI window closed by operator");
            }
          dispose();
          }
        };

      // Creation below needs 'iframe' before 'window', because
      // otherwise members are null.
      iframe=new JInternalFrame(globalPoolData.poolName+" - Pooled Session "+getClientConnectionData().getConnectionID(),true,true,true,true);
      iframe.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
      iframe.addInternalFrameListener(onClose);
      }

    // Create the host session manager.
    trace("Creating HostSessionManager");
    try
      {
      hostSessionManager=new HostSessionManager(this,iframe);
      }
    catch(IOException e)
      {
      String s="CREATE HOST SESSION MANAGER FAILURE: "+e.toString();
      trace(s);
      logError(s);
      onHostSessionFailure(s);
      return;
      }

    // Display the window.
    final JInternalFrame f=iframe;
    if ( f!=null )
      {
      EventQueue.invokeLater(new Runnable()
        {
        @Override
        public void run()
          {
          windowCount=(windowCount+1)%100;
          f.setBounds(20*(windowCount%10-windowCount/10),20*(windowCount%10),700,450);
          desktopPane.add(iframe,JLayeredPane.DEFAULT_LAYER);
          f.moveToFront();
          f.validate();
          f.repaint();
          f.setVisible(true);
          f.requestDefaultFocus();
          }
        });
      }

    // Bind HostSessionManager to us.
    hostSessionManager.bindHostSessionManagerListener(this);

    // Start connection.
    hostSessionManager.setCurrentSession(globalPoolData.hostID);

    trace("Create session - END");
    }

  //////////////////////
  /// Host Functions ///
  //////////////////////

  /**
   * This function clears any outstanding errors and returns the current
   * host session. If the error cannot be cleared, a host error is thrown.
   *
   * @return  the current host session.
   *
   * @throws  SessionPoolingScriptHostError  if there is no current host session
   *                                         or if there is a host error.
   */
  public HostSession getCurrentClearedHostSession() throws SessionPoolingScriptHostError
    {
    // Get current host session.
    HostSession hostSession=hostSessionManager.getCurrentSession();
    if ( hostSession==null )
      throw new SessionPoolingScriptHostError();

    if ( !hostSession.isConnected() || hostSession.hasError() )
      throw new SessionPoolingScriptHostError();

    // Do get the current matching screen (forces updates of host fields and screen + 5250 err msg).
    getSingleMatchingScreen();

    // Return the current host session.
    return hostSession;
    }

  /**
   * Process host field refresh, pop-up window analysis
   * and screen identification after a host screen refresh.
   */
  private void processHostRefreshed(boolean doFields,boolean doScreen)
    {
    HostScreen screen=hostSessionManager.getScreen();
    if ( doFields )
      hostSessionManager.refreshHostFields();

    // When screen is updated or host fields are:
    //  - re-analyze the host pop-up windows.
    //  - re-check the matching screen.
    if ( doFields || doScreen )
      {
      // Analyze pop-up windows.
      screen.analyzePopupWindows();

      // Re-check screens. Check if no runtime is defined.
      PhantomRuntime rt=globalPoolData.runtime;
      if ( rt==null )
        return;

      // Find the new matching screen: not in synchronized part, because it may take a while...
      PhantomHostScreen newScreen=rt.getHostData().getMatchingScreen(screen);

      // Check for change in screen and for AS/400 system error message in a pop-up window.
      if ( newScreen!=null && newScreen.isPopup() )
        {
        HostSession hs=hostSessionManager.getCurrentSession();
        if ( hs!=null && !hs.is3270() && hs.hasError() )
          {
          String msg=screen.getStringRelative(1,newScreen.getHeight()-2,newScreen.getWidth()-2).trim();
          if ( msg.length()>0 && !msg.equals(hs.getLastError(false)) )
            hs.setLastError(msg);
          }
        }

      // Set the current screen.
      currentScreen=newScreen;
      }
    }

  /**
   * Waits for some data on a screen after a new connection. The maximum wait time
   * between two host events is 20 seconds. If the <code>data</code> parameter is null,
   * disposal of script is not checked, but disposal of the session is.
   *
   * @throws  SessionPoolingDisposed  when the connection fails.
   */
  public void waitInitialScreen(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    HostScreen screen=hostSessionManager.getScreen();
    HostSession hostSession=hostSessionManager.getCurrentSession();
    if ( hostSession==null )
      throw new SessionPoolingDisposed("No current host session");

    synchronized(this)
      {
      for ( ;; )
        {
        // Check for disposal.
        if ( data==null )
          checkDisposed();
        else
          data.checkScriptDisposed();

        // If connected and not connecting and the screen is non-empty, we're done.
        getSingleMatchingScreen();
        if ( hostSession.isConnected() && !hostSession.isConnecting() && !screen.isEmpty() )
          break;

        // Check if not connected and not connecting...
        if ( !hostSession.isConnected() && !hostSession.isConnecting() )
          throw new SessionPoolingDisposed("Host session disconnected");

        // Wait max 20 seconds for a change.
        try { wait(20000); }
        catch(InterruptedException e) {}
        }
      }
    }

  /**
   * Rematches the current matching screens and returns the single matching screen.
   * This function only performs the matching if there has been some kind of host change
   * that affects the match process since the last call.
   *
   * @return  null  if no screen or multiple screens match, otherwise the single matching screen.
   */
  public PhantomHostScreen getSingleMatchingScreen()
    {
    synchronized(this)
      {
      boolean doFields=hasHostFieldsChanged;
      boolean doScreen=hasHostScreenChanged;
      hasHostFieldsChanged=false;
      hasHostScreenChanged=false;
      if ( doFields || doScreen )
        processHostRefreshed(doFields,doScreen);
      }
    return currentScreen;
    }

  /**
   * Waits a certain time for a condition to be true.
   *
   * <p>If the parameter <code>data</code> is non-null, the following parameters are
   * extracted from the <code>data.currentElement</code> tag:
   *
   * <pre>
   *   screen="SCREENNAME" [multiple] [negate]
   * </pre>
   * or
   * <pre>
   *   hosttext="text" [nocase] [negate] column="xx" row="yy" [relative] [width="nn" [wildcard]]
   * </pre>
   * or
   * <pre>
   *   hostfield="NAME" text="text" [nocase] [negate] [line="nn"] [wildcard]
   * </pre>
   *
   * Any of the combinations of the above parameters are allowed, e.g. waiting for a screen and a host field,
   * a screen and a host text.
   *
   * <p>The wildcard match option enables text to be matched with Windows-like wildcards:
   * <br> ? is any character,
   * <br> * is any number of characters,
   * <br> ^ is the escape character, i.e. the meaning of '?' and '*' becomes the
   *        actual character that directly follows,
   * <br>^^ two escape characters becomes the escape character itself ('^').
   *
   * @return  true  if the condition(s) are met, false otherwise.
   *
   * @throws  SessionPoolingDisposed         when a script has been disposed.
   * @throws  SessionPoolingScriptHostError  when a host error is encountered.
   */
  public boolean hostWait(SessionPoolingScriptData data,boolean hostlock,boolean hoststable,int time,int timeout) throws SessionPoolingDisposed
    {
    boolean multiple=false,wildcard=false,nocase=false,relative=false;
    boolean doNegate=false;
    String text=null,hostText=null;
    PhantomHostScreen phantomScreen=null;
    PhantomHostField hostField=null;
    int col=0,row=0,width=-1,line=-1;

    // Get the current matching screen.
    PhantomHostScreen singleMatchScreen=getSingleMatchingScreen();

    if ( data!=null )
      {
      // Set the negate option.
      doNegate=data.hasElementAttribute("multiple");
               
      ///
      /// Check for screen definition.
      ///
      String s=data.getParamString("screen");
      if ( s.length()>0 )
        {
        // Get the screen in the runtime file.
        PhantomRuntime rt=globalPoolData.runtime;
        if ( rt==null )
          {
          s="Waiting for screen "+s+" error, runtime file not loaded: no screen is assumed";
          data.trace(s);
          data.logWarning(s);
          }
        else
          {
          // Find the host screen and get the multiple option.
          phantomScreen=rt.getHostData().getScreen(s);
          if ( phantomScreen==null )
            {
            s="Waiting for screen "+s+" error, screen name not found: no screen is assumed";
            data.trace(s);
            data.logWarning(s);
            }
          else
            multiple=data.hasElementAttribute("multiple");
          }
        }

      ///
      /// Check for hostfield definition.
      ///
      s=data.getParamString("hostfield");
      if ( s.length()>0 )
        {
        PhantomHostScreen hfScreen=phantomScreen;
        if ( hfScreen==null && singleMatchScreen==null )
          {
          s="Waiting for hostfield "+s+", but no screen matches: no field assumed";
          data.trace(s);
          data.logWarning(s);
          }
        else
          {
          // Find the host field to match for.
          if ( hfScreen==null )
            hfScreen=singleMatchScreen;
          hostField=hfScreen.getHostField(s.toUpperCase());
          if ( hostField==null )
            {
            s="Waiting for hostfield "+s+" on screen "+hfScreen.getName()+", but host field is not found: no field assumed";
            data.trace(s);
            data.logWarning(s);
            }
          else
            {
            text=data.getParamString("text");
            line=data.getParamInt("line")-1;
            if ( line<0 ) line=-1;
            nocase=data.hasElementAttribute("nocase");
            wildcard=data.hasElementAttribute("wildcard");
            }
          }
        }

      ///
      /// Check for host text definition.
      ///
      s=data.getParamString("hosttext");
      if ( s.length()>0 )
        {
        hostText=s;
        col=data.getParamInt("column")-1;
        if ( col<0 ) col=0;
        row=data.getParamInt("row")-1;
        if ( row<0 ) row=0;
        nocase=data.hasElementAttribute("nocase");
        relative=data.hasElementAttribute("relative");
        width=data.getParamInt("width");
        wildcard=(width>0 && data.hasElementAttribute("wildcard"));
        }
      }

    ///
    /// Check for just wait (no host operations involved)...
    ///
    if ( text==null && hostText==null && phantomScreen==null && hostField==null && !hostlock && !hoststable )
      {
      if ( time>0 )
        {
        try { Thread.sleep(time); }
        catch(InterruptedException e) {}
        }
      return true;
      }

    // Get the current host session, no match if none...
    HostSession hostSession=hostSessionManager.getCurrentSession();
    if ( hostSession==null )
      return false;

    // Check for no-case-sensitive for comparisons.
    if ( nocase )
      {
      if ( text    !=null ) text    =text    .toUpperCase();
      if ( hostText!=null ) hostText=hostText.toUpperCase();
      }
    HostScreen screen=hostSessionManager.getScreen();

    ///
    /// Do the wait...
    ///
    boolean returnCode=true;
    synchronized(this)
      {
      hasHostChanged=false;
      for ( long timeoutTime=System.currentTimeMillis()+timeout;; )
        {
        // First check if there the conditions match, start by getting the currently
        // matching screen.
        PhantomHostScreen matchScreen=getSingleMatchingScreen();

        // Check for error.
        if ( hostSession.hasError() || !hostSession.isConnected() )
          throw new SessionPoolingScriptHostError();

        // Check lock state released.
        boolean ok=true;
        if ( hostlock )
          ok=!hostSession.isLocked();

        // Check screen matching.
        if ( ok && phantomScreen!=null )
          {
          ok=(matchScreen==phantomScreen || (multiple && doesScreenMatch(phantomScreen)));
          if ( doNegate )
            ok=!ok;
          }

        // Check host field match.
        if ( ok && hostField!=null )
          {
          String s=hostField.getHiddenHostString(screen,line);
          if ( nocase )
            s=s.toUpperCase();
          ok=(wildcard)? WindowsLikeFilenameFilter.isMatching(s,text): s.equals(text);
          if ( doNegate )
            ok=!ok;
          }

        // Check host text match.
        if ( ok && hostText!=null )
          {
          if ( wildcard )
            {
            String s=(relative)?
              screen.getHiddenStringRelative(col,row,width):
              screen.getHiddenStringAbsolute(col,row,width);
            if ( nocase )
              s=s.toUpperCase();
            ok=WindowsLikeFilenameFilter.isMatching(s,hostText);
            }
          else
            {
            String s=(relative)?
              screen.getHiddenStringRelative(col,row,hostText.length()):
              screen.getHiddenStringAbsolute(col,row,hostText.length());
            if ( nocase )
              s=s.toUpperCase();
            ok=s.equals(hostText);
            }
          if ( doNegate )
            ok=!ok;
          }

        // Check if the conditions match.
        if ( ok )
          {
          // Situation must be stable...
          if ( time>0 )
            {
            hasHostChanged=false;
            try { wait(time); }
            catch(InterruptedException e) {}
            if ( hasHostChanged )
              continue;
            }

          // Conditions did match...
          break;
          }

        // If the host has changed, check the conditions again.
        if ( hasHostChanged )
          {
          hasHostChanged=false;
          continue;
          }
        
        // Don't wait if specified.
        if ( time==0 )
          {
          returnCode=ok;
          break;
          }

        // Wait for an event up to the remaining timeout.
        long remainingTime=timeoutTime-System.currentTimeMillis();
        if ( remainingTime<=0 )
          {
          returnCode=false;
          break;
          }

        // Wait for any change up to the timeout.
        hasHostChanged=false;
        try { wait(remainingTime); }
        catch(InterruptedException e) {}
        if ( !hasHostChanged || System.currentTimeMillis()>=timeoutTime )
          {
          returnCode=false;
          break;
          }
        }
      }

    // Check for error.
    if ( hostSession.hasError() || !hostSession.isConnected() )
      throw (new SessionPoolingScriptHostError());

    // Return the OK state.
    return returnCode;
    }

  ///////////////////////////////////////////////////////
  /// Methods overridden in HostSessionManagerAdapter ///
  ///////////////////////////////////////////////////////

  /**
   * Gets a lock object that is used by all threads modifying
   * the host screen. The instance of this class is used to
   * handle all synchronization.
   */
  @Override
  public Object getLockObject()
    {
    return this;
    }

  /**
   * Informs that the current host session had a connection change.
   */
  @Override
  public void onHostConnectChange(boolean connected)
    {
    synchronized(this)
      {
      // Notify all waiting threads.
      hasHostScreenChanged=true;
      hasHostFieldsChanged=true;
      hasHostCursorChanged=true;
      hasHostChanged=true;
      notifyAll();
      }
    }

  /**
   * Informs that the current host session had a screen change.
   */
  @Override
  public void onHostScreenChange()
    {
    synchronized(this)
      {
      // Set flag & notify all waiting threads.
      hasHostScreenChanged=true;
      hasHostChanged=true;
      notifyAll();
      }
    }

  /**
   * Informs that the cursor position has changed.
   */
  @Override
  public void onHostCursorPositionChange(int x,int y)
    {
    synchronized(this)
      {
      // Set flag & notify all waiting threads.
      hasHostCursorChanged=true;
      hasHostChanged=true;
      notifyAll();
      }
    }

  /**
   * Informs that the current host session had a field change.
   */
  @Override
  public void onHostFieldChange()
    {
    synchronized(this)
      {
      // Set flag & notify all waiting threads.
      hasHostFieldsChanged=true;
      hasHostChanged=true;
      notifyAll();
      }
    }

  /**
   * Informs that the current host session had a state change.
   */
  @Override
  public void onHostStateChange()
    {
    synchronized(this)
      {
      // Notify all waiting threads.
      hasHostChanged=true;
      notifyAll();
      }
    }

  /**
   * Informs that the current host session had a failure.
   */
  @Override
  public void onHostSessionFailure(String description)
    {
    synchronized(this)
      {
      // Log the error.
      logError("Host session failure: "+description);
      
      // Immediatly dispose of the session, regardless what is going on.
      if ( !isDisposed )
        dispose();
        
      // Notify all waiting threads.
      hasHostScreenChanged=true;
      hasHostFieldsChanged=true;
      hasHostCursorChanged=true;
      hasHostChanged=true;
      notifyAll();
      }
    }

  /**
   * Checks if a host screen currently matches.
   */
  @Override
  public boolean doesScreenMatch(PhantomHostScreen phantomScreen)
    {
    // Check if the screen is a a normal screen, i.e. not a popup.
    HostScreen screen=hostSessionManager.getScreen();
    if ( !phantomScreen.isPopup() )
      {
      // Set no pop-up window matching...
      screen.setCurrentPopupWindow(-1);
      return phantomScreen.isScreenMatching(screen,0,0);
      }

    // Screen is a popup. Check all possible windows identified for a match.
    int w=phantomScreen.getWidth();
    int h=phantomScreen.getHeight();
    for ( int ii=0, cc=screen.getPopupWindowCount(); ii<cc; ++ii )
      {
      HostPopupWindow.Rect rect=screen.getPopupWindow(ii);
      if ( rect.cx==w && rect.cy==h )
        {
        if ( phantomScreen.isScreenMatching(screen,rect.x,rect.y) )
          {
          // Set the current pop-up window...
          screen.setCurrentPopupWindow(ii);
          return true;
          }
        }
      }

    // Screen doesn't match. Set no pop-up window matching...
    screen.setCurrentPopupWindow(-1);
    return false;
    }

  ///////////////////////////
  /// The Session Actions ///
  ///////////////////////////

  /**
   * Runs the starts script for the session.
   *
   * <p>Called when a session is started in the pool. The start time is
   * set before the terminal session is created. The script is called once
   * the host session is unlocked and has a non-empty host screen. Typically,
   * the script would navigate in the host so as to reach the "starting point
   * screen".
   *
   * @throws  SessionPoolingDisposed  if the session is disposed.
   */
  public void runStartScript() throws SessionPoolingDisposed
    {
    // Create the session first!
    createHostSessionManager();

    // Wait for the initial screen.
    waitInitialScreen(null);

    // Set start state.
    trace("runStartScript()");
    currentState=STATE_START;

    // Run script...
    globalPoolData.executeScript(this,SessionPoolingData.SCRIPT_START);

    // Set ready state and last ping time.
    setCompleteTime();
    currentState=STATE_READY;
    
    // Notify readiness.
    globalPoolData.sessionStarted();
    }

  /**
   * Runs the check script.
   *
   * <p>Called to check if a session is OK for usage for a new client
   * session. Typically, the script would check that the host session is
   * unlocked and not in error, and that the current screen is the "starting
   * point screen".
   *
   * @return  true  if it's OK to use this session, false otherwise.
   */
  private boolean runCheckScript()
    {
    // Set check state.
    trace("runCheckScript()");
    currentState=STATE_CHECK;

    // Run script...
    try
      {
      boolean rc=globalPoolData.executeScript(this,SessionPoolingData.SCRIPT_CHECK);
      setCompleteTime();
      currentState=STATE_READY;
      return rc;
      }
    catch(SessionPoolingDisposed e)
      {
      return false;
      }
    }

  /**
   * Runs the ping script.
   *
   * <p>Called periodically by the session pool manager (optional setting).
   * This enables, for example, the script to perform some action in the host
   * in order to make sure it is not going to be disconnected by or logged from
   * the host. The script should also verify that the host is at the "starting
   * point screen".
   *
   * @throws  SessionPoolingDisposed  if the session is disposed.
   */
  public void runPingScript() throws SessionPoolingDisposed
    {
    // Set ping state.
    trace("runPingScript()");
    currentState=STATE_PING;

    // Run script...
    globalPoolData.executeScript(this,SessionPoolingData.SCRIPT_PING);
 
    // Set ready state and last ping time.
    setCompleteTime();
    currentState=STATE_READY;
    }

  /**
   * Runs the reclaim script.
   *
   * <p>Called when a client session is closed in order to reclaim the host
   * session into the pool. This enables the script to, for example, back out
   * from a series of host screens to a starting point. When the reclaim action
   * is called, the user who owned the host session could have navigated "deep
   * down" in the host system.
   *
   * @throws  SessionPoolingDisposed  if the session is disposed.
   */
  public void runReclaimScript() throws SessionPoolingDisposed
    {
    // Set ping state.
    trace("runReclaimScript()");
    currentState=STATE_RECLAIM;

    // Run script...
    globalPoolData.executeScript(this,SessionPoolingData.SCRIPT_RECLAIM);

    // Set ready state and last ping time.
    setCompleteTime();
    currentState=STATE_READY;
    }

  /**
   * Runs the dispose script for the session.
   *
   * <p>Called prior to disposing of the terminal session. Typically,
   * the script would logoff from the host system in a controlled way.
   * Note that the current host screen could be "anywhere" in the host system.
   *
   * @throws  SessionPoolingDisposed  if the session is disposed.
   */
  public void runDisposeScript() throws SessionPoolingDisposed
    {
    trace("runDisposeScript()");
    currentState=STATE_DISPOSING;

    // Run script...
    globalPoolData.executeScript(this,SessionPoolingData.SCRIPT_DISPOSE);
    
    // Check if not disposed yet.
    synchronized(this)
      {
      if ( isDisposed )
        return;
      dispose();
      }
    }

  ///////////////////////////////////
  /// The internal script methods ///
  ///////////////////////////////////

  /**
   * Processes an <code>if</code> begin tag (and the else or elseif tags that follows).
   *
   * <p>Syntax:
   * <pre>
   *   (if)
   *     (conditions [negate])
   *       ... conditions ...
   *     (/conditions)
   *     ... instructions ...
   *   (/if)
   *   (elseif)
   *     (conditions [negate])
   *       ... conditions ...
   *     (/conditions)
   *     ... instructions ...
   *   (/elseif)
   *   (else)
   *     ... instructions ...
   *   (/else)
   * </pre>
   *
   * The elseif block can be repeated any number of times or not specified at
   * all. The else block may only be specified once and can also be omitted.
   *
   * <p>Return code: true if there was an if or elseif condition that was true,
   * false otherwise. This applies when no instructions have been executed.
   * The return code is otherwise the return code of the last executed
   * instruction (i.e. tag).
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptIF(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Get the current element (the condition tag).
    Element parent=data.currentElement;

    // Do this until either ELSE or successful IF (or ELSEIF) has been processed.
    try
      {
      for ( ;; )
        {
        // Execute the next statement (if it's a condition block, it will handle execution of
        // all it's children.
        data.returnCode=true;
        data.currentElement=parent;
        boolean doNegate=data.hasElementAttribute("negate");
        data.currentElement=data.getFirstElement(parent);
        if ( data.currentElement==null )
          {
          data.trace("No condition tag found after if tag");
          data.logError("No condition tag found after if tag");
          disposeFatal();
          }

        // Execute the condition.
        data.invokeScriptMethod();

        // If the return code is false, check if there is a next
        // ELSEIF or ELSE statement.
        if ( data.returnCode==doNegate )
          {
          Element next=data.getNextElement(parent);
          if ( next==null )
            break;
          String name=next.getTagName();
          if ( name.equals("elseif") )
            {
            // ELSEIF: continue loop as the IF statement that started
            // the loop as the new parent.
            parent=next;
            continue;
            }
          else
          if ( name.equals("else") )
            {
            // ELSE: execute all its child elements and set pointer
            // after the ELSE.
            data.currentElement=data.getFirstElement(next);
            data.executeRemainingElements();
            data.currentElement=next;
            data.setNextCurrentElement();
            }
          else
            {
            // Other tag follows. Set this to be the next one to execute.
            data.currentElement=next;
            }
          break;
          }

        // Process all statements after the condition. Then set the pointer
        // after the IF or ELSEIF statement.
        data.executeRemainingElements();
        parent=data.getNextElement(parent);
                
        // Skip all ELSEIF tags after the current parent.
        while ( parent!=null && parent.getTagName().equals("elseif") )
          parent=data.getNextElement(parent);

        // Skip the ELSE tag after the current parent.
        if ( parent!=null && parent.getTagName().equals("else") )
          parent=data.getNextElement(parent);

        // Set the next element.
        data.currentElement=parent;
        break;
        }
      }
    catch(SessionPoolingScriptHostError e)
      {
      // Skip the IF statement.
      if ( parent.getTagName().equals("if") )
        parent=data.getNextElement(parent);

      // Skip all ELSEIF tags after the current parent.
      while ( parent!=null && parent.getTagName().equals("elseif") )
        parent=data.getNextElement(parent);

      // Skip the ELSE tag after the current parent.
      if ( parent!=null && parent.getTagName().equals("else") )
        parent=data.getNextElement(parent);

      // Set the next element.
      data.currentElement=parent;
      throw e;
      }
    }

  /**
   * The conditions tag should normally be used together with the if,
   * elseif and the while tags. All tags inside the "conditions block"
   * are combined with the logical and operator. The conditions tag can
   * also be used to stop processing in a block as soon as e.g. a wait tag
   * returns false.
   *
   * <p>The negate option logically negates each condition tag processed
   * before the logical and operation is performed. That is, a return code
   * of true in a condition tag stops execution of the remaining tags,
   * returning a value of false.
   *
   * <p>Syntax:
   * <pre>
   *    (conditions [negate])
   *      ... conditions ...
   *    (/conditions)
   * </pre>
   *
   * Return code: true if all tags inside the "conditions" block are true,
   * false otherwise. Processing of execution of the tags inside the block
   * is immediately stopped if one of the tags returns false (or true for
   * the negate option).
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptCONDITIONS(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Check the negate parameter.
    boolean doNegate=data.hasElementAttribute("negate");

    // Get the current element (the condition tag) and set
    // the initial return code to true.
    Element parent=data.currentElement;
    data.returnCode=true;

    // Process all child nodes, but handle the on error exception.
    try
      {
      for ( data.currentElement=data.getFirstElement(parent); data.currentElement!=null; )
        {
        // Execute the line.
        data.invokeScriptMethod();
        if ( doNegate )
          data.returnCode=!data.returnCode;

        // Stop for false.
        if ( data.returnCode==false )
          break;
        }
      }
    catch(SessionPoolingScriptHostError e)
      {
      // Set next statement to execute and rethrow the error.
      data.currentElement=parent;
      data.setNextCurrentElement();
      throw e;
      }

    // Set next statement to execute.
    data.currentElement=parent;
    data.setNextCurrentElement();
    }

  /**
   * The while instructions are executed as long as the conditions are true.
   *
   * <p>Syntax:
   * <pre>
   *   (while)
   *     (conditions [negate])
   *       ... conditions ...
   *     (/conditions)
   *     ... instructions ...
   *   (/while)
   * </pre>
   *
   * or the do-while style:
   *
   * <pre>
   *   (while)
   *     ... instructions ...
   *     (conditions [negate])
   *       ... conditions ...
   *     (/conditions)
   *   (/while)
   * </pre>
   *
   * Return code: The return code of the last executed instruction (i.e. tag).
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptWHILE(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Get the current element (the condition tag) and set
    // the initial return code to true.
    boolean doNegate=data.hasElementAttribute("negate");
    Element parent=data.currentElement;
    data.returnCode=true;

    // Get first child element.
    Element firstChild=data.getFirstElement(parent);
    Element lastConditionsChild=null;
    if ( firstChild==null )
      {
      String s="While method has no children nodes";
      data.trace(s);
      data.logError(s);
      disposeFatal();
      }

    // Check if the conditions test is last or first.
    // If first element is not "conditions", check last element.
    if ( !firstChild.getTagName().equals("conditions") )
      {
      // Check for last element being "conditions".
      Element child=firstChild;
      for ( ;; )
        {
        Element next=data.getNextElement(child);
        if ( next==null )
          {
          if ( child!=firstChild && child.getTagName().equals("conditions") )
            lastConditionsChild=child;
          break;
          }
        child=next;
        }
      }

    // Handle the break and host error exceptions.
    try
      {
      if ( lastConditionsChild!=null )
        {
        // Do-while style, i.e. the last first.
        for ( ;; )
          {
          // Process all children up to the last conditions tag.
          data.currentElement=firstChild;
          data.executeRemainingElements(lastConditionsChild);

          // Check if while loop should be broken or not.
          data.currentElement=lastConditionsChild;
          data.invokeScriptMethod();
          if ( data.returnCode==doNegate )
            break;
          }
        }
      else
        {
        // "Normal" while statement, i.e. conditions first.
        for ( ;; )
          {
          // Check if while loop should be broken or not.
          data.currentElement=firstChild;
          data.invokeScriptMethod();
          if ( data.returnCode==doNegate )
            break;

          // Process all children that follows.
          data.executeRemainingElements();
          }
        }
      }
    catch(SessionPoolingScriptBreak e)
      {
      // Nothing.
      }
    catch(SessionPoolingScriptHostError e)
      {
      // Set next statement to execute and rethrow the error.
      data.currentElement=parent;
      data.setNextCurrentElement();
      throw e;
      }

    // Set next statement to execute.
    data.currentElement=parent;
    data.setNextCurrentElement();
    }

  /**
   * This tag resets any error condition present in host session,
   * after e.g. typing data in a protected field or receiving a System
   * Message for 5250.
   *
   * <p>Syntax:
   * <pre>
   *    (reset/)
   * </pre>
   *
   * Return code: true if the function was successful or no error state existed
   * for the session, false otherwise (host was not in an error state that could
   * not be reset due to e.g. communications failure).
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptRESET(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    HostSession hostSession=hostSessionManager.getCurrentSession();
    boolean ok=false;
    if ( hostSession!=null )
      {
      // Return code should be false if no previous error existed, true otherwise.
      ok=!hostSession.hasError();
      if ( !ok && hostSession.sendKey(TerminalKeys.KEY_Reset,true) )
        ok=(ok && !hostSession.hasError());
      }

    data.returnCode=ok;

    // Set next statement to execute.
    data.setNextCurrentElement();
    }

  /**
   * An error state is typically set if data is input in a protected field,
   * there is some communications failure or, for 5250, if a "System Message"
   * is present. To remove this condition, the reset key ("@R") must be sent
   * or the reset tag executed.
   *
   * <p>Syntax:
   * <pre>
   *   (hosterror/)
   * </pre>
   *
   * Return code: true if the host is presently in an error state. If no error
   * condition is present, false is returned.
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptHOSTERROR(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    data.returnCode=false;
    HostSession hostSession=hostSessionManager.getCurrentSession();
    if ( hostSession!=null && (hostSession.hasError() || !hostSession.isConnected()) )
      data.returnCode=true;

    // Set next statement to execute.
    data.setNextCurrentElement();
    }

  /**
   * When an error condition occurs in a script, the next on error tag
   * instructions are executed. If the session is not in an error state,
   * this tag is skipped.
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptONERROR(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Nothing, because the host error situation is checked elsewhere
    // in this class. Just set next statement to execute.
    data.setNextCurrentElement();
    }

  /**
   * Logs an event in the NetPhantom Event Log.
   *
   * <p>Syntax:
   * <pre>
   *   (log [level="nn"] text="text" [lasterror]/)
   * </pre>
   *
   * The value <i>nn</i> can be 0=informational (default), 1=warning, 2=error,
   * 3=critical. It can also be the first character, i.e. I, W, E or C.
   *
   * <p>If the <i>lasterror</i> parameter is specified, the last encountered error
   * message for the terminal session is added. Note for 5250 sessions: a system
   * error message always begins with the text "last error 5250:".
   *
   * <p>Return code: always true.
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptLOG(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Get the text for the trace.
    String text=data.getParamString("text");

    // Check for last error option.
    if ( data.hasElementAttribute("lasterror") )
      {
      HostSession hostSession=hostSessionManager.getCurrentSession();
      if ( hostSession!=null && !hostSession.is3270() )
        {
        String err=hostSession.getLastError(false);

        if ( err==null )
          err="<none>";

        text+=": last error 5250: "+err;
        }
      }

    // Check the level (0=I=info(default), 1=W=Warning, 2=E=Error).
    int level=0;
    String s=data.getParamString("level").toUpperCase();
    if ( s.length()>0 )
      {
      char ch=s.charAt(0);
      if ( ch>='0' && ch<='2' ) level=(ch-'0');
      else if ( ch=='W' ) level=1;
      else if ( ch=='E' ) level=2;
      }

    // Log the event.
    switch(level)
      {
      default: logInfo   (text); break;
      case 1 : logWarning(text); break;
      case 2 : logError  (text); break;
      }

    // Set return code.
    data.returnCode=true;

    // Set next statement to execute.
    data.setNextCurrentElement();
    }

  /**
   * Adds text to the trace file.
   *
   * <p>If the lasterror parameter is specified, the last encountered error message
   * for the terminal session is added.
   *
   * <p>Note: for 5250 sessions, a system error message always begins with the text
   * "last error 5250:".
   *
   * <p>If the parameter dumpscreen is present, the host screen character data is appended.
   * <pre>
   *   (/trace text="text" [lasterror] [dumpscreen]/)
   * </pre>
   *
   * Return code: always true.
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptTRACE(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Get the text for the trace.
    String text=data.getParamString("text");

    // Check for last error option.
    if ( data.hasElementAttribute("lasterror") )
      {
      HostSession hostSession=hostSessionManager.getCurrentSession();
      if ( hostSession!=null && !hostSession.is3270() )
        {
        String err=hostSession.getLastError(false);

        if ( err==null )
          err="<none>";

        text+=": last error 5250: "+err;
        }
      }

    // Check for dumpscreen option.
    if ( !data.hasElementAttribute("dumpscreen") )
      trace("Script trace: "+text);
    else
      {
      HostScreen screen=hostSessionManager.getScreen();
      String [] scr;
      synchronized(this)
        {
        int cc=screen.getHeight();
        scr=new String [cc];
        for ( int ii=0; ii<cc; ++ii )
          scr[ii]=screen.getStringAbsolute(0,ii,cc);
        }
      trace("Script trace: "+text,scr);
      }

    // Set return code.
    data.returnCode=true;

    // Set next statement to execute.
    data.setNextCurrentElement();
    }

  /**
   * Breaks out of a while loop.
   *
   * <p>If not inside a while loop, this tag would have the same processing as
   * a return tag, optionally setting the return code to true or false.
   *
   * <p>If the return code is not set, the last return code is used.
   *
   * <p>Syntax:
   * <pre>
   *   (break [true]/)
   *
   *   (break [false]/)
   * </pre>
   *
   * Return code: true or false options if the parameters are set,
   * otherwise the return code of the last instruction (i.e. tag).
   *
   * @throws  SessionPoolingDisposed     when a script has been disposed.
   * @throws  SessionPoolingScriptBreak  always.
   */
  public void scriptBREAK(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Get optional parameters for return code.
    if ( data.hasElementAttribute("true") )
      data.returnCode=true;
    else
    if ( data.hasElementAttribute("false") )
      data.returnCode=false;

    throw new SessionPoolingScriptBreak();
    }

  /**
   * Stops execution in a script with the specified return code.
   *
   * <p>Syntax:
   * <pre>
   *   (return true/)
   *
   *   (return false/)
   * </pre>
   *
   * Return code: true or false depending on the parameter.
   *
   * @throws  SessionPoolingDisposed      when a script has been disposed.
   * @throws  SessionPoolingScriptReturn  always.
   */
  public void scriptRETURN(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Get parameters for return code.
    boolean rc=false;
    if ( data.hasElementAttribute("true") )
      rc=true;
    else
    if ( !data.hasElementAttribute("false") )
      data.logWarning("Return element does not have 'true' or 'false' specified, 'false' is assumed");

    data.returnCode=rc;
    throw new SessionPoolingScriptReturn();
    }

  /**
   * Immediately disposes of this pooled session without executing a single additional tag.
   *
   * <p>Syntax:
   * <pre>
   *   (dispose/)
   * </pre>
   *
   * This tag does not return a value, an exception is thrown and the script is terminated.
   *
   * @throws  SessionPoolingDisposed  when a script has been disposed.
   */
  public void scriptDISPOSE(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    disposeNow("Dispose script tag");
    }

  /**
   * This tag is used to send a series of keystrokes to the terminal session formatted
   * according to EHLLAPI codes (e.g. "userid@Tpassword@E").
   *
   * <p>Syntax:
   * <pre>
   *   (send string="string"/)
   *
   *   (send userid="USERID"/)
   * </pre>
   *
   * The <code>userid</code> option will send the password for the "USERID"
   * that is specified in the NetPhantom Users using the Server Administration program.
   *
   * <p>The send tag always waits for the session to be unlocked (due to e.g. sending an
   * AID key such as Enter). The maximum wait time is the default timeout (60 seconds)
   * or if it is changed using the <code>set</code> function.
   *
   * <p>An error with this function will resume execution at the next onerror tag.
   *
   * <p>Note that the type-ahead of keystrokes is enabled only if this option is
   * enabled for host sessions using the Server Administration program.
   *
   * <p>Return code: always true.
   *
   * @throws  SessionPoolingDisposed         when a script has been disposed.
   * @throws  SessionPoolingScriptHostError  when a host error is encountered.
   */
  public void scriptSEND(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Clear error state if possible, otherwise this throws an error.
    HostSession hostSession=getCurrentClearedHostSession();

    // Find the text.
    boolean ok;
    if ( data.hasElementAttribute("string") )
      {
      ok=hostSession.sendString(data.getParamString("string"));
      }
    else
    if ( data.hasElementAttribute("userid") )
      {
      String userid=data.getParamString("userid");
      AdminConfigUser user=AdminConfigUsers.getUser(userid);
      if ( user==null )
        {
        data.logWarning("Send element cannot find userid '"+userid+"'");
        data.returnCode=true;

        // Set next statement to execute.
        data.setNextCurrentElement();
        return;
        }
      ok=hostSession.sendCharacterString(user.getPassword());
      }
    else
      {
      data.logWarning("Send element does not specify either 'string' or 'userid'");
      data.returnCode=true;

      // Set next statement to execute.
      data.setNextCurrentElement();
      return;
      }

    // Check for error.
    if ( !ok || hostSession.hasError() || !hostSession.isConnected() )
      throw new SessionPoolingScriptHostError();

    // Wait for the host to unlock the maximum timeout time.
    hostWait(null,true,false,-1,data.handler.timeoutValue);
    data.returnCode=true;

    // Set next statement to execute.
    data.setNextCurrentElement();
    }

  /**
   * The wait tag is used to wait a specified time in milliseconds or a maximum
   * time in milliseconds (default 60000 milliseconds, or changed by the set timeout tag).
   *
   * <p>It is also used to wait for the host lock state, the host session to be stabilized
   * within the timeout value, a screen to match or a host text to be present (using
   * absolute or relative screen position, also including hidden/non-display text as an
   * option). When the time option is defined with waiting functions for the host (e.g.
   * screen and/or the host locked or host stable options), this function will wait an
   * additional time before returning, e.g. if the host screen is changed or the cursor
   * moved during this time, the wait time is reset to zero.
   * 
   * <p>The option <i>negate</i> is used to invert this result.
   *
   * <p>Note that the (optional) parameters column, row and line are "one-based" numerical
   * values.
   *
   * <p>The wildcard match option enables text to be matched with Windows-like wildcards:
   * <br> ? is any character,
   * <br> * is any number of characters,
   * <br> ^ is the escape character, i.e. the meaning of '?' and '*' becomes the
   *        actual character that directly follows,
   * <br>^^ two escape characters becomes the escape character itself ('^').
   *
   * <p>This tag is also used as a condition tag.
   *
   * <p>Syntax:
   * <pre>
   *   (wait time="milliseconds"
   *         [hostlocked] [cursorstable]/)
   *
   *   (wait screen="SCREENNAME" [multiple] [negate]
   *         [hostlocked] [hoststable]
   *         [time="milliseconds"]
   *         [timeout="milliseconds"]/)
   *
   *   (wait hosttext="text" [nocase] [negate]
   *         column="xx" row="yy" [relative] [width="nn" [wildcard]]
   *         [hostlocked] [hoststable]
   *         [time="milliseconds"]
   *         [timeout="milliseconds"]/)
   *
   *   (wait hostfield="NAME" text="text" [nocase] [negate]
   *         [line="nn"] [wildcard]
   *         [hostlocked] [hoststable]
   *         [time="milliseconds"]
   *         [timeout="milliseconds"]/)
   * </pre>
   *
   * Return code: true when the condition is reached within the timeout time or false
   * if not.
   *
   * @throws  SessionPoolingDisposed         when a script has been disposed.
   * @throws  SessionPoolingScriptHostError  when a host error is encountered.
   */
  public void scriptWAIT(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    // Do the wait with the parameters set.
    data.returnCode=hostWait(data,
                             data.hasElementAttribute("hostlock"),
                             data.hasElementAttribute("hoststable"),
                             data.getParamInt("time"),
                             data.getParamInt("timeout",timeoutValue,false));

    // Set next statement to execute.
    data.setNextCurrentElement();
    }

  /**
   * The instructions inside the block are executed if the current host screen name
   * matches uniquely. The option <i>multiple</i> allows other screen names to match
   * as well. If this option is set, the current host screen is set to the screen named
   * in the tag. If the option <i>negate</i> is used, then the block inside this tag
   * is executed if the screen does not match.
   *
   * <p>If the screen does not match, execution continues at the tag below.
   *
   * <p>Syntax:
   * <pre>
   *   (screen name="ABC" [multiple] [negate])
   *     ... instructions ...
   *   (/screen)
   * </pre>
   *
   * Return code: true if the screen matches (uniquely), false otherwise. The
   * <i>negate</i> option inverts this result.
   *
   * @throws  SessionPoolingDisposed         when a script has been disposed.
   * @throws  SessionPoolingScriptHostError  when a host error is encountered.
   */
  public void scriptSCREEN(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    PhantomHostScreen matchScreen=getSingleMatchingScreen();

    // Get the screen to match.
    PhantomRuntime rt=globalPoolData.runtime;
    String s=data.getParamString("name");
    if ( rt==null )
      {
      s="Screen matching "+s+" error, runtime file not loaded: skipping statement";
      data.trace(s);
      data.logWarning(s);

      // Set next statement to execute.
      data.setNextCurrentElement();
      
      // Set no match.
      data.returnCode=false;
      return;
      }
    PhantomHostScreen phantomScreen=(s==null)? null: rt.getHostData().getScreen(s);
    if ( phantomScreen==null )
      {
      s="Screen matching "+s+" error, screen name not found: skipping statement";
      if ( data!=null )
        {
        data.trace(s);
        data.logWarning(s);

        // Set no match.
        data.returnCode=false;
        }
      }
    else
      {
      // Check screen matching (or the negate option: no match).
      boolean multiple=data.hasElementAttribute("multiple");
      boolean doNegate=data.hasElementAttribute("negate");
      boolean match=(matchScreen==phantomScreen || (multiple && doesScreenMatch(phantomScreen)));
      if ( doNegate )
        match=!match;
      if ( match )
        {
        // Execute all children and handle the on error exception.
        Element parent=data.currentElement;
        try
          {
          data.currentElement=data.getFirstElement(parent);
          data.executeRemainingElements();
          data.currentElement=parent;
          }
        catch(SessionPoolingScriptHostError e)
          {
          // Set return code, next element and throw the on error.
          data.returnCode=match;
          data.currentElement=parent;
          data.setNextCurrentElement();
          throw e;
          }
        }

      // Set the return code.
      data.returnCode=match;
      }

    // Set next statement to execute.
    data.setNextCurrentElement();
    }

  /**
   * This tag can be used to set data in a host field of the currently matching
   * host screen. It can also set the cursor position to xx and yy with absolute as an
   * option (used when inside a host pop-up window).
   *
   * <p>Syntax:
   * <pre>
   *   (set hostfield="NAME" [line="nn"] text="text"/)
   * </pre>
   * or
   * <pre>
   *   (set hostfield="NAME" [line="nn"] userid="USERID"/)
   * </pre>
   * or
   * <pre>
   *   (set cursor [relative] column="xx" row="yy"/)
   * </pre>
   * or
   * <pre>
   *   (set cursor hostfield="NAME" [line="nn"] [pos="charpos"]/)
   * </pre>
   * or
   * <pre>
   *   (set timeout="milliseconds"/)
   * </pre>
   *
   * <p>The userid option will set the password for the "USERID" that is specified in
   * the NetPhantom Users using the Server Administration program.
   *
   * <p>Note: all numeric parameters are "one-based" for line, pos, column, row.
   *
   * <p>Note: the currently matching screen is only set when the screen or wait screen
   * tag has been executed prior to this tag (this also applies to host pop-up window
   * recognition only executed with these tags).
   *
   * <p>An error with this function will resume execution at the next onerror tag.
   *
   * <p>Return code: true if the function caused a change, false otherwise (i.e. cursor
   * didn't move, timeout didn't change, host field already contained the text to set).
   *
   * @throws  SessionPoolingDisposed         when a script has been disposed.
   * @throws  SessionPoolingScriptHostError  when a host error is encountered.
   */
  public void scriptSET(SessionPoolingScriptData data) throws SessionPoolingDisposed
    {
    String s=data.getParamString("hostfield");
    boolean changed=false;
    if ( data.hasElementAttribute("cursor") )
      {
      ///
      /// Set cursor.
      ///
      if ( s.length()>0 )
        {
        // According to host field.
        PhantomHostScreen matchScreen=getSingleMatchingScreen();
        if ( matchScreen==null )
          {
          s="Set cursor to hostfield "+s+" error, no matching screen: ignored";
          data.trace(s);
          data.logWarning(s);
          }
        else
          {
          PhantomHostField hf=matchScreen.getHostField(s.toUpperCase());
          if ( hf==null )
            {
            s="Set cursor to hostfield "+s+" error, field not found: ignored";
            data.trace(s);
            data.logWarning(s);
            }
          else
            {
            int line=data.getParamInt("line")-1;
            if ( line<0 ) line=-1;
            int pos=data.getParamInt("pos")-1;
            if ( pos<0 ) pos=0;
            HostSession hostSession=getCurrentClearedHostSession();
            Point p=hostSession.getCursor();
            changed=hf.setCursor(hostSession,line,pos);
            if ( changed )
              {
              Point p2=hostSession.getCursor();
              changed=(p.x!=p2.x || p.y!=p2.y);
              }
            }
          }
        }
      else
        {
        // According to row/column.
        int x=data.getParamInt("column")-1;
        int y=data.getParamInt("row")-1;
        if ( x>=0 && y>=0 )
          {
          if ( data.hasElementAttribute("relative") )
            {
            HostScreen screen=hostSessionManager.getScreen();
            x+=screen.getCurrentPopupWindowXOffset();
            y+=screen.getCurrentPopupWindowYOffset();
            }
          HostSession hostSession=getCurrentClearedHostSession();
          Point p=hostSession.getCursor();
          if ( p.x!=x || p.y!=y )
            changed=hostSession.setCursor(x,y);
          }
        }
      }
    else
    if ( s.length()>0 )
      {
      ///
      /// Set host field.
      ///
      PhantomHostScreen matchScreen=getSingleMatchingScreen();
      if ( matchScreen==null )
        {
        s="Set hostfield text "+s+" error, no matching screen: ignored";
        data.trace(s);
        data.logWarning(s);
        }
      else
        {
        PhantomHostField hf=matchScreen.getHostField(s.toUpperCase());
        if ( hf==null )
          {
          s="Set hostfield text "+s+" error, field not found: ignored";
          data.trace(s);
          data.logWarning(s);
          }
        else
          {
          int line=data.getParamInt("line")-1;
          String text=data.getParamString("userid");
          if ( text.length()>0 )
            {
            // Set host field user ID.
            AdminConfigUser user=AdminConfigUsers.getUser(text);
            if ( user==null )
              {
              data.logWarning("Set hostfield userid error: cannot find userid '"+text+"'");
              text=null;
              }
            else
              text=user.getPassword();
            }
          else
            {
            // Set host field text.
            text=data.getParamString("text");
            }

          // Check for OK.
          if ( text!=null )
            {
            HostScreen screen=hostSessionManager.getScreen();
            HostSession hostSession=getCurrentClearedHostSession();
            if ( line<0 )
              {
              String oldText=hf.getHiddenHostString(screen);
              changed=hf.setHostField(hostSession,text);
              if ( changed )
                changed=!oldText.equals(text);
              }
            else
              {
              String oldText=hf.getHiddenHostString(screen,line);
              changed=hf.setHostField(hostSession,text,line);
              if ( changed )
                changed=!oldText.equals(text);
              }
            }
          }
        }
      }
    else
      {
      int to=data.getParamInt("timeout");
      if ( to>0 )
        {
        ///
        /// Set timeout.
        ///
        changed=(timeoutValue!=to);
        timeoutValue=to;
        data.trace("Timeout set to "+to+ "milliseconds");
        }
      else
        {
        // Nothing matching.
        data.trace("Set tag contains no valid parameters, skipping");
        data.logWarning("Set tag contains no valid parameters, skipping");
        }
      }

    // Set the return code.
    data.returnCode=changed;

    // Set next statement to execute.
    data.setNextCurrentElement();
    }
}