KeyReader.java

/*
** Module   : KeyReader.java
** Abstract : helper methods to manage reading a key/char and accessing the
**            last key read
**
** Copyright (c) 2005-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
** 001 GES 20051109   @23284 Created initial version, supporting helper
**                           methods to manage reading a key/char and
**                           accessing the last key read.
** 002 NVS 20051212   @23679 LogicalTerminal.readKey() is called which was
**                           previously commented out since LT wasn't
**                           fully implemented.
** 003 GES 20060115   @23922 Better safety code for stream access.
** 004 GES 20060123   @24020 Moved to context-local storage.
** 005 GES 20060207   @24326 Ensure that lastkey is set to -1 in case
**                           there is a StopConditionException while
**                           reading.
** 006 GES 20060207   @24346 Added new readkey signatures and otherwise
**                           handle new unnamed streams interface.
** 007 NVS 20070725   @34686 Renamed isInOK() method to isIn() for the
**                           sake of uniformity.
** 008 RK  20080227   @37177 Javadoc has been updated to state the
**                           different initial values for different 
**                           terminals.
** 009 GES 20080320   @37632 Added some LAST-EVENT system handle support
**                           (for the FUNCTION attribute).
** 010 GES 20090624   @42884 isIn() usage to determine if the stream represents
**                           the terminal is switched to isTerm(). This fixes
**                           a problem where a terminal was incorrectly
**                           calculated when there was a closed input stream
**                           (which should fail with an error instead of
**                           automatically redirecting to the terminal).
** 011 CA  20130112          Added asHandle, getType, isValid and isUnknown 
**                           methods, accessed by the LAST-EVENT handle.
** 012 CA  20130221          Added LABEL support. getType was renamed to getResourceType.
**                           DATA-TYPE, COLUMN-LABEL and FORMAT attributes are not supported (but
**                           forced to be defined by CommonField interface) so proper error APIs 
**                           are called.
** 013 OM  20130304          Refactored isValid and isUnknown of WrappedResource to valid 
**                           and unknown.
** 014 CA  20130927          Static proxies need the unknown() API. Resource type is determined 
**                           from LegacyResource constants.
** 015 EVL 20131014          Adding annotation for legacy attributes to be ignored for the class.
** 016 VIG 20131122          Adding support of LAST-EVENT:LABEL and 
**                           LAST-EVENT:EVENT-TYPE attributes.
** 017 GES 20140116          Code formatting and javadoc cleanup.
** 018 CA  20141021          Added support for legacy MOUSE events.
** 019 CA  20141029          A minor improvement for setting the LAST-EVENT attrs, when the key is
**                           read via READKEY.
** 020 CA  20150502          The resource ID needs to be kept at the resource, so it can be 
**                           accessed after the resource is deleted.
** 021 IAS 20150801          WIDGET-LEAVE/WIDGER-ENTER support.
** 022 OM  20150926          X/Y attributes support.
** 023 CA  20160327          Finished X/Y attribute implementation.
** 024 CA  20171218          Fixed LAST-EVENT:WIDGET-ENTER/LEAVE attributes, in cases when 
**                           ROW-ENTRY/LEAVE events are used.
** 025 HC  20181212          Implemented support for asynchronous key state read emulation.
** 026 CA  20200211          Allow fractional seconds (convert to millis based on the legacy version).
** 027 GES 20200627          Renamed Coordinates interface to be more generic.  Added the undocumented
**                           LAST-EVENT:SET-LASTKEY() method.  Added the LAST-EVENT:COLUMN/ROW attributes
**                           support.
**     OM  20201030          Invalid attribute API support for getters/setters.
**     OM  20201203          Fixed handling of READ/ONLY attributes.
** 028 RFB 20210512          Initial value of lastkey changed from 401 to -1. Ref #5363.
**     VVT 20211206          'state' replaced by 'modifiers' in code related to key modifiers.
**                           See #5850-26.
**     VVT 20211215          New methods added: setX(int), setY(int). See #5899.
**     VVT 20221003          CommonHandle.getResourceType() method renamed to resourceType() to prevent conflicts
**                           with namesakes in DMO. See #6694.
** 029 EVL 20230620          Consider the WINDOW UI mark bit to get legacy lastKey value.
** 030 CA  20230828          'getLastKey' must return 'integer', and not Java 'int'.
** 031 AB  20231107          Modified getWidgetEnter() to handle correctly the entry event when the frame is
**                           opened.
** 032 SVL 20231124          Fix for ROW-ENTRY event.
** 033 VVT 20240206          setLastKey(key, modifiers) now does not set LAST-EVENT:LABEL. See #8068.
**                           Code cleanup.
** 034 SVL 20240530          Disabled usage of keyReader.label.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import com.goldencode.p2j.security.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.client.event.*;
import com.goldencode.proxy.*;

/**
 * Provides Progress compatible helper methods to manage reading a key/char
 * and accessing the last key read.  This class provides a generic interface
 * and uses the {@link Stream} implementation or the 
 * <code>LogicalTerminal</code> implementation to back the service based
 * on runtime state.
 * <p>
 * <b>The initial last key value is currently set to 401 which is valid for
 * only a sub-set of terminal types (e.g xterm/vt100). It is known that for
 * vt220/vt320 type terminals this would need to be corrected (they report
 * -1 in Progress).  For other terminal types, the initial value is not known.
 * The solution will require that a terminal specific value be reported
 * depending on runtime data. This solution has not yet been defined. Part of
 * the problem with the solution is that the server has no knowledge of the
 * terminal type nor does it currently have any other terminal type
 * dependencies.</b>
 */
