# EnvironmentOps.java

    /*
    ** Module   : EnvironmentOps.java
    ** Abstract : Progress 4GL compatible environment related methods
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ---------------------------Description----------------------------
    ** 001 GES 20050628   @21564 Created initial version, supporting common
    **                           global variables that are called built-in
    **                           functions in Progress but which take no
    **                           parameters, no empty parens. These also
    **                           are read-only (they cannot be assigned).
    ** 002 GES 20050729   @21921 Added getSourceName() and modified the
    **                           default propath.
    ** 003 ECF 20051214   @23753 Removed getCurrentDatabaseName(). This was a
    **                           placeholder for P2J's DBNAME replacement
    **                           implementation. This is now provided in the
    **                           ConnectionManager class instead.
    ** 004 GES 20060123   @24034 Move to a directory based implementation for
    **                           the Progress compatibility global vars.
    ** 005 GES 20060125   @24071 Shift to SourceNameMapper backed stack
    **                           processing.
    ** 006 EVL 20060713   @28035 Changing the approach to get the source file
    **                           name from index of the Progress procedure on
    **                           the stack.
    ** 007 GES 20070321   @32506 Added support for assigning the PROPATH.
    ** 008 GES 20071214   @36369 Fix for NPE in getSourceName().
    ** 009 GES 20080123   @36902 Better propath and separator defaults.
    ** 010 GES 20080321   @37628 Added SESSION:BATCH-MODE support.
    ** 011 GES 20080414   @38014 Added SESSION:CHARSET support.
    ** 012 GES 20080924   @39940 Mask off inner classes from program-name
    **                           processing.
    ** 013 GES 20090512   @42147 Minor optimization for program-name.
    ** 014 ECF 20101005          Added getTempDirectory method.
    ** 015 GES 20110713          Changed default 4GL version string to "10.2B".
    ** 016 CS  20121104          Implement PROPATH assignment.
    ** 017 EVL 20130108          Fixing the PROPATH handling. We have to substitute
    **                           system path separator with comma when getting
    **                           PROPATH value. Also the default path and file
    **                           separators value is chaged to return the current
    **                           OS dependent one from File.pathSeparator and
    **                           File.separator. Also the getLegacyCaseSensitive()
    **                           method has been changed to set the default value
    **                           depending on the current OS.
    ** 018 EVL 20130128          Adding support for GET-KEY-VALUE, PUT-KEY-VALUE,
    **                           USE, LOAD, UNLOAD statements. The COLOR and FONT
    **                           options for PUT-KEY-VALUE is not yet supported.
    **                           The corresponting Java methods are just stubs to
    **                           be implemented later.
    ** 019 VMN 20130219          Added support for attributes
    **                           SESSION:FIRST-BUFFER (KW_FIRST_BU)
    ** 020 CA  20130222          Removed getFirstBuffer (stub is in SessionUtils).
    ** 021 EVL 20130529          Changing the setSearchPath method to take into account the current
    **                           PROPATH value while setting up the new one. The new item is adding
    **                           in the head of the string replacing the variable part of the PROPATH
    **                           value. Also the methods splitPath and fixupPropath were modified to
    **                           consider the propath separator is not the same as OS dependent file
    **                           separator. The PROPATH_SEPARATOR string constant has been added to
    **                           centralize this value. The fixed part of the PROPATH value has been
    **                           initialized only once from server ini file and used in every propath
    **                           modification as additional appendix after comma separator. The new
    **                           change is adding the one more member to the WorkArea to separate the
    **                           fixed and changeable parts. The member propath has been removed from
    **                           main class, this value is getting now dynamically.
    ** 022 CA  20130604          Added setter for the batch mode state.
    ** 023 EVL 20130625          Changing the handling of incoming load() call to work with
    **                           environment. In case of only two character or String parameters in
    **                           this call we have environment and base-key input options. Because
    **                           the directory option is useless in this case - this option is valid
    **                           only for INI file based environment but without base-key "INI" the
    **                           load() call is referred to the registry related case. For registry
    **                           on the other hand the directory option can not be applied. So two
    **                           input parameters means only environment and base-key variant.
    ** 024 CA  20140218          PROGRAM-NAME is now implemented by ProcedureManager.getStackEntry,
    **                           which is called by getSourceName().
    ** 025 EVL 20140325          Changing request the OS name, type and path separator to the client
    **                           side detection.
    ** 026 CA  20140702          Added isCustomEnvironment and getEnvironmentName APIs.
    **                           Added generic's.  Removed unused WorkArea.pkgroot field.
    ** 027 CA  20140715          Fixed getTempDirectory - it needs to interrogate the P2J client.
    ** 028 MAG 20140822          Added getLegacyPlatform APIs.
    ** 029 MAG 20140908          Added COLOR-TABLE and FONT-TABLE notification.
    ** 030 MAG 20140912          INI files are available on all platforms regarding client type.
    ** 031 GES 20141030          Added CPINTERNAL.
    ** 032 SVL 20150114          For admin client use server-side OS version.
    ** 033 ECF 20150130          Added CPSTREAM, currently hard-coded to return UTF-8.
    ** 034 GES 20150615          PROGRAM-NAME() is updated to accept 64-bit integer values (this is
    **                           valid in the 4GL.
    ** 035 ECF 20150715          Replace StringBuffer with StringBuilder.
    ** 036 IAS 20160331          Fixed potential NPE.
    ** 037 GES 20170228          Added javadoc.
    ** 038 GES 20171204          Improved comments and code formatting.
    ** 039 ECF 20180104          Added runtime support for converted RT-OPSYS global variable (FWD
    **                           extension).
    ** 040 EVL 20180220          Now the current printer name is getting from client OS base.
    ** 041 CA  20180221          Added listeners to be notified when PROPATH has been set.
    ** 042 GES 20180322          Added getProcessArchitecture().
    ** 043 CA  20180313          Javadoc fix.
    ** 044 SBI 20180522          Renamed file-system container name into legacy-system and mapped
    **                           legacyPlatform to legacy-system/opsys and removed legacyPlatform,
    **                           added getLegacyAbsolutePathPrefixMap(DirectoryService).
    ** 045 SBI 20180531          Changed to use common Utils.getDirectoryNodeMap, added
    **                           getLegacyEnvironmentVariables.
    ** 046 ECF 20180614          Performance improvement: cached frequently accessed configuration
    **                           values to avoid expensive directory lookups.
    ** 047 CA  20180514          Added support for CURRENT-LANGUAGE setter.
    **     CA  20180523          Fixed environment name, when used in maps: registries are
    **                           case-insensitive, stanza INI are case-sensitive.
    **                           Fixed APIs when LOAD is used with mix of DIR, NEW and BASE-KEY.
    ** 048 SBI 20181207          Added getResoleFilePath() and getSupportedHelperApplications().
    ** 049 EVL 20190426          Rework for CPINTERNAL 4GL getter.  Removed 'hack' to convert UTF-8 to
    **                           ISO8859-1.  Moved CHARSET, CPINTERNAL, CPSTREAM getters to I18nOps.
    ** 050 CA  20190529          Added load(character, boolean, character) signature.
    ** 051 EVL 20190730          The session:temp-directory should be finished with File.separator
    **                           char unlike general OS temporary directory name.
    ** 052 GES 20191001          Added undocumented variants of proversion that take a parameter.
    **                           Proper error handling, unknown handling and a default version of
    **                           11.6 were also added.
    ** 053 RFB 20200311          Added load(character, character, boolean, character) signature.
    ** 054 MP  20200324          Check if TEMP folder already have separator at the end.
    ** 055 RFB 20200427          Updated versionWorker() to cache version info in WorkArea, and added
    **                           versionNumber(), isV9(), isV10(), isV11(), and is V12().
    **     RFB 20200428          Since we init versionNumber to 0, the worker needs to set the default.
    **                           Made some improvements based upon code review.
    **     RFB 20200609          It is valid for USE to accept a null string. Our implementation was returning
    **                           4052 in that situation.
    **     CA  20210628          Added 'isLegacyPlatformWindows', to check if the legacy OS was Windows OS.
    **     AL2 20210630          Stop node editing if can't open batch when setting current-language.
    **                           Reworked current-language getter and setter.
    **     HC  20211001          Implementation of i18n support.
    **     CA  20220514          The active session is now set for leaf sessions, too - this is required for a
    **                           change in RemoteObject.obtainInstance, where first a check is done for an existing
    **                           local proxy, and after that a network instance is obtained (a requirement for the
    **                           server-side OS resources support, like memptr).
    **     SVL 20220620          Fixed USE. Support for USE("ininame") for the config file specified at the client
    **                           startup.
    **     SVL 20220720          Fixed assignment of color tables to windows.
    **     EVL 20221013          Changed directory location and name for resolve-file-paths option.
    **     SBI 20221202          Reused new static method getCurrentLanguageString() and changed setCurrentLanguage
    **                           to propagate language changes to the client.
    **     CA  20220514          Added setter for PROMSGS builtin variable.
    ** 056 TT  20230307          Implemented configurable directory override for the PROGRESS global variable
    ** 057 DDF 20230620          Replaced static initialization of values from the directory configuration with
    **                           a method called at server bootstrap.
    **     DDF 20230707          Initialize propathFixed with a default empty string.
    ** 058 GBB 20230718          Implemented SESSION:FWD-LOGFILE getter method.
    ** 059 SAT 20231017          Updated getKeyValue() to return unknown when Environments.getKeyValue() return
    **                           empty string.
    **     SAT 20231020          Replaced result.equals("") from getKeyValue() with result.isEmpty(). Updated
    **                           getCurrentEnvironmentName() to return "" instead of null when no custom
    **                           environment is found.
    ** 060 GBB 20231027          Fallback env for non-Win, non-ini LOAD, UNLOAD, SET, GET-KEY-VALUE, PUT-KEY-VALUE.
    ** 061 AD  20240125          Changed access modifier of method "addSourcePathListener(Consumer)" to public.
    ** 062 SB  20240617          Use DirectoryManager to get operating system. Refs #8368.
    ** 063 ES  20240521          Propagate a ConditionException.
    ** 064 GBB 20240709          Hard-coded config name replaced by ConfigItem constant.
    ** 065 GBB 20240826          Replacing Environments with OSResourceManager.getEnvironments().
    **                           Removing OperatingSystemInspector. Call setCurrentLanguage rpc.
    ** 066 GBB 20240829          Adds back a check if running as server before using OSResourceManager.
    ** 067 ICP 20250131          Used character constants to leverage cached instances.
    ** 068 GBB 20250403          Don't make the client call in setCurrentLanguage for MSA.
    */

    /*
    ** 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 java.io.*;
    import java.util.*;
    import java.util.Map.Entry;
    import java.util.function.*;

    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.util.appserver.*;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.p2j.util.osresource.*;
    import com.google.common.base.Splitter;
    import com.google.common.collect.Lists;

    /**
     * Progress compatible environment-related static utility methods.
     * <p>
     * This supports some common global variables that are called built-in
     * functions in Progress but which take no parameters, not even empty
     * parenthesis. These variables are read-only (they cannot be assigned),
     * except for <code>PROPATH</code> which can be modified as it can be in
     * Progress as well.
     * <p>
     * The following is the list of supported variables:
     * <p>
     * <pre>
     * Progress Variable                    Java Equivalent
     * ---------------------                ----------------------
     * current-language                     {@link #getCurrentLanguage}
     * dataservers                          {@link #getDataServerList}
     * gateways                             {@link #getDataServerList}
     * opsys                                {@link #getOperatingSystem}
     * progress                             {@link #getRuntimeType}
     * promsgs                              {@link #getMessageSource}
     * propath                              {@link #getSearchPath}
     * proversion                           {@link #getVersion}
     * </pre>
     * <p>
     * <pre>
     * Progress Language Statement          Java Equivalent
     * ---------------------------          ----------------------
     * get-key-value                        {@link #getKeyValue}
     * load                                 {@link #load}
     * propath                              {@link #setSearchPath}
     * put-key-value                        {@link #setKeyValue}
     * unload                               {@link #unload}
     * use                                  {@link #use}
     * </pre>
     * <p>
     * <pre>
     * Progress Function                    Java Equivalent
     * ---------------------                ----------------------
     * program-name                         {@link #getSourceName}
     * </pre>
     * <p>
     * <pre>
     * System Handle:Attribute              Java Equivalent
     * -------------------------            ----------------------
     * session:batch-mode                   {@link #isBatchMode}       (get)
     * session:temp-directory               {@link #getTempDirectory}  (get)
     * session:fwd-logfile                  {@link #getFwdLogfile}     (get)
     * </pre>
     */
    public class EnvironmentOps
    {
       /** Progress OPSYS string for Unix systems. */
       public static final String OS_UNIX = "UNIX";

       /** Progress OPSYS string for Windows systems. */
       public static final String OS_WIN = "WIN32";

       /** Progress PROPATH separator string for all systems. */
       public static final String PROPATH_SEPARATOR = ",";

       /**
        * Maps known Java os.name system property entries to a given Progress
        * OPSYS string.  Entries should be inserted in lowercase and all keys
        * should be lowercased before reading from the map.
        */
       public static final Map<String, String> osMap = new HashMap<>();

       /**
        * The helper splitter that trims strings and omits empty strings.
        */
       private static final Splitter SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();

       /** Logger  */
       private static final CentralLogger LOG = CentralLogger.get(EnvironmentOps.class);

       /** The fixed default part of the PROPATH value. */
       private static String propathFixed = "";

       static
       {
          // unix variants
          osMap.put("linux",    OS_UNIX);
          osMap.put("aix",      OS_UNIX);
          osMap.put("solaris",  OS_UNIX);
          osMap.put("sunos",    OS_UNIX);
          osMap.put("hp-ux",    OS_UNIX);
          osMap.put("freebsd",  OS_UNIX);
          osMap.put("mac os x", OS_UNIX);

          // windows variants
          osMap.put("windows xp",   OS_WIN);
          osMap.put("windows 2000", OS_WIN);
          osMap.put("windows nt",   OS_WIN);
          osMap.put("windows me",   OS_WIN);
          osMap.put("windows 98",   OS_WIN);
          osMap.put("windows 95",   OS_WIN);
       }

       /** Stores context-local state variables. */
       private static ContextContainer work = new ContextContainer();

       /**
        * Method called at server bootstrap that initializes values from the directory
        * configuration. Until this method is called, default values are used.
        */
       public static void bootstrap()
       {
          // Initialize default PROPATH value
          propathFixed = fixupPropath(Utils.getDirectoryNodeString(null, "searchpath-fixed", ""));
       }

       /**
        * Retrieves the session-specific current language, the character value of CURRENT-LANGUAGE.
        *
        * @return   The name of the current language.
        */
       public static character getCurrentLanguage()
       {
          return character.of(work.obtain().currentLanguage);
       }

       /**
        * Retrieves the string value of the session-specific current language.
        *
        * @return   The name of the current language.
        */
       public static String getCurrentLanguageString()
       {
          return work.obtain().currentLanguage;
       }

       /**
        * Sets the name of the current language being used for session.
        *
        * @param    val
        *           The new CURRENT-LANGUAGE value.
        */
       public static void setCurrentLanguage(String val)
       {
          work.obtain().currentLanguage = val;
          TranslationManager tm = TranslationManager.getInstance();
          tm.setCurrentLanguages(val);
          OSResourceManager.getEnvironments().setKeyValue("client", "lang", val);
          if (!MultiSessionAppserverManager.getInstance().isMsaContext())
          {
             LogicalTerminal.getClient().setCurrentLanguage(val);
          }
       }

       /**
        * Sets the name of the current language being used for session.
        *
        * @param    val
        *           The new CURRENT-LANGUAGE value.
        */
       public static void setCurrentLanguage(character val)
       {
          setCurrentLanguage(val.isUnknown() ? null : val.toStringMessage());
       }

       /**
        * Gets the list of the data servers available in Progress compatible
        * environment.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/dataServers
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/dataServers
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/dataServers
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/dataServers
        * <p>
        * If no value is found via this lookup, then the default value of
        * "PROGRESS,ORACLE,AS400,ODBC,MSS" will be returned.
        *
        * @return   The list of data servers.
        */
       public static character getDataServerList()
       {
          String def     = "PROGRESS,ORACLE,AS400,ODBC,MSS";
          String dserver = Utils.getDirectoryNodeString(null,
                                                        "dataServers",
                                                        def);
          return new character(dserver);
       }

       /**
        * Gets the name of the operating system upon which the Progress compatible
        * client environment is running.  The first attempt to resolve this value
        * will occur via a lookup in the directory.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/opsys/override
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/opsys/override
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/opsys/override
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/opsys/override
        * <p>
        * If no value is found via this lookup, then the Java system property
        * "os.name" is queried.  This is then translated via an optional
        * mapping table in the directory (that same paths and precedence as
        * above but ending in "opsys/mapping/&lt;lowercased_system_propert_name&gt;.
        * <p>
        * If no mapping is found, then an internal mapping table in this class
        * is consulted.  This table maps the following:
        * <p>
        * <pre>
        * os.name          Progress OPSYS
        * ---------------- ---------------
        * "linux"          "UNIX"
        * "aix"            "UNIX"
        * "solaris"        "UNIX"
        * "sunos"          "UNIX"
        * "hp-ux"          "UNIX"
        * "freebsd"        "UNIX"
        * "mac os x"       "UNIX"
        * "windows xp"     "WIN32"
        * "windows 2000"   "WIN32"
        * "windows nt"     "WIN32"
        * "windows me"     "WIN32"
        * "windows 98"     "WIN32"
        * "windows 95"     "WIN32"
        * </pre>
        * <p>
        * If there is still no match, then any <code>os.name</code> with the
        * lowercase text "unix" will return "UNIX" and any <code>os.name</code>
        * with the lowercase text "windows" will return "WIN32".
        * <p>
        * If there is still no match, the <code>os.name</code> returned by
        * Java will be returned.
        *
        * @return   The name of the current operating system.
        */
       public static character getOperatingSystem()
       {
          WorkArea wa = work.obtain();

          if (wa.clientOsName == null)
          {
             // look for a hard coded value
             String osname = DirectoryManager.getInstance().getString(Directory.ID_RELATIVE, "opsys/override", "");

             // if there is a hard coded operating system override, use it
             if (osname.length() == 0)
             {
                osname = getClientOpSysName(wa);
             }

             // Store per session OS name string to avoid every time call down to client
             wa.clientOsName = osname;

             // Calculate the cache for boolean Windows flag
             wa.isWindowsClient = osname.toLowerCase().contains("win");
          }

          return character.of(wa.clientOsName);
       }

       /**
        * Gets the name of the operating system being upon which the Progress compatible client
        * environment is running.  This method differs from {@link #getOperatingSystem()} in that
        * the first attempt to resolve this value will NOT occur via a lookup in the directory under
        * the {@code opsys/override} key. However, the rest of the resolution of this value is the
        * same as is described in {@link #getOperatingSystem()}.
        *
        * @return   The name of the current operating system upon which the client is running.
        */
       public static character getRuntimeOperatingSystem()
       {
          WorkArea wa = work.obtain();

          if (wa.clientRuntimeOsName == null)
          {
             wa.clientRuntimeOsName = getClientOpSysName(wa);
          }

          return character.of(wa.clientRuntimeOsName);
       }

       /**
        * Check if client legacy platform is Windows OS.
        * <p>
        * Sometimes we need to specify a legacy platform regarding the real platform on which the
        * clients actually runs. @link ColorSpec#convert. By default the client legacy platform is
        * the same as OPSYS built-in returned value in Progress.
        * <p>
        * The value is read from server directory. If no value is set for <code>legacy-system/opsys</code>
        * then the returned value is the same as returned by the <code>getOperatingSystem</code> call.
        * If set in the directory, the value of opsys MUST be UNIX or WIN32.
        *
        * @return  <code>true</code> if the legacy platform is Windows OS.
        */
       public static boolean isLegacyPlatformWindows()
       {
          WorkArea wa = work.obtain();

          if (wa.legacyPlatform == null)
          {
             getLegacyPlatform();
          }

          return wa.legacyPlatformWindows;
       }

       /**
        * Checks is the current OS the P2J is running on belongs to the Windows
        * family.
        *
        * @return   <code>true</code> if the current OS is from Windows family or
        *           <code>false</code> otherwise.
        */
       public static boolean isUnderWindowsFamily()
       {
          WorkArea wa = work.obtain();

          if (wa.clientOsName == null)
          {
             getOperatingSystem();
          }

          return wa.isWindowsClient;
       }

       /**
        * Gets the current runtime type being provided by the Progress
        * compatible environment.  The P2J environment only provides a
        * "Full" environment.
        *
        * @return   Returns the directory value "legacy-system/progress" if it exists
        *           or defaults to "Run-time"
        */
       public static character getRuntimeType()
       {
          return new character(Utils.getDirectoryNodeString(DirectoryService.getInstance(),
                                                            "legacy-system/progress",
                                                            "Run-time",
                                                            true));
       }

       /**
        * Gets the name of the directory where temporary files are stored.
        * Corresponds with with the <code>SESSION:TEMP-DIRECTORY</code> system
        * handle attribute.
        *
        * @return  Name of temporary directory.
        */
       public static character getTempDirectory()
       {
          FileSystem fileSystem = OSResourceManager.getFileSystem();
          String tempDir = fileSystem.getTempDirectory();
          String fileSeparator = fileSystem.getFileSeparator();

          if (!tempDir.endsWith(fileSeparator))
             tempDir += fileSeparator;

          return new character(tempDir);
       }

       /**
        * Gets the name of the current message source being used for the Progress
        * compatible environment.
        *
        * @return   Always returns the string "promsgs".
        */
       public static character getMessageSource()
       {
          return new character("promsgs");
       }

       /**
        * Sets the current message source being used.
        *
        * @param    src
        *           The message source.
        */
       public static void setMessageSource(String src)
       {
          UnimplementedFeature.unsupported("PROMSGS builtin variable can not be changed.");
       }

       /**
        * Sets the current message source being used.
        *
        * @param    src
        *           The message source.
        */
       public static void setMessageSource(Text src)
       {
          UnimplementedFeature.unsupported("PROMSGS builtin variable can not be changed.");
       }

       /**
        * Sets the Progress search path on the TARGET P2J system which is
        * used for the Progress compatible environment.  This corresponds to the
        * assignment of the "PROPATH" variable in Progress and represents a
        * mechanism to override the default that is available from the directory.
        * <p>
        * The PROPATH is also used in the implementation of
        * {@link FileSystemOps#searchPath}.  The actual I/O operations when
        * executed via the Progress compatible interface (see
        * {@link FileSystemOps} and {@link Stream}) for a P2J system are executed
        * on the P2J client.  This is the TARGET system on which this path is
        * valid.
        * <p>
        * This must be distinguished from the original Progress SOURCE system's
        * PROPATH which must be maintained for certain runtime translations
        * of dynamically generated names (the algorithms of which were designed
        * with that path assumed) but which is not necessarily a valid path on
        * the system on which this is running.  The original PROPATH can be
        * obtained via the {@link #getLegacySearchPath} method.
        *
        * @param    path
        *           The replacement path to Progress source files.
        * @param    cvt
        *           This marks if the function is being called at conversion time or not.
        */
       public static void setSearchPath(String path, boolean cvt)
       {
          if (path != null)
          {
             WorkArea wa = work.obtain();

             // the passed in value is the variable PROPATH part; since this was potentially
             // built up at runtime using converted 4GL, the code might be relying upon
             // the legacy system-specific propath separator character instead of the comma
             // (which is platform neutral); convert the legacy path-separator to the comma
             // here
             String override = fixupPropath(path);

             String newpath = override;

             // combine the final value from variable and fixed parts, if needed
             if (!propathFixed.equals(""))
             {
                newpath = newpath + PROPATH_SEPARATOR + propathFixed;
             }

             // save the new effective propath
             wa.propath = newpath;

             // save the variable part
             wa.propathOverride = override;

             if (!cvt)
             {
                wa.notifySourcePath(wa.propath);
             }
          }
       }

       /**
        * Sets the Progress search path on the TARGET P2J system which is
        * used for the Progress compatible environment.  This corresponds to the
        * assignment of the "PROPATH" variable in Progress and represents a
        * mechanism to override the default that is available from the directory.
        * <p>
        * The PROPATH is also used in the implementation of
        * {@link FileSystemOps#searchPath}.  The actual I/O operations when
        * executed via the Progress compatible interface (see
        * {@link FileSystemOps} and {@link Stream}) for a P2J system are executed
        * on the P2J client.  This is the TARGET system on which this path is
        * valid.
        * <p>
        * This must be distinguished from the original Progress SOURCE system's
        * PROPATH which must be maintained for certain runtime translations
        * of dynamically generated names (the algorithms of which were designed
        * with that path assumed) but which is not necessarily a valid path on
        * the system on which this is running.  The original PROPATH can be
        * obtained via the {@link #getLegacySearchPath} method.
        *
        * @param    path
        *           The replacement path to Progress source files.
        */
       public static void setSearchPath(String path)
       {
          setSearchPath(path, false);
       }

       /**
        * Sets the Progress search path on the TARGET P2J system which is
        * used for the Progress compatible environment.  This corresponds to the
        * assignment of the "PROPATH" variable in Progress and represents a
        * mechanism to override the default that is available from the directory.
        * <p>
        * The PROPATH is also used in the implementation of
        * {@link FileSystemOps#searchPath}.  The actual I/O operations when
        * executed via the Progress compatible interface (see
        * {@link FileSystemOps} and {@link Stream}) for a P2J system are executed
        * on the P2J client.  This is the TARGET system on which this path is
        * valid.
        * <p>
        * This must be distinguished from the original Progress SOURCE system's
        * PROPATH which must be maintained for certain runtime translations
        * of dynamically generated names (the algorithms of which were designed
        * with that path assumed) but which is not necessarily a valid path on
        * the system on which this is running.  The original PROPATH can be
        * obtained via the {@link #getLegacySearchPath} method.
        *
        * @param    path
        *           The replacement path to Progress source files.
        */
       public static void setSearchPath(character path)
       {
          setSearchPath(path.toStringMessage());
       }

       /**
        * Gets any context-specific override for the Progress search path which
        * was previously set via {@link #setSearchPath}.
        *
        * @return   The path override or <code>null</code> if no override was
        *           ever set.
        */
       public static String getSearchPathOverride()
       {
          return work.obtain().propath;
       }

       /**
        * Gets the Progress search path on the TARGET P2J system which is
        * used for the Progress compatible environment.  This corresponds to the
        * use of the "PROPATH" variable in Progress and is also used in the
        * implementation of {@link FileSystemOps#searchPath}.  The actual I/O
        * operations when executed via the Progress compatible interface (see
        * {@link FileSystemOps} and {@link Stream}) for a P2J system are executed
        * on the P2J client.  This is the TARGET system on which this path is
        * valid.
        * <p>
        * This must be distinguished from the original Progress SOURCE system's
        * PROPATH which must be maintained for certain runtime translations
        * of dynamically generated names (the algorithms of which were designed
        * with that path assumed) but which is not necessarily a valid path on
        * the system on which this is running.  The original PROPATH can be
        * obtained via the {@link #getLegacySearchPath} method.
        * <p>
        * If a specific PROPATH has been assigned in this user's context, then
        * that "override" will be returned.  Otherwise, the directory will be
        * consulted for the stored version.
        * <p>
        * The value returned from the directory may have been found via a search
        * algorithm that is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/searchpath
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/searchpath
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/searchpath
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/searchpath
        * <p>
        * If no value is found via this lookup, then the default value of
        * ".," will be returned.
        *
        * @return   The path to Progress source files.
        */
       public static character getSearchPath()
       {
          WorkArea wa      = work.obtain();
          String   propath = wa.propath;

          // is there a context-specific override?
          if (propath == null)
          {
             // support the override
             propath = wa.propathOverride;
             // There is system default, add to the end
             if (!propathFixed.equals(""))
             {
                propath = propath + PROPATH_SEPARATOR + propathFixed;
             }
             // Save the local copy of the effective propath
             wa.propath = propath;

             wa.notifySourcePath(propath);
          }

          return new character(propath);
       }

       /**
        * Substitutes the path separator values with the comma character in
        * the given string and returns the 4GL compatible PROPATH value that is
        * the combination of the directory entries separated by comma.
        *
        * @param    propathInitial
        *           The PROPATH that possibly needs to be fixed due to containing
        *           of the path separators instead of the expected comma values.
        *
        * @return   The PROPATH value with comma separation instead of possible
        *           path separators.
        */
       public static String fixupPropath(String propathInitial)
       {
          // First select the right path separator
          String pathSep = Utils.getDirectoryNodeString(null, "path-separator", File.pathSeparator);
          StringBuilder sb = new StringBuilder();
          int pathSepLength = pathSep.length();

          // The starting position of the next PROPATH element
          int nextSegment = 0;
          // Scan input parameter for the next occurence of the path separator.
          // The next PROPATH element starting point is the sum of the current
          // PROPATH element end point and length of the path sepatator.
          for (int i = 0;
               (i = propathInitial.indexOf(pathSep, nextSegment)) >= 0;
               nextSegment = i + pathSepLength)
          {
             // Extract the PROPATH entry up to the nearest path separator
             sb.append(propathInitial.substring(nextSegment, i));
             // Replace path separator with comma
             sb.append(PROPATH_SEPARATOR);
          }
          // We have to handle the case when new PROPATH sting is not finished
          // with path separator.
          if (nextSegment < propathInitial.length())
             sb.append(propathInitial.substring(nextSegment));

          return sb.toString();
       }

       /**
        * Gets the original Progress SOURCE system's PROPATH which must be
        * maintained for certain runtime translations of dynamically generated
        * names (the algorithms of which were designed with that path assumed)
        * but which is not necessarily a valid path on the system on which this
        * is running.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/legacy-system/propath
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/legacy-system/propath
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/legacy-system/propath
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/legacy-system/propath
        * <p>
        * If no value is found via this lookup, then the default value of
        * "" (empty string) will be returned.
        * <p>
        * This must be distinguished from Progress source file/search path on the
        * TARGET P2J system which is used for the Progress compatible
        * environment.  To obtain that value, use {@link #getSearchPath}.
        *
        * @return   The path to Progress source files.
        */
       public static String getLegacySearchPath()
       {
          return Utils.getDirectoryNodeString(null, "legacy-system/propath", "");
       }

       /**
        * Gets the path separator character used to parse the Progress search
        * path (see {@link #getSearchPath}).
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/path-separator
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/path-separator
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/path-separator
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/path-separator
        * <p>
        * If no value is found via this lookup, then the default value of
        * ':' will be returned.
        *
        * @return   The path to Progress source files.
        */
       public static String getPathSeparator()
       {
          if (work == null)
          {
             return Utils.getDirectoryNodeString(null, "path-separator", File.pathSeparator);
          }

          WorkArea wa = work.obtain();

          if (wa.clientPathSeparator == null)
          {
             // check the directory for an override value
             String pathSep = Utils.getDirectoryNodeString(null, "path-separator", "");

             // read OS value as fallback for path separator value
             if (pathSep.length() == 0 && isServer())
             {
                pathSep = OSResourceManager.getFileSystem().getFileSeparator();
             }

             // if we still don't have a value, just use the server's OS value
             if (pathSep == null || pathSep.length() == 0)
             {
                pathSep = File.pathSeparator;
             }

             // save the calculated value
             wa.clientPathSeparator = pathSep;
          }

          return wa.clientPathSeparator;
       }

       /**
        * Gets the original Progress SOURCE system's path separator which must be
        * maintained for certain runtime translations of dynamically generated
        * names (the algorithms of which were designed with that path assumed)
        * but which is not necessarily a valid path on the system on which this
        * is running.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/legacy-system/path-separator
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/legacy-system/path-separator
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/legacy-system/path-separator
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/legacy-system/path-separator
        * <p>
        * If no value is found via this lookup, then the default value of
        * ":" will be returned.
        * <p>
        * This must be distinguished from the path separator on the (runtime)
        * TARGET P2J system which is used for the Progress compatible
        * environment.  To obtain that value, use {@link #getPathSeparator}.
        *
        * @return   The path separator used on the SOURCE system.
        */
       public static String getLegacyPathSeparator()
       {
          return Utils.getDirectoryNodeString(null,
                                              "legacy-system/path-separator",
                                              File.pathSeparator);
       }

       /**
        * Gets the list of directories in the Progress source file path.  Each
        * element of the array is a directory in the source path.  This source
        * path is split based on the path separator used in this path
        * specification which may be different than that reported by the
        * system (<code>File.pathSeparator</code>.  The resulting array is
        * returned to the caller.
        * <p>
        * The {@link #getSearchPath} is used for the path specification and
        * the {@link #getPathSeparator} is used to split the path.
        *
        * @return   The array of directory names.
        */
       public static String[] getSearchPathList()
       {
          return splitPath(getSearchPath().toStringMessage(), PROPATH_SEPARATOR);
       }

       /**
        * Gets the original Progress SOURCE system's file separator which must be
        * maintained for certain runtime translations of dynamically generated
        * names (the algorithms of which were designed with that path assumed)
        * but which is not necessarily a valid path on the system on which this
        * is running.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/legacy-system/file-separator
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/legacy-system/file-separator
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/legacy-system/file-separator
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/legacy-system/file-separator
        * <p>
        * If no value is found via this lookup, then the default value of
        * "/" will be returned.
        *
        * @return   The legacy system's file separator.
        */
       public static String getLegacyFileSeparator()
       {
          return work.obtain().legacyFileSeparator;
       }

       /**
        * Gets the absolute path prefix map from the current directory settings.
        *
        * @return   The absolute path prefix map
        */
       public static Map<String, String> getLegacyAbsolutePathPrefixMap()
       {
          WorkArea wa = work.obtain();

          if (wa.pathPrefixMap == null)
          {
             wa.pathPrefixMap = Utils.getDirectoryNodeMap(DirectoryService.getInstance(),
                                                          "legacy-system/absolute-path-prefix-map",
                                                          null,
                                                          true,
                                                          false);
          }

          return wa.pathPrefixMap;
       }

       /**
        * Gets the environment variables map from the current directory settings.
        *
        * @param    caseInsensitiveKeys
        *           {@code true} to use case-insensitive keys for the environment variables map.
        *
        * @return   The environment variables map
        */
       public static Map<String, String> getLegacyEnvironmentVariables(boolean caseInsensitiveKeys)
       {
          return Utils.getDirectoryNodeMap(DirectoryService.getInstance(),
                                           "legacy-system/environment-variables",
                                           null,
                                           true,
                                           caseInsensitiveKeys);
       }

       /**
        * Gets the original Progress SOURCE file system's case-sensitive setting
        * which must be maintained for certain runtime translations of dynamically
        * generated names (the algorithms of which were designed with that path
        * assumed) but which is not necessarily a valid path on the system on
        * which this is running.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/legacy-system/case-sensitive
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/legacy-system/case-sensitive
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/legacy-system/case-sensitive
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/legacy-system/case-sensitive
        * <p>
        * If no value is found via this lookup, then the default value of
        * <code>true</code> will be returned.
        *
        * @return   <code>true</code> if the legacy file system was
        *           case-sensitive.
        */
       public static boolean getLegacyCaseSensitive()
       {
          WorkArea wa = work.obtain();

          if (wa.legacyCaseSensitive == null)
          {
             wa.legacyCaseSensitive = Utils.getDirectoryNodeBoolean(null,
                                                                    "legacy-system/case-sensitive",
                                                                    !isUnderWindowsFamily());
          }

          return wa.legacyCaseSensitive;
       }

       /**
        * Gets the current version string of the Progress compatible environment.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/version
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/version
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/version
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/version
        * <p>
        * If no value is found via this lookup, then the default value of
        * "11.6" will be returned.
        *
        * @return   The current version string.
        */
       public static character getVersion()
       {
          return new character(versionWorker(0));
       }

       /**
        * Gets the current version string of the Progress compatible environment.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/version
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/version
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/version
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/version
        * <p>
        * If no value is found via this lookup, then the default value of
        * "11.6.3.0.1407" will be returned.
        *
        * @param    lvl
        *           The level of verbosity.  0 for the short version (11.6) or
        *           1 for the long version (11.6.3.0.1407).  Any other value
        *           generates an error except for unknown value which is treated
        *           as 0.
        *
        * @return   The current version string.
        */
       public static character getVersion(integer lvl)
       {
          int i = 0;

          if (lvl != null && !lvl.isUnknown())
          {
             i = lvl.intValue();
          }

          return new character(getVersion(i));
       }

       /**
        * Gets the current version string of the Progress compatible environment.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/version
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/version
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/version
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/version
        * <p>
        * If no value is found via this lookup, then the default value of
        * "11.6.3.0.1407" will be returned.
        *
        * @param    lvl
        *           The level of verbosity.  0 for the short version (11.6) or
        *           1 for the long version (11.6.3.0.1407).  Any other value
        *           generates an error except for unknown value which is treated
        *           as 0.
        *
        * @return   The current version string.
        */
       public static character getVersion(long lvl)
       {
          if (lvl != 0 && lvl != 1)
          {
             String msg = "The parameter value for the PROVERSION function, if specified, " +
                          "can only be 0 or 1";
             ErrorManager.recordOrThrowError(16742, msg);
             return new character();
          }

          return new character(versionWorker(lvl));
       }

       /**
        * Retrieve the Progress version number. If it has not been cached yet, it will read it.
        *
        * @return  The current Progress version number.
        */
       public static float versionNumber()
       {
          WorkArea wa = work.obtain();

          // If we have not cached the value, get it
          if (wa.versionNumber == 0.0f)
          {
             versionWorker(0);
          }

          return wa.versionNumber;
       }

       /**
        * Check if the Progress major version is 9.
        *
        * @return  <code>true</code> if Progress major version is 9.
        */
       public static boolean isV9()
       {
          float versionNumber = versionNumber();

          return (versionNumber >= 9.0f && versionNumber < 10.0f);
       }

       /**
        * Check if the Progress major version is 10.
        *
        * @return  <code>true</code> if Progress major version is 10.
        */
       public static boolean isV10()
       {
          float versionNumber = versionNumber();

          return (versionNumber >= 10.0f && versionNumber < 11.0f);
       }

       /**
        * Check if the Progress major version is 11.
        *
        * @return  <code>true</code> if Progress major version is 11.
        */
       public static boolean isV11()
       {
          float versionNumber = versionNumber();

          return (versionNumber >= 11.0f && versionNumber < 12.0f);
       }

       /**
        * Check if the Progress major version is 12.
        *
        * @return  <code>true</code> if Progress major version is 12.
        */
       public static boolean isV12()
       {
          float versionNumber = versionNumber();

          return (versionNumber >= 12.0f && versionNumber < 13.0f);
       }

       /**
        * Returns the Progress source file name that matches the program at the given index in the
        * stack trace if the given index position exists. The Progress source file name will be
        * returned if possible using the {@link ProcedureManager#getStackEntry}.
        *
        * @param    index
        *           The 1-based index position in the call stack to be returned. Decimal values are
        *           not allowed in the 4GL (compile-time error), but any 64-bit integer value is
        *           allowed.
        *
        * @return   The original Progress 4GL source file name (if the class is a converted 4GL
        *           program).  If the index is out of bounds or is unknown, then the unknown value
        *           is returned.
        */
       public static character getSourceName(int64 index)
       {
          if (index.isUnknown())
          {
             return character.UNKNOWN;
          }

          return getSourceName(index.longValue());
       }

       /**
        * Returns the Progress source file name that matches the program at the given index in the
        * stack trace if the given index position exists. The Progress source file name will be
        * returned if possible using the {@link ProcedureManager#getStackEntry}.
        *
        * @param    index
        *           The 1-based index position in the call stack to be returned. Decimal values are
        *           not allowed in the 4GL (compile-time error), but any 64-bit integer value is
        *           allowed.
        *
        * @return   The original Progress 4GL source file name (if the class is a converted 4GL
        *           program).  If the index is out of bounds, then the unknown value is returned.
        */
       public static character getSourceName(long index)
       {
          return new character(ProcedureManager.getStackEntry((int) (index - 1)));
       }

       /**
        * Gets the supported helper applications with their mime types.
        *
        * @return   The the supported helper applications
        */
       public static Map<String, List<String>> getSupportedHelperApplications()
       {
          WorkArea wa = work.obtain();

          if (wa.supportedHelperApplications == null)
          {
             Map<String, String> applicationsMap = ConfigItem.HELPER_APPS.read(null, DirectoryService.getInstance());
             Map<String, List<String>> accum = new HashMap<>(applicationsMap.size());

             for(Entry<String, String> entry : applicationsMap.entrySet())
             {
                List<String> listOfValues = Lists.<String>newArrayList(SPLITTER.split(
                                                                                   entry.getValue()));
                accum.put(entry.getKey(), listOfValues);
             };

             wa.supportedHelperApplications = accum;
          }

          return wa.supportedHelperApplications;
       }

       /**
        * Gets the current version string of the Progress compatible environment.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/version
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/version
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/version
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/version
        * <p>
        * If no value is found via this lookup, then the default value of
        * "11.6.3.0.1407" will be returned.
        * <p>
        * Once found via the above steps, short and verbose version values are cached.
        *
        * @param    lvl
        *           The level of verbosity.  0 for the short version (10.2B) or
        *           1 for the long version (11.6.3.0.1407).
        *
        * @return   The current version string.
        */
       private static String versionWorker(long lvl)
       {
          WorkArea wa = work.obtain();
          float  versionNumber;

          if (wa.versionShort == null || wa.versionVerbose == null)
          {
             String version = Utils.getDirectoryNodeString(null, "version", "11.6.3.0.1407");

             int idx = version.indexOf('.');
             if (idx != -1)
             {
                idx = version.indexOf('.', idx + 1);
             }

             wa.versionShort   = (idx != -1) ? version.substring(0, idx) : version;
             wa.versionVerbose = version;

             String ver = version;
             idx = ver.indexOf('.');
             if (idx != -1)
             {
                idx = ver.indexOf('.', idx + 1);
             }
             version = (idx != -1) ? ver.substring(0, idx) : ver;

             // At this point, there could be a non-numeric in version, like "10.2B"
             // We'll pre-check but also catch NumberFormatExceptions in case
             for (int i = 0; i < version.length(); i++)
             {
                if (!Character.isDigit(version.charAt(i)) && (version.charAt(i) != '.'))
                {
                   version = version.substring(0, i);
                   break;
                }
             }
             try
             {
                wa.versionNumber = Float.parseFloat(version);
             }
             catch (NumberFormatException e)
             {
                // Set a default value
                wa.versionNumber = 11.6f;
             }
          }

          return (lvl == 0) ? wa.versionShort : wa.versionVerbose;
       }

       /**
        * Splits a path using the alternative separator string
        *
        * @param path
        *        The <code>String</code> path to split into an array of directory
        *        names.
        * @param separator
        *        The <code>String</code> separator to split into an array of directory
        *        names.
        *
        * @return The array of directory names.
        */
       private static String[] splitPath(String path, String separator)
       {
          if (separator == null)
          {
             separator = getPathSeparator();
          }
          if (path != null)
          {
             return path.split(separator, -1);
          }
          else
          {
             return null;
          }
       }

       /**
        * Reports if the current context is running in batch mode.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/batchMode
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/batchMode
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/batchMode
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/batchMode
        * <p>
        * If no value is found via this lookup, then the default value of
        * <code>false</code> will be returned.
        *
        * @return   <code>true</code> if the current context is in batch mode.
        */
       public static logical isBatchMode()
       {
          WorkArea wa = work.obtain();
          if (wa.batchMode == null)
          {
             wa.batchMode = Utils.getDirectoryNodeBoolean(null, "batchMode", false);
          }

          return new logical(wa.batchMode);
       }

       /**
        * Force the batch mode of the current context to the specified state.
        *
        * @param   batchMode
        *          <code>true</code> if this context must be in batch mode.
        */
       public static void setBatchMode(boolean batchMode)
       {
          WorkArea wa = work.obtain();
          wa.batchMode = batchMode;
       }

       /**
        * Sets the FWD client logger file path of this session.
        *
        * @param   fwdLogfile
        *          The FWD client logger file path of this session.
        */
       public static void setFwdLogfile(String fwdLogfile)
       {
          WorkArea wa = work.obtain();
          wa.fwdLogfile = fwdLogfile;
       }

       /**
        * Returns the FWD client logger file path of this session. The path has specifiers and placeholders
        * resolved.
        *
        * @return   See above.
        */
       public static character getFwdLogfile()
       {
          return new character(work.obtain().fwdLogfile);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        */
       public static void load(character env)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(env, (character)null, new logical(false), (character)null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        */
       public static void load(String env)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(new character(env), (character)null, new logical(false), (character)null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        */
       public static void load(character env, boolean flagNew)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(env, (character)null, new logical(flagNew), (character)null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        */
       public static void load(String env, boolean flagNew)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(new character(env), (character)null, new logical(flagNew), (character)null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, boolean flagNew, character baseKey)
       {
          load(env, (character)null, new logical(flagNew), baseKey);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, boolean flagNew, String baseKey)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(env, (character)null, new logical(flagNew), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(String env, boolean flagNew, String baseKey)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(new character(env), (character)null, new logical(flagNew), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(String env, boolean flagNew, character baseKey)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(new character(env), (character)null, new logical(flagNew), baseKey);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        */
       public static void load(character env, character directory)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(env, directory, new logical(false), null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        */
       public static void load(String env, character directory)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(new character(env), directory, new logical(false), null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        */
       public static void load(character env, String directory)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(env, new character(directory), new logical(false), null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        */
       public static void load(String env, String directory)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(new character(env), new character(directory), new logical(false), null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        */
       public static void load(character env, character directory, boolean flagNew)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(env, directory, new logical(flagNew), (character)null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, character directory, boolean flagNew, character baseKey)
       {
          load(env, directory, new logical(flagNew), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        */
       public static void load(String env, character directory, boolean flagNew)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(new character(env), directory, new logical(flagNew), (character)null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        */
       public static void load(character env, String directory, boolean flagNew)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(env, new character(directory), new logical(flagNew), (character)null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        */
       public static void load(String env, String directory, boolean flagNew)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          load(new character(env), new character(directory), new logical(flagNew), (character)null);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, character directory, String baseKey)
       {
          load(env, directory, new logical(false), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, character directory, character baseKey)
       {
          load(env, directory, new logical(false), baseKey);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, String directory, String baseKey)
       {
          load(env, new character(directory), new logical(false), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(String env, character directory, String baseKey)
       {
          load(new character(env), directory, new logical(false), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(String env, character directory, character baseKey)
       {
          load(new character(env), directory, new logical(false), baseKey);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(String env, String directory, String baseKey)
       {
          load(new character(env), new character(directory),
               new logical(false), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, character directory, boolean flagNew, String baseKey)
       {
          load(env, directory, new logical(flagNew), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, String directory, boolean flagNew, String baseKey)
       {
          load(env, new character(directory), new logical(flagNew), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(String env, character directory, boolean flagNew, String baseKey)
       {
          load(new character(env), directory, new logical(flagNew), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(String env, character directory, boolean flagNew, character baseKey)
       {
          load(new character(env), directory, new logical(flagNew), baseKey);
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(String env, String directory, boolean flagNew, String baseKey)
       {
          load(new character(env), new character(directory),
               new logical(flagNew), new character(baseKey));
       }

       /**
        * Creates application defaults implementing Progress LOAD statement.
        *
        * @param env
        *        The environment to create or load. Can be the registry key,
        *        initialization file.
        * @param directory
        *        The directory path for existing initialization file or place
        *        to create initialization file.
        * @param flagNew
        *        If <code>true</code> - the new file or registry key will be
        *        created overwriting the possibly existed one.
        * @param baseKey
        *        When the parameter is specified the serching is performed under
        *        the particular key value. It is possible to specify "INI" as
        *        parameter. In this case search or creation will be dome inside
        *        initialization file specified in env parameter.
        */
       public static void load(character env, character directory, logical flagNew, character baseKey)
       {
          // Preparing initial parameters for EnvironmentAccessor interface
          String envAsString = null;
          String directoryAsString = null;
          String baseKeyAsString = null;
          boolean flagNewAsBoolean = false;

          if (env != null)
          {
             envAsString = env.toStringMessage();
          }
          if (directory != null)
          {
             directoryAsString = directory.toStringMessage();
          }
          if (flagNew != null)
          {
             flagNewAsBoolean = flagNew.booleanValue();
          }
          if (baseKey != null)
          {
             baseKeyAsString = baseKey.toStringMessage();
          }

          if (!OSResourceManager.getEnvironments().load(envAsString, directoryAsString, flagNewAsBoolean, baseKeyAsString))
          {
             onLoadError(env, directory);
          }
       }

       /**
        * On load error message.
        *
        * @param    env
        *           The environment to create or load. Can be the registry key,
        *           initialization file.
        * @param    directory
        *           The directory path for existing initialization file or place
        *           to create initialization file.
        */
       private static void onLoadError(character env, character directory)
       {
          // This is support for NO-ERROR option
          if (directory == null || directory.isUnknown())
          {
             ErrorManager.recordOrThrowError(4450, "The LOAD of " + env.toStringMessage() + " failed");
          }
          else
          {
             ErrorManager.recordOrThrowError(4449, "The LOAD of " + env.toStringMessage() +
                                                   " in path " +  directory.toStringMessage() +
                                                   " failed");
          }
       }

       /**
        * Specifies environment defaults to use in subsequent windows.
        *
        * @param env
        *        The environment to load. If the parameter is empty string ("") the default
        *        environment becomes the current one.
        */
       public static void use(String env)
       {
          use(new character(env));
       }

       /**
        * Specifies environment defaults to use in subsequent windows.
        *
        * @param env
        *        The environment to load. If the parameter is empty string ("") the default
        *        environment becomes the current one.
        */
       public static void use(character env)
       {
          String envNew = env != null ? env.toStringMessage() : "";
          String envName = OSResourceManager.getEnvironments().use(envNew);

          if (envName != null)
          {
             onUse(envName);
          }
          else
          {
             ErrorManager.conditionalShowErrorAndAbend(4452, "The USE of " + envNew + " failed");
          }
       }

       /**
        * Check if a custom environment is in use.
        *
        * @return   See above.
        */
       public static boolean isCustomEnvironment()
       {
          return (isUnderWindowsFamily() || isIniType())
                   && OSResourceManager.getEnvironments().isCustomEnvironment();
       }

       /**
        * Get the current environment's name.
        *
        * @return   See above.
        */
       public static String getEnvironmentName()
       {
          return OSResourceManager.getEnvironments().getEnvironmentName();
       }

       /**
        * Get the current environment type.
        *
        * @return   See above.
        */
       public static String getEnvironmentType()
       {
          return OSResourceManager.getEnvironments().getEnvironmentType();
       }

       /**
        * Get the name of the current printer.
        *
        * @return  the printer name.
        */
       public static character getCurrentPrinterName()
       {
          // getting current printer from system
          String prnName = isServer() ?
             OSResourceManager.getEnvironments().getCurrentPrinterName() :
             null;

          // return it if not null
          return new character(prnName == null || prnName.isEmpty() ?
                                  PrintingService.PDF_PRINTER_DESCR :
                                  prnName);
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(character section, String value)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          setKeyValue(section, (character)null, new character(value));
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(String section, character value)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          setKeyValue(new character(section), (character)null, value);
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(String section, String value)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          setKeyValue(new character(section), (character)null, new character(value));
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(character section, character value)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          setKeyValue(section, (character)null, value);
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param key
        *        The name of the key key to modify or default key if not specified.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(character section, character key, String value)
       {
          setKeyValue(section, key, new character(value));
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param key
        *        The name of the key key to modify or default key if not specified.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(character section, String key, character value)
       {
          setKeyValue(section, new character(key), value);
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param key
        *        The name of the key key to modify or default key if not specified.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(String section, character key, character value)
       {
          setKeyValue(new character(section), key, value);
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param key
        *        The name of the key key to modify or default key if not specified.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(character section, String key, String value)
       {
          setKeyValue(section, new character(key), new character(value));
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param key
        *        The name of the key key to modify or default key if not specified.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(String section, String key, character value)
       {
          setKeyValue(new character(section), new character(key), value);
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param key
        *        The name of the key key to modify or default key if not specified.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(String section, character key, String value)
       {
          setKeyValue(new character(section), key, new character(value));
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param key
        *        The name of the key key to modify or default key if not specified.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(String section, String key, String value)
       {
          setKeyValue(new character(section), new character(key), new character(value));
       }

       /**
        * Adds, modifies and deletes keys in current environment.
        *
        * @param section
        *        The name of the section containing the key to modify.
        * @param key
        *        The name of the key key to modify or default key if not specified.
        * @param value
        *        The new value of the key under modification.
        */
       public static void setKeyValue(character section, character key, character value)
       {
          String sectionAsString = null;
          String keyAsString = null;
          String valueAsString = null;

          if (section != null)
          {
             // Unknown section value should be handled the same way as empty string
             if (section.isUnknown())
             {
                sectionAsString = "";
             }
             else
             {
                sectionAsString = section.toStringMessage();
             }
          }
          if (key != null)
          {
             // Unknown key value should be handled the same way as empty string
             if (key.isUnknown())
             {
                keyAsString = "";
             }
             else
             {
                keyAsString = key.toStringMessage();
             }
          }
          if (value != null)
          {
             // Unknown value should be handled the same way as empty string
             if (value.isUnknown())
             {
                valueAsString = "";
             }
             else
             {
                valueAsString = value.toStringMessage();
                if (valueAsString.equals("?"))
                {
                   valueAsString = "";
                }
             }
          }

          try
          {
             // Using the EnvironmentAccessor interface
             OSResourceManager.getEnvironments().setKeyValue(sectionAsString, keyAsString, valueAsString);
          }
          catch (ErrorConditionException e)
          {
             ErrorManager.recordOrThrowError(e.getProgressErrorCode(), e.getMessage());
          }
       }

       /**
        * Getting the default key value from the current environment.
        *
        * @param section
        *        The name of the section containing the key to get.
        *
        * @return The value of the key.
        */
       public static character getKeyValue(character section)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          return getKeyValue(section, (character)null);
       }

       /**
        * Getting the default key value from the current environment.
        *
        * @param section
        *        The name of the section containing the key to get.
        *
        * @return The value of the key.
        */
       public static character getKeyValue(String section)
       {
          // Using null as missing parameter means the usage the default
          // value for parameter, not the same as unknown. Because the
          // Progress unknown value is the legal value and can be passed
          // withing not-null variable instance.
          return getKeyValue(new character(section), (character)null);
       }

       /**
        * Getting the key value from the current environment.
        *
        * @param section
        *        The name of the section containing the key to get.
        * @param key
        *        The name of the key key to get or default key if not specified.
        *
        * @return The value of the key.
        */
       public static character getKeyValue(String section, String key)
       {
          return getKeyValue(new character(section), new character(key));
       }

       /**
        * Getting the key value from the current environment.
        *
        * @param section
        *        The name of the section containing the key to get.
        * @param key
        *        The name of the key key to get or default key if not specified.
        *
        * @return The value of the key.
        */
       public static character getKeyValue(character section, String key)
       {
          return getKeyValue(section, new character(key));
       }

       /**
        * Getting the key value from the current environment.
        *
        * @param section
        *        The name of the section containing the key to get.
        * @param key
        *        The name of the key key to get or default key if not specified.
        *
        * @return The value of the key.
        */
       public static character getKeyValue(String section, character key)
       {
          return getKeyValue(new character(section), key);
       }

       /**
        * Getting the key value from the current environment.
        *
        * @param section
        *        The name of the section containing the key to get.
        * @param key
        *        The name of the key key to get or default key if not specified.
        *
        * @return The value of the key.
        */
       public static character getKeyValue(character section, character key)
       {
          String sectionAsString = null;
          String keyAsString = null;

          if (section != null)
          {
             // Unknown section value should be handled the same way as empty string
             if (section.isUnknown())
             {
                sectionAsString = "";
             }
             else
             {
                sectionAsString = section.toStringMessage();
             }
          }
          if (key != null)
          {
             // Unknown key value should be handled the same way as empty string
             if (key.isUnknown())
             {
                keyAsString = "";
             }
             else
             {
                keyAsString = key.toStringMessage();
             }
          }

          // Using the EnvironmentAccessor interface
          String result = null;
          try
          {
             result = OSResourceManager.getEnvironments().getKeyValue(sectionAsString, keyAsString);
          }
          catch (StopConditionException exe)
          {
             throw exe;
          }
          catch (Exception e)
          {
             LOG.severe("getKeyValue exception:", e);
          }
          return (result != null && result.isEmpty()) ? new character() : new character(result);
       }

       /**
        * Unloads specifies environment from the current one.
        *
        * @param env
        *        The environment to unload. Can be the registry or initialization
        *        file. The environment to unload should be previously loaded by
        *        load() method.
        */
       public static void unload(String env)
       {
          unload(new character(env));
       }

       /**
        * Unloads specified environment from the current one if possible.
        *
        * @param env
        *        The environment to unload. Can be the registry or initialization
        *        file. The environment to unload should be previously loaded by
        *        load() method.
        */
       public static void unload(character env)
       {
          String envName = OSResourceManager.getEnvironments().unload(env.toStringMessage());

          if (envName != null)
          {
             onUnload(envName);
          }
          else
          {
             String msg = "The UNLOAD of " + env.toStringMessage() + " failed";
             ErrorManager.recordOrThrowError(4451, msg);
          }
       }

       /**
        * Get current environment name. If no custom environment is set return default environment
        * name which is a <code>null</code> value.
        *
        * @return  see above.
        */
       public static String getCurrentEnvironmentName()
       {
          return isCustomEnvironment() ? getEnvironmentName() : "";
       }

       /**
        * Get client legacy platform name.
        * <p>
        * Sometimes we need to specify a legacy platform regarding the real platform on which the
        * clients actually runs. @link ColorSpec#convert. By default the client legacy platform is
        * the same as OPSYS built-in returned value in Progress.
        * <p>
        * The value is read from server directory. If no value is set for <code>legacy-system/opsys</code>
        * then the returned value is the same as returned by the <code>getOperatingSystem</code> call.
        * If set in the directory, the value of opsys MUST be UNIX or WIN32.
        *
        * @return  Client legacy platform name:
        *          UNIX  - Unix/Linux OS familly.
        *          WIN32 - Windows OS familly.
        */
       public static String getLegacyPlatform()
       {
          WorkArea wa = work.obtain();

          if (wa.legacyPlatform == null)
          {
             String os = getOperatingSystem().toStringMessage();
             wa.legacyPlatform = Utils.getDirectoryNodeString(null, "legacy-system/opsys", os);
             wa.legacyPlatformWindows = wa.legacyPlatform.toLowerCase().indexOf("win") != -1;
          }

          return wa.legacyPlatform;
       }

       /**
        * Returns the word size of the client's compiled native code, which may be different
        * from the "bitness" of the client's OS. For example, the client may be compiled as
        * 32-bit but executed on a 64-bit OS.
        *
        * @return   32 or 64 depending on the compiled word size of the client's native code. If
        *           there is an error in calling down to the client, unknown value will be
        *           returned.
        */
       public static integer getProcessArchitecture()
       {
          int arch = memptr.compiledWordSize();

          // TODO: is there any case where this fails (and in 4GL it would not) so
          //       we introduce unknown unexpectedly?

          return (arch == -1) ? new integer() : new integer(arch);
       }

       /**
        * Returns the value of clientConfig/resolveFilePaths boolean value.
        *
        * @return   The value of clientConfig/resolveFilePaths or false if this value isn't set.
        */
       public static boolean getResolveFilePaths()
       {
          WorkArea wa = work.obtain();

          if (wa.resolveFilePaths == null)
          {
             wa.resolveFilePaths = Utils.getDirectoryNodeBoolean(null,
                                                                 "legacy-system/resolve-file-paths",
                                                                 false);
          }

          return wa.resolveFilePaths;
       }
       /**
        * Add a new listener to be notified when the PROPATH has been set.
        *
        * @param    listener
        *           The listener to be notified.
        */
       public static void addSourcePathListener(Consumer<String> listener)
       {
          WorkArea wa = work.obtain();

          wa.listeners.add(listener);
       }

       /**
        * Gets the name of the operating system upon which the Progress compatible client environment
        * is running.  The Java system property "os.name" of the client JVM is queried (unless running
        * in headless mode, in which case that of the server JVM is used).  This is then translated
        * via an optional mapping table in the directory (using the same paths and precedence as
        * described for the {@link #getOperatingSystem()} method, but ending in
        * {@code opsys/mapping/&lt;lowercased_system_propert_name&gt;}.
        * <p>
        * If no mapping is found, then an internal mapping table in this class is consulted.  This
        * table maps the following:
        * <p>
        * <pre>
        * os.name          Progress OPSYS
        * ---------------- ---------------
        * "linux"          "UNIX"
        * "aix"            "UNIX"
        * "solaris"        "UNIX"
        * "sunos"          "UNIX"
        * "hp-ux"          "UNIX"
        * "freebsd"        "UNIX"
        * "mac os x"       "UNIX"
        * "windows xp"     "WIN32"
        * "windows 2000"   "WIN32"
        * "windows nt"     "WIN32"
        * "windows me"     "WIN32"
        * "windows 98"     "WIN32"
        * "windows 95"     "WIN32"
        * </pre>
        * <p>
        * If there is no match, then any {@code os.name} with the lowercase text "unix" will return
        * "UNIX" and any {@code os.name} with the lowercase text "windows" will return "WIN32".
        * <p>
        * If there is still no match, the {@code os.name} returned by Java will be returned.
        *
        * @param    wa
        *           Context-local work area.
        *
        * @return   The name of the current operating system upon which the client is running.
        */
       private static String getClientOpSysName(WorkArea wa)
       {
          String osname = null;

          // query the JVM value (it will always be present)
          String fallback = null;

          // We have client running.
          // If the client is in the headless mode (e.g. admin client), there is no physical
          // terminal, so the OS can't be accessed.
          if (!TransactionManager._isHeadless() && isServer())
          {
             fallback = OSResourceManager.getEnvironments().getOperatingSystem();
          }
          // We make conversion, take standalone/server value
          else
          {
             fallback = System.getProperty("os.name");
          }
          // New candidate
          String sysprop  = fallback.toLowerCase();

          // check if there is a mapping in the directory for this exact
          // value
          osname = "opsys/mapping/" + sysprop;
          osname = Utils.getDirectoryNodeString(null, osname, "");

          // use the mapping if there was one
          if (osname.length() == 0)
          {
             // attempt to translate it by using our predefined tables
             osname = (String) osMap.get(sysprop);

             // no match?
             if (osname == null)
             {
                // last try
                if (sysprop.indexOf("unix") != -1)
                {
                   osname = OS_UNIX;
                }
                else if (sysprop.indexOf("windows") != -1)
                {
                   osname = OS_WIN;
                }
                else
                {
                   // all else fails, return what Java specified
                   osname = fallback;
                }
             }
          }

          return osname;
       }

       /**
        * A notification when <code>USE environment</code> statement is executed.
        *
        * @param   envName
        *          Environment name.
        */
       private static void onUse(String envName)
       {
          BaseSysTable.use(envName);
       }

       /**
        * A notification when <code>UNLOAD environment</code> statement is executed.
        *
        * @param   envName
        *          Environment name.
        */
       private static void onUnload(String envName)
       {
          BaseSysTable.unload(envName);
       }

       /**
        * Check if it's a INI file environment type.
        *
        * @return  <code>true</code> if INI file environment type.
        */
       private static boolean isIniType()
       {
          return EnvironmentReader.TYPE_INI.equals(getEnvironmentType());
       }

       /**
        * Stores global data relating to the state of the current context.
        */
       private static class WorkArea
       {
          /** Stores the current major.minor version value of the Progress compatible environment. */
          private float versionNumber = 0.0f;

          /** Stores the current verbose version string of the Progress compatible environment. */
          private String versionVerbose = null;

          /** Stores the current shortened version string of the Progress compatible environment. */
          private String versionShort = null;

          /** Stores the current-language of the session. */
          private String currentLanguage = Utils.getDirectoryNodeString(null, "currentLanguage", "?");

          /** Stores any context-specific full effective override to the <code>PROPATH</code>. */
          private String propath = null;

          /** Stores any context-specific override to the <code>PROPATH</code>, changeable part. */
          private String propathOverride =
             fixupPropath(Utils.getDirectoryNodeString(null, "searchpath", ""));

          /** The batch-mode state of this context. */
          private Boolean batchMode = null;

          /** The FWD client log file path of this session. */
          private String fwdLogfile = null;

          /** The name of the OS on the client side or OPSYS override specified in the directory. */
          private String clientOsName = null;

          /** The name of the OS on the client side. */
          private String clientRuntimeOsName = null;

          /** The flag indicating Windows on client side. */
          private boolean isWindowsClient;

          /** The string representing delimiter between path entries on the client side. */
          private String clientPathSeparator = null;

          /** Client legacy platform name. */
          private String legacyPlatform = null;

          /** Flag indicating if the client legacy platform is Windows OS. */
          private boolean legacyPlatformWindows = false;

          /** Set of listeners to be notified when the {@link #propath} has been set. */
          private Set<Consumer<String>> listeners = new HashSet<>();

          /** Mapping of legacy absolute file path prefixes to new environment path prefixes */
          private Map<String, String> pathPrefixMap = null;

          /** File name case-sensitivity of the legacy platform */
          private Boolean legacyCaseSensitive = null;

          /** Resolve file paths on the client system */
          private Boolean resolveFilePaths = null;

          /** Mapping of a helper application to a mime type associated with it */
          private Map<String, List<String>> supportedHelperApplications = null;

          /** File name separator of the legacy platform */
          private String legacyFileSeparator =
             Utils.getDirectoryNodeString(null, "legacy-system/file-separator", File.separator);

          /**
           * Send notifications that the PROPATH has been set.
           *
           * @param    sourcePath
           *           The new PROPATH value.
           */
          protected void notifySourcePath(String sourcePath)
          {
             for (Consumer<String> pl : listeners)
             {
                pl.accept(sourcePath);
             }
          }
       }

       /**
        * Returns <code>true</code> if the running as a server, <code>false</code> otherwise.
        *
        * @return  See above.
        */
       private static boolean isServer()
       {
          return SessionManager.get() != null && !SessionManager.get().isLeaf();
       }

       /**
        * Simple container that stores and returns a context-local instance of
        * the global work area.
        */
       private static class ContextContainer
       extends ContextLocal<WorkArea>
       {
          /**
           * Obtains the context-local instance of the contained global work
           * area.
           *
           * @return   The work area associated with this context.
           */
          public WorkArea obtain()
          {
             return this.get();
          }

          /**
           * Initializes the work area, the first time it is requested within a
           * new context.
           *
           * @return   The newly instantiated work area.
           */
          protected synchronized WorkArea initialValue()
          {
             return new WorkArea();
          }
       }
    }