SessionUtils.java

/*
** Module   : SessionUtils.java
** Abstract : Defines APIs used to access the SESSION attributes and methods.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 CA  20130112 Created initial version.
** 002 CS  20130214 Added support for SESSION: EXPORT, SERVER-OPERATING-MODE, REMOTE attributes.
** 003 CA  20130221 getType was renamed to getResourceType.
** 004 CA  20130222 Added stubs for more SESSION-related attributes and methods: REMOTE,
**                  PARAMETER. The implementations for EXPORT, SERVER-OPERATING-MODE and REMOVE 
**                  attrs call their ServerImpl counterparts.
** 005 OM  20130304 Refactored isValid and isUnknown of WrappedResource to valid and unknown.
** 006 CA  20130529 Added implementations for FIRST-SERVER, LAST-SERVER, REMOTE, EXPORT and
**                  SERVER-OPERATING-MODE.
** 007 OM  20130510 Added support for date-related SESSION attributes so they can be queried from 
**                  client side.
** 008 CA  20130822 Allow access to the DATE-FORMAT attribute (set/getDateOrder) outside of a P2J
**                  server.
** 009 CA  20130927 Static proxies need the unknown() API. Resource type is determined from 
**                  LegacyResource constants. Implemented the following attributes: FIRST-BUFFER,
**                  FIRST-QUERY, FIRST-/LAST-SOCKET, FIRST-/LAST-SERVER-SOCKET.
** 010 CA  20131002 Implemented the PARAMETER attribute.
** 011 CA  20140701 Added PIXELS-PER-ROW and PIXELS-PER-COLUMN.
** 012 CA  20140702 Implemented PIXELS-PER-ROW and PIXELS-PER-COLUMN.
** 013 OM  20141110 Refactored get/setDateOrder() to get/setDateFormat().
** 014 EVL 20141216 Adding SESSION:TOOLTIPS attribute handling, server side part.
** 015 GES 20141228 Completed PARAMETER attribute implementation by storing it in a context-local
**                  and allowing the client command line/bootstrap to override any default value
**                  found in the directory.
** 016 GES 20150227 Added FIRST-CHILD and LAST-CHILD attribute support.
** 017 HC  20150508 Improvements to GUI window scrolling support.
** 018 CA  20150502 The resource ID needs to be kept at the resource, so it can be accessed after
**                  the resource is deleted.
** 019 OM  20150915 Added stub for SESSION:DEBUG-ALERT, DATA-ENTRY-RETURN and APPL-ALERT-BOXES
**                  attributes and method. Fixed set3D method.  Added LOAD-ICON.
** 020 HC  20160222 Fixed error reporting when assigning session read-only attributes.
** 021 IAS 20160302 Support for the APPL-ALERT-BOXES, DATA-ENTRY-RETURN, DEBUG-ALERT and
**                  IMMEDIATE-DISPLAY SESSION attributes.
** 022 IAS 20160328 Client log support.
** 023 OM  20160401 Fixed method name collision for static proxy.
** 024 HC  20170811 Added conversion support for SESSION attributes PRINTER-NAME, PRINTER-PORT,
**                  PRINTER-CONTROL-HANDLE, PRINTER-HDC and the method GET-PRINTERS.
**     HC  20170817 Added conversion support and runtime stubs for SESSION:STARTUP-PARAMETERS and
**                  SESSION:SUPPRESS-WARNINGS.
** 025 HC  20171003 Implemented SESSION:SUPPRESS-WARNINGS runtime support and the ability to
**                  handle (display or output depending on the runtime state) legacy warnings.
** 026 HC  20171024 Runtime support for SYSTEM-DIALOG PRINT-SETUP and OUTPUT TO PRINTER.
** 027 EVL 20180220 Now the current printer name is getting from client OS base.
** 028 CA  20180502 Fixed issues with time-related attributes used by SessionExports. 
** 029 CA  20180503 Added MULTITASKING-INTERVAL, SYSTEM-ALERT-BOXES and SERVER-CONNECTION-ID. 
**                  Renamed/moved some SESSION attribute getters/setters to fix issues with using
**                  the SESSION via a handle.
** 030 CA  20181213 Added SESSION:FIRST-OBJECT and LAST-OBJECT attributes (stubs).
**                  Added loadIcon(character).
** 031 CA  20190122 Implemented FIRST-OBJECT and LAST-OBJECT.
** 032 CA  20190416 Added LOCAL-VERSION-INFO and CURRENT-REQUEST-INFO stubs.
**     OM  20190416 Added FIRST-DATASET and FIRST-DATA-SOURCE methods.
** 033 CA  20190423 Added these attributes: CURRENT-RESPONSE-INFO, ICFPARAMETER, CONTEXT-HELP-FILE,
**                  SERVER-CONNECTION-BOUND-REQUEST, stubs only.
** 034 EVL 20190513 Added I18nOps for SESSION CP attributes getters moved from EnvironmentOps.
** 035 CA  20190520 Fixed the name of some OO classes.
** 036 CA  20190628 Added SERVER-CONNECTION-BOUND and SERVER-CONNECTION-CONTEXT stubs.
** 037 CA  20190710 LOCAL-VERSION-INFO and LOCAL-REQUEST-INFO return proper instances.
**     CA  20190717 Added ICFPARAMETER runtime support.
** 038 RFB 20200304 Added SESSION:EXIT-CODE handlers.
** 039 IAS 20200410 Reworked support of the date-related SESSION attributes.
**     CA  20200427 Added support for SERVER-CONNECTION-BOUND, SERVER-CONNECTION-CONTEXT, 
**                  CURRENT-REQUEST-INFO, CURRENT-RESPONSE-INFO, CONTEXT-HELP-FILE, 
**                  SERVER-CONNECTION-BOUND-REQUEST.
** 040 OM  20201030 Invalid attribute API support for getters/setters.
**     OM  20201203 Fixed handling of READ/ONLY attributes.
**     CA  20210213 The requestInfo and responseInfo must be vars.
**     OM  20210226 Added DATA-SOURCE sibling navigation support. Fixed Dataset handles order.
**     CA  20210920 Send MULTITASKING-INTERVAL to the client-side (which has runtime support).
**     CA  20220114 Added SESSION:CLIENT-DISCONNECTED and TERMINATION-HOOK FWD extension attributes.
**     CA  20220128 TERMINATION-HOOK can not be set for remote sessions (appserver).
**     CA  20220208 Added ERROR-STACK-TRACE and -errorstack runtime.
**     VVT 20221003 CommonHandle.getResourceType() method renamed to resourceType() to prevent conflicts
**                  with namesakes in DMO. See #6694.
**     CA  20220930 Cache the proxy associated with the SESSION system handle.
**     EVL 20221007 Adding runtime support for SESSION:LOAD-ICON.
**     CA  20220426 Added SUPPRESS-WARNINGS-LIST attribute stubs.
**     VVT 20220913 CommonHandle.getResourceType() method renamed to resourceType() to prevent conflicts
**                  with namesakes in DMO. See #6694.
**     CA  20221123 FIRST-/LAST-CHILD do not report anything for batch or appserver clients (as WINDOW widgets
**                  don't exist).
** 041 GBB 20230313 isClientLog param removed.
** 042 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 043 GBB 20230628 Added SESSION-ID attribute.
** 044 GBB 20230719 Fix for SESSION-ID, moved to a separate interface.
** 045 TT  20230621 Added support for the StartupParameters.
**     TT  20230628 Added the functionality for session:suppress-warnings-list.
** 046 TT  20230901 Moved StartupParameters into a ContextLocal container with isResetAllowed = false.
** 047 GBB 20230825 Adding support for AUTH-BLOB.
** 048 GBB 20231102 Added SESSION:RELATED-SESSION-ID.
**                  Added SESSION:NEW-SESSION(character, character).
** 049 GBB 20231204 Adding overloading methods for SESSION:NEW-SESSION.
** 050 GBB 20240214 Added DEVICE-ID.
** 051 GBB 20240301 Added multiple custom session attributes.
** 052 GBB 20240515 Added SESSION:GET-LOGIN-PARAMETER method.
** 053 GBB 20240529 getLoginParam() to accept 1-based index as in OE.
** 054 GBB 20240610 Adds conditions for unknown CURRENT-RESPONSE-INFO / CURRENT-REQUEST-INFO.
** 055 HC  20240630 Implemented support for WEB-FILE-UPLOAD statement.
** 056 GBB 20240709 Fixing getLoginParam javadoc.
** 057 GBB 20240826 Storing and retrieving SSO storageId.
** 058 GBB 20240912 Don't call the client with date updates if ALL OS resources are enabled server-side.
** 059 ICP 20250129 Used logical constant to leverage cached instances.
** 060 ES  20250321 Added getLastAsyncRequest and getFirstAsyncRequest method, for FIRST-ASYNC-REQUEST and 
**                  LAST-ASYNC-REQUEST attributes.
** 061 GBB 20250403 AppServerManager method renamed to isMultiSession.
*/