public class KeyReader
{
   /** LAST-EVENT:EVENT-TYPE for high-level Progress events */
   private static final String PROGRESS_EVTYPE = "PROGRESS";

   /** LAST-EVENT:EVENT-TYPE for keyboard Progress events */
   private static final String KEY_EVTYPE = "KEYPRESS";

   /** LAST-EVENT:EVENT-TYPE for mouse Progress events */
   private static final String MOUSE_EVTYPE = "MOUSE";
   
   /** Context-local data area for last key state. */
   private static ContextContainer workArea = new ContextContainer();
   
   /**
    * Get a the instance for the LAST-EVENT system handle.  Is obtained using
    * a call to the {@link StaticProxy#obtain(Class, Class[])}, using the
    * {@link CommonLastEvent} interface and its methods implemented by these
    * classes: {@link KeyReader}.
    * 
    * @return   See above.
    */
   public static handle asHandle()
   {
      WorkArea wa = workArea.obtain();

      Class[] classes = { KeyReader.class };
      CommonLastEvent proxy = StaticProxy.obtain(CommonLastEvent.class, classes);

      if (wa.id == null)
      {
         wa.id = handle.resourceId(proxy);
      }

      return new handle(proxy);
   }

   /**
    * Get this resource's ID.
    * 
    * @return   The resource's ID.
    */
   public static Long id()
   {
      WorkArea wa = workArea.obtain();
      
      return wa.id;
   }
   
   /**
    * Set this resource's ID.
    * <p>
    * This is a no-op for system handles.
    *  
    * @param    id
    *           The resource's ID.
    */
   public static void id(@SuppressWarnings("unused") long id)
   {
      // no-op
   }

   /**
    * Implementation for the {@link WrappedResource#valid()} API.
    * 
    * @return   Always true.
    */
   public static boolean valid()
   {
      return true;
   }
   
   /**
    * Implementation for the {@link WrappedResource#unknown()} API.
    * 
    * @return   Always false.
    */
   public static boolean unknown()
   {
      return false;
   }
   
   /**
    * API needed to implement read-only attribute assignment (a 4GL "feature").
    * 
    * @param    attribute
    *           The attribute's name.
    *           
    * @see      handle#readOnlyError(handle, String)
    */
   public static void readOnlyError(String attribute)
   {
      handle.readOnlyError(asHandle(), attribute);
   }
   
   /**
    * API needed to implement read-only attribute assignment (a 4GL "feature").
    * 
    * @param    attribute
    *           The attribute's name.
    * @param    expr
    *           The value which is attempted to be assigned to the read-only attribute. 
    *
    * @see      handle#readOnlyError(handle, String, Object)
    */
   public static void readOnlyError(String attribute, @SuppressWarnings("unused") Object expr)
   {
      handle.readOnlyError(asHandle(), attribute);
   }
   
   /**
    * Obtains the last key read from any source (stream or terminal).
    *
    * @return   The last key read.
    */
   public static integer lastKey()
   {
      return getLastKey();
   }
   
   /**
    * Obtains the last key read from any source (stream or terminal).
    *
    * @return   The last key read.
    */
   public static integer getLastKey() 
   {
      return new integer(workArea.obtain().lastkey);
   }
   
