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();
      }   
   }   
}