# 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;
       }
    }