   /**
    * Sets the event type and last key read from any source (stream or terminal). This is an undocumented
    * method for the LAST-EVENT system handle that was found in customer source code.
    *
    * @param    etype
    *           The event-type of the last event.  This may be 0 (for KEYPRESS), 1 (for MOUSE).  If one of
    *           these valid values is passed, then the method will change the event state and return 
    *           {@code true}.  With any other value, the method will not change state and will return
    *           {@code false}.
    * @param    kcode
    *           The key-code of the last event.  Any value may be passed but any changes will only be saved
    *           if the {@code etype} is valid (and the method thus returns {@code true}).  Any integer
    *           between -32768 and 32767 (inclusive) will set the key code to that same number. Any other 
    *           number will be cast into a 16-bit signed integer before assignment (which will drop the most
    *           significant 48-bits).
    *
    * @return   {@code true} if the {@code etype} is valid and state is changed.  {@code false} if 
    *           {@code etype} is NOT valid and state is NOT changed.
    */
   public static logical _setLastKey(long etype, long kcode)
   {
      // TODO: should we be setting the keyFunction, label and lastKeyState?
      
      if (etype == 0 || etype == 1)
      {
         WorkArea wa = workArea.obtain();
         
         wa.eventType = (etype == 0) ? KEY_EVTYPE : MOUSE_EVTYPE;
         wa.lastkey   = (short) kcode;
         
         return new logical(true);
      }
      
      return new logical(false);
   }

   /**
    * Sets the event type and last key read from any source (stream or terminal). This is an undocumented
    * method for the LAST-EVENT system handle that was found in customer source code.
    *
    * @param    etype
    *           The event-type of the last event.  This may be 0 (for KEYPRESS), 1 (for MOUSE).  If one of
    *           these valid values is passed, then the method will change the event state and return 
    *           {@code true}.  With any other value, the method will not change state and will return
    *           {@code false}.  Passing unknown value is equivalent to KEYPRESS.
    * @param    kcode
    *           The key-code of the last event.  Any value may be passed but any changes will only be saved
    *           if the {@code etype} is valid (and the method thus returns {@code true}).  Any integer
    *           between -32768 and 32767 (inclusive) will set the key code to that same number. Any other 
    *           number will be cast into a 16-bit signed integer before assignment (which will drop the most
    *           significant 48-bits).
    *
    * @return   {@code true} if the {@code etype} is valid and state is changed.  {@code false} if 
    *           {@code etype} is NOT valid and state is NOT changed.
    */
   public static logical _setLastKey(NumberType etype, long kcode)
   {
      return _setLastKey((etype == null || etype.isUnknown()) ? 0L : etype.longValue(), kcode);
   }

   /**
    * Sets the event type and last key read from any source (stream or terminal). This is an undocumented
    * method for the LAST-EVENT system handle that was found in customer source code.
    *
    * @param    etype
    *           The event-type of the last event.  This may be 0 (for KEYPRESS), 1 (for MOUSE).  If one of
    *           these valid values is passed, then the method will change the event state and return 
    *           {@code true}.  With any other value, the method will not change state and will return
    *           {@code false}.
    * @param    kcode
    *           The key-code of the last event.  Any value may be passed but any changes will only be saved
    *           if the {@code etype} is valid (and the method thus returns {@code true}).  Any integer
    *           between -32768 and 32767 (inclusive) will set the key code to that same number. Any other 
    *           number will be cast into a 16-bit signed integer before assignment (which will drop the most
    *           significant 48-bits).  Passing unknown value is the same as passing 0.
    *
    * @return   {@code true} if the {@code etype} is valid and state is changed.  {@code false} if 
    *           {@code etype} is NOT valid and state is NOT changed.
    */
   public static logical _setLastKey(long etype, NumberType kcode)
   {
      return _setLastKey(etype, (kcode == null || kcode.isUnknown()) ? 0L : kcode.longValue());
   }

   /**
    * Sets the event type and last key read from any source (stream or terminal). This is an undocumented
    * method for the LAST-EVENT system handle that was found in customer source code.
    *
    * @param    etype
    *           The event-type of the last event.  This may be 0 (for KEYPRESS), 1 (for MOUSE).  If one of
    *           these valid values is passed, then the method will change the event state and return 
    *           {@code true}.  With any other value, the method will not change state and will return
    *           {@code false}.  Passing unknown value is equivalent to KEYPRESS.
    * @param    kcode
    *           The key-code of the last event.  Any value may be passed but any changes will only be saved
    *           if the {@code etype} is valid (and the method thus returns {@code true}).  Any integer
    *           between -32768 and 32767 (inclusive) will set the key code to that same number. Any other 
    *           number will be cast into a 16-bit signed integer before assignment (which will drop the most
    *           significant 48-bits).  Passing unknown value is the same as passing 0.
    *
    * @return   {@code true} if the {@code etype} is valid and state is changed.  {@code false} if 
    *           {@code etype} is NOT valid and state is NOT changed.
    */
   public static logical _setLastKey(NumberType etype, NumberType kcode)
   {         
      return _setLastKey((etype == null || etype.isUnknown()) ? 0L : etype.longValue(),
                        (kcode == null || kcode.isUnknown()) ? 0L : kcode.longValue());
   }

