# FileSystemOps.java

    /*
    ** Module   : FileSystemOps.java
    ** Abstract : file system utility methods
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 GES 20050706   @21665 Created initial version, supporting common
    **                           language statements and built-in functions
    **                           for manipulating and querying the file
    **                           system.
    ** 002 GES 20050728   @21922 Added searchPath to support the built-in
    **                           search() function.
    ** 003 GES 20050831   @22425 Added FILE-INFO system handle support.
    ** 004 GES 20060123   @24033 Moved to a context-local implementation.
    ** 005 GES 20060211   @24540 Split the implementation from the Progress
    **                           interface/semantic. The FileSystem interface
    **                           was designed to allow this. This enables
    **                           a remote implementation of the interface.
    ** 006 GES 20060322   @25202 Search path must be aware of the mapping
    **                           between Progress source filenames and the
    **                           Java equivalent.
    ** 007 GES 20080123   @36900 Changed getValue to toStringMessage which is
    **                           safer. Added null checks everywhere to make
    **                           the code safer.
    ** 008 GES 20080321   @37631 Added full support for nearly all remaining
    **                           FILE-INFO features.  The remaining features
    **                           are stubbed out.
    ** 009 GES 20110712          Initialize the case-sensitivity setting of the
    **                           legacy system to allow searching to honor it.
    ** 010 GES 20111020          Modified per-session initialization to detect
    **                           when the client code is running inside the server
    **                           process AND to initialize for local redirection
    **                           instead of using a remote object.
    ** 011 CS  20121104          Implement PROPATH assignment.
    ** 012 CA  20130112          Added asHandle, isValid, getType and isUnknown
    **                           methods, accessed by the FILE-INFO handle.
    **                           FILE-NAME attribute is converted to getFileName,
    **                           as it needs to be consistent with the procedure's
    **                           FILE-NAME attribute.
    ** 013 CA  20130221          getType getter for TYPE attribute was renamed to getResourceType.
    ** 014 OM  20130304          Refactored isValid and isUnknown of WrappedResource to valid
    **                           and unknown.
    ** 015 CS  20130325          Task 1613: finish FILE-INFO system handle support
    ** 016 OM  20130618          Used default constructors for unknown values.
    ** 017 CA  20130927          Static proxies need the unknown() API. Resource type is determined
    **                           from LegacyResource constants.
    ** 018 EVL 20150119          Add init server call to push the PROPATH related values to client.
    ** 019 CA  20150502          The resource ID needs to be kept at the resource, so it can be
    **                           accessed after the resource is deleted.
    ** 020 OM  20160527          searchPath() returns legacy procedure names instead of java class.
    ** 021 OM  20160927          Fixed getProperty() to return unknown value when property not set.
    ** 022 SVL 20170126          Added MEMBER function.
    **     GES 20170228          Added javadoc.
    ** 023 SVL 20171109          Added getFileContents.
    ** 024 CA  20180221          Use listeners to be notified when PROPATH has been set.
    ** 025 SBI 20180522          Changed to set legacy file system parameters.
    ** 026 SBI 20180531          Added environment variables map.
    **     ECF 20180609          Added {get|set}WorkingDirectory methods.
    ** 027 ECF 20180614          Adapt to changes in EnvironmentOps API signatures.
    **         20180615          Refactored and moved code from LogicalTerminal to search for
    **                           resources in jar files.
    ** 028 SBI 20180704          Changed searchPath to resolve a legacy path name into a valid system
    **                           path name on the client system.
    ** 029 SBI 20181207          Changed FileSystemDaemon setup taking into account the resolveFilePaths
    **                           parameter.
    ** 030 OM  20190317          Fixed paths name conversion for Windows.
    ** 031 RFB 20200421          Added full support for LIBRARY() and MEMBER(). Revisited and made some
    **                           performance upgrades.
    **     RFB 20200424          Enhanced the javadoc in searchPath (SEARCH) due to some restrictions
    **                           on the capabilities FWD supports with respect to library and member.
    **                           Also moved new private static method to the correct location.
    ** 032 RFB 20200627          Cleaned up a Javadoc error in procLibraryParser.
    **     EVL 20201111          Adding ability to log missed path entries and bypass client side search for ones
    **                           that included in ignore list.  Used HashSet instead of ArrayList to speed up
    **                           string search.
    **     OM  20201203          Fixed handling of READ/ONLY attributes.
    ** 033 SBI 20210414          Fixed getRefinedPathName in the case it will be used to normalize a directory
    **                           path ended with a path separator character.
    **         20210414          Moved getRefinedPathName, JarResource to Utils, moved jclCache to JarClassLoader.
    **     CA  20210527          FILE-INFO now will search for converted program files, too.
    **     SBI 20210813          Changed searchResourceJars according to Utils.searchResourceJars.
    **     EVL 20220404          Added ability to get file size during initialization of FileInfo class instance.
    **     EVL 20220407          Opening missed file should not interrupt processing.  We just have no ability to
    **                           get file size during init FileInfo class. Made another error protection to
    **                           disable messages on client side.
    **     VVT 20221003          CommonHandle.getResourceType() method renamed to resourceType() to prevent
    **                           conflicts with namesakes in DMO. See #6694.
    **     EVL 20221103          Adding fixup file/directory names to avoid using invalid separator sequence.
    ** 034 HC  20230427          Implemented server-side file-system resource. File operations
    **                           performed on client can be optionally performed directly on server
    **                           without the expensive remote call.
    ** 035 EVL 20230512          Fix for empty file name handling for initFileInfo().  Need to reset all
    **                           internals to unknown values.
    ** 036 DDF 20230705          Replace static field initialization with a method called at server bootstrap.
    ** 037 CA  20240225          Added 'getFileSeparator'.  Now 'getWorkingDir' returns 'clientConfig/workingDir'
    **                           when server-side file-system and normal working dir, otherwise.
    ** 038 GBB 20240404          JarResource class and searchResourceJars() moved from Utils to JarUtil.
    ** 039 CA  20240619          'initFileInfo' uses an internal silent mode, and it needs to clear the pending
    **                           error state.
    ** 040 GBB 20240709          Hard-coded config name replaced by ConfigItem constant.
    ** 041 GBB 20240826          Reflect changes in OSResourceManager interface and package.
    ** 042 GBB 20240912          OSResourceManager method isServerSide to use an enum param.
    ** 043 OM  20250410          Local optimizations. Code maintenance.
    ** 044 SP  20250526          Changed searchPath() to return character.UNKNOWN when filename is empty.
    */

    /*
    ** 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.util.*;
    import java.util.regex.*;
    import java.util.function.*;
    import java.io.*;
    import com.goldencode.p2j.classloader.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.security.ContextLocal;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.p2j.util.osresource.*;
    import com.goldencode.proxy.StaticProxy;
    import com.goldencode.util.PlatformHelper;
    //WARNING: Do NOT add usage of Guava.  We plan to remove the current usage.
    import com.google.common.base.Supplier;
    import com.google.common.base.Suppliers;

    /**
     * Progress compatible static file-system 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).
     * <p>
     * A partial implementation of the <code>FILE-INFO</code> system handle is
     * available but only the <code>FILE-NAME</code> and <code>FILE-SIZE</code>
     * attributes are supported.
     * <p>
     * The following is the list of supported Progress 4GL features:
     * <p>
     * <pre>
     * Progress Variable                    Java Equivalent
     * ---------------------                ----------------------
     * os-error                             {@link #getLastError}
     * os-drives                            {@link #getRootList}
     * os-getenv                            {@link #getProperty}
     * </pre>
     * <p>
     * <pre>
     * Language Statement                   Java Equivalent
     * ---------------------                ----------------------
     * os-append                            {@link #append}
     * os-copy                              {@link #copy}
     * os-create-dir                        {@link #mkdir}
     * os-delete                            {@link #delete}
     * os-rename                            {@link #rename}
     * </pre>
     * <p>
     * <pre>
     * Function                             Java Equivalent
     * ---------------------                ----------------------
     * member                               {@link #member}
     * search                               {@link #searchPath}
     * </pre>
     * <p>
    * <pre>
     * System Handle:Attribute              Java Equivalent
     * -------------------------            ----------------------
     * file-info:file-create-date           {@link #fileInfoGetCreationDate} (get)
     * file-info:file-create-time           {@link #fileInfoGetCreationTime} (get)
     * file-info:file-mod-date              {@link #fileInfoGetModDate}      (get)
     * file-info:file-mod-time              {@link #fileInfoGetModTime}      (get)
     * file-info:file-name                  {@link #initFileInfo}            (set)
     * file-info:file-name                  {@link #getFileName}             (get)
     * file-info:file-size                  {@link #fileInfoGetSize}         (get)
     * file-info:file-type                  {@link #fileInfoGetType}         (get)
     * file-info:full-pathname              {@link #fileInfoGetFullPath}     (get)
     * file-info:pathname                   {@link #fileInfoGetPathName}     (get)
     * </pre>
     * <p>
     * This class implements the Progress interface/semantic. However, the actual
     * implementation of this function is provided by a network object that
     * implements the {@link com.goldencode.p2j.util.osresource.FileSystem} interface. This means that the actual
     * operations occur on the remote node associated with this user's session.
     * Please see {@link FileSystemDaemon} and the <code>RemoteObject</code> in
     * the <code>net</code> package.
     */
    public class FileSystemOps
    {
       /** Logger. */
       private static final CentralLogger LOG = CentralLogger.get(FileSystemOps.class);

       /** File name separator used in jar file entries */
       private static final char JAR_SEP = '/';

       /** Stores pre-compiled pattern for LIBRARY and MEMBER built-ins. */
       private static final Pattern patternLibraryMember = Pattern.compile("(.*)<<(.*)>>");

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

       /** Application package root path */
       private static String pkgRootPath = "";

       /**
        * 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()
       {
          pkgRootPath = Utils.getDirectoryNodeString(null, "legacy-system/pkgroot", "").replace('.', JAR_SEP) +
                        JAR_SEP;
       }

       /**
        * Get the instance for the FILE-INFO system handle.  Is obtained using
        * a call to the {@link StaticProxy#obtain(Class, Class[])}, using the
        * {@link CommonFileInfo} interface and its methods implemented by these
        * classes: {@link FileSystemOps}.
        *
        * @return   See above.
        */
       public static handle asHandle()
       {
          WorkArea wa = work.obtain();

          Class<?>[] classes = { FileSystemOps.class };
          CommonFileInfo proxy = StaticProxy.obtain(CommonFileInfo.class, classes);

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

          return new handle(proxy);
       }

       /**
        * Get this resource's ID.
        *
        * @return   The resource's ID.
        */
       public static Long id()
       {
          WorkArea wa = work.obtain();

          return wa.id;
       }

       /**
        * Set this resource's ID.
        * <p>
        * This is a no-op for system handles.
        *
        * @param    id
        *           The resource's ID.
        */
       public static void id(long id)
       {
          // no-op
       }

       /**
        * Implementation for the {@link WrappedResource#valid()} API.
        *
        * @return   Always true.
        */
       public static boolean valid()
       {
          return true;
       }

       /**
        * Implementation for the {@link WrappedResource#unknown()} API.
        *
        * @return   Always false.
        */
       public static boolean unknown()
       {
          return false;
       }

       /**
        * Push the PROPATH values to the client side.
        */
       public static void init()
       {
          work.obtain();
       }

       /**
        * API needed to implement read-only attribute assignment (a 4GL "feature").
        *
        * @param    attribute
        *           The attribute's name.
        *
        * @see      handle#readOnlyError(handle, String)
        */
       public static void readOnlyError(String attribute)
       {
          handle.readOnlyError(asHandle(), attribute);
       }

       /**
        * API needed to implement read-only attribute assignment (a 4GL "feature").
        *
        * @param    attribute
        *           The attribute's name.
        * @param    expr
        *           The value which is attempted to be assigned to the read-only attribute.
        *
        * @see      handle#readOnlyError(handle, String, Object)
        */
       public static void readOnlyError(String attribute, Object expr)
       {
          handle.readOnlyError(asHandle(), attribute);
       }

       /**
        * Get the error code of the last operating system operation.
        *
        * @return   The Progress-compatible error code.
        */
       public static integer getLastError()
       {
          return new integer(work.obtain().lastError);
       }

       /**
        * Sets the error code of the last operating system operation.
        *
        * @param    err
        *           The Progress-compatible error code.
        */
       public static void setLastError(int err)
       {
          work.obtain().lastError = err;
       }

       /**
        * Search for the specified file and initialize the related data structures if found.  Relative filenames
        * are searched for via the {@link EnvironmentOps#getSearchPath}.  If <code>name</code> is
        * <code>unknown</code> or not found, the current data structures are cleared.
        * <p>
        * If no file is found on the file system, a lookup will be done through the converted legacy code, jar
        * resources, etc, via {@link #searchPath}.  If a program is found, that will be used in the data structures.
        *
        * @param    name
        *           Relative or absolute filename to obtain information on.
        */
       public static void initFileInfo(character name)
       {
          if (name == null || name.isUnknown())
          {
             work.obtain().info = null;
             return;
          }

          initFileInfo(name.toStringMessage());
       }

       /**
        * Search for the specified file and initialize the related data structures if found.  Relative filenames
        * are searched for via the {@link EnvironmentOps#getSearchPath}.  If <code>name</code> is
        * <code>unknown</code> or not found, the current data structures are cleared.
        * <p>
        * If no file is found on the file system, a lookup will be done through the converted legacy code, jar
        * resources, etc, via {@link #searchPath}.  If a program is found, that will be used in the data structures.
        *
        * @param    name
        *           Relative or absolute filename to obtain information on.
        */
       public static void initFileInfo(String name)
       {
          WorkArea wa = work.obtain();
          // for empty input file name we need to reset all internal to unknown values
          if (name == null || name.isEmpty())
          {
             // reset internal structures
             wa.info = null;
             return;
          }

          // fixup possible file name errors like double '/'
          name = resolveLegacyPath(name, wa);
          wa.info = (name == null) ? null : wa.fs.accessFileInfo(name);

          if (name != null && (wa.info == null || wa.info.pathname == null))
          {
             // try a lookup jar resources, converted programs, other files via PROPATH
             character found = searchPath(name);
             if (!found.isUnknown())
             {
                // initialize the FileInfo structure
                wa.info = new FileInfo();
                wa.info.name = name;
                wa.info.pathname = found.toStringMessage();
                wa.info.fullname = found.toStringMessage(); // WARNING: this is not a full path
                // trying to get file size, should work for both jar and OS file system sources
                if (wa.info.pathname != null)
                {
                   boolean silentCurrent = ErrorManager.isSilent();
                   try
                   {
                      ErrorManager.setSilent(true);
                      Stream fs = StreamFactory.openFileStream(wa.info.pathname, false, false);
                      if (fs != null)
                      {
                         wa.info.size = fs.getLength().longValue();
                         fs.close();
                      }
                      else
                      {
                         wa.info.size = -1; // not available
                      }
                   }
                   catch (Exception e)
                   {
                      // the problems while opening file, may be missing
                      wa.info.size = -1; // not available
                   }
                   finally
                   {
                      ErrorManager.setSilent(silentCurrent);
                      ErrorManager.clearPending();
                   }
                }
                else
                {
                   wa.info.size = -1; // not available
                }
                wa.info.type = "FR"; // file, read-only
                wa.info.modification = -1; // not available
                wa.info.creation = -1; // not available
             }
          }
       }

       /**
        * If the current file information data structures are initialized, return
        * the file name, otherwise return the <code>unknown value</code>.
        *
        * @return   The file name of the current file being inspected as input
        *           by the caller (even if the file could not be found) or the
        *           <code>unknown value</code> if the data structures were never
        *           initialized.
        */
       public static character getFileName()
       {
          WorkArea wa = work.obtain();

          if (wa.info == null || wa.info.name == null)
          {
             return character.UNKNOWN;
          }

          return character.of(wa.info.name);
       }

       /**
        * If the current file information data structures are initialized, return
        * the absolute or relative path name, otherwise return the
        * <code>unknown value</code>. If the specified file name was absolute,
        * then an absolute pathname will be returned.  Otherwise the relative
        * path that was found via {@link #searchPath} will be returned.
        *
        * @return   The path name of the current file being inspected or the
        *           <code>unknown value</code> if the data structures were never
        *           initialized or if initialization failed.
        */
       public static character fileInfoGetPathName()
       {
          WorkArea wa = work.obtain();

          if (wa.info == null || wa.info.pathname == null)
          {
             return character.UNKNOWN;
          }

          return character.of(wa.info.pathname);
       }

       /**
        * If the current file information data structures are initialized, return
        * the absolute (canonicalized) path name, otherwise return the
        * <code>unknown value</code>.
        *
        * @return   The absolute path name of the current file being inspected or
        *           the <code>unknown value</code> if the data structures were
        *           never initialized or if initialization failed.
        */
       public static character fileInfoGetFullPath()
       {
          WorkArea wa = work.obtain();

          if (wa.info == null || wa.info.fullname == null)
          {
             return character.UNKNOWN;
          }

          return character.of(wa.info.fullname);
       }

       /**
        * If the current file information data structures are initialized, return
        * the file's size, otherwise return the <code>unknown value</code>.
        *
        * @return   The size in bytes of the current file being inspected or the
        *           <code>unknown value</code> if the data structures were never
        *           initialized or if initialization failed.
        */
       public static integer fileInfoGetSize()
       {
          WorkArea wa = work.obtain();

          if (wa.info == null || wa.info.size == -1)
          {
             return new integer();
          }

          return new integer(wa.info.size);
       }

       /**
        * If the current file information data structures are initialized, return
        * a string with characters that specify attributes about the file,
        * otherwise return the <code>unknown value</code>.
        * <p>
        * One of the following mutually exclusive characters will be present:
        * <p>
        * <pre>
        * Character   Meaning
        * ---------   -----------------------------
        * D           directory
        * F           file
        * X           unknown
        * </pre>
        * <p>
        * <b>There is no support for "S" (UNIX special devices) or "M" (procedure
        * library members) at this time.</b>
        * <p>
        * One or more of the following characters will be present:
        * <p>
        * <pre>
        * Character   Meaning
        * ---------   -----------------------------
        * H           hidden
        * R           readable
        * W           writable
        * </pre>
        * <p>
        * <b>There is no support for "L" (UNIX symbolic links) or "P" (pipes)
        * at this time.</b>
        *
        * @return   The file attributes of the current file being inspected or
        *           the <code>unknown value</code> if the data structures were
        *           never initialized or if initialization failed.
        */
       public static character fileInfoGetType()
       {
          WorkArea wa = work.obtain();

          if (wa.info == null || wa.info.type == null)
          {
             return character.UNKNOWN;
          }

          return character.of(wa.info.type);
       }

       /**
        * If the current file information data structures are initialized, return
        * the file's last modification date, otherwise return the
        * <code>unknown value</code>.
        *
        * @return   The last modification date of the current file being inspected
        *           or the <code>unknown value</code> if the data structures were
        *           never initialized or if initialization failed.
        */
       public static date fileInfoGetModDate()
       {
          WorkArea wa = work.obtain();

          if (wa.info == null || wa.info.modification == -1)
          {
             return new date();
          }

          return new date(date.neutralMillisToDate(wa.info.modification, null));
       }

       /**
        * If the current file information data structures are initialized, return
        * the file's last modification time (in seconds from midnight), otherwise
        * return the <code>unknown value</code>.
        *
        * @return   The last modification time in seconds from midnight of the
        *           current file being inspected or the <code>unknown value</code>
        *           if the data structures were never initialized or if
        *           initialization failed.
        */
       public static integer fileInfoGetModTime()
       {
          WorkArea wa = work.obtain();

          if (wa.info == null || wa.info.modification == -1)
          {
             return new integer();
          }

          // turn the modification millis into a specific date (this drops the
          // time value and leaves just the date as a julian day number)
          Date cd     = date.neutralMillisToDate(wa.info.modification, null);
          date create = new date(cd);

          // convert the julian day number into a base millis value for that day
          long base = date.julianDayToMillis(create.longValue());

          // get the difference and convert to seconds
          return new integer((wa.info.modification - base) / 1000);
       }

       /**
        * If the current file information data structures are initialized, return
        * the file's creation date, otherwise return the
        * <code>unknown value</code>.
        *
        * @return   Always <code>unknown value</code> at this time.
        */
       public static date fileInfoGetCreationDate()
       {
          // the creation date is only given on windows, on Unix unknown value
          // will be returned.
          if (PlatformHelper.isUnderWindowsFamily())
          {
             WorkArea wa = work.obtain();

             // if creation is -1 error occurred
             if (wa.info == null || wa.info.creation == -1)
             {
                return new date();
             }
             Date createDate = new Date(wa.info.creation);

             return new date(createDate);
          }
          else
          {
             return new date();
          }
       }

       /**
        * If the current file information data structures are initialized, return
        * the file's creation time (in seconds from midnight), otherwise
        * return the <code>unknown value</code>.
        *
        * @return   Always returns <code>unknown value</code> at this time.
        */
       public static integer fileInfoGetCreationTime()
       {
          // the creation time is only given on windows, on Unix no value
          // will be returned.
          if (PlatformHelper.isUnderWindowsFamily())
          {
             WorkArea wa = work.obtain();
             // if creation is -1 error occurred
             if (wa.info == null || wa.info.creation == -1)
             {
                return new integer();
             }
             Date createDate = new Date(wa.info.creation);
             GregorianCalendar calendar = new GregorianCalendar();
             calendar.setTime(createDate);
             return new integer(date.secondsSinceMidnight(calendar));
          }
          else
          {
             return new integer();
          }
       }

       /**
        * Get the comma-separated list of filesystem roots (drive letters on
        * platforms that support the concept, otherwise returns the equivalent
        * of the empty string).
        *
        * @return   The comma-separated list of drive letters.
        */
       public static character getRootList()
       {
          try
          {
             return character.of(work.obtain().fs.getRootList());
          }
          catch (IOException ioe)
          {
             setLastError(FileSystem.ERR_IO);
             return character.EMPTY_STRING;
          }
       }

       /**
        * Get the system property (user-defined properties can be specified at the Java command line)
        * associated with the passed key.  If the specified system property does not exist,
        * {@code unknown} value is returned.
        *
        * @return   The system property found or {@code unknown} if the property does not exist.
        */
       public static character getProperty(character key)
       {
          if (key == null || key.isUnknown())
          {
             return character.UNKNOWN;
          }

          return getProperty(key.toStringMessage());
       }

       /**
        * Get the system property (user-defined properties can be specified at the Java command line)
        * associated with the passed key.  If the specified system property does not exist,
        * {@code unknown} value is returned.
        *
        * @return   The system property found or {@code unknown} if the property does not exist.
        */
       public static character getProperty(String key)
       {
          if (key == null)
          {
             return character.UNKNOWN;
          }

          WorkArea wa = work.obtain();

          try
          {
             String transformedKey = key;

             if (!wa.legacyCaseSensitive)
             {
                transformedKey = key.toUpperCase(Locale.ROOT);
             }

             String value = wa.environmentVariables.get(transformedKey);

             if (value != null)
             {
                return character.of(value);
             }

             //try interceptors
             Function<String, String> interceptor = wa.keysInterceptors.get(transformedKey);

             if (interceptor != null)
             {
                key = interceptor.apply(transformedKey);
             }

             // fall back to the client system properties
             return character.of(wa.fs.getProperty(key));
          }
          catch (Exception exc)
          {
             // return the unknown value if a null pointer, security or illegal argument occurs
             return character.UNKNOWN;
          }
       }

       /**
        * Appends the specified source file to the end of the target file. If
        * any error occurs, no exception will be thrown. The {@link #getLastError}
        * can be used to retrieve the error code.
        *
        * @param    src
        *           The name of the source file.  Must NOT be a directory name.
        * @param    tar
        *           The name of the target file.  If this is a directory name or
        *           if this is a non-existent filename, this method is converted
        *           into a copy operation.
        */
       public static void append(character src, character tar)
       {
          if (src == null || src.isUnknown() || tar == null || tar.isUnknown())
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             append(src.toStringMessage(), tar.toStringMessage());
          }
       }

       /**
        * Appends the specified source file to the end of the target file. If
        * any error occurs, no exception will be thrown. The {@link #getLastError}
        * can be used to retrieve the error code.
        *
        * @param    src
        *           The name of the source file.  Must NOT be a directory name.
        * @param    tar
        *           The name of the target file.  If this is a directory name or
        *           if this is a non-existent filename, this method is converted
        *           into a copy operation.
        */
       public static void append(character src, String tar)
       {
          if (src == null || src.isUnknown() || tar == null)
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             append(src.toStringMessage(), tar);
          }
       }

       /**
        * Appends the specified source file to the end of the target file. If
        * any error occurs, no exception will be thrown. The {@link #getLastError}
        * can be used to retrieve the error code.
        *
        * @param    src
        *           The name of the source file.  Must NOT be a directory name.
        * @param    tar
        *           The name of the target file.  If this is a directory name or
        *           if this is a non-existent filename, this method is converted
        *           into a copy operation.
        */
       public static void append(String src, character tar)
       {
          if (src == null || tar == null || tar.isUnknown())
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             append(src, tar.toStringMessage());
          }
       }

       /**
        * Appends the specified source file to the end of the target file. If
        * any error occurs, no exception will be thrown. The {@link #getLastError}
        * can be used to retrieve the error code.
        *
        * @param    src
        *           The name of the source file.  Must NOT be a directory name.
        * @param    tar
        *           The name of the target file.  If this is a directory name or
        *           if this is a non-existent filename, this method is converted
        *           into a copy operation.
        */
       public static void append(String src, String tar)
       {
          WorkArea wa = work.obtain();

          if (src == null || tar == null)
          {
             wa.lastError = FileSystem.ERR_NO_ERROR;
          }
          else
          {
             wa.lastError = wa.fs.copy(resolveLegacyPath(src, wa), resolveLegacyPath(tar, wa), false);
          }
       }

       /**
        * Copies the specified source file to or over the target file. This
        * method always overwrites the target file.  If any error occurs, no
        * exception will be thrown. The {@link #getLastError} can be used to
        * retrieve the error code.
        *
        * @param    src
        *           The name of the source file.  Must NOT be a directory name.
        * @param    tar
        *           The name of the target file.  If this is a directory name,
        *           the source file name will be used as the target file in
        *           the directory given.
        */
       public static void copy(character src, character tar)
       {
          if (src == null || src.isUnknown() || tar == null || tar.isUnknown())
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             copy(src.toStringMessage(), tar.toStringMessage());
          }
       }

       /**
        * Copies the specified source file to or over the target file. This
        * method always overwrites the target file.  If any error occurs, no
        * exception will be thrown. The {@link #getLastError} can be used to
        * retrieve the error code.
        *
        * @param    src
        *           The name of the source file.  Must NOT be a directory name.
        * @param    tar
        *           The name of the target file.  If this is a directory name,
        *           the source file name will be used as the target file in
        *           the directory given.
        */
       public static void copy(character src, String tar)
       {
          if (src == null || src.isUnknown() || tar == null)
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             copy(src.toStringMessage(), tar);
          }
       }

       /**
        * Copies the specified source file to or over the target file. This
        * method always overwrites the target file.  If any error occurs, no
        * exception will be thrown. The {@link #getLastError} can be used to
        * retrieve the error code.
        *
        * @param    src
        *           The name of the source file.  Must NOT be a directory name.
        * @param    tar
        *           The name of the target file.  If this is a directory name,
        *           the source file name will be used as the target file in
        *           the directory given.
        */
       public static void copy(String src, character tar)
       {
          if (src == null || tar == null || tar.isUnknown())
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             copy(src, tar.toStringMessage());
          }
       }

       /**
        * Copies the specified source file to or over the target file. This
        * method always overwrites the target file.  If any error occurs, no
        * exception will be thrown. The {@link #getLastError} can be used to
        * retrieve the error code.
        *
        * @param    src
        *           The name of the source file.  Must NOT be a directory name.
        * @param    tar
        *           The name of the target file.  If this is a directory name,
        *           the source file name will be used as the target file in
        *           the directory given.
        */
       public static void copy(String src, String tar)
       {
          WorkArea wa = work.obtain();

          if (src == null || tar == null)
          {
             wa.lastError = FileSystem.ERR_NO_ERROR;
          }
          else
          {
             wa.lastError = wa.fs.copy(src, tar, true);
          }
       }

       /**
        * Creates each of the directories requested, if it doesn't exist and all
        * path segments up to the last segment do exist.  If the directory name
        * already exists in the filesystem (even if it is a file), this method
        * just returns (this is the Progress behavior).  If any error occurs, no
        * exception will be thrown, but no subsequent directories in the list
        * will be created. The {@link #getLastError} can be used to retrieve
        * the error code.
        *
        * @param    dirs
        *           The list of directories to create.
        */
       public static void mkdir(String[] dirs)
       {
          if (dirs == null)
          {
             return;
          }
          WorkArea wa = work.obtain();

          // check the dir list for valid separator usage
          for (int i = 0; i < dirs.length; i++)
          {
             dirs[i] = resolveLegacyPath(dirs[i], wa);
          }

          wa.lastError = wa.fs.mkdir(dirs);
       }

       /**
        * Deletes each of the directories or files specified.  If a non-empty
        * directory name is specified, an error will occur unless the
        * <code>recursive</code> flag is <code>true</code>.  If any error occurs,
        * no exception will be thrown, the deletion process just continues. The
        * {@link #getLastError} can be used to retrieve the error code.
        *
        * @param    list
        *           The list of files and directories to delete.
        * @param    recursive
        *           Recursively decend through all subdirectories, deleting the entire subtree.
        */
       public static void delete(String[] list, boolean recursive)
       {
          if (list == null)
          {
             return;
          }
          WorkArea wa = work.obtain();

          // check the dir list for valid separator usage
          for (int i = 0; i < list.length; i++)
          {
             list[i] = resolveLegacyPath(list[i], wa);
          }

          wa.lastError = wa.fs.delete(list, recursive);
       }

       /**
        * Get the file contents from the client-side file system.
        *
        * @param  filename
        *         Name of the file in the client-side file system (relatively to the current
        *         directory on the client side).
        *
        * @return file contents as array of bytes.
        *
        * @throws java.io.IOException
        *         If file cannot not be found or read, or is too large.
        */
       public static byte[] getFileContents(String filename)
       throws IOException
       {
          WorkArea wa = work.obtain();
          return wa.fs.getFileContents(resolveLegacyPath(filename, wa));
       }

       /**
        * Renames (and moves if the path is different) the specified source file
        * to the target filename (and path).  If any error occurs, no
        * exception will be thrown. The {@link #getLastError} can be used to
        * retrieve the error code.
        *
        * @param    src
        *           The name of the source file or directory.
        * @param    tar
        *           The name of the target file or directory.  If this is a
        *           directory name for a directory that already exists and the
        *           <code>source</code> is a file, then the source file name will
        *           be used as the target file in the directory given.
        */
       public static void rename(character src, character tar)
       {
          if (src == null || src.isUnknown() || tar == null || tar.isUnknown())
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             rename(src.toStringMessage(), tar.toStringMessage());
          }
       }

       /**
        * Renames (and moves if the path is different) the specified source file
        * to the target filename (and path).  If any error occurs, no
        * exception will be thrown. The {@link #getLastError} can be used to
        * retrieve the error code.
        *
        * @param    src
        *           The name of the source file or directory.
        * @param    tar
        *           The name of the target file or directory.  If this is a
        *           directory name for a directory that already exists and the
        *           <code>source</code> is a file, then the source file name will
        *           be used as the target file in the directory given.
        */
       public static void rename(character src, String tar)
       {
          if (src == null || src.isUnknown() || tar == null)
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             rename(src.toStringMessage(), tar);
          }
       }

       /**
        * Renames (and moves if the path is different) the specified source file
        * to the target filename (and path).  If any error occurs, no
        * exception will be thrown. The {@link #getLastError} can be used to
        * retrieve the error code.
        *
        * @param    src
        *           The name of the source file or directory.
        * @param    tar
        *           The name of the target file or directory.  If this is a
        *           directory name for a directory that already exists and the
        *           <code>source</code> is a file, then the source file name will
        *           be used as the target file in the directory given.
        */
       public static void rename(String src, character tar)
       {
          if (src == null || tar == null || tar.isUnknown())
          {
             setLastError(FileSystem.ERR_NO_ERROR);
          }
          else
          {
             rename(src, tar.toStringMessage());
          }
       }

       /**
        * Renames (and moves if the path is different) the specified source file
        * to the target filename (and path).  If any error occurs, no
        * exception will be thrown. The {@link #getLastError} can be used to
        * retrieve the error code.
        *
        * @param    src
        *           The name of the source file or directory.
        * @param    tar
        *           The name of the target file or directory.  If this is a
        *           directory name for a directory that already exists and the
        *           <code>source</code> is a file, then the source file name will
        *           be used as the target file in the directory given.
        */
       public static void rename(String src, String tar)
       {
          WorkArea wa = work.obtain();

          if (src == null || tar == null)
          {
             wa.lastError = FileSystem.ERR_NO_ERROR;
          }
          else
          {
             wa.lastError = wa.fs.rename(resolveLegacyPath(src, wa), resolveLegacyPath(tar, wa));
          }
       }

       /**
        * Search each directory listed in the {@link EnvironmentOps#getSearchPath} for the given file.
        * If the file system object exists and is a file, then the filename is returned. If the path
        * contains an empty string or a '.', then the current directory is searched, otherwise only
        * the listed directories are searched.
        * <p>
        * Special processing is provided in case the original Progress code was checking for the
        * availability of an external procedure.  In this case the filename is checked using the
        * {@link SourceNameMapper#convertName} method. If such the Java class exists, its legacy
        * filename is returned.  This will "fake" the converted code into thinking that the external
        * procedure this still exists.
        * <p>
        * This method intentionally does not support the search of 4GL procedure libraries (either
        * in the {@link EnvironmentOps#getSearchPath} or as an explicitly listed library). In the 4GL,
        * SEARCH allows passing either a member to search for in the procedure libraries in the
        * PROPATH, or a specific library plus member using the syntax "path-name<<member-name>>". In
        * either case the 4GL returns the full path to the procedure library while this method does
        * not.  This is a known restriction.
        *
        * @param   filename
        *          The absolute or relative filename for which to search.
        *
        * @return  If the file is found in the current directory, the relative filename is returned.
        *          If the file is absolute or found in any other path, the full absolute filename is
        *          returned.  If the file is not found or the referenced file system object is not a
        *          file, the {@code unknown value} is returned.
        */
       public static character searchPath(character filename)
       {
          if (filename == null || filename.isUnknown())
          {
             return character.UNKNOWN;
          }

          return searchPath(filename.toStringMessage());
       }

       /**
        * Search each directory listed in the {@link EnvironmentOps#getSearchPath} for the given file.
        * If the file system object exists and is a file, then the filename is returned. If the path
        * contains an empty string or a '.', then the current directory is searched, otherwise only
        * the listed directories are searched.
        * <p>
        * Special processing is provided in case the original Progress code was checking for the
        * availability of an external procedure.  In this case the filename is checked using the
        * {@link SourceNameMapper#convertName} method. If such the Java class exists, its legacy
        * filename is returned.  This will "fake" the converted code into thinking that the external
        * procedure this still exists.
        * <p>
        * Application resource jars are searched before the remote file system.
        * <p>
        * This method intentionally does not support the search of 4GL procedure libraries (either
        * in the {@link EnvironmentOps#getSearchPath} or as an explicitly listed library). In the 4GL,
        * SEARCH allows passing either a member to search for in the procedure libraries in the
        * PROPATH, or a specific library plus member using the syntax "path-name<<member-name>>". In
        * either case the 4GL returns the full path to the procedure library while this method does
        * not.  This is a known restriction.
        *
        * @param   filename
        *          The absolute or relative filename for which to search.
        *
        * @return  If the file is found in the current directory, the relative filename is returned.
        *          If the file is absolute or found in any other path, the full absolute filename is
        *          returned.  If the file is not found or the referenced file system object is not a
        *          file, the {@code unknown value} is returned.
        */
       public static character searchPath(String filename)
       {
          if (filename == null || filename.isEmpty())
          {
             return character.UNKNOWN;
          }
          WorkArea wa = work.obtain();

          // if the Progress code was checking for the availability of an external procedure, we
          // have to "fake" it out into thinking that this still exists, try to match this given
          // filename with a known program name, optionally honoring any absolute prefix passed in
          String legacy = SourceNameMapper.lookupLegacyName(filename);
          if (legacy != null)
          {
             // this is a program name, return the Java class name instead
             return character.of(resolveLegacyPath(legacy, wa));
          }

          // avoid expensive client/jar files search for missing procedure names
          if (!wa.ignoreClientSearch.isEmpty() && wa.ignoreClientSearch.contains(filename))
          {
             return character.UNKNOWN;
          }

          // check resource jar files first, before checking remote file system
          JarUtil.JarResource jarRes = searchResourceJars(filename, null);

          String result = (jarRes != null ? jarRes.getName() : wa.fs.searchPath(filename));

          // the path entry was not found even on client side so if logging enabled, append new entry
          if (result == null && wa.captureMissedFiles)
          {
             appendMissedPathEntry(filename);
          }

          return result == null ? character.UNKNOWN : character.of(result);
       }

       /**
        * Search the resource jar files for the resource with the given filename, appending the
        * extensions provided in the optional list, if present.
        * <p>
        * The algorithm first attempts to find an exact match for the filename. If not found, it
        * applies the PROPATH to the filename, first prepending the package root to the name.
        *
        * @param   filename
        *          Filename of the resource, optionally including a path.
        * @param   extensions
        *          Optional list of extensions to apply to the name. If this list is non-null,
        *          {@code filename} is assumed to be the file's base name, sans extension.
        *
        * @return  A {@code JarResult} object, if the search succeeds, else {@code null}.
        */
       public static JarUtil.JarResource searchResourceJars(String filename, List<String> extensions)
       {
          final char WIN_SEP = '\\';
          final String JAR_SEP_STR = "/";
          boolean caseSens = EnvironmentOps.getLegacyCaseSensitive();

          String clientOS = EnvironmentOps.getRuntimeOperatingSystem().toJavaType();
          String legacyOS = EnvironmentOps.getLegacyPlatform();

          PathResolver pathResolver = null;

          if (!legacyOS.equalsIgnoreCase(clientOS))
          {
             String fileSeparator = EnvironmentOps.getLegacyFileSeparator();

             Map<String, String> pathMap = EnvironmentOps.getLegacyAbsolutePathPrefixMap();

             pathResolver = Utils.getPathResolver(fileSeparator, caseSens, pathMap);
          }

          PathResolverContainer safePathResolver = new PathResolverContainer(pathResolver);

          Function<JarClassLoader, Function<String, String>> coreFn = jcl -> (n -> jcl.getResourceName(n, caseSens));

          // if no extensions were provided, do a straight search for the resource name in the jar;
          // otherwise, concatenate each extension to the base resource name before searching the jar, and
          // continue until we have a hit, or no more extensions to try
          Function<JarClassLoader,Function<String, String>> searchFn =
             extensions == null
             ? coreFn
             : jcl -> (n -> Utils.tryWhileNull(extensions, e -> coreFn.apply(jcl).apply(n.concat(e))));

          final Supplier<String[]> propathHeadersSuplier = Suppliers.memoize(EnvironmentOps::getSearchPathList);

          Function<JarClassLoader, Function<String, String>> propathSearchFn = jcl -> n ->
          {
             // not found in root continue with PROPATH entries
             String found = null;

             final String[] propathHeader = propathHeadersSuplier.get();

             for (String header : propathHeader)
             {
                header = safePathResolver.resolvePath(header);

                // look through PROPATH related locations, clean up possible Windows separators
                header = header.replace(WIN_SEP, JAR_SEP);

                // inside jar we need to use UNIX-style file separator even on Windows
                String name = Utils.getRefinedPathName(pkgRootPath + header + JAR_SEP + n, JAR_SEP_STR);

                // try to search new name based on PROPATH header
                found = searchFn.apply(jcl).apply(name);
                if (found != null)
                {
                   return found;
                }

                // try one more time, but using the root of the jar instead of pkgRootPath
                found = searchFn.apply(jcl).apply(Utils.getRefinedPathName(header + JAR_SEP + n, JAR_SEP_STR));
                if (found != null)
                {
                   return found;
                }
             }

             return found;
          };

          return JarUtil.searchResourceJars(LOG,
                                            filename,
                                            searchFn,
                                            propathSearchFn,
                                            StandardServer::getResourceJarPaths);
       }

       /**
        * Parses a reference to 4GL r-code library and returns the simple library name.
        *
        * @param  filename
        *         Pathname of the r-code library.
        *
        * @return The function parses a character string in the form
        *         <code>path-name&lt;&lt;member-name&gt;&gt;</code>, where <code>path-name</code> is
        *         the pathname of a library and <code>member-name</code> is the name of a file within
        *         the library, and returns <code>path-name</code>.
        */
       public static character library(String filename)
       {
          return library(character.of(resolveLegacyPath(filename, work.obtain())));
       }

       /**
        * Parses a reference to 4GL r-code library and returns the simple library name.
        *
        * @param  filename
        *         Pathname of the r-code library.
        *
        * @return The function parses a character string in the form
        *         <code>path-name&lt;&lt;member-name&gt;&gt;</code>, where <code>path-name</code> is
        *         the pathname of a library and <code>member-name</code> is the name of a file within
        *         the library, and returns <code>path-name</code>.
        */
       public static character library(character filename)
       {
          return procLibraryParser(filename, 1);
       }

       /**
        * Parses a reference to a member of an 4GL r-code library and returns the simple member name.
        *
        * @param  filename
        *         Pathname of the file in the r-code library.
        *
        * @return The function parses a character string in the form
        *         <code>path-name&lt;&lt;member-name&gt;&gt;</code>, where <code>path-name</code> is
        *         the pathname of a library and <code>member-name</code> is the name of a file within
        *         the library, and returns <code>member-name</code>.
        */
       public static character member(String filename)
       {
          return member(character.of(resolveLegacyPath(filename, work.obtain())));
       }

       /**
        * Parses a reference to a member of an 4GL r-code library and returns the simple member name.
        *
        * @param  filename
        *         Pathname of the file in the r-code library.
        *
        * @return The function parses a character string in the form
        *         <code>path-name&lt;&lt;member-name&gt;&gt;</code>, where <code>path-name</code> is
        *         the pathname of a library and <code>member-name</code> is the name of a file within
        *         the library, and returns <code>member-name</code>.
        */
       public static character member(character filename)
       {
          return procLibraryParser(filename, 2);
       }

       /**
        * Get the type of its associated handle.
        *
        * @return   Always return the PSEUDO-WIDGET value.
        */
       public static character resourceType()
       {
          return character.of(LegacyResource.PSEUDO_WIDGET);
       }

       /**
        * Get the current working directory.
        *
        * @return  Current working directory.
        */
       public static character getWorkingDirectory()
       {
          WorkArea wa = work.obtain();

          String dir = wa.serverSideFs ? ConfigItem.WORKING_DIR.read(".") : wa.fs.getWorkingDirectory();

          return character.of(dir);
       }

       /**
        * Get the {@link com.goldencode.p2j.util.osresource.FileSystem#getFileSeparator() file-separator}.
        *
        * @return   The file separator.
        */
       public static String getFileSeparator()
       {
          return work.obtain().fs.getFileSeparator();
       }

       /**
        * Helper to handle pattern matching for breaking up the parameters passed to {@link #library} and
        * {@link #member}. Since the same pattern is used for both, this helper can optimize computation.
        *
        * @param    name
        *           The path that is passed in to break up.
        * @param    group
        *           Group 1 is the library, group 2 is the member.
        *
        * @return   Unknown character or the matched pattern.
        */
       private static character procLibraryParser(character name, int group)
       {
          if (name != null && !name.isUnknown())
          {
             Matcher matcher = patternLibraryMember.matcher(name.toStringMessage());
             if (matcher.matches())
             {
                return character.of(matcher.group(group));
             }
          }
          return character.UNKNOWN;
       }

       /**
        * Set the current working directory. Does nothing if argument represents unknown value.
        *
        * @param   workDir
        *          New working directory.
        */
       public static void setWorkingDirectory(character workDir)
       {
          if (workDir.isUnknown())
          {
             return;
          }

          work.obtain().fs.setWorkingDirectory(workDir.toStringMessage());
       }

       /**
        * 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.
        *
        * @param   searchPath
        *          The new PROPATH value.
        * @param   wa
        *          The contextual work area.
        */
       private static void setSearchPath(String searchPath, WorkArea wa)
       {
          // split the list
          String[] list = EnvironmentOps.getSearchPathList();

          if (list != null)
          {
             // use the resulting propath to set run time value on the client
             wa.fs.setSearchPath(list, EnvironmentOps.getLegacyCaseSensitive());
          }
       }

       /**
        * Transforms the given legacy path into the valid client system path.
        *
        * @param   legacyPath
        *          The legacy path name to the target resource.
        * @param   wa
        *          The contextual work area.
        *
        * @return   The resolved host path on the client system
        */
       private static String resolveLegacyPath(String legacyPath, WorkArea wa)
       {
          if (legacyPath == null || legacyPath.isEmpty())
          {
             return legacyPath;
          }

          if (wa.clientOS.equalsIgnoreCase("unix"))
          {
             return legacyPath.replaceAll(FileSystem.ANY_PATH_SEPARATOR, "/");
          }
          else
          {
             return legacyPath.replaceAll(FileSystem.ANY_PATH_SEPARATOR, "\\\\");
          }
       }

       /**
        * Appends yet another missed path entry to the log file.  The path entry is missed on even client side
        * search so it is a good candidate to include into bypass list.
        *
        * @param   pathName
        *          Path name entry missed in client side search.
        */
       private static void appendMissedPathEntry(String pathName)
       {
          // this will be in deploy/server directory
          File inpFile = new File("./client_search_ignore.log");

          // open log file for appending another missed path entry
          try (PrintWriter log = new PrintWriter(new BufferedWriter(new FileWriter(inpFile, true))))
          {
             log.println(pathName);
          }
          catch (Exception ex)
          {
             // in case of exception here assume there is no ignore list file
          }
          // something seriously wrong happening but we ignore this
       }

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

          /** Stores the Progress error code for the last operation. */
          private int lastError = 0;

          /** File information for the current file being examined. */
          private FileInfo info = null;

          /** Remote instance of our file system worker. */
          private FileSystem fs = null;

          /** Flag indicating the file-system is server-side. */
          private boolean serverSideFs = false;

          /** The legacy OS case-sensitive mode */
          private boolean legacyCaseSensitive;

          /** The environment variables map */
          private Map<String, String> environmentVariables;

          /** The client OS */
          private String clientOS;

          /** The client property key interceptors */
          private Map<String, Function<String, String>> keysInterceptors;

          /** The list of missed files to bypass client side search. */
          private Set<String> ignoreClientSearch;

          /** When TRUE we log missed files into separate log file. */
          private boolean captureMissedFiles;
       }

       /**
        * 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()
          {
             WorkArea wa = new WorkArea();

             // obtain file system resource
             wa.serverSideFs = OSResourceManager.getInstance().isServerSide(OsResourceType.FILESYSTEM);
             wa.fs = OSResourceManager.getFileSystem();

             // push down our search path to ensure that it is always there
             String[] paths    = EnvironmentOps.getSearchPathList();
             boolean  caseSens = EnvironmentOps.getLegacyCaseSensitive();

             wa.legacyCaseSensitive = caseSens;

             // listener is added last... we already push the PROPATH above, so no need to notify;
             // plus, it would lead to a double-initialization of this ContextContainer, as the
             // listener (setSearchPath) uses the WorkArea.fs
             EnvironmentOps.addSourcePathListener(searchPath -> setSearchPath(searchPath, wa));

             String fileSeparator = EnvironmentOps.getLegacyFileSeparator();

             Map<String, String> pathMap = EnvironmentOps.getLegacyAbsolutePathPrefixMap();

             String clientOS = EnvironmentOps.getRuntimeOperatingSystem().toJavaType();

             wa.clientOS = clientOS;

             String legacyOS = EnvironmentOps.getLegacyPlatform();

             boolean resolveFilePaths = EnvironmentOps.getResolveFilePaths();

             // if legacy OS platform is not the same as client OS platform, then legacy path names
             // must be resolved on the client OS.
             boolean areLegacyPathsNotResolved = !legacyOS.equalsIgnoreCase(clientOS) ||
                                                 resolveFilePaths;

             wa.fs.setLegacyFileSystemParameters(fileSeparator,
                                                 caseSens,
                                                 pathMap,
                                                 paths,
                                                 areLegacyPathsNotResolved);

             wa.environmentVariables = EnvironmentOps.getLegacyEnvironmentVariables(!caseSens);

             // add client property keys interceptors

             wa.keysInterceptors = new HashMap<>();

             Function<String, String> usernameInterceptor = new Function<String, String>()
             {
                /**
                 * Returns the OS dependent client user key if it applies to the legacy user key.
                 *
                 * @param    key
                 *           The given legacy user key
                 *
                 * @return   The OS dependent client user key
                 */
                @Override
                public String apply(String key)
                {
                   if (wa.clientOS.equalsIgnoreCase("unix"))
                   {
                      return "USER";
                   }

                   return "USERNAME";
                }
             };

             wa.keysInterceptors.put("USER", usernameInterceptor);
             wa.keysInterceptors.put("USERNAME", usernameInterceptor);

             // prepare per session ignore file list
             Directory dir = DirectoryManager.getInstance();
             wa.ignoreClientSearch = new HashSet<>(dir.getStrings(Directory.ID_RELATIVE,
                                                                  "no_client_path_search"));
             // missed files capturing flag
             wa.captureMissedFiles = dir.getBoolean(Directory.ID_RELATIVE, "capture_missing_search_path_entries",
                                                    false);

             return wa;
          }
       }
    }