/*
** 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.main.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.net.Session;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.util.osresource.*;
import com.goldencode.proxy.*;

import java.util.*;
import java.util.stream.*;

/**
 * This class provides implementation for SESSION attributes and methods.
 * Mainly, the APIs implemented here are defined in the {@link CommonSession}
 * interface (or one of its super interfaces) and are not statically 
 * implemented by the classes used by the {@link #asHandle()}.
 */
public class SessionUtils
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(SessionUtils.class);
   
   /** Context local proxy that allows SESSION attributes to be pushed from server to client. */
   private static final ContextLocal<WorkArea> work = new ContextLocal<WorkArea>()
   {
      /**
       * Initializes the work area, the first time it is requested within a
       * new context.
       * <p>
       * This will obtain a unique instance of <code>SessionExports</code> depending on which
       * side (server or client) it is called the returned value can be a local or a network
       * proxy instance.
       *
       * @return   The newly instantiated work area.
       */
      @Override
      protected WorkArea initialValue()
      {
         WorkArea wa = new WorkArea();
         
         if (!ErrorManager.isHeadless() && 
            !SessionManager.get().isLeaf() && 
            !OSResourceManager.getInstance().isServerSide(OsResourceType.ALL))
         {
            wa.exports = (SessionExports) RemoteObject.obtainInstance(SessionExports.class, true);
         }

         return wa;
      }
   };

   /** Lock for synchronizing device id writes. */
   private static final Object DEVICE_ID_LOCK = new Object();

   /** Context local proxy that allows the startup parameters to be pushed from server to client. */
   private static final ContextLocal<StartupParameters> startupParameters = new ContextLocal<StartupParameters>()
   {
      /**
       * Initializes the work area, the first time it is requested within a new context.
       *
       * @return The newly instantiated startup configuration.
       */
      @Override
      protected StartupParameters initialValue()
      {
         return new StartupParameters();
      }

      /**
       * Check if this instance is allowed to be reset.
       *
       * @return <code>false</code> by default.
       */
      @Override
      protected boolean isResetAllowed()
      {
         return false;
      }
   };

   /**
    * Get a the instance for the SESSION system handle.  Is obtained using
    * a call to the {@link StaticProxy#obtain(Class, Class[])}, using the
    * {@link CommonSession} interface and its methods implemented by these
    * classes: {@link SessionUtils}, {@link EnvironmentOps}, {@link date},
    * {@link LogicalTerminal}, {@link ProcedureManager}.
    * 
    * @return   See above.
    */
   public static handle asHandle()
   {
      WorkArea wa = work.get();

      if (wa.proxy != null)
      {
         return new handle(wa.proxy);
      }
      
      Class[] classes = 
      {
         SessionUtils.class,
         EnvironmentOps.class,
         date.class,
         I18nOps.class,
         LogicalTerminal.class,
         ProcedureManager.class,
         NumberType.class
      };
      CommonSession proxy = StaticProxy.obtain(CommonSession.class, classes);
      
      wa.proxy = proxy;
      
      if (wa.id == null)
      {
         wa.id = handle.resourceId(proxy);
      }

      return new handle(proxy);
   }

   /**
    * Returns the session unique identifier.
    *
    * @return   The session unique identifier.
    */
   public static character getSessionId()
   {
      WorkArea wa = work.get();
      if (wa.sessionId == null)
      {
         wa.sessionId = UUID.randomUUID().toString();
      }
      return new character(wa.sessionId);
   }

   /**
    * Set the SESSION-ID Attribute.
    *
    * @param    sessionId
    *           The value of the SESSION-ID attribute.
    */
   public static void setSessionId(character sessionId)
   {
      readOnlyError("SESSION-ID");
   }

   /**
    * Set the SESSION-ID Attribute.
    *
    * @param    sessionId
    *           The value of the SESSION-ID attribute.
    */
   public static void setSessionId(String sessionId)
   {
      readOnlyError("SESSION-ID");
   }

   /**
    * Returns the authentication blob content.
    *
    * @return   The authentication blob content.
    */
   public static character getAuthBlob()
   {
      return new character(work.get().authBlob);
   }

   /**
    * Returns the device ID.
    *
    * @return   The device ID.
    */
   public static character getDeviceId()
   {
      return new character(work.get().deviceId);
   }
   
   /**
    * Sets the authentication blob content.
    *
    * @param    authBlob
    *           The authentication blob content.
    */
   public static void setAuthBlob(String authBlob)
   {
      work.get().authBlob = authBlob;
   }
   
   /**
    * Sets the device ID. It can be set only once in the lifespan of the session.
    *
    * @param    deviceId
    *           The device ID.
    */
   public static void setDeviceId(String deviceId)
   {
      synchronized(DEVICE_ID_LOCK)
      {
         if (work.get().deviceId != null)
         {
            LOG.info("Device ID can't be changed for the session.");
            return;
         }
         work.get().deviceId = deviceId;
      }
   }
   
   /**
    * Returns the unique ID of the original session that was forked. Corresponds with the  
    * <code>SESSION:RELATED-SESSION-ID</code> system handle attribute.
    *
    * @return   The unique ID of the original session that was forked.
    */
   public static character getRelatedSessionId()
   {
      WorkArea wa = work.get();
      if (wa.relatedSessionId == null)
      {
         wa.relatedSessionId = getSessionId().toStringMessage();
      }
      return new character(wa.relatedSessionId);
   }

   /**
    * Sets the RELATED-SESSION-ID attribute. Setter can't be accessed in 4GL.
    *
    * @param    relatedSessionId
    *           The unique ID of the original session that was forked.
    */
   public static void setRelatedSessionId(String relatedSessionId)
   {
      work.get().relatedSessionId = relatedSessionId;
   }

   /**
    * Starts a new forked session. Corresponds with the <code>SESSION:NEW-SESSION(character, character)
    * </code> system handle method.
    *
    * @param   cfgOverrides
    *          New session startup parameters.
    * @param   authBlob
    *          The value to be assigned to SESSION:AUTH-BLOB in the new session.
    */
   public static void forkNewSession(character cfgOverrides, character authBlob)
   {
      String cfgOverridesString = cfgOverrides == null || cfgOverrides.isUnknown() ? null :
         cfgOverrides.toStringMessage();
      String authBlobString = authBlob == null || authBlob.isUnknown() ? null : authBlob.toStringMessage();
      forkNewSession(cfgOverridesString, authBlobString);
   }

   /**
    * Starts a new forked session. Corresponds with the <code>SESSION:NEW-SESSION(String, character)
    * </code> system handle method.
    *
    * @param   cfgOverrides
    *          New session startup parameters.
    * @param   authBlob
    *          The value to be assigned to SESSION:AUTH-BLOB in the new session.
    */
   public static void forkNewSession(String cfgOverrides, character authBlob)
   {
      String authBlobString = authBlob == null || authBlob.isUnknown() ? null : authBlob.toStringMessage();
      forkNewSession(cfgOverrides, authBlobString);
   }

   /**
    * Starts a new forked session. Corresponds with the <code>SESSION:NEW-SESSION(character, String)
    * </code> system handle method.
    *
    * @param   cfgOverrides
    *          New session startup parameters.
    * @param   authBlob
    *          The value to be assigned to SESSION:AUTH-BLOB in the new session.
    */
   public static void forkNewSession(character cfgOverrides, String authBlob)
   {
      String cfgOverridesString = cfgOverrides == null || cfgOverrides.isUnknown() ? null :
         cfgOverrides.toStringMessage();
      forkNewSession(cfgOverridesString, authBlob);
   }

   /**
    * Starts a new forked session. Corresponds with the <code>SESSION:NEW-SESSION(String, String)
    * </code> system handle method.
    *
    * @param   cfgOverrides
    *          New session startup parameters.
    * @param   authBlob
    *          The value to be assigned to SESSION:AUTH-BLOB in the new session.
    */
   public static void forkNewSession(String cfgOverrides, String authBlob)
   {
      if (!StandardServer.getClientParameters().web)
      {
         LOG.warning("Attempt to fork a new session for a non-web client failed.");
         return;
      }

      SessionForkManager.getInstance().forkNewSession(cfgOverrides, authBlob);
   }

   /**
    * Get this resource's ID.
    * 
    * @return   The resource's ID.
    */
   public static Long id()
   {
      WorkArea wa = work.get();
      
      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(long id)
   {
      // no-op
   }

   /**
    * Get the type of its associated handle.
    * 
    * @return   Always return the PSEUDO-WIDGET value.
    */
   public static character resourceType()
   {
      return new character(LegacyResource.PSEUDO_WIDGET);
   }
   
   /**
    * 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)
   {
      // the following attribute names are reported in their shortest forms
      if (attribute.toUpperCase().startsWith("DISPLAY-TY"))
      {
         attribute = "DISPLAY-T";
      }
      else if (attribute.toUpperCase().startsWith("HEIGHT-C"))
      {
         attribute = "HEIGHT";
      }
      else if (attribute.toUpperCase().startsWith("WIDTH-C"))
      {
         attribute = "WIDTH";
      }
      
      // the error message for session read-only attributes is different from the one emitted by
      // handle.readOnlyError() - settable is spelled with two 't' and there is no word 'widget' at the end of
      // the message.
      ErrorManager.recordOrShowError(4052, attribute.toUpperCase(), "settable", LegacyResource.PSEUDO_WIDGET);
   }
   
   /**
    * Emitted and to be used only in cases the attribute is read-only. It displays appropriate error message.
    * <br>
    * TODO: decide whether the following message can be raised here:<br>
    * {@code **Unable to assign UNKNOWN value to attribute <attribute> on <widget id>.}
    *
    * @param    attribute
    *           The read-only attribute.
    * @param    expr
    *           The value which is attempted to be assigned to the read-only attribute. 
    */
   public static void readOnlyError(String attribute, Object expr)
   {
      readOnlyError(attribute);
   }   
   
   /**
    * Indicates if the session is for the context of an appserver.
    * 
    * @return   <code>true</code> if the session is for the context of an appserver.
    */
   public  static logical isRemote()
   {
      return new logical(_isRemote());
   }

   /**
    * Indicates if the session is for the context of an appserver.
    * 
    * @return   <code>true</code> if the session is for the context of an appserver.
    */
   public static boolean _isRemote()
   {
      return AppServerManager.isRemote();
   }

   /**
    * Get the CLIENT-DISCONNECTED attribute of this session.
    * 
    * @return   <code>true</code> if the FWD client has disconnected (due to an abend, socket close, etc).
    */
   public static logical isClientDisconnected()
   {
      if (work.get().clientDisconnected)
      {
         return new logical(true);
      }
      
      // check if the client socket is gone
      Session session = SessionManager.get().getSession();
      return new logical(!session.isRunning());
   }
   
   /**
    * Get the TERMINATION-HOOK attribute of this session.
    * 
    * @return   See above.
    */
   @LegacyAttribute(name = "TERMINATION-HOOK")
   public static character getTerminationHook()
   {
      return new character(work.get().terminationHook);
   }
   
   /**
    * Set the TERMINATION-HOOK attribute of this session.
    * <p>
    * This attribute is valid only for non-remote sessions - for a remote session, a message will be logged.
    * 
    * @param   hook
    *          The hook, a character expression with an external program name or a static method reference
    *          following the <code>com.goldencode.Foo:someStaticMethod</code> syntax.
    */
   @LegacyAttribute(name = "TERMINATION-HOOK", setter = true)
   public static void setTerminationHook(character hook)
   {
      if (_isRemote())
      {
         ErrorManager.recordOrShowError(-1001, "TERMINATION-HOOK can not be set for remote sessions.", 
                                        false, false);
         return;
      }
      work.get().terminationHook = hook.isUnknown() ? null : hook.toStringMessage();
   }
   
   /**
    * Set the TERMINATION-HOOK attribute of this session.
    * 
    * @param   hook
    *          The hook, a character expression with an external program name or a static method reference
    *          following the <code>com.goldencode.Foo:someStaticMethod</code> syntax.
    */
   public static void setTerminationHook(String hook)
   {
      work.get().terminationHook = hook;
   }

   /**
    * Method implementation for getting the SERVER-OPERATING-MODE attribute of the session handle.
    * <p>
    * Possible values:
    * <ul>
    *    <li>State-reset
    *    <li>State-aware
    *    <li>Stateless
    *    <li>State-free
    * </ul>
    * 
    * @return   The corresponding SERVER-OPERATING-MODE attribute of the session handle.
    */
   public static character getServerOperatingMode()
   {
      return AppServerManager.getServerOperatingMode();
   }
   
   /**
    * Method that specifies the remote procedures the client can execute from the AppServer 
    * session.
    * 
    * @param    procList
    *           The list of procedure  and  name-patterns separated by comma.
    *         
    * @return   <code>true</code> if the method succeeded.
    */
   public static logical export(String procList)
   {
      return AppServerManager.export(procList);
   }
   
   /**
    * Method that specifies the remote procedures the client can execute from the AppServer 
    * session.
    * 
    * @param    procList
    *           The list of procedure  and  name-patterns separated by comma.
    *         
    * @return   <code>true</code> if the method succeeded.
    */
   public static logical export(character procList)
   {
      return AppServerManager.export(procList);
   }

   /**
    * Get the value of the PARAMETER attribute, which is set to the value of the 4GL 
    * <code>-param</code> command line option specified for the current session.
    * <p>
    * The following is the precedence order (highest to lowest) for obtaining the
    * value of this parameter:
    * <p>
    * <ul>
    *    <li> The client command line.
    *    <li> The client bootstrap configuration.
    *    <li> The directory using relative lookup and the "startup_parameter" node.
    * </ul>
    *
    * @return   The <code>SESSION:PARAMETER</code> value for this context.
    */
   public static character getParameter()
   {
      return new character(startupParameters.get().getParameter());
   }

   /**
    * Returns the handle for the first dynamic buffer in the first table containing a dynamic 
    * buffer.
    * 
    * @return   See above.
    */
   public static handle firstBuffer()
   {
      // TODO: this should be OK, but more testing is needed to check if 4GL doesn't sort them in
      // a different order (i.e. using the table name)
      return HandleChain.firstResource(LegacyResource.BUFFER);
   }

   /**
    * Returns the handle for the first window in the session.
    * 
    * @return   The first window in the session's list of windows.
    */
   public static handle firstChild()
   {
      if (EnvironmentOps.isBatchMode().booleanValue() || AppServerManager.isRemote())
      {
         return new handle();
      }

      return HandleChain.firstResource(LegacyResource.WINDOW);
   }

   /**
    * Returns the handle for the last window in the session.
    * 
    * @return   The last window in the session's list of windows.
    */
   public static handle lastChild()
   {
      if (EnvironmentOps.isBatchMode().booleanValue() || AppServerManager.isRemote())
      {
         return new handle();
      }

      return HandleChain.lastResource(LegacyResource.WINDOW);
   }

   /**
    * Returns the handle for the first created dynamic query.
    * <p>
    * TODO: this should call a worker method from the persist package.
    * 
    * @return   See above.
    */
   public static handle firstQuery()
   {
      return HandleChain.firstResource(LegacyResource.QUERY);
   }
   
   /**
    * Get the FIRST-SERVER attribute of this session.
    * 
    * @return   See above.
    */
   public static handle firstServer()
   {
      return HandleChain.firstResource(LegacyResource.SERVER);
   }
   
   /**
    * Get the FIRST-SERVER-SOCKET attribute of this session.
    * 
    * @return   See above.
    */
   public static handle firstServerSocket()
   {
      return HandleChain.firstResource(LegacyResource.SERVER_SOCKET);
   }
   
   /**
    * Get the FIRST-SOCKET attribute of this session.
    * 
    * @return   See above.
    */
   public static handle firstSocket()
   {
      return HandleChain.firstResource(LegacyResource.SOCKET);
   }
   
   /**
    * Get the LAST-SERVER attribute of this session.
    * 
    * @return   See above.
    */
   public static handle lastServer()
   {
      return HandleChain.lastResource(LegacyResource.SERVER);
   }
   
   /**
    * Get the LAST-SERVER-SOCKET attribute of this session.
    * 
    * @return   See above.
    */
   public static handle lastServerSocket()
   {
      return HandleChain.lastResource(LegacyResource.SERVER_SOCKET);
   }
   
   /**
    * Get the LAST-SOCKET attribute of this session.
    * 
    * @return   See above.
    */
   public static handle lastSocket()
   {
      return HandleChain.lastResource(LegacyResource.SOCKET);
   }
   
   /**
    * Obtain a handle to the first dynamic {@code DataSet} object created in the current session.
    * <p>
    * The list of Datasets is reversed (it is a stack instead of a queue).
    *
    * @return  the value of {@code SESSION:FIRST-DATASET}, as described above.
    */
   public static handle getFirstDataSet()
   {
      return HandleChain.lastResource(LegacyResource.DATASET);
   }
   
   /**
    * Obtain a handle to the first dynamic {@code DataSource} object created in the current session.
    * <p>
    * The list of DataSources is reversed (it is a stack instead of a queue).
    *
    * @return  the value of {@code SESSION:FIRST-DATA-SOURCE}, as described above.
    */
   public static handle getFirstDataSource()
   {
      return HandleChain.lastResource(LegacyResource.DATA_SOURCE);
   }
   
   /** 
    * Sets the order of date components (SESSION:DATE-FORMAT) for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components as a 3 character string using "M", "D" and "Y" once 
    * each.  The order from index 0 to index 2 represents the left to right ordering of the 
    * components. So the leftmost date component will be defined by the character at index 
    * position 0.
    * <p>
    * The value set is specific to the current user's context.
    *
    * @param    dateFormat
    *           Defines the order of date components for conversion to/from strings.
    */
   public static void setDateFormat(String dateFormat)
   {
      setDateFormat(new character(dateFormat));
   }
   
   /** 
    * Sets the order of date components (SESSION:DATE-FORMAT) for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components as a 3 character string using "M", "D" and "Y" once 
    * each.  The order from index 0 to index 2 represents the left to right ordering of the 
    * components. So the leftmost date component will be defined by the character at index 
    * position 0.
    * <p>
    * The value set is specific to the current user's context.
    *
    * @param    dateFormat
    *           Defines the order of date components for conversion to/from strings.
    */
   public static void setDateFormat(character dateFormat)
   {
      SessionDateUtils.INSTANCE.setDateFormat(dateFormat);
      WorkArea wa = work.get();
      
      if (wa.exports != null)
      {
         wa.exports.setDateFormat(dateFormat);
      }
   }
   
   /** 
    * Gets the order of date components (SESSION:DATE-FORMAT) for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components is a 3 character string using
    * "M", "D" and "Y" once each.  The order from index 0 to index 2
    * represents the left to right ordering of the components. So the leftmost
    * date component will be defined by the character at index position 0.
    * <p>
    * The value accessed is specific to the current user's context.
    *
    * @return   Defines the order of date components for conversion to/from strings.
    */
   public static character getDateFormat()
   {
      return SessionDateUtils.INSTANCE.getDateFormat();
   }
   
   /**
    * Get the state of SESSION:DEBUG-ALERT attribute. 
    * 
    * @return  the state of SESSION:DEBUG-ALERT attribute.
    */
   public static logical isDebugAlert()
   {
      return new logical(work.get().isDebugAlert);
   }
   
   /**
    * Set the state of SESSION:DEBUG-ALERT attribute.
    * 
    * @param   on
    *          The new state for SESSION:DEBUG-ALERT attribute.
    */
   public static void setDebugAlert(logical on)
   {
      setDebugAlert(on == null || on.isUnknown() ? false : on.booleanValue());
   }
   
   /**
    * Set the state of SESSION:DEBUG-ALERT attribute.
    * 
    * @param   on
    *          The new state for SESSION:DEBUG-ALERT attribute.
    */
   public static void setDebugAlert(boolean on)
   {
      work.get().isDebugAlert = on;
   }
   
   /**
    * Get the state of SESSION:DATA-ENTRY-RETURN attribute. 
    * 
    * @return  the state of SESSION:DATA-ENTRY-RETURN attribute.
    */
   public static logical isDataEntryReturn()
   {
      return LogicalTerminal._isDataEntryReturn();
   }
   
   /**
    * Set the state of SESSION:DATA-ENTRY-RETURN attribute.
    * 
    * @param   on
    *          The new state for SESSION:DATA-ENTRY-RETURN attribute.
    */
   public static void setDataEntryReturn(logical on)
   {
      if (on == null || on.isUnknown())
      {
         return;
      }
      setDataEntryReturn(on.booleanValue());
   }
   
   /**
    * Set the state of SESSION:DATA-ENTRY-RETURN attribute.
    * 
    * @param   on
    *          The new state for SESSION:DATA-ENTRY-RETURN attribute.
    */
   public static void setDataEntryReturn(boolean on)
   {
      LogicalTerminal.setDataEntryReturn(on);
   }
   
   /**
    * Set the state of SESSION:LOAD-ICON attribute.
    *
    * @param   iconName
    *          The file name of the icon to be loaded
    *  @return <code>true</code> if successful
    */
   public static logical loadIcon(String iconName)
   {
      // simple parameter check
      if (iconName == null || iconName.isEmpty())
      {
         return new logical(false);
      }
      
      return new logical(LogicalTerminal.setWindowDefaultIcon(iconName));
   }
   
   /**
    * Set the state of SESSION:LOAD-ICON attribute.
    *
    * @param   iconName
    *          The file name of the icon to be loaded
    *  @return <code>true</code> if successful
    */
   public static logical loadIcon(character iconName)
   {
      // simple parameter check
      return iconName == null || iconName.isUnknown() ? new logical(false) : loadIcon(iconName.getValue());
   }
   
   /**
    * Sets the SESSION:TIMEZONE used as the default offset in minutes from
    * UTC for this user's context.
    *
    * @param    offset
    *           The new timezone offset which must be between -840 and 840
    *           inclusive.  The value may also be <code>unknown</code> which
    *           is the equivalent of resetting this attribute to the default.
    */
   public static void setSessionTimeZone(NumberType offset)
   {
      SessionDateUtils.INSTANCE.setSessionTimeZone(offset);
      WorkArea wa = work.get();
      
      if (wa.exports != null)
      {
         wa.exports.setSessionTimeZone(offset);
      }
   }
   
   /**
    * Accesses the SESSION:TIMEZONE used as the default offset in minutes from
    * UTC for this user's context.
    *
    * @return   The timezone offset. If this has never been set, then
    *           <code>unknown</code> will be returned.
    */
   public static integer getSessionTimeZone()
   {
      return SessionDateUtils.INSTANCE.getSessionTimeZone();
   }
   
   /**
    * Obtains the currently configured TIME-SOURCE attribute for the current
    * user's context.  This attribute is can be read and written but it is
    * not honored for date/time processing in P2J.  The intention of the
    * attribute is to define the system from which time values will be obtained.
    * In P2J, time values are always obtained from the application server. 
    * <p>
    * The default value is "local" which in Progress means that the client
    * machine is the source  (this is the same as the empty string "").  It is
    * common to use a database name in the 4GL, which makes the application
    * use the database server as the time source.
    *
    * @return   The time source attribute.
    */
   public static character getSessionTimeSource()
   {
      return SessionDateUtils.INSTANCE.getSessionTimeSource();
   }
   
   /**
    * Sets the <code>TIME-SOURCE</code> attribute for the user's context.
    * This attribute is can be read and written but it is not honored for 
    * date/time processing in P2J.  The intention of the attribute is to define
    * the system from which time values will be obtained. In P2J, time values
    * are always obtained from the application server.
    *
    * @param    timeSource
    *           The new value to set. If <code>null</code>, then the empty
    *           string will be used, which in the 4GL has the same affect as
    *           "local".
    */
   public static void setSessionTimeSource(character timeSource)
   {
      SessionDateUtils.INSTANCE.setSessionTimeSource(timeSource);
      WorkArea wa = work.get();
      
      if (wa.exports != null)
      {
         wa.exports.setSessionTimeSource(timeSource);
      }
   }
   
   /**
    * Obtains the currently configured DISPLAY-TIMEZONE attribute for the current
    * user's context. This attribute is used for displaying DATETIME-TZ values that do not have 
    * the timezone specified in the display format. 
    *
    * @return   The display-timezone attribute.
    */
   public static integer getSessionDisplayTimeZone()
   {
      return SessionDateUtils.INSTANCE.getSessionDisplayTimeZone();
   }
   
   /**
    * Sets the <code>DISPLAY-TIMEZONE</code> attribute for the user's context.
    * This attribute is used for displaying DATETIME-TZ values that do not have 
    * the timezone specified in the display format. 
    *
    * @param    displayTimezone
    *           The new value to set.
    */
   public static void setSessionDisplayTimeZone(NumberType displayTimezone)
   {
      SessionDateUtils.INSTANCE.setSessionDisplayTimeZone(displayTimezone);
      WorkArea wa = work.get();
      
      if (wa.exports != null)
      {
         wa.exports.setSessionDisplayTimeZone(displayTimezone);
      }
   }
   
   /**
    * Implementation of SESSION:YEAR-OFFSET attribute getter. Returns the 
    * currently configured start year for the two-digit Y2K windowing range.
    * This is the equivalent to the Progress -yy command line parameter. The 
    * default value is 1950 unless otherwise specified in the directory (see
    * {@link date#getWindowingYear}).
    * 
    * @return  The start year for the two-digit windowing range.
    */
   public static integer getYearOffset()
   {
      return SessionDateUtils.INSTANCE.getYearOffset();
   }
   
   /**
    * Implementation of SESSION:YEAR-OFFSET attribute setter. Sets the 
    * configured start year for the two-digit Y2K windowing range. This is
    * the equivalent of the Progress -yy command line parameter. The 
    * default value is 1950 unless otherwise specified in the directory (see
    * {@link date#getWindowingYear}).
    * 
    * @param   year
    *          The new start year for the two-digit windowing range.
    */
   public static void setYearOffset(NumberType year)
   {
      SessionDateUtils.INSTANCE.setYearOffset(year);
      WorkArea wa = work.get();
      
      if (wa.exports != null)
      {
         wa.exports.setYearOffset(year);
      }
   }
   
   /**
    * Get the PIXELS-PER-ROW attribute of this session.
    * 
    * @return   See above.
    */
   public static integer getPixelsPerRow()
   {
      return new integer(LogicalTerminal.getClient().getPixelsPerRow());
   }
   
   /**
    * Get the PIXELS-PER-COLUMN attribute of this session.
    * 
    * @return   See above.
    */
   public static integer getPixelsPerColumn()
   {
      return new integer(LogicalTerminal.getClient().getPixelsPerColumn());
   }

   /**
    * Sets the SESSION:TOOLTIPS attribute for this session.
    *
    * @param    tooltips
    *           The new value for session related TOOLTIPS attribute.
    */
   public static void setSessionTooltips(logical tooltips)
   {
      WorkArea wa = work.get();

      // change the local copy
      if (tooltips != null && !tooltips.isUnknown())
      {
         wa.tooltips = tooltips.booleanValue();
      }
      else
      {
         wa.tooltips = false;
      }

      // and push new value to the client
      LogicalTerminal.getClient().setSessionTooltips(wa.tooltips);
   }
   
   /**
    * Accesses the SESSION:TOOLTIPS attribute for this session.
    *
    * @return   <code>true</code> if tooltips are available for this session
    *           <code>false</code> otherwise.
    */
   public static logical getSessionTooltips()
   {
      WorkArea wa = null;
      try
      {
         wa = work.get();
      }
      catch (Throwable e)
      {
         // NOTE: this is solely for debug: when the client stops with a strange NPE in
         //       ThinClient.setSessionTooltips(). This is the fist remote call so if things go
         //       wrong with initialization of the context on server-side here is the error
         //       accessible./visible.
         LOG.severe("", e);
         // rethrow
         throw e;
      }
      return new logical(wa.tooltips);
   }
   
   /**
    * Get THREE-D attribute value for SESSION handle.
    * 
    * @return  the attribute value
    */
   public static logical is3D()
   {
      return LogicalTerminal.is3D();
   }
   
   /**
    * Set THREE-D attribute value for SESSION handle.
    * 
    * @param   value
    *          The value to set.
    */
   public static void set3D(logical value)
   {
      LogicalTerminal.set3D(value);
   }

   /**
    * Get the state of SESSION:PRINTER-NAME attribute.
    *
    * @return  the state of SESSION:PRINTER-NAME attribute.
    */
   public static character getPrinterName()
   {
      return EnvironmentOps.getCurrentPrinterName();
   }

   /**
    * Set the state of SESSION:PRINTER-NAME attribute.
    *
    * @param   value
    *          The new state for SESSION:PRINTER-NAME attribute.
    */
   public static void setPrinterName(character value)
   {
      // currently only the built-in PDF printer is supported, so this method is a no-op
   }

   /**
    * Get the state of SESSION:PRINTER-PORT attribute.
    *
    * @return  the state of SESSION:PRINTER-PORT attribute.
    */
   public static character getPrinterPort()
   {
      // currently only the built-in PDF printer is supported, so this method always returns an unknown
      return new character();
   }

   /**
    * Set the state of SESSION:PRINTER-PORT attribute.
    *
    * @param   value
    *          The new state for SESSION:PRINTER-PORT attribute.
    */
   public static void setPrinterPort(character value)
   {
      readOnlyError("PRINTER-PORT");
   }

   /**
    * Get the state of SESSION:PRINTER-CONTROL-HANDLE attribute.
    *
    * @return  the state of SESSION:PRINTER-CONTROL-HANDLE attribute.
    */
   public static integer getPrinterControlHandle()
   {
      return new integer(PrintingService.getControlHandle());
   }

   /**
    * Set the state of SESSION:PRINTER-CONTROL-HANDLE attribute.
    *
    * @param   value
    *          The new state for SESSION:PRINTER-CONTROL-HANDLE attribute.
    */
   public static void setPrinterControlHandle(integer value)
   {
      PrintingService.resetControlHandle();
   }

   /**
    * Get the state of SESSION:PRINTER-HDC attribute.
    *
    * @return  the state of SESSION:PRINTER-HDC attribute.
    */
   public static integer getPrinterHDC()
   {
      return new integer(PrintingService.getHDC());
   }

   /**
    * The SESSION:GET-PRINTERS method.
    *
    * @return  the result of SESSION:GET-PRINTERS method.
    */
   public static character getPrinters()
   {
      // currently only the built-in PDF printer is supported
      return new character(PrintingService.PDF_PRINTER_DESCR);
   }

   /**
    * Get the state of SESSION:SUPPRESS-WARNINGS attribute.
    *
    * @return  the state of SESSION:SUPPRESS-WARNINGS attribute.
    */
   public static logical isSuppressWarnings()
   {
      return new logical(ErrorManager.isSuppressWarnings());
   }

   /**
    * Set the state of SESSION:SUPPRESS-WARNINGS attribute.
    *
    * @param   value
    *          The new state for SESSION:SUPPRESS-WARNINGS attribute.
    */
   public static void setSuppressWarnings(logical value)
   {
      if (value.isUnknown())
      {
         // UNKNOWN is silently ignored
         return;
      }

      setSuppressWarnings(value.booleanValue());
   }

   /**
    * Set the state of SESSION:SUPPRESS-WARNINGS attribute.
    *
    * @param   value
    *          The new state for SESSION:SUPPRESS-WARNINGS attribute.
    */
   @LegacyAttribute(name = "SUPPRESS-WARNINGS", setter = true)
   public static void setSuppressWarnings(boolean value)
   {
      ErrorManager.setSuppressWarnings(value);
   }

   /**
    * Get the state of SESSION:STARTUP-PARAMETERS attribute.
    *
    * @return  the state of SESSION:STARTUP-PARAMETERS attribute.
    */
   public static character getStartupParameters()
   {
      return new character(startupParameters.get().toString());
   }

   /**
    * Get the startup parameters.
    *
    * @return the startup parameters object.
    */
   public static StartupParameters _startupParameters()
   {
      return startupParameters.get();
   }

   /**
    * Sets the startup parameters.
    */
   public static void setStartupParameters(StartupParameters startupParameters)
   {
      SessionUtils.startupParameters.set(startupParameters);
      work.get().isDebugAlert = startupParameters.getDebugAlert();
      work.get().errorStackTrace = startupParameters.getErrorStack();
   }

   /**
    * Set the state of SESSION:SYSTEM-ALERT-BOXES attribute.
    * 
    * @param   on
    *          The new state for SESSION:SYSTEM-ALERT-BOXES attribute.
    */
   public static void setSystemAlertBoxes(boolean on)
   {
      ErrorManager.setSystemAlertBoxes(on);
   }
   
   /**
    * Set the state of SESSION:SYSTEM-ALERT-BOXES attribute.
    * 
    * @param   on
    *          The new state for SESSION:SYSTEM-ALERT-BOXES attribute.
    */
   public static void setSystemAlertBoxes(logical on)
   {
      setSystemAlertBoxes(on == null || on.isUnknown() ? false : on.booleanValue());
   }

   /**
    * Get the state of SESSION:SYSTEM-ALERT-BOXES attribute. 
    * 
    * @return  the state of SESSION:SYSTEM-ALERT-BOXES attribute.
    */
   public static logical isSystemAlertBoxes()
   {
      return new logical(ErrorManager.isSystemAlertBoxes());
   }
   
   /**
    * Set the value of SESSION:MULTITASKING-INTERVAL attribute. 
    * 
    * @param   interval 
    *          The new value of SESSION:MULTITASKING-INTERVAL attribute.
    */
   public static void setMultitaskingInterval(NumberType interval)
   {
      long val = interval.isUnknown() ? 0 : interval.longValue();
      work.get().multitaskingInterval = val;
      LogicalTerminal.getClient().setMultitaskingInterval(val);
   }
   
   /**
    * Get the value of SESSION:MULTITASKING-INTERVAL attribute. 
    * 
    * @return  the value of SESSION:MULTITASKING-INTERVAL attribute.
    */
   public static integer getMultitaskingInterval()
   {
      return new integer(work.get().multitaskingInterval);
   }
   
   /**
    * Set the value of SESSION:EXIT-CODE attribute. 
    * 
    * @param   value 
    *          The new value of SESSION:EXIT-CODE attribute.
    */
   public static void setExitCode(long value)
   {
      // TODO: The exit code needs to get out of this call, and be returned from the JVM to the OS
      work.get().exitCode = value;      
   }
   
   /**
    * Get the value of SESSION:EXIT-CODE attribute. 
    * 
    * @return  the value of SESSION:EXIT-CODE attribute.
    */
   public static long getExitCode()
   {
      return work.get().exitCode;
   }
   
   /**
    * Get the SERVER-CONNECTION-BOUND attribute of this session.
    * 
    * @return   See above.
    */
   public static logical isServerConnectionBound()
   {
      return AppServerManager.isServerConnectionBound();
   }
   
   /**
    * Get the SERVER-CONNECTION-CONTEXT attribute of this session.
    * 
    * @return   See above.
    */
   public static character getServerConnectionContext()
   {
      return new character(AppServerManager.getServerConnectionContext());
   }
   
   /**
    * Set the SERVER-CONNECTION-CONTEXT attribute of this session.
    * 
    * @param    val
    *           The attribute's value.
    */
   public static void setServerConnectionContext(String val)
   {
      AppServerManager.setServerConnectionContext(val);
   }
   
   /**
    * Set the SERVER-CONNECTION-CONTEXT attribute of this session.
    * 
    * @param    val
    *           The attribute's value.
    */
   public static void setServerConnectionContext(character val)
   {
      AppServerManager.setServerConnectionContext(val.getValue());
   }
   
   /**
    * Get the value of SESSION:SERVER-CONNECTION-ID attribute. 
    * 
    * @return  the value of SESSION:SERVER-CONNECTION-ID attribute.
    */
   public static character getServerConnectionId()
   {
      String id = AppServerManager.getServerConnectionId();
      return new character(id == null ? "" : id);
   }
   
   
   /**
    * Get the FIRST-OBJECT attribute of this session.
    * 
    * @return   See above.
    */
   public static object<?> firstObject()
   {
      handle obj = HandleChain.firstResource(LegacyResource.OBJECT);
      return new object<>((ObjectResource) obj.get());
   }
   
   /**
    * Get the LAST-OBJECT attribute of this session.
    * 
    * @return   See above.
    */
   public static object<?> lastObject()
   {
      handle obj = HandleChain.lastResource(LegacyResource.OBJECT);
      return new object<>((ObjectResource) obj.get());
   }
   
   /**
    * Get the LOCAL-VERSION-INFO attribute of this session.
    * 
    * @return   See above.
    */
   public static object<? extends OeversionInfo> localVersionInfo()
   {
      return ObjectOps.newInstance(OeversionInfo.class);
   }

   /**
    * This is a helper method setting the value of OEClientType for the session's OeversionInfo.
    * Not exposed in 4GL.
    *
    * @param    clientTypes
    *           The client types for the session.
    */
   public static void setClientTypes(ClientType... clientTypes)
   {
      work.get().clientTypes = clientTypes;
   }

   /**
    * This is a helper method returning the value of OEClientType for the session's OeversionInfo.
    * Not exposed in 4GL.
    *
    * @return   See above.
    */
   public static ClientType[] getClientTypes()
   {
      return work.get().clientTypes;
   }

   /**
    * This is a helper method returning the value of OEClientType for the session's OeversionInfo.
    * Not exposed in 4GL.
    *
    * @return   See above.
    */
   public static character getClientType()
   {
      ClientType[] clientTypes = work.get().clientTypes;

      if (clientTypes == null || clientTypes.length == 0)
      {
         return new character();
      }

      List<String> validTypes = Arrays.stream(clientTypes)
                                   .filter(clientType -> clientType != null && clientType != ClientType.UNKNOWN)
                                   .map(ClientType::toString)
                                   .collect(Collectors.toList());

      return validTypes.isEmpty() ? new character() : new character(String.join(", ", validTypes));
   }
   
   /**
    * Get the CURRENT-REQUEST-INFO attribute of this session.
    * 
    * @return   See above.
    */
   public static object<? extends OerequestInfo> currentRequestInfo()
   {
      WorkArea wa = work.get();
      
      if (!AppServerManager.isMultiSession() && !AppServerManager.isRemote())
      {
         // non-MSA appserver clients return unknown
         return new object<>();
      }
      if (wa.currentRequestInfo == null)
      {
         wa.currentRequestInfo = new ObjectVar<>(OerequestInfo.class);
         wa.currentRequestInfo.assign(ObjectOps.newInstance(OerequestInfo.class));
      }
      
      return new object<>(wa.currentRequestInfo);
   }

   /**
    * Get the CURRENT-RESPONSE-INFO attribute of this session.
    * 
    * @return   See above.
    */
   public static object<? extends OerequestInfo> currentResponseInfo()
   {
      WorkArea wa = work.get();
      
      if (!AppServerManager.isMultiSession() && !AppServerManager.isRemote())
      {
         // non-MSA clients return unknown
         return new object<>();
      }
      
      if (wa.currentResponseInfo == null)
      {
         wa.currentResponseInfo = new ObjectVar<>(OerequestInfo.class);
         wa.currentResponseInfo.assign(ObjectOps.newInstance(OerequestInfo.class));
      }
      
      return new object<>(wa.currentResponseInfo);
   }
   
   /**
    * Get the ICFPARAMETER attribute of this session.
    * 
    * @return   See above.
    */
   public static character getICFParameter()
   {
      return new character(startupParameters.get().getDynamicsIcfParam());
   }
   
   /**
    * Get the CONTEXT-HELP-FILE attribute of this session.
    * 
    * @return   See above.
    */
   public static character getContextHelpFile()
   {
      return new character(work.get().contextHelpFile);
   }
   
   /**
    * Set the CONTEXT-HELP-FILE attribute of this session.
    * 
    * @param   file
    *          The file name.
    */
   public static void setContextHelpFile(String file)
   {
      work.get().contextHelpFile = file;
   }
   
   /**
    * Set the CONTEXT-HELP-FILE attribute of this session.
    * 
    * @param   file
    *          The file name.
    */
   public static void setContextHelpFile(character file)
   {
      work.get().contextHelpFile = file.getValue();
   }
   
   /**
    * Get the SERVER-CONNECTION-BOUND-REQUEST attribute of this session.
    * 
    * @return   See above.
    */
   public static logical getServerConnectionBoundRequest()
   {
      return AppServerManager.isServerConnectionBoundRequest();
   }
   
   /**
    * Set the SERVER-CONNECTION-BOUND-REQUEST attribute of this session.
    * 
    * @param   state
    *          The bound state.
    */
   public static void setServerConnectionBoundRequest(boolean state)
   {
      AppServerManager.setServerConnectionBoundRequest(state);
   }
   
   /**
    * Set the SERVER-CONNECTION-BOUND-REQUEST attribute of this session.
    * 
    * @param   state
    *          The bound state.
    */
   public static void setServerConnectionBoundRequest(logical state)
   {
      AppServerManager.setServerConnectionBoundRequest(state.booleanValue());
   }

   /**
    * Set the ERROR-STACK-TRACE attribute of this session.
    * 
    * @param   state
    *          The attribute's state.
    */
   public static void setErrorStackTrace(logical state)
   {
      work.get().errorStackTrace = state.booleanValue();
   }
   
   /**
    * Set the ERROR-STACK-TRACE attribute of this session.
    * 
    * @param   state
    *          The attribute's state.
    */
   public static void setErrorStackTrace(boolean state)
   {
      work.get().errorStackTrace = state;
   }
   
   /**
    * Get the ERROR-STACK-TRACE attribute of this session.
    * 
    * @return   See above.
    */
   public static logical isErrorStackTrace()
   {
      return logical.of(work.get().errorStackTrace);
   }

   /**
    * Mark this context as the client has disconnected (due to an abend, socket disconnect, etc).
    */
   static void clientDisconnected()
   {
      work.get().clientDisconnected = true;
   }

   /**
    * Get the SUPPRESS-WARNINGS-LIST of this session.
    * 
    * @return   See above.
    */
   public static character getSuppressWarningsList()
   {
      return new character(ErrorManager.getSuppressWarningsList());
   }

   /**
    * Set the SUPPRESS-WARNINGS-LIST attribute of this session.
    * 
    * @param   list
    *          The warning code list.
    */
   public static void setSuppressWarningsList(String list)
   {
      setSuppressWarningsList(new character(list));
   }
   
   /**
    * Set the SUPPRESS-WARNINGS-LIST attribute of this session.
    * 
    * @param   list
    *          The warning code list.
    */
   public static void setSuppressWarningsList(Text list)
   {
      ErrorManager.setSuppressWarningsList(list.value);
   }

   /**
    * Returns the external IP of the end-user machine, where the js client runs. Available with web sessions
    * only.
    *
    * @return   See above.
    */
   public static character getBrowserExternalIp()
   {
      return new character(work.get().browserExternalIp);
   }
   
   /**
    * Setter for the external IP of the end-user machine, where the js client runs. Available with web 
    * sessions only.
    *
    * @param    browserExternalIp
    *           The external IP of the end-user machine
    */
   public static void setBrowserExternalIp(String browserExternalIp)
   {
      WorkArea wa = work.get();
      if (wa.browserExternalIp == null)
      {
         wa.browserExternalIp = browserExternalIp;
      }
   }

   /**
    * Returns the TCP port of the end-user machine, on which the WebSocket runs in the browser. Available 
    * with web sessions only.
    *
    * @return   See above.
    */
   public static integer getBrowserPort()
   {
      return new integer(work.get().browserPort);
   }

   /**
    * Setter for the TCP port of the end-user machine, on which the WebSocket runs in the browser. Available 
    * with web sessions only.
    *
    * @param    browserPort
    *           The TCP port of the end-user machine, on which the WebSocket runs in the browser
    */
   public static void setBrowserPort(int browserPort)
   {
      WorkArea wa = work.get();
      if (wa.browserPort == null)
      {
         wa.browserPort = browserPort;
      }
   }

   /**
    * Returns the offset in minutes from UTC for the browser timezone. Available with web sessions only.
    *
    * @return   See above.
    */
   public static integer getBrowserTimezone()
   {
      return new integer(work.get().browserTimezone);
   }

   /**
    * Setter for the offset in minutes from UTC for the browser timezone. Available with web sessions only.
    *
    * @param    browserTimezone
    *           The offset in minutes from UTC for the browser timezone
    */
   public static void setBrowserTimezone(int browserTimezone)
   {
      WorkArea wa = work.get();
      if (wa.browserTimezone == null)
      {
         wa.browserTimezone = browserTimezone;
      }
   }

   /**
    * Returns the UTC datetime of the capture time for BROWSER-EXTERNAL-IP. Available with web sessions only.
    *
    * @return   See above.
    */
   public static datetime getIpCaptureTime()
   {
      Long ipCaptureTimestamp = work.get().ipCaptureTimestamp;
      if (ipCaptureTimestamp == null)
      {
         return new datetime();
      }
      Date datetime = new Date(ipCaptureTimestamp);
      return new datetime(datetime, TimeZone.getTimeZone("UTC"));
   }

   /**
    * Setter for the datetime of the capture time for BROWSER-EXTERNAL-IP. Available with web sessions only.
    *
    * @param    ipCaptureTimestamp
    *           The UTC timestamp of the capture time for BROWSER-EXTERNAL-IP
    */
   public static void setIpCaptureTime(long ipCaptureTimestamp)
   {
      WorkArea wa = work.get();
      if (wa.ipCaptureTimestamp == null)
      {
         wa.ipCaptureTimestamp = ipCaptureTimestamp;
      }
   }

   /**
    * Returns the external IP of the client machine.
    *
    * @return   See above.
    */
   public static character getClientExternalIp()
   {
      return new character(work.get().clientExternalIp);
   }

   /**
    * Setter for the external IP of the client machine.
    *
    * @param    clientExternalIp
    *           The external IP of the client machine
    */
   public static void setClientExternalIp(String clientExternalIp)
   {
      WorkArea wa = work.get();
      if (wa.clientExternalIp == null)
      {
         wa.clientExternalIp = clientExternalIp;
      }
   }

   /**
    * Returns the TCP port of the client machine.
    *
    * @return   See above.
    */
   public static integer getClientPort()
   {
      return new integer(work.get().clientPort);
   }

   /**
    * Setter for the TCP port of the client machine.
    *
    * @param    clientPort
    *           The TCP port of the client machine.
    */
   public static void setClientPort(int clientPort)
   {
      WorkArea wa = work.get();
      if (wa.clientPort == null)
      {
         wa.clientPort = clientPort;
      }
   }

   /**
    * Returns the offset in minutes from UTC for the client machine timezone.
    *
    * @return   See above.
    */
   public static integer getClientTimezone()
   {
      return new integer(work.get().clientTimezone);
   }

   /**
    * Setter for the offset in minutes from UTC for the client machine timezone.
    *
    * @param    clientTimezone
    *           The offset in minutes from UTC for the client machine timezone
    */
   public static void setClientTimezone(int clientTimezone)
   {
      WorkArea wa = work.get();
      if (wa.clientTimezone == null)
      {
         wa.clientTimezone = clientTimezone;
      }
   }

   /**
    * Returns the UTC datetime of the capture time for CLIENT-EXTERNAL-IP.
    *
    * @return   See above.
    */
   public static datetime getClientIpCaptureTime()
   {
      Long ipCaptureTimestamp = work.get().clientIpCaptureTimestamp;
      if (ipCaptureTimestamp == null)
      {
         return new datetime();
      }
      Date datetime = new Date(ipCaptureTimestamp);
      return new datetime(datetime, TimeZone.getTimeZone("UTC"));
   }

   /**
    * Setter for the datetime of the capture time for CLIENT-EXTERNAL-IP.
    *
    * @param    ipCaptureTimestamp
    *           The UTC timestamp of the capture time for CLIENT-EXTERNAL-IP
    */
   public static void setClientIpCaptureTime(long ipCaptureTimestamp)
   {
      WorkArea wa = work.get();
      if (wa.clientIpCaptureTimestamp == null)
      {
         wa.clientIpCaptureTimestamp = ipCaptureTimestamp;
      }
   }

   /**
    * Returns the client screen color depth.
    *
    * @return   See above.
    */
   public static integer getScreenColorDepth()
   {
      return new integer(work.get().screenColorDepth);
   }

   /**
    * Setter for the client screen color depth.
    *
    * @param    screenColorDepth
    *           The client screen color depth
    */
   public static void setScreenColorDepth(int screenColorDepth)
   {
      WorkArea wa = work.get();
      if (wa.screenColorDepth == null)
      {
         wa.screenColorDepth = screenColorDepth;
      }
   }

   /**
    * Returns the client screen height.
    *
    * @return   See above.
    */
   public static integer getScreenHeight()
   {
      return new integer(work.get().screenHeight);
   }

   /**
    * Setter for the client screen height.
    *
    * @param    screenHeight
    *           The client screen height
    */
   public static void setScreenHeight(int screenHeight)
   {
      WorkArea wa = work.get();
      if (wa.screenHeight == null)
      {
         wa.screenHeight = screenHeight;
      }
   }

   /**
    * Returns the client screen scaling factor.
    *
    * @return   See above.
    */
   public static decimal getScreenScalingFactor()
   {
      return new decimal(work.get().screenScalingFactor);
   }

   /**
    * Setter for the client screen scaling factor.
    *
    * @param    screenScalingFactor
    *           The client screen scaling factor
    */
   public static void setScreenScalingFactor(float screenScalingFactor)
   {
      WorkArea wa = work.get();
      if (wa.screenScalingFactor == null)
      {
         wa.screenScalingFactor = screenScalingFactor;
      }
   }

   /**
    * Returns the client screen width.
    *
    * @return   See above.
    */
   public static integer getScreenWidth()
   {
      return new integer(work.get().screenWidth);
   }

   /**
    * Setter for the client screen width.
    *
    * @param    screenWidth
    *           The client screen width
    */
   public static void setScreenWidth(int screenWidth)
   {
      WorkArea wa = work.get();
      if (wa.screenWidth == null)
      {
         wa.screenWidth = screenWidth;
      }
   }
   
   /**
    * Returns the user agent info of the browser where the js client runs. Available with web sessions only.
    *
    * @return   See above.
    */
   public static character getUserAgent()
   {
      return new character(work.get().userAgent);
   }

   /**
    * Setter for the user agent info of the browser where the js client runs. Available with web sessions only.
    *
    * @param    userAgent
    *           The user agent
    */
   public static void setUserAgent(String userAgent)
   {
      WorkArea wa = work.get();
      if (wa.userAgent == null)
      {
         wa.userAgent = userAgent;
      }
   }

   /**
    * Setter for the storage ID of the end-user (available with SSO).
    *
    * @param    storageId
    *           The storage ID
    */
   public static void setStorageId(String storageId)
   {
      WorkArea wa = work.get();
      if (wa.storageId == null)
      {
         wa.storageId = storageId;
      }
   }

   /**
    * Getter for the storage ID of the end-user (available with SSO).
    *
    * @return  The storage ID.
    */
   public static String getStorageId()
   {
      return work.get().storageId;
   }

   /**
    * Returns the first value of the login parameter.
    *
    * @param   paramName
    *          The name of the login parameter.
    *
    * @return  See above.
    */
   public static character getLoginParam(character paramName)
   {
      return getLoginParam(paramName, 1);
   }
   
   /**
    * Returns the first value of the login parameter.
    *
    * @param   paramName
    *          The name of the login parameter.
    *
    * @return  See above.
    */
   public static character getLoginParam(String paramName)
   {
      return getLoginParam(paramName, 1);
   }

   /**
    * Returns the index value of the login parameter.
    *
    * @param   paramName
    *          The name of the login parameter.
    * @param   oeIndex
    *          The index of the value of the login parameter. 1-based as in Progress.
    *
    * @return  See above.
    */
   public static character getLoginParam(character paramName, integer oeIndex)
   {
      if (paramName == null || paramName.isUnknown() || oeIndex == null || oeIndex.isUnknown())
      {
         return new character();
      }
      return getLoginParam(paramName.toStringMessage(), oeIndex.intValue());
   }

   /**
    * Returns the index value of the login parameter.
    *
    * @param   paramName
    *          The name of the login parameter.
    * @param   oeIndex
    *          The index of the value of the login parameter. 1-based as in Progress.
    *
    * @return  See above.
    */
   public static character getLoginParam(character paramName, int oeIndex)
   {
      if (paramName == null || paramName.isUnknown())
      {
         return new character();
      }
      return getLoginParam(paramName.toStringMessage(), oeIndex);
   }

   /**
    * Returns the index value of the login parameter.
    *
    * @param   paramName
    *          The name of the login parameter.
    * @param   oeIndex
    *          The index of the value of the login parameter. 1-based as in Progress.
    *
    * @return  See above.
    */
   public static character getLoginParam(String paramName, integer oeIndex)
   {
      if (paramName == null || oeIndex == null || oeIndex.isUnknown())
      {
         return new character();
      }
      
      return getLoginParam(paramName, oeIndex.intValue());
   }

   /**
    * Returns the index value of the login parameter.
    *
    * @param   paramName
    *          The name of the login parameter.
    * @param   oeIndex
    *          The index of the value of the login parameter. 1-based as in Progress.
    *
    * @return  See above.
    */
   public static character getLoginParam(String paramName, int oeIndex)
   {
      if (paramName == null ||
         work.get().loginParams == null ||
         !work.get().loginParams.containsKey(paramName) || 
         oeIndex < 1)
      {
         return new character();
      }
      
      int index = oeIndex - 1;
      String[] values = work.get().loginParams.get(paramName);
      if (values == null || values.length < index + 1)
      {
         return new character();
      }
      return new character(values[index]);
   }

   /**
    * Sets the params sent by the login request. Available with web sessions only.
    *
    * @param    loginParams
    *           The map of name : values for the params sent by the login request.
    */
   public static void setLoginParams(Map<String, String[]> loginParams)
   {
      WorkArea wa = work.get();
      if (wa.loginParams == null)
      {
         wa.loginParams = loginParams;
      }
   }
   
   /**
    * Returns all login parameters.
    *
    * @return  See above.
    */
   public static Map<String, String[]> getLoginParams()
   {
      return work.get().loginParams;
   }

   /**
    * Returns comma delimited string of local file system paths of
    * the files uploaded during direct upload or a comma delimited string
    * of file names chosen during direct file upload.
    *
    * @return See above.
    */
   public static character getWebUploadFiles()
   {
      WorkArea wa = work.get();
      return new character(wa.webUploadFiles);
   }

   /**
    * Returns {code true} when the current direct upload operation ended with
    * an error, {@code false} otherwise.
    *
    * @return See above.
    */
   public static logical getWebUploadError()
   {
      WorkArea wa = work.get();
      return new logical(wa.webUploadError);
   }

   /**
    * Assigns the upload information related to the current direct upload.
    *
    * @param   uploadFiles
    *          Comma delimited string of local file system paths of
    *          the files uploaded during direct upload or a comma delimited string
    *          of file names chosen during direct file upload.
    *
    * @param   error
    *          {code true} when the current direct upload operation ended with
    *          an error, {@code false} otherwise.
    */
   public static void setWebUploadInfo(String[] uploadFiles, boolean error)
   {
      WorkArea wa = work.get();
      wa.webUploadFiles = String.join(",", uploadFiles);
      wa.webUploadError = error;
   }
   
   /**
    * Returns a handle to the first asynchronous requests in the AppServer or WebService list of
    * asynchronous requests.
    * 
    * @return   See above.
    */
   public static handle getFirstAsyncRequest()
   {
      UnimplementedFeature.missing("SESSION:FIRST-ASYNC-REQUEST");
      return new handle();
   }

   /**
   * Returns a handle to the last asynchronous requests in the AppServer or WebService list of
   * asynchronous requests.
   * 
   * @return   See above.
   */
   public static handle getLastAsyncRequest()
   {
      UnimplementedFeature.missing("SESSION:LAST-ASYNC-REQUEST");
      return new handle();
   }
   
   /**
    * Stores global data relating to the state of the current context.
    */
   private static class WorkArea
   {
      /** Cached instance of the SESSION resource. */
      public CommonSession proxy = null;

      /** The resource's ID. */
      private Long id = null;

      /** The session unique identifier. */
      private String sessionId = null;
      
      /** The unique ID of the original session that was forked.  */
      private String relatedSessionId = null;
      
      /** Remote client interface. */
      private SessionExports exports = null;
      
      /** The state of SESSION:DEBUG-ALERT attribute */
      private boolean isDebugAlert = startupParameters.get().getDebugAlert();

      /** The flag indicating whether the widgets have the tooltip feature. */
      private boolean tooltips = Utils.getDirectoryNodeBoolean(null, "session-tooltips", true);
      
      /** This is just a holder for SESSION:MULTITASKING-INTERVAL. */
      private long multitaskingInterval = 0;
      
      /** The resultant value the 4GL may want to return to the OS (SESSION:EXIT-CODE) */
      /*
       * TODO: The return of this value needs to be implemented somewhere in the ConversionDriver
       *       so it can make its way back to the OS.
       */
      private long exitCode = 0;

      /** The value of the CONTEXT-HELP-FILE attribute. */
      private String contextHelpFile = null;
      
      /** The value of the CURRENT-REQUEST-INFO attribute. */
      private object<OerequestInfo> currentRequestInfo = null;
      
      /** The value of the CURRENT-RESPONSE-INFO attribute. */
      private object<OerequestInfo> currentResponseInfo = null;

      /** The value of the OEVersionInfo:OEClientType attribute. */
      private ClientType[] clientTypes;
      
      /**
       * Flag indicating if the FWD client has disconnected (due to an abend, socket close, etc). 
       * Will be set to <code>true</code> by the FWD when {@link TransactionManager#abnormalEnd} is being
       * called for an unrecoverable exception.
       */
      private boolean clientDisconnected = false;
      
      /**
       * A hook which will be executed before the root top-level block terminates.  Can specify either an
       * external program or a static method reference following the 
       * <code>com.goldencode.Foo:someStaticMethod</code> syntax.
       */
      private String terminationHook = null;
      
      /**
       * Flag indicating if the {@link ProError#getCallStack()} should be populated with the stacktrace when
       * an legacy OO error is thrown.
       */
      private boolean errorStackTrace = startupParameters.get().getErrorStack();

      /** The authentication blob. */
      private String authBlob;

      /** The device ID. */
      private String deviceId;

      /**
       * The external IP of the end-user machine, where the js client runs. Available with web sessions only.
       */
      private String browserExternalIp;

      /**
       * The TCP port of the end-user machine, on which the WebSocket runs in the browser. Available with 
       * web sessions only.
       */
      private Integer browserPort;

      /** The offset in minutes from UTC for the browser timezone. Available with web sessions only. */
      private Integer browserTimezone;

      /** The UTC timestamp of the capture time for BROWSER-EXTERNAL-IP. Available with web sessions only. */
      private Long ipCaptureTimestamp;

      /** The external IP of the client machine. */
      private String clientExternalIp;

      /** The TCP port of the client machine. */
      private Integer clientPort;

      /** The offset in minutes from UTC for the client machine. */
      private Integer clientTimezone;

      /** The UTC timestamp of the capture time for CLIENT-EXTERNAL-IP. */
      private Long clientIpCaptureTimestamp;
      
      /**
       * The client screen color depth. In web clients represents the value accessible from the browser.
       */
      private Integer screenColorDepth;

      /**
       * The client screen height. In web clients represents the value accessible from the browser.
       */
      private Integer screenHeight;

      /**
       * The client screen scaling factor. In web clients represents the value accessible from the browser.
       */
      private Float screenScalingFactor;

      /**
       * The client screen width. In web clients represents the value accessible from the browser.
       */
      private Integer screenWidth;

      /** The user agent info of the browser where the js client runs. Available with web sessions only. */
      private String userAgent;
      
      /** The storage ID of the end-user (available with SSO). */
      private String storageId;

      /** The map of name : values for the params sent by the login request. */
      private Map<String, String[]> loginParams;

      /** The files uploaded during the direct upload */
      private String webUploadFiles;

      /** Indicates an error during direct file upload */
      private boolean webUploadError;
   }
}