   /**
    * Sets the last key read and main attributes: LABEL, FUNCTION, EVENT-TYPE
    *
    * @param    key
    *           The last key read (LASTKEY).
    */
   public static void setLastKey(int key)
   {
      setLastKey(key, 0);
   }

   /**
    * Sets the last key read and main attributes: LABEL, FUNCTION, EVENT-TYPE
    *
    * @param    key
    *           The last key read (LASTKEY).
    * @param    modifiers
    *           The last extended keys modifiers (MODIFIERS).
    */
   public static void setLastKey(final int key, final int modifiers)
   {
      WorkArea wa = workArea.obtain();
      wa.lastkey = key > 0 ? key & ~Keyboard.WINDOW_UI_EVENT_MASK : key;
      wa.lastKeyModifiers = modifiers;
      
      wa.eventType = Keyboard.isPGEvent(key)
         ? PROGRESS_EVTYPE
         : Keyboard.isMouseEvent(key)
         ? MOUSE_EVTYPE
         : KEY_EVTYPE;

      wa.keyFunction = Keyboard.keyFunction(key);
   }

   /**
    * Returns the last extended keys state.
    *
    * @return  see above.
    */
   public static int getLastKeyModifiers()
   {
      return workArea.obtain().lastKeyModifiers;
   }

   /**
    * Implementation of the LAST-EVENT system handle's FUNCTION attribute.
    *
    * @return   The key function of the last key event.
    */
   public static character getLastFunction()
   {
      //  The FUNCTION attribute read at this step can originate from two locations:
      //   - value from ClientState, cached in LogicalTerminal or
      //   - value set explicitly through setLastKey() or readKey()
      //     
      //  TODO: double check if this is read/written correctly, as it doesn't look right
      
      int functionKey = LogicalTerminal.getFunctionKey();
      
      if (functionKey != KeyInput.CHAR_UNDEFINED)
      {
         setKeyFunctionWorker(functionKey);
      }
      
      return new character(workArea.obtain().keyFunction);
   }
   
   /**
    * Used internally by P2J to set the {@link WorkArea#keyFunction} 
    * 
    * @param    func
    *           The FUNCTION to be set.
    */
   public static void setKeyFunctionWorker(int func)
   {
      WorkArea wa = workArea.obtain();
      wa.keyFunction = Keyboard.eventName(func);
      wa.eventType = Keyboard.isPGEvent(func) ? PROGRESS_EVTYPE
                                              : Keyboard.isMouseEvent(func) ? MOUSE_EVTYPE 
                                                                            : KEY_EVTYPE;
   }
  
