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