   /**
    * Read a character from the unnamed input stream (if open) or from the 
    * <code>LogicalTerminal</code>, blocking until input is received.
    * <p>
    * The exact character read will be stored as the lastkey and can be
    * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
    * is any interruption or error during processing or will be set to -2 at
    * <code>EOF</code>.
    *
    * @return   The character read, -1 if there is any interruption or
    *           error during processing or -2 at <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   public static int readKey()
   throws StopConditionException
   {      
      return readKey(null, -1);
   }
  
   /**
    * Read a character from the given stream or from the 
    * <code>LogicalTerminal</code>, blocking until input is received.
    * <p>
    * The exact character read will be stored as the lastkey and can be
    * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
    * is any interruption or error during processing or will be set to -2 at
    * <code>EOF</code>.
    *
    * @param    in
    *           The stream from which to read.  If <code>null</code> or if
    *           it is an instance of {@link StreamWrapper} which has no
    *           backing stream, then the <code>LogicalTerminal</code> will
    *           be read.
    *
    * @return   The character read, -1 if there is any interruption or
    *           error during processing or -2 at <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   public static int readKey(Stream in)
   throws StopConditionException
   {      
      return readKey(in, -1);
   }
  
   /**
    * Read a character from the unnamed input stream (if open) or from the 
    * <code>LogicalTerminal</code>.
    * <p>
    * The exact character read will be stored as the lastkey and can be
    * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
    * is any interruption or error during processing or will be set to -2 at
    * <code>EOF</code>.
    *
    * @param    pause
    *           The number of seconds to wait for a character to be available
    *           for reading. Use 0 to get an immediate response (it will
    *           return -1 if there is no character immediately available).
    *           Use -1 to specify an infinite timeout (it will never
    *           timeout).  If set to the <code>unknown value</code>, an
    *           indefinite wait will occur.
    *
    * @return   The character read, -1 if there is any interruption or
    *           error during processing or -2 at <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   public static int readKey(NumberType pause)
   throws StopConditionException
   {      
      return readKey(null, pause);
   }
  
   /**
    * Read a character from the unnamed input stream (if open) or from the 
    * <code>LogicalTerminal</code>.
    * <p>
    * The exact character read will be stored as the lastkey and can be
    * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
    * is any interruption or error during processing or will be set to -2 at
    * <code>EOF</code>.
    *
    * @param    pause
    *           The number of seconds to wait for a character to be available
    *           for reading. Use 0 to get an immediate response (it will
    *           return -1 if there is no character immediately available).
    *           Use -1 to specify an infinite timeout (it will never
    *           timeout).
    *
    * @return   The character read, -1 if there is any interruption or
    *           error during processing or -2 at <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   public static int readKey(double pause)
   throws StopConditionException
   {      
      return readKey(null, pause);
   }
  
   /**
    * Read a character from the given stream or from the 
    * <code>LogicalTerminal</code>.
    * <p>
    * The exact character read will be stored as the lastkey and can be
    * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
    * is any interruption, the timeout expires without a character being read
    * or there is an error during processing.  The lastkey will be set to -2
    * at <code>EOF</code>.
    *
    * @param    in
    *           The stream from which to read.  If <code>null</code> or if
    *           it is an instance of {@link StreamWrapper} which has no
    *           backing stream, then the <code>LogicalTerminal</code> will
    *           be read.
    * @param    pause
    *           The number of seconds to wait for a character to be available
    *           for reading. Use 0 to get an immediate response (it will
    *           return -1 if there is no character immediately available).
    *           Use -1 to specify an infinite timeout (it will never
    *           timeout).  If set to the <code>unknown value</code>, an
    *           indefinite wait will occur.
    *
    * @return   The character read, -1 if there is any interruption, timeout
    *           or error during processing or -2 at <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   public static int readKey(Stream in, NumberType pause)
   throws StopConditionException
   {      
      return readKey(in, (pause.isUnknown() ? -1 : pause.doubleValue()));
   }
   
   /**
    * Read a character from the given stream or from the 
    * <code>LogicalTerminal</code>.
    * <p>
    * The exact character read will be stored as the lastkey and can be
    * accessed via {@link #lastKey}.  The lastkey will be set to -1 if there
    * is any interruption, the timeout expires without a character being read
    * or there is an error during processing.  The lastkey will be set to -2
    * at <code>EOF</code>.
    *
    * @param    in
    *           The stream from which to read.  If <code>null</code> or if
    *           it is an instance of {@link StreamWrapper} which has no
    *           backing stream, then the <code>LogicalTerminal</code> will
    *           be read.
    * @param    pause
    *           The number of seconds to wait for a character to be available
    *           for reading. Use 0 to get an immediate response (it will
    *           return -1 if there is no character immediately available).
    *           Use -1 to specify an infinite timeout (it will never
    *           timeout).
    *
    * @return   The character read, -1 if there is any interruption, timeout
    *           or error during processing or -2 at <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   public static int readKey(Stream in, double pause)
   throws StopConditionException
   {      
      int result = -1;
      
      // default the key to -1 in case we are interrupted
      WorkArea wa = workArea.obtain();
      wa.lastkey = result;
      
      boolean terminal = false;
      
      if (in == null)
      {
         in       = UnnamedStreams.input();
         terminal = (in == null);
      }
      else
      {
         if (in instanceof StreamWrapper)
         {
            terminal = in.isTerm();
         }
      }
      
      int millis = LogicalTerminal.toMilliseconds(pause);
      
      if (terminal)
      {
         // read from the terminal
         result = LogicalTerminal.readKey(millis);
      }
      else
      {
         // read from a named or unnamed stream
         result = in.readChar(millis).intValue();
      }
      
      // save the key that was read
      setLastKey(result);
      
      return result;
   }
   
   /**
    * Get the type of its associated handle.
    * 
    * @return   Always return the PSEUDO-WIDGET value.
    */
   public static character resourceType()
   {
      return new character(LegacyResource.PSEUDO_WIDGET);
   }

   /**
    * Calls {@link handle#invalidAttribute}, as the FORMAT attribute is not supported.
    * 
    * @return   unknown value.
    */
   @LegacyAttribute(name = "FORMAT", ignore = true)
   public static character getFormat()
   {
      handle.invalidAttribute("FORMAT", asHandle(), false);
      
      return new character();
   }

   /**
    * Calls {@link handle#readOnlyError} as this attribute is not supported.
    *
    * @param    format
    *           Not used.
    */
   @LegacyAttribute(name = "FORMAT", setter = true, ignore = true)
   public static void setFormat(@SuppressWarnings("unused") character format)
   {
      handle.readOnlyError(asHandle(), "FORMAT");
   }

   /**
    * Calls {@link handle#readOnlyError} as this attribute is not supported.
    *
    * @param    format
    *           Not used.
    */
   @LegacyAttribute(name = "FORMAT", setter = true, ignore = true)
   public static void setFormat(@SuppressWarnings("unused") String format)
   {
      handle.readOnlyError(asHandle(), "FORMAT");
   }

   /**
    * Calls {@link handle#invalidAttribute}, as the COLUMN-LABEL attribute is not supported.
    * 
    * @return   unknown value.
    */
   @LegacyAttribute(name = "COLUMN-LABEL", ignore = true)
   public static character getColumnLabel()
   {
      handle.invalidAttribute("COLUMN-LABEL", asHandle(), false);
      
      return new character();
   }

   /**
    * Calls {@link handle#readOnlyError} as this attribute is not supported.
    *
    * @param    columnLabel
    *           Not used.
    */
   @LegacyAttribute(name = "COLUMN-LABEL", setter = true, ignore = true)
   public static void setColumnLabel(@SuppressWarnings("unused") character columnLabel)
   {
      handle.readOnlyError(asHandle(), "COLUMN-LABEL");
   }

   /**
    * Calls {@link handle#readOnlyError} as this attribute is not supported.
    *
    * @param    columnLabel
    *           Not used.
    */
   @LegacyAttribute(name = "COLUMN-LABEL", setter = true, ignore = true)
   public static void setColumnLabel(@SuppressWarnings("unused") String columnLabel)
   {
      handle.readOnlyError(asHandle(), "COLUMN-LABEL");
   }
 
   /**
    * Calls {@link handle#invalidAttribute}, as the DATA-TYPE attribute is not supported.
    *
    * @return   unknown value.
    */
   @LegacyAttribute(name = "DATA-TYPE", ignore = true)
   public static character getDataType()
   {
      handle.invalidAttribute("DATA-TYPE", asHandle(), false);
      
      return new character();
   }

   /**
    * Calls {@link handle#readOnlyError} as this attribute is not supported.
    *
    * @param    dataType
    *           Not used.
    */
   @LegacyAttribute(name = "DATA-TYPE", setter = true, ignore = true)
   public static void setDataType(@SuppressWarnings("unused") String dataType)
   {
      handle.readOnlyError(asHandle(), "DATA-TYPE");
   }

   /**
    * Calls {@link handle#readOnlyError} as this attribute is not supported.
    *
    * @param    dataType
    *           Not used.
    */
   @LegacyAttribute(name = "DATA-TYPE", setter = true, ignore = true)
   public static void setDataType(@SuppressWarnings("unused") character dataType)
   {
      handle.readOnlyError(asHandle(), "DATA-TYPE");
   }
   
   /**
    * Returns the value of the LAST-EVENT:WIDGET-LEAVE attribute
    * 
    * @return  character
    *          current value of LAST-EVENT:WIDGET-LEAVE
    */
   public static handle getWidgetLeave() 
   {
      EventInfo info = LogicalTerminal.getEventInfo();
      if (info == null)
      {
         return new handle();
      }

      if ("ENTRY".equals(info.name) || "ROW-ENTRY".equals(info.name))
      {
         return LogicalTerminal.getWidgetHandleById(info.otherId);
      }
      else if ("LEAVE".equals(info.name) || "ROW-LEAVE".equals(info.name)) 
      {
         return LogicalTerminal.getWidgetHandleById(info.widgetId);
      }
      
      return new handle();
   }
   
   /**
    * Returns the value of the LAST-EVENT:WIDGET-ENTER attribute
    * 
    * @return  character
    *          current value of LAST-EVENT:WIDGET-ENTER
    */
   public static handle getWidgetEnter() 
   {
      EventInfo info = LogicalTerminal.getEventInfo();
      if (info == null)
      {
         return new handle();
      }
      
      if ("ENTRY".equals(info.name))
      {
         if (info.otherId == -1)
         {
            return new handle();
         }
         return LogicalTerminal.getWidgetHandleById(info.widgetId);
      }
      else if ("ROW-ENTRY".equals(info.name))
      {
         return LogicalTerminal.getWidgetHandleById(info.widgetId);
      }
      else if ("LEAVE".equals(info.name) || "ROW-LEAVE".equals(info.name)) 
      {
         return LogicalTerminal.getWidgetHandleById(info.otherId);
      }
      
      return new handle();
   }
   
   /**
    * Returns the value of the LAST-EVENT:EVENT-TYPE attribute
    * 
    * @return  character
    *          current value of LAST-EVENT:EVENT-TYPE
    */
   public static character getEventType() 
   {
      return new character(workArea.obtain().eventType);
   }
   
   /**
    * Returns the value of the LABEL attribute
    *
    * @return    Current value of the LABEL attribute.
    */
   public static character getLabel()
   {
       // The case LABEL=ENTRY at start of procedure when a field got a focus. This case stands   
       // out from the main line of implementation. The value in this case is coming from 
       // the ClientState unlike the main scenario when a label is set via setLabel(), 
       // setLabelWorker(), readKey() methods.
       //  
       // This code is commented because it interferes the main scenario, so the problem is not 
       // solved. TODO: find solution for this case.
       // 
       //       int functionKey = LogicalTerminal.getFunctionKey();
       //       if (functionKey != -1)
       //       {
       //          setLabelWorker(functionKey);
       //       }
      
      final WorkArea keyReader = workArea.obtain();
      //return new character(PROGRESS_EVTYPE.equals(keyReader.eventType) ? keyReader.label
      //         : LogicalTerminal.getEventLabel());
      return new character(LogicalTerminal.getEventLabel());
   }
   
   /**
    * Set the <code>name</code> attribute of handle.
    *
    * @param label
    *        The LABEL to be set.
    */
   @LegacyAttribute(name = "LABEL", setter = true, ignore = true)
   public static void setLabel(@SuppressWarnings("unused") String label)
   {
      // setLabel(new character(label));
      handle.readOnlyError(asHandle(), "LABEL");
   }

   /**
    * Set the <code>name</code> attribute of handle.
    *
    * @param label
    *        The LABEL to be set.
    */
   @LegacyAttribute(name = "LABEL", setter = true, ignore = true)
   public static void setLabel(@SuppressWarnings("unused") character label)
   {
      handle.readOnlyError(asHandle(), "LABEL");
   }

   /**
    * Used internally by P2J to set the {@link WorkArea#label} in server-side event cases. 
    * 
    * @param label
    *        The LABEL to be set.
    */
   public static void setLabelWorker(String label)
   {
      WorkArea wa = workArea.obtain();
      
      wa.label = label;
      
      // for all server-side events EVENT-TYPE=PROGRESS
      wa.eventType = PROGRESS_EVTYPE;
      
      // TODO: should we set the function here?
   }
   
   /**
    * Get the COLUMN attribute.
    * 
    * @return  The column value in characters.
    */
   @LegacyAttribute(name = "COLUMN")
   public static decimal getColumn()
   {
      // TODO: we need testcases to confirm behavior
      Double col = LogicalTerminal.getEventColumn();
      return col == null ? new decimal() : new decimal(col);
   }

   /**
    * Get the ROW attribute.
    * 
    * @return  The row value in characters.
    */
   @LegacyAttribute(name = "ROW")
   public static decimal getRow()
   {
      // TODO: we need testcases to confirm behavior
      Double row = LogicalTerminal.getEventRow();
      return row == null ? new decimal() : new decimal(row);
   }
   
   /**
    * Implementation of the LAST-EVENT system handle's X attribute.
    *
    * @return  The pixel location of the mouse cursor relative to the left edge of the display.
    */
   @LegacyAttribute(name = "X")
   public static integer getX()
   {
      Integer x = LogicalTerminal.getEventX();
      return x == null ? new integer() : new integer(x);
   }
   
   /**
    * Implementation of the LAST-EVENT system handle's Y attribute.
    *
    * @return  The pixel location of the mouse cursor relative to the left edge of the display.
    */
   @LegacyAttribute(name = "Y")
   public static integer getY()
   {
      Integer y = LogicalTerminal.getEventY();
      return y == null ? new integer() : new integer(y);
   }
   
   /**
    * Set the X attribute.
    *
    * @param   x
    *          The X attribute.
    */
   @LegacyAttribute(name = "X", setter = true)
   public static void setX(@SuppressWarnings("unused") NumberType x)
   {
      readOnlyError("X");
   }

   /**
    * Set the Y attribute.
    *
    * @param   y
    *          The Y attribute.
    */
   @LegacyAttribute(name = "Y", setter = true)
   public static void setY(@SuppressWarnings("unused") NumberType y)
   {
      readOnlyError("Y");
   }
   
   /**
    * Set the X attribute.
    *
    * @param   x
    *          The X attribute.
    */
   @LegacyAttribute(name = "X", setter = true)
   public static void setX(@SuppressWarnings("unused") int x)
   {
      readOnlyError("X");
   }

   /**
    * Set the Y attribute.
    *
    * @param   y
    *          The Y attribute.
    */
   @LegacyAttribute(name = "Y", setter = true)
   public static void setY(@SuppressWarnings("unused") int y)
   {
      readOnlyError("Y");
   }
   
   /**
    * Set the COLUMN attribute.
    * 
    * @param   column
    *          The column value in characters.
    */
   @LegacyAttribute(name = "COLUMN", setter = true)
   public static void setColumn(@SuppressWarnings("unused") double column)
   {
      readOnlyError("COLUMN");
   }

   /**
    * Set the COLUMN attribute.
    * 
    * @param   column
    *          The column value in characters.
    */
   @LegacyAttribute(name = "COLUMN", setter = true)
   public static void setColumn(@SuppressWarnings("unused") NumberType column)
   {
      readOnlyError("COLUMN");
   }

   /**
    * Set the ROW attribute.
    * 
    * @param   row
    *          The row value in characters.
    */
   @LegacyAttribute(name = "ROW", setter = true)
   public static void setRow(@SuppressWarnings("unused") double row)
   {
      readOnlyError("ROW");
   }

   /**
    * Set the ROW attribute.
    * 
    * @param   row
    *          The row value in characters.
    */
   @LegacyAttribute(name = "ROW", setter = true)
   public static void setRow(@SuppressWarnings("unused") NumberType row)
   {
      readOnlyError("ROW");
   }
   
   /**
    * Implementation of the ON-FRAME-BORDER system handle's EVENT-TYPE attribute.
    *
    * @return  {@code YES} if the last event was a mouse event that occurred on a frame border.
    */
   @LegacyAttribute(name = "ON-FRAME-BORDER")
   public static logical isOnFrameBorder()
   {
      UnimplementedFeature.missing("LAST-EVENT:ON-FRAME-BORDER attribute not implemented.");
      return new logical();
   }

   /**
    * Stores data relating to the state of the current context's key reading.
    */
   private static class WorkArea
   {  
      /** The resource's ID. */
      private Long id = null;

      /** Stores the last key read from any source (stream or terminal). */
      private int lastkey = -1;

      /** Stores the last extended keys state. */
      private int lastKeyModifiers = 0;
      
      /** 
       * LAST-EVENT:LABEL value that can be set explicitly (setLabelWorker) or implicitly 
       * ({@link KeyReader#setLastKey(int)}, or in {@link KeyReader#readKey()}). Commonly have 
       * a name of pressed key, but in some specific cases can have the same value as  
       * {@link #keyFunction} for example values "CHOOSE" and "ENTRY" - at the start of procedure.
       */
      private String label;
      
      /** 
       * LAST-EVENT:EVENT-TYPE attribute. Can have on of following values:
       * <ul> 
       *   <li>"PROGRESS",</li>
       *   <li>"KEYPRESS",</li>
       *   <li>"MOUSE".</li>
       * </ul>  
       * It has the "PROGRESS" values for different cases:
       * <ul>
       *   <li>CHOOSE event (button press),</li>
       *   <li>ENTRY event,</li>
       *   <li>server events such as CONNECT, PROCEDURE-COMPLETE, READ-RESPONSE,</li>
       *   <li>VALUE-CHANGED event,</li>
       *   <li>WINDOW events at OS level</li>
       * </ul>
       * and "KEYPRESS" in other cases.
       */
      private String eventType;
      
      /** 
       * LAST-EVENT:FUNCTION attribute value that can be set explicitly (setKeyFunctionWorker).
       * The high-level name of action based on EVENT-TYPE. For EVENT-TYPE="KEYPRESS" contains
       * key function (for example "RETURN"), for EVENT-TYPE="PROGRESS" contains some high-level
       * widget event name such as "CHOOSE" or "LEAVE".
       */
      private String keyFunction;
   }
   
   /**
    * Simple container that stores and returns a context-local instance of
    * the global work area.
    */
   private static class ContextContainer
   extends ContextLocal
   {
      /**
       * Obtains the context-local instance of the contained global work
       * area.
       *
       * @return   The work area associated with this context.
       */
      public WorkArea obtain() 
      {
         return (WorkArea) this.get();
      }   
      
      /**
       * Initializes the work area, the first time it is requested within a
       * new context.
       *
       * @return   The newly instantiated work area.
       */
      @Override
      protected synchronized Object initialValue()
      {
         return new WorkArea();
      }   
   }
}