SourceNameMapper.java

/*
** Module   : SourceNameMapper.java
** Abstract : Progress procedure file name to Java class name run-time mapping (bidirectional)
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 SIY 20050817   @22156 Created initial version.
** 002 GES 20060124   @24072 Rewrite to move to a directory based approach for storage of the 
**                           mapping data. In addition, the mapping data used at runtime is 
**                           stored in context-local instances  to ensure that multiple
**                           configurations can be run simultaneously. Added a reverse mapping
**                           from Java to Progress names.
** 003 GES 20060201   @24185 Added external init option so that the class can be initialized from
**                           a source other than the directory.
** 004 NVS 20070126   @32009 "class-mapping" node of the directory is searched as an account 
**                           level standard search item.
** 005 ECF 20070220   @32186 Made map access thread-safe. Synchronized initialization of mapping
**                           data with read and write requests. This fixes a race condition
**                           which was resulting in an NPE.
** 006 GES 20080722   @39167 Added a fallback for convert() to allow a parameter which is already
**                           an existing fully qualified class name, to be returned. Major
**                           rewrite to load the name mapping data from a JAR file resource.  The
**                           new resource has a much simpler format.
** 007 SVL 20090310   @41448 Keys into methodMap for the Progress proc/function name to Java
**                           name mapping are always lower-cased.
** 008 GES 20090515   @42235 Import change.
** 009 ECF 20101004          Handle unqualified file name references in convertName method.
** 010 CA  20121220          Reworked the mapping to include the new emitted info (by 
**                           collect_names.rules), needed by the
**                           persistent and super procedures implementation.
** 017 CA  20130209          Added dynamic EXTENT and EXTENT parameter support.
** 018 CS  20130304          Added support for checking if a class exists in a
**                           certain package as result of conversion from file
** 019 EVL 20130412          Fixing the regular expressions for windows runtime in
**                           initMappingData() method to properly handle windows file separator.
** 020 EVL 20130529          Replacing getLegacySearchPath() with getSearchPath() to obtain
**                           full effective PROPATH variable value to reflect the changes in
**                           PROPATH approach implementation.
** 021 CA  20130813          Added constants for the parameter modes.
** 022 CA  20130925          Added support for PUBLISHED-EVENTS attribute.
** 023 CA  20131116          Added support for the FUNCTION ... MAP TO clause.
** 024 GES 20131218          Rework to move InternalEntry, Parameter and other logic/data out
**                           into separate classes. This makes the overall code easier to
**                           understand and more extendable.  Refactored some methods to use
**                           common code. Completed native API support.
** 025 CA  20140218          Changes related to PROGRAM-NAME implementation: added 
**                           convertJavaMethod to resolve a Java method name to a legacy name.
**                           Removed convertStackEntry, as implemenetation was moved to 
**                           ProcedureManager.
** 026 EVL 20140310          The canonicalize() and convertNames() methods have been modified to
**                           take into account possible usage of the \ file separator in Windows
**                           for program name.  Currently in run-time case the file separator has
**                           constant value of "/".  All Windows separators will be replaced with
**                           this one.  For Windows the convertNames() method handles the absolute
**                           file names with drive letter included and relative file names.  For
**                           this support the properly configured searchpath value in server's
**                           directory.xml file is required.  But the existence of the 4GL source
**                           code is not expected on the target system.  The canonicalize() method
**                           was removed from the code part where is does nothing useful(because
**                           the input parameters name already "canonicalized").
** 027 ECF 20140913          Reduced memory footprint.
** 028 ECF 20150424          Minor memory tweak.
** 029 ECF 20150601          Minor changes to accommodate InternalEntry changes. Removed some
**                           unnecessary intern()'ing of strings.
** 030 OM  20150710          Added support for obtaining the type of a parameter.
** 031 ECF 20150805          Refactored synchronization to improve throughput. Replaced
**                           StringBuffer with StringBuilder.
** 032 CA  20160418          Added API to resolve an internal-entry (or external program) list of
**                           parameters.
** 033 OM  20160421          Identified 4GL source searched by their legacy absolute path. Added
**                           generics. Javadoc fixes. foreach iterations.
** 034 OM  20160527          SourceNameMapper.convertName() was hidden and exposed two methods that
**                           return converted class-name and legacy file name.
** 035 OM  20160711          Added MEMPTR & RAW support in getTypeForParam() method.
** 036 CA  20161020          Fixed case-insensitive resolution with .r (rcode) ext program names.
** 037 GES 20171204          Improved comments and formatting.
** 038 CA  20180216          Reworked convertName - 4gl-src-root will provide a list of absolute
**                           root paths which will be matched against the program name and against
**                           the propath entries.
**                           Also, it fixes resolution so that the search is done first via the:
**                           - absolute name
**                           - PROPATH
**                           - direct match.
** 039 CA  20180504          Added support for folder aliasing: these are specified via the
**                           "path-aliases" per-server directory node, and provide a mapping of
**                           alias folder name to their 'source' counterpart.  Also, added support
**                           to identify the target source program for a RUN program.r statement.
** 040 SBI 20180518          Renamed file-system/pkgroot and file-system/4gl-src-root into 
**                           legacy-system/pkgroot and legacy-system/4gl-src-root.
** 041 CA  20180727          Added getPackageRoot.
** 042 CA  20180801          If the legacy program name starts with '.', then use this name in 
**                           convertName - 4GL doesn't change the legacy name, as the lookup is 
**                           always done in current folder.
** 043 CA  20181029          Moved code from getTypeForParam to BDT.fromTypeName.
** 044 CA  20181216          Fixed loading OO methods, constructors and destructors.
** 045 CA  20190122          Runtime support for legacy 4GL classes.
**     CA  20190219          Enhanced so that legacy 4GL classes are managed via LegacySignature
**                           annotations - name_map.xml is just a registry for the classes.
**                           Builtin legacy 4GL classes are registered on-the-fly, when they are
**                           first accessed.
**     ECF 20190323          Reimplemented InternalEntry list as a map for faster searches.
** 045 CA  20190520          buildLegacyClass will register only OO method, ctor or dtor.
**     CA  20190526          Fixed runtime and conversion support for legacy class properties used 
**                           as OUTPUT or INPUT-OUTPUT arguments.
** 046 CA  20190611          Added support for legacy services (REST).
** 047 CA  20190703          Added support for WebHandler services.
** 048 OM  20190508          Avoid NPE, added DATASET support to name_map file. Logged missing
**                           xml data file event.
** 049 CA  20190711          Added API to explicitly load the legacy services.
**     CA  20190717          The OO accessor or method must be looked in super-classes, too.
** 050 CA  20200412          Added incremental conversion support.
** 051 CA  20200514          Emit the LegacySignature at the external program's 'execute' method and all its
**                           internal entries.
**                           Added support for SOAP web services.
**     CA  20200605          Added a lazy approach of resolving the Java method(s) for the entries in an 
**                           external program - the first access of InternalEntry.getMethod() will load the
**                           class and resolve the Java methods for all internal entries.
**     RFB 20200706          Modified convertName to handle the situation with a procedure name not passed in
**                           with an extension, which works fine in 4GL if it is backed by a .r, .p, or .w.
**     RFB 20200730          Moved pathSep and caseSens to the WorkArea so that they can be kept separate 
**                           per session. Also had to duplicate the mappings to include both case-sensitive
**                           and case-insensitive lists. Only one or the other was loaded, depending upon
**                           whether client or server token is loaded first, when one is on Linux, the other
**                           on Windows. This is critical when multiple project-tokens are used.
**     ECF 20200916          Avoid context-local lookups when possible.
**     CA  20200927          Cache failed Class.forName strings, to avoid the lookup overhead.
**     CA  20200930          Cache the InternalKey hashcode.
**     OM  20210203          Avoided NPE and proper logging in initMappingData().
**     CA  20210221          Default to Progress.Lang.Object if an 'object' parameter or method is missing its 
**                           qualified annotation.
**     CA  20210609          Added the extent for properties/variables (where it applies).
**     CA  20210628          Fixed convertName and others - the Windows OS must be checked for the legacy
**                           platform under which the application ran, and not the running FWD client's OS.
**     HC  20211102          Improved error handling during loading of name mapping data.
**     CA  20220208          Added findLegacyEntryName, a helper to resolve the legacy name for a Java method
**                           defined in class, when only the method's name is known.
**     OM  20220316          Added awareness for 'altFileSep' separator found in ABL paths. 
**     CA  20220329          Added support for REST services written directly in Java.
**     CA  20220420          REST services written directly in Java can be configured via the 'rest-service'
**                           element.
**     TJD 20220822          Prevent using uninitialized mapping data
**     TJD 20220823          Prevent recursive calls to initMappingData() workaround
**     TJD 20220824          Prevent recursive calls to initMappingData() workaround in more effective way
**     SBI 20221215          Filter the same REST java methods having erased parameters types.
**     CA  20220520          Initialize SourceNameMapper at server startup.
**                           The constructor (static or instance) and destructor lookup now relies on the
**                           LegacySignature annotation to resolve them, instead of the Java method name - 
**                           this is required for ctor overload support at conversion and runtime.
**     CA  20220606          Added lazy loading of legacy converted classes.  As SourceNameMapper is 
**                           initialized at server startup, the 'initMappingData' calls were removed from the
**                           APIs.
**     CA  20220609          Fixed a regression in destructor processing.
** 052 HC  20230118          Eliminated some of the uses of String.toUpperCase and/or
**                           String.toLowerCase for performance.
**     HC  20230120          The case insensitive maps reimplemented with CaseInsensitiveHashMap.
**     HC  20230130          Improved performance of the methods removeUpPath and canonicalize.
**                           Replaced some occurrences of split and replaceAll with pre-compiled
**                           regular expression patterns.
**                           Introduced caching for some session context lookups.
**                           Improved class lookup in the method convertName.
**                           Replaced a use of HashMap with CaseInsensitiveHashMap.
** 053 CA  20230221          Javadoc fixes.
** 054 CA  20230322          Cache the suffix pattern used by 'getOverloads'.
** 055 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 056 CA  20230724          Resolve and cache the 'execute methods' at LegacyClass.executeMethods.
** 057 ME  20230829          Added support for bind parameter flag.
**     ME  20230919          Add new public buildInternalEntry method (used by reflection).
** 058 CA  20231009          Resolving a legacy method needs to exclude super-class overridden methods.
** 059 CA  20231031          The constructor and destructor 'internal entry name' is the exact legacy name 
**                           for this entry, as it is reported by PROGRAM-NAME and error callstack.
** 060 CA  20231030          Added overloads/overrides support for interfaces.
** 061 DDF 20230926          Lookup the unqualified program name firstly when converting a progress
**                           file name to a java class name.
**     DDF 20231002          Short-circuit files that have extensions and do not match any extensions
**                           of the existent legacy program files. Added r-code file searching.
**     DDF 20231020          caseSens value of the WorkArea should be taken into consideration when 
**                           searching r-code files, removed unnecessary local.get() call in 
**                           convertName() and made code adjustments.
**     DDF 20231102          Implemented a cache for search-specs found during conversion. 
**     DDF 20231107          Improved the cache implementation and used of AssociatedThread
**                           to create new cache entries for propaths.
**     DDF 20231110          Improved cache, store additional entries besides path-mappings.
**     DDF 20231113          Removed SOURCE_CACHE_SIZE and made the sourceCache configurable
**                           through CacheManager. Replaced ExecutorService usage with a normal
**                           thread when initializing the path-mappings map.
**     DDF 20231113          Even if there are no path-mappings, we should still allow the
**                           sourceCache to be initialized and populated with search-specs
**                           during runtime.
**     DDF 20231116          Fixed NPE when there are no path-mapping nodes in name_map.xml.
**     DDF 20231117          Cached searchPath and propath hash code.
**     DDF 20231117          Synchronized searchPathCache.
**     DDF 20231121          Changed lock/synchronization for sourceCache, initializationPaths
**                           and convertedNames.
**     DDF 20231128          Replaced the Executor with a ThreadPoolExecutor. 
**     DDF 20231205          Modified mappingExecutor, added SourceThreadFactory.
**     DDF 20231208          Created MonitorRunnable to avoid a source cache entry from 
**                           creating a task twice. Made searchPathCache configurable.
**     DDF 20231212          Remove identifier after source cache entry is created.
** 062 CA  20231213          The synthetic 'execute' methods emitted for legacy classes can be dropped if there
**                           is nothing emitted in them.  These Java methods are also marked with Type.EXECUTE
**                           LegacySignature annotation, and their BlockManager API removed - the FWD runtime
**                           will run this only once.
** 063 CA  20231223          The resolved name cache must be written/created only after the initialization 
**                           finished (pathMappingInitialized is true).
**                           'createSourceCacheEntry' must not not process (or overwrite) the same propath hash. 
** 064 SB  20231218          Added getter functions for the setters and getters found in ClassMap.
** 065 AD  20240125          The search for a Java class name from a 4GL procedure name can now use a given
**                           list of directories instead of searching in the PROPATH by default.
** 066 GBB 20240304          LegacyService annotation not found logged at lower level.
** 067 GBB 20240404          Support for multiple name_map.xml files.
** 068 OM  20240430          Allow name_map.xml files to be loaded also from file system (developer mode).
** 069 GBB 20240516          Fix for comparing resolved programs in multiple name_map.xml
**                           (ooname read as empty, but saved as null).
** 070 DDF 20240514          Fix NPE when the legacy OO type received as a parameter is an interface, which
**                           does not have a superclass (findLegacyPropertyExtent and findLegacyAccessor).
** 071 ICP 20240705          Fixed method of obtaining the class loader.
** 072 CA  20240809          Cache the legacy method name as an interned strings and move the map to an 
**                           identity map, to avoid lowercase keys.
** 073 VVT 20240826          convertJavaProg() renamed. See #8613-16.
** 074 AP  20250127          Added method to check whether a class is legacy or converted.
** 075 PBB 20250509          Modified implementation of DYNAMIC-PROPERTY function to introduce support for
**                           DYNAMIC-INVOKE builtin function that returns or might return an extent.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.*;
import java.util.logging.*;
import java.util.regex.*;
import java.net.*;

import com.goldencode.p2j.classloader.*;
import com.goldencode.p2j.util.logging.*;
import org.w3c.dom.*;
import com.goldencode.util.*;

import com.goldencode.p2j.util.InternalEntry.Type;
import com.goldencode.cache.CapacityPolicy;
import com.goldencode.cache.LRUCache;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.library.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.persist.CacheManager;
import com.goldencode.p2j.rest.*;
import com.goldencode.p2j.soap.*;
import com.goldencode.p2j.security.*;
import org.xml.sax.*;

import javax.xml.parsers.*;

/**
 * This class performs a bidirectional run-time mapping of a Progress
 * procedure file name to and from a Java class name.  It also supports
 * run-time translation of Progress internal procedure names into Java
 * method names.
 * <p>
 * <b>The mapping data is loaded from the directory into data structures that
 * are <code>static</code>.  This means that at this time this mapping data
 * is global to the entire server rather than being context-local.  The
 * reason for this is to reduce the amount of memory used for this data
 * since 2+ map entries exist for each original source file (thus these maps
 * each contain thousands of entries for any reasonably sized
 * application).</b>
 * <p>
 * <b>The current implementation assumes a single root path for all Progress
 * procedure file names and single base package path for the generated Java
 * classes.  Despite these assumptions it is possible to translate
 * Progress procedure names incorrectly (to a Java class name which does not
 * belong to the subtree specified by the single root or which does not 
 * belong to the base package path).  These problems are caused by using
 * mismatching absolute paths for Progress names or names which start with
 * a dot ('&#46;') for Java class names.</b> The leading dot is stripped
 * from the returned class name and is not returned as a result of a
 * mapping.
 * <p>
 * The source file name can be in one of the many forms: relative path,
 * absolute path, can contain references to current and to upper level
 * directories. Also, the source file name can use the file naming format of
 * the original operating system used to run the Progress program rather than
 * the current operating system used to run the converted Java application.
 * To this the directory contains values that define the original source
 * system's parameters such as path delimiter, file name delimiter and case 
 * sensitivity.
 */
public class SourceNameMapper
{
   /** Constant identifying no-extent parameters. */
   public static final int NO_EXTENT = -2;
   
   /** Constant identifying dynamic-extent parameters. */
   public static final int DYNAMIC_EXTENT = -1;
   
   /** The name of the file containing mappings. */
   private static final String NAME_MAP_FILE = "name_map.xml";
   
   /** The overload suffix pattern used by {@link #getOverloads}. */
   private static final Pattern OVERLOAD_SUFFIX = Pattern.compile("_[0-9]+");
   
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(SourceNameMapper.class);

   /** Context-local data. */
   private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
   {
      public WorkArea initialValue()
      {
         WorkArea wa = new WorkArea();
         
         EnvironmentOps.addSourcePathListener(SourceNameMapper::setSearchPath);
         wa.isWindows = EnvironmentOps.isLegacyPlatformWindows();

         return wa;
      }
   };

   /**
    * Set of {@link Class#forName} lookups by {@link #convertName} which resolve (or not) to a Java class for
    * the given program name.
    */
   private static final Map<String, Boolean> CLASS_LOOKUPS = new ConcurrentHashMap<>();
   
   /** Rest service mappings element. */
   private static final String ELEM_REST_SERVICE = "rest-service";
   
   /** Class mappings element. */
   private static final String ELEM_CMAP = "class-mapping";
   
   /** Methods mappings element. */
   private static final String ELEM_MMAP = "method-mapping";
   
   /** Path mappings element. */
   private static final String ELEM_PMAP = "path-mapping";
   
   /** Parameter info element. */
   private static final String ELEM_PARAM = "parameter";
   
   /** Progress name attribute. */
   private static final String ATTR_PNAME = "pname";
   
   /** Java name attribute. */
   private static final String ATTR_JNAME = "jname";
   
   /** Filename name attribute. [<path-to-file>/file] */
   private static final String ATTR_PATHNAME = "pathname";
   
   /** TYPE attribute. */
   private static final String ATTR_TYPE = "type";
   
   /** LIBNAME attribute. */
   private static final String ATTR_LIBNAME = "libname";
   
   /** MAP-TO attribute. */
   private static final String ATTR_MAP_TO = "map-to";
   
   /** Parameter MODE attribute. */
   private static final String ATTR_PARAM_MODE = "mode";
   
   /** Named events attribute. */
   private static final String PUBLISHED_EVENTS = "published-events";
   
   /** 'with-services' attribute. */
   private static final String WITH_SERVICES = "with-services";
   
   /** Qualified OO name attribute. */
   private static final String OONAME = "ooname";
   
   /** Counter for the number of active threads populating {@link #initializationPaths}. */
   private static final AtomicInteger PATH_INIT_THREADS_COUNTER = new AtomicInteger(-1);
   
   /** 
    * Counter value that is used when adding new values to {@link #initializationPaths}.
    * The value is modified when reading the initial path-mapping nodes and whenever
    * a new entry is added to an existent cache. 
    */
   private static final AtomicInteger MAPPING_COUNTER = new AtomicInteger(0);
   
   /** Map of aliases (r-code folders) to their source folders (conversion folders). */
   private static Map<String, String> pathAliases = null;
   
   /** Map of aliases (r-code folders) to their source folders (conversion folders). - case-insensitive */
   private static Map<String, String> pathAliases_ci = null;
   
   /** Progress procedure file name to Java class name map table. */
   private static volatile Map<String, ExternalProgram> p2jMap = null;
   
   /** Progress procedure file name to Java class name map table (case-insensitive). */
   private static volatile Map<String, ExternalProgram> p2jMap_ci = null;
   
   /** Map of program names without extension to the programs which can be resolved with this name. */
   private static volatile Map<String, List<ExternalProgram>> p2jNonExtMap = null;
   
   /** Map of program names without extension to the programs which can be resolved with this name (ci). */
   private static volatile Map<String, List<ExternalProgram>> p2jNonExtMap_ci = null;
   
   /** Map of lookup instances for unqualified program names. */
   private static Map<String, PathLookup> pathLookups = null;
   
   /** Progress Java class name to procedure file name map table. */
   private static Map<String, ExternalProgram> j2pMap = null;

   /** Cache of preprocessed mapped paths to target procedures. */
   private static LRUCache<Long, SourceNameMapperCache> sourceCache = null;
   
   /** Search-spec paths found during conversion and read from name_map.xml. */
   private static volatile Map<String, Integer> initializationPaths = null;
   
   /** Set of file extensions that were found for all procedure file name nodes */
   private static Set<String> allowedLegacyExtensions = null;
   
   /** 
    * Cache of searchPaths to a String array containing the resulted propath and originalPrograpth.
    * The order is: propath and originalPropath. 
    */
   private static LRUCache<String, String[][]> searchPathCache = null;
   
   /** The list of programs exposed as services (the 'with-services' attribute or 'rest-service' nodes). */
   private static List<ExternalProgram> servicePrograms = null;
   
   /** Association between converted legacy 4GL programs to their defined internal entries. */
   private static volatile Map<Class<?>, LegacyClass> classMap = null;
   
   /** 
    * The 4GL legacy roots for source files. Used for detecting full-paths file names.
    * Each entry identifies a project which is run on the current JVM. 
    */
   private static String[] legacyRoots = null;
   
   /** Base path for all Java packages. */
   private static String pkgroot = null;
   
   /** Custom package to init data for. */
   private static String custpkgroot = null;
   
   /** Base path for Progress 4GL code. */
   private static String proroot = null;
   
   /** Mapping data initialization completion indicator */ 
   private volatile static boolean mappingDataInitialized = false;
   
   /** Mapping data initialization in progress indicator */ 
   private volatile static boolean mappingDataInitInProgess = false;
   
   /** 
    * Name element separator used for the Progress file name data.  At runtime this is OS
    * independent value used internally in name_map.xml file to resolve procedure names.
    */
   private static String fileSep = "/";

   /** Character form of fileSep */
   private static char fileSepChar = '/';
   
   /** Regular expression pattern for fileSep field */
   private static final Pattern fileSepRegexp = Pattern.compile("/");

   /** Double backslash precompiled regular expression pattern */
   private static final Pattern dblBackslashRegexp = Pattern.compile("\\\\");

   /**
    * Alternative name element separator used for the Progress file name data. If the path is received from
    * Windows ABL code, it may contain a mix of both {@code fileSep} and this. 
    */
   private static final String altFileSep = "\\";
   
   /** 
    * Executor for operations that build source cache entries.
    * Threads should not be kept idle since it is possible for it to keep the same
    * SecurityContext when it should be different.
    */
   private static final ExecutorService mappingExecutor = 
            new ThreadPoolExecutor(0,
                                   1,
                                   0,
                                   TimeUnit.SECONDS,
                                   new LinkedBlockingQueue<>(),
                                   new SourceThreadFactory());
   
   /**
    * Initialize all the maps - this can be time-consuming, as the legacy OO classes are initialized from the
    * Java annotations.
    */
   public static void initialize()
   {
      initMappingData();
   }
   
   /**
    * Register all exposed legacy services.
    */
   public static void registerServices()
   {
      if (servicePrograms == null)
      {
         return;
      }

      for (ExternalProgram ep : servicePrograms)
      {
         registerServices(ep, addPkg(ep.jname));
      }
   }
   
   /**
    * Given a program name, replace all folder names which have an associated 'source' folder,
    * and return the new path.
    * 
    * @param    pname
    *           The legacy program name.
    *           
    * @return   The resolved program names or empty array if no change was possible.
    */
   public static String[] resolvePathAliases(Map<String, String> pathAliases, String pname)
   {
      String[] entries = pname.split(fileSep);
      
      // for now, we replace all found aliases with their source folders;  if needed, we can 
      // compute all power sets made from pathAliases (will be 2^(pathAliases.size()) subsets)
      
      StringBuilder sb = new StringBuilder();
      boolean changed = false;
      for (int i = 0; i < entries.length - 1; i++)
      {
         String entry = entries[i];
         
         // if this entry is an alias, get its source
         String source = pathAliases.get(entry);
         
         changed = changed || source != null;
         
         sb.append(source == null ? entry : source).append(fileSep);
      }
      sb.append(entries[entries.length - 1]);
      
      return !changed ? new String[0] : new String[] { sb.toString() };
   }

   /**
    * Get the legacy 4gL name for the Java class name. 
    * 
    * @param   javaClassName
    *          The external-program's converted java name.
    * 
    * @return  The legacy program name or <code>null</code> if the given java
    *          program is not registered in name_map.xml.
    */
   public static String getLegacySourceName(String javaClassName)
   {
      ExternalProgram extp = j2pMap.get(removePkg(javaClassName));
      return extp == null ? null : extp.pname;
   }

   /**
    * Get the legacy 4gL class name for the Java class name. 
    * 
    * @param   javaClassName
    *          The converted FWD class mapping a legacy 4GL class name.
    * 
    * @return  The legacy program name or <code>null</code> if the given java
    *          program is not registered in name_map.xml.
    */
   public static String getLegacyClassName(final String javaClassName)
   {
      ExternalProgram extp = j2pMap.get(removePkg(javaClassName));
      return extp == null ? null : extp.ooname;
   }
   
   /**
    * Get the legacy 4gL name for the Java class name or, if not a legacy program, return the 
    * java class name, without the base package. 
    * 
    * @param   jname
    *          A java class name which may or may not be associated with a legacy 4GL program.
    * 
    * @return  The legacy program name or the java class name, without the base package.
    */
   public static String convertJavaProgSafe(String jname)
   {
      String pname = getLegacySourceName(jname);
      if (pname == null)
      {
         pname = removePkg(jname);
      }
      
      return pname;
   }
   
   /**
    * Get the legacy 4GL name for the Java class name. 
    * 
    * @param   javaClassName
    *          The external-program's converted java name.
    * @param   customRoot
    *          Custom root package name.
    * 
    * @return  The legacy program name or <code>null</code> if the given java
    *          program is not registered in name_map.xml.
    */
   public static String getLegacySourceName(String customRoot, String javaClassName)
   {
      custpkgroot = customRoot;
      initialize();
      
      return getLegacySourceName(javaClassName);
   }
   
   /**
    * Convert a java method name to a legacy name.
    * 
    * @param    clsName
    *           The class name where the method is defined.
    * @param    methodName
    *           The method's name.
    * @param    function
    *           Flag indicating that this method maps a legacy function (when {@code true)} or 
    *           procedure (when {@code false}).
    *           
    * @return   The legacy method name or {@code null} if not found.
    */
   public static String convertJavaMethod(String clsName, String methodName, boolean function)
   {
      ExternalProgram extp = j2pMap.get(removePkg(clsName));
      
      if (extp != null)
      {
         Map<String, InternalEntry> ieMap = (function ? extp.j2pf : extp.j2pp);
         InternalEntry ie = ieMap.get(methodName);
         return (ie == null ? null : ie.pname);
      }
      
      // if not found
      return null;
   }
   
   /**
    * Normalize the given legacy external program name, considering the {@link WorkArea#caseSens} state.
    * 
    * @param    legacy
    *           The legacy name to be normalized
    *           
    * @return   The normalized legacy name.
    */
   public static String normalizeLegacyName(String legacy)
   {
      if (legacy != null && !getLegacyCaseSensitive())
      {
         legacy = legacy.toLowerCase();
      }
      
      return legacy;
   }
   
   /**
    * Uses the {@link #convertName(String, WorkArea, String[])} worker for finding the name of the Java class name of
    * a converted P4GL procedure name. 
    * 
    * @param   legacy
    *          Progress file name.
    *          
    * @param   searchPaths
    *          Paths where to search for the file instead of searching in the entire PROPATH.
    *
    * @return  the Java class name for the given Progress file name if it is identified as a
    *          converted procedure from legacy sources. If no appropriate mapping is present,
    *          {@code null} is returned.
    */
   public static String convertNameToClass(String legacy, String[] searchPaths)
   {
      WorkArea wa = local.get();
      if (wa.isWindows)
      {
         legacy = dblBackslashRegexp.matcher(legacy).replaceAll(fileSep);
      }
      if (wa.caseSens == null)
      {
         wa.caseSens = getLegacyCaseSensitive();
      }
      
      String[] paths = convertName(legacy, wa, searchPaths);
      return (paths != null) ? paths[0] : null;
   }

   /**
    * Uses the {@link #fastConvertName(String)} worker for finding the name of the Java class name of
    * a converted P4GL procedure name. 
    * 
    * @param   legacy
    *          Progress file name.
    * @return  the Java class name for the given Progress file name if it is identified as a
    *          converted procedure from legacy sources. If no appropriate mapping is present,
    *          {@code null} is returned.
    */
   public static String convertNameToClass(String legacy)
   {
      return convertNameToClass(legacy, true);
   }
   
   /**
    * Uses the {@link #fastConvertName(String)} or {@link #convertName(String, WorkArea, String[])} 
    * worker for finding the name of the Java class name of a converted P4GL procedure name. 
    * 
    * @param   legacy
    *          Progress file name.
    * @param   fastConvert
    *          True if we should use the {@link #fastConvertName(String)} worker.
    *          False if we should use the {@link #convertName(String, WorkArea, String[])}  worker.
    * @return  the Java class name for the given Progress file name if it is identified as a
    *          converted procedure from legacy sources. If no appropriate mapping is present,
    *          {@code null} is returned.
    */
   public static String convertNameToClass(String legacy, boolean fastConvert)
   {
      if (fastConvert)
      {
         String[] paths = fastConvertName(legacy);
         return (paths != null) ? paths[0] : null;
      }
      else 
      {
         return convertNameToClass(legacy, null);
      }
   }
   
   /**
    * Convert a legacy 4GL class name to a converted Java class file.
    * 
    * @param    legacyClsName
    *           The qualified legacy class name.
    *           
    * @return  the Java class name for the given legacy class name if it is identified as a
    *          converted procedure from legacy sources. If no appropriate mapping is present,
    *          {@code null} is returned.
    */
   public static String convertOOToClass(String legacyClsName)
   {
      String lname = legacyClsName;
      lname = lname.replace('.', '/');
      lname = lname + ".cls";
      
      String[] paths = fastConvertName(lname);
      
      if (paths == null || paths[2] == null)
      {
         return null;
      }
      
      boolean caseSens = getLegacyCaseSensitive();
      if ((caseSens && legacyClsName.equals(paths[2])) || 
          (!caseSens && legacyClsName.equalsIgnoreCase(paths[2])))
      {
         // if the found program file defines the expected class name, then return its java name
         return paths[0];
      }
      
      return null;
   }
   
   /**
    * Uses the {@link #fastConvertName(String)} worker for detecting the if the {@code legacy} is a
    * name of a converted legacy procedure. In affirmative case, the normalized name of the 
    * procedure filename is returned.
    *
    * @param   legacy
    *          Progress file name.
    *
    * @return  The legacy procedure name. If no appropriate mapping is present {@code null} is
    *          returned.
    */
   public static String lookupLegacyName(String legacy)
   {
      String[] paths = fastConvertName(legacy);
      return (paths != null) ? paths[1] : null;
   }

   /**
    * Get the {@link #pkgroot package root}.
    * 
    * @return   The package root.
    */
   public static String getPackageRoot()
   {
      return pkgroot;
   }
   
   /**
    * Get the java name of the passed internal-entry and 4GL program.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    ieName
    *           Legacy internal-entry name.
    * @param    function
    *           <code>true</code> if the search is for a user-defined 
    *           function.
    *           
    * @return   The Java name of the given internal-entry, if this entry 
    *           exists in the given program. Else, <code>null</code>
    */
   public static String getMethodName(String  pname, String  ieName, boolean function)
   {
      ExternalProgram extProg = getExternalProgram(pname);
      if (extProg == null)
      {
         return null;
      }
      
      InternalEntry method = extProg.searchp(ieName, function);
      if (method == null)
      {
         return null;
      }
      
      return method.jname;
   }

   /**
    * Get the map-to attribute of the passed internal-entry. The internal entry must be a 
    * function.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    ieName
    *           Legacy internal-entry name.
    *           
    * @return   The map-to attribute of the passed internal-entry, if is a function. 
    *           <code>null</code> if the {@link #ATTR_MAP_TO} does not exist or the internal
    *           entry does not resolve to a function.
    */
   public static String getMapTo(String pname, String ieName)
   {
      ExternalProgram extProg = getExternalProgram(pname);
      if (extProg == null)
      {
         return null;
      }
      
      InternalEntry ie = extProg.search(ieName, true);
      if (ie == null)
      {
         return null;
      }
      
      return ie.getAttribute(ATTR_MAP_TO);
   }

   /**
    * Get the library name attribute of the given internal-entry.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    iename
    *           Legacy internal-entry name.
    *           
    * @return   The library name attribute of the given internal-entry name,
    *           assuming the program exists and the internal-entry is a native
    *           procedure definition. Else, <code>null</code>.
    */
   public static String getLibraryName(String pname, String iename)
   {
      InternalEntry ie = getInternalEntry(pname, iename, false);
      
      if (ie == null)
      {
         return null;
      }
      
      String type = ie.getAttribute(ATTR_TYPE);
      
      if (InternalEntry.Type.DLL_ENTRY.toString().equalsIgnoreCase(type))
      {
         return ie.getAttribute(ATTR_LIBNAME);
      }
      
      return null;
   }

   /**
    * Get the string representation of the parameter modes for this external
    * procedure.  If there are no parameters, return null.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    *           
    * @return   See above.
    */
   public static String getParameterModes(String pname)
   {
      return getParameterModes(pname, null, false);
   }

   /**
    * Get the string representation of the parameter modes for this internal
    * entry. If there are no parameters, return null.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    ieName
    *           Legacy internal-entry name. If null or the empty string, the
    *           external procedure's parameters are used.
    * @param    function
    *           <code>true</code> if this is a function call.
    *           
    * @return   See above.
    */
   public static String getParameterModes(String pname, String ieName, boolean function)
   {
      return getParameterModes(pname, null, ieName, function);
   }
   
   /**
    * Get the string representation of the parameter modes for this internal
    * entry. If there are no parameters, return null.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    ie
    *           If non-null, the already resolved internal entry.
    * @param    ieName
    *           Legacy internal-entry name. If null or the empty string, the
    *           external procedure's parameters are used.
    * @param    function
    *           <code>true</code> if this is a function call.
    *           
    * @return   See above.
    */
   public static String getParameterModes(String        pname, 
                                          InternalEntry ie,
                                          String        ieName, 
                                          boolean       function)
   {
      if (ie != null)
      {
         return ie.getParameterModes();
      }
      
      ExternalProgram extProg = getExternalProgram(pname);
      if (extProg == null)
      {
         return null;
      }

      if (ieName == null || "".equals(ieName))
      {
         return extProg.main.getParameterModes();
      }
      
      ie = extProg.search(ieName, function);
      
      return ie == null ? null : ie.getParameterModes();
   }

   /**
    * Get the parameters for this internal entry. If there are no parameters, return an empty list.
    * If the internal entry can't be found, return null.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    ie
    *           If non-null, the already resolved internal entry.
    * @param    ieName
    *           Legacy internal-entry name. If null or the empty string, the
    *           external procedure's parameters are used.
    * @param    function
    *           <code>true</code> if this is a function call.
    *           
    * @return   See above.
    */
   public static List<Parameter> getParameters(String        pname, 
                                               InternalEntry ie, 
                                               String        ieName, 
                                               boolean       function)
   {
      if (ie != null)
      {
         return ie.getParameterList();
      }
      
      ExternalProgram extProg = getExternalProgram(pname);
      if (extProg == null)
      {
         return null;
      }

      if (ieName == null || "".equals(ieName))
      {
         return extProg.main.getParameterList();
      }
      
      ie = extProg.search(ieName, function);
      
      return ie == null ? null : ie.getParameterList();
   }
   
   /**
    * Get the 4GL-style signature of the given internal-entry.  If the program
    * does not exist or the internal-entry does not belong to this program,
    * return <code>null</code>.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    ieName
    *           Legacy internal-entry name.
    *           
    * @return   See above.
    */
   public static String getSignature(String pname, String ieName)
   {
      ExternalProgram extProg = getExternalProgram(pname);
      if (extProg == null)
      {
         return "";
      }
      
      if (ieName == null || "".equals(ieName))
      {
         return extProg.main.getSignature();
      }
      
      InternalEntry ief = extProg.search(ieName, true);

      // if both func and proc are with the same name, then func has priority
      InternalEntry ie = ief != null ? ief : extProg.search(ieName, false);
      
      return ie == null ? "" : ie.getSignature();
   }

   /**
    * Get the internal-entries attribute of the given external procedure.
    * If the program does not exist, return <code>null</code>.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    prv
    *           Flag to include or not the private members.
    *           
    * @return   See above.
    */
   public static String getInternalEntries(String pname, boolean prv)
   {
      ExternalProgram extProg = getExternalProgram(pname);
      
      return (extProg == null) ? null : extProg.getInternalEntries(prv);
   }
   
   /**
    * Get the published named events by the specified procedure.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    *           
    * @return   The comma-separated list of published named events, when the event name is 
    *           hard-coded.
    */
   public static String getPublishedEvents(String pname)
   {
      ExternalProgram extProg = getExternalProgram(pname);
      
      return (extProg == null) ? null : extProg.publishedEvents;
   }

   /**
    * Get the extent of the parameter at 0-based position (for the given internal entry).
    * If the parameter is dynamic-extent, then return 0, not -1.
    *  
    * @param    pname
    *           Legacy 4GL program name.
    * @param    ie
    *           If non-null, the already resolved internal entry.
    * @param    iename
    *           Legacy internal-entry name.
    * @param    func
    *           Flag indicating if this is a function or a procedure.
    * @param    param
    *           0-based index of the parameter to be checked.
    *
    * @return   See above.
    */
   public static int getExtentForParam(String        pname, 
                                       InternalEntry ie,
                                       String        iename, 
                                       boolean       func, 
                                       int           param)
   {
      if (ie != null)
      {
         return ie.getParameter(param).getExtent();
      }

      Parameter p = getParameter(pname, iename, func, param);
      return (p == null) ? -1 : p.getExtent();
   }
   
   /**
    * Get the type/class of the parameter at 0-based position (for the given internal entry).
    *  
    * @param   pname
    *          Legacy 4GL program name.
    * @param   ie
    *          If non-null, the already resolved internal entry.
    * @param   iename
    *          Legacy internal-entry name.
    * @param   func
    *          Flag indicating if this is a function or a procedure.
    * @param   param
    *          0-based index of the parameter to be checked.
    *
    * @return  the class of the requested parameter or {@code null} if parameters do not point
    *          to a valid parameter.
    */
   public static Class getTypeForParam(String        pname, 
                                       InternalEntry ie, 
                                       String        iename, 
                                       boolean       func, 
                                       int           param)
   {
      if (ie != null)
      {
         return BaseDataType.fromTypeName(ie.getParameter(param).getType());
      }
      
      Parameter p = getParameter(pname, iename, func, param);
      if (p == null) 
      {  
         return null;
      }
      
      return BaseDataType.fromTypeName(p.getType());
   }
   
   /**
    * Check if the specified internal-entry is defined IN SUPER. If the program
    * does not exist or the internal entry does not belong to the program,
    * return <code>false</code>.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    iename
    *           Legacy internal-entry name.
    * @param    function
    *           Flag indicating if this is a function or a procedure.
    *
    * @return   See above.
    */
   public static boolean isSuper(String pname, String iename, boolean function)
   {
      InternalEntry ie = getInternalEntry(pname, iename, function);
      
      return (ie == null) ? false : ie.isSuper();
   }

   /**
    * Check if the specified internal-entry is a function. If the program
    * does not exist or the internal entry does not belong to the program,
    * return <code>false</code>.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    iename
    *           Legacy internal-entry name.
    *
    * @return   See above.
    */
   public static boolean isFunction(String pname, String iename)
   {
      return getInternalEntry(pname, iename, true) != null;
   }

   /**
    * Check if the specified internal-entry is defined PRIVATE. If the program
    * does not exist or the internal entry does not belong to the program,
    * return <code>false</code>.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    iename
    *           Legacy internal-entry name.
    * @param    function
    *           Flag indicating if this is a function or a procedure.
    *
    * @return   See above.
    */
   public static boolean isPrivate(String pname, String iename, boolean function)
   {
      InternalEntry ie = getInternalEntry(pname, iename, function);
      
      return (ie == null) ? false : ie.isPrivate();
   }

   /**
    * Check if the specified internal-entry is defined IN handle. If the 
    * program does not exist or the internal entry does not belong to the
    * program, return <code>false</code>.
    * 
    * @param    pname
    *           Legacy 4GL program name.
    * @param    iename
    * @param    ie
    *           If non-null, the already resolved internal entry.
    *           Legacy internal-entry name.
    * @param    function
    *           Flag indicating if this is a function or a procedure.
    *
    * @return   See above.
    */
   public static boolean isInHandle(String        pname, 
                                    InternalEntry ie, 
                                    String        iename, 
                                    boolean       function)
   {
      if (ie != null)
      {
         return ie.isInHandle();
      }
      
      ie = getInternalEntry(pname, iename, function);
      
      return (ie == null) ? false : ie.isInHandle();
   }

   /**
    * Helper function to obtain the Java object that holds the state of a single internal entry.
    *
    * @param    pname
    *           Legacy 4GL program name (the external procedure).
    * @param    iename
    *           Legacy internal-entry name.
    * @param    func
    *           <code>true</code> if the search should be for a function.
    *           
    * @return   The internal entry or <code>null</code> if the external procedure does not exist
    *           or if the external procedure has no such internal entry.
    */
   public static InternalEntry getInternalEntry(String pname, String iename, boolean func)
   {
      ExternalProgram extProg = getExternalProgram(pname);
      
      if (extProg == null)
      {
         return null;
      }
      
      return extProg.search(iename, func);
   }

   /**
    * Get the list of defined constructors for the legacy type.
    * 
    * @param    type
    *           The legacy class.
    */
   public static List<InternalEntry> getConstructors(Class<? extends _BaseObject_> type)
   {
      registerLegacyClass(type);

      LegacyClass lcls = classMap.get(type);
      return new LinkedList<>(lcls.ctors);
   }

   /**
    * Get the static constructor for the legacy type.
    * 
    * @param    type
    *           The legacy class.
    */
   public static InternalEntry getStaticConstructor(Class<? extends _BaseObject_> type)
   {
      registerLegacyClass(type);

      LegacyClass lcls = classMap.get(type);
      return lcls.staticCtor;
   }

   /**
    * Get {@link LegacyClass#executeMethods execute method names} names for this legacy class and its hierarchy.
    * 
    * @param    type
    *           The legacy class for which the method name must be resolved.
    *           
    * @return   The method names, under the form '__qualified_converted_name_execute__'.
    */
   public static Method[] getExecuteMethods(Class<? extends _BaseObject_> type)
   {
      registerLegacyClass(type);

      LegacyClass lcls = classMap.get(type);
      return lcls.executeMethods;
   }
   
   /**
    * Get the destructor for the legacy type.
    * 
    * @param    type
    *           The legacy class.
    *           
    * @return   The list of destructors for this class (and all super-classes), with the order from the 
    *           super-class parents to this class.
    */
   public static List<InternalEntry> getDestructors(Class<? extends _BaseObject_> type)
   {
      registerLegacyClass(type);

      LegacyClass lcls = classMap.get(type);
      return lcls.dtors;
   }
   
   /**
    * Get all the overloaded methods in the given legacy class, with the specified name,
    * and with name with possible _[0-9]+ suffix. 
    * 
    * @param    type
    *           The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.
    * @param    iename
    *           The legacy name.
    * @return   list of the overloaded methods.
    */
   public static List<InternalEntry> getOverloads(Class<?> type, String iename)
   {
      registerLegacyClass(type);

      String name = iename.toString().toLowerCase().intern();
      int len = name.length();
      Map<String, List<InternalEntry>> methods = classMap.get(type).overloads;
      List<InternalEntry> overloads = new LinkedList<>();
      if (methods.containsKey(name))
      {
         overloads.addAll(methods.get(name));
      }
      methods.entrySet().stream().
               filter(e -> e.getKey().startsWith(name) &&  
                     OVERLOAD_SUFFIX.matcher(e.getKey().substring(len)).matches()).
               map(Map.Entry::getValue).forEach(overloads::addAll);
      
//      List<InternalEntry> overloads = methods.get(name);
      
      return overloads.isEmpty() ? null : overloads;
   }
  
   /**
    * Get all overloads of this method name; only the overrides of the methods are included (their super-class
    * definitions matching name and parameters will not be included).
    *  
    * @param    type
    *           The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.
    * @param    iename
    *           The legacy name.
    *           
    * @return   list of the overloaded methods, from all super-classes.
    */
   public static List<InternalEntry> getOverrides(Class<?> type, String iename)
   {
      registerLegacyClass(type);

      LegacyClass lc = classMap.get(type);
      
      return new ArrayList<>(lc.getOverrides(type, iename));
   }
   
   /**
    * Get all GET functions of a given class; only the getters for the class are included (any super-class GET
    * function will not be included).
    *  
    * @param    type
    *           The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.     
    * @return   list of the GET methods from the _BaseObject_ sub-class.
    */
   public static Map<String, List<InternalEntry>> getGetters(Class<? extends _BaseObject_> type)
   {
      registerLegacyClass(type);
      
      LegacyClass lcls = classMap.get(type);
      
      return new HashMap<>(lcls.getters);
   }
   
   /**
    * Get all SET functions of a given class; only the getters for the class are included (any super-class SET
    * function will not be included).
    *  
    * @param    type
    *           The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.     
    * @return   list of the SET methods from the _BaseObject_ sub-class.
    */
   public static Map<String, List<InternalEntry>> getSetters(Class<? extends _BaseObject_> type)
   {
      registerLegacyClass(type);
      
      LegacyClass lcls = classMap.get(type);
      
      return new HashMap<>(lcls.setters);
   }
   
   /**
    * Register the specified legacy class.
    * <p>
    * This is used to register legacy builtin 4GL classes - these always need to be marked with
    * the {@link LegacyResource} annotation.
    * 
    * @param    type
    *           The class representing a builtin 4GL type.
    */
   public static void registerLegacyClass(Class<?> type)
   {
      if (!classMap.containsKey(type))
      {
         synchronized (SourceNameMapper.class)
         {
            LegacyClass lc = classMap.get(type);
            
            if (lc != null)
            {
               return;
            }
            
            String pname = null;
            if (type.getPackage().getName().startsWith("com.goldencode.p2j.oo"))
            {
               // must be a builtin legacy class
               LegacyResource legacyResource = type.getAnnotation(LegacyResource.class);
               if (legacyResource == null)
               {
                  if (type == _BaseObject_.class)
                  {
                     pname = "Progress.Lang.Object";
                  }
                  else
                  {
                     throw new RuntimeException("No @LegacyResource annotation for " + type.getName());
                  }
               }
               else
               {
                  pname = legacyResource.resource();
               }
            }
            else
            {
               String jname = type.getName();
               jname = jname.substring(getPackageRoot().length() + 1);
               ExternalProgram extProg = j2pMap.get(jname);
            
               if (extProg != null)
               {
                  // this is an application class
                  pname = extProg.ooname;
               }
            }

            if (pname == null)
            {
               throw new RuntimeException("Type " + type + " is not associated with a legacy class!");
            }
            
            LegacyClass lcls = buildLegacyClass(type);
            SourceNameMapper.classMap.put(type, lcls);
         }
      }
   }

   /**
    * Find the property's extent, if it applies.
    * 
    * @param    type
    *           The legacy OO class.
    * @param    pname
    *           The legacy OO property name.
    *           
    * @return   The property's extent, or <code>null</code> if it does not apply.
    */
   public static Integer findLegacyPropertyExtent(Class<?> type, String pname)
   {
      registerLegacyClass(type);
      
      LegacyClass cls = classMap.get(type);

      Integer extent = cls.extents.get(pname);
      if (extent != null)
      {
         return extent;
      }
      
      if (type.isInterface())
      {
         return null;
      }
      
      type = type.getSuperclass();
      
      return _BaseObject_.class.isAssignableFrom(type) ? findLegacyPropertyExtent(type, pname) : null;
   }
   
   /**
    * Look into all the properties with the specified legacy name (in the given legacy type) and 
    * return the getter or setter matching exactly the specified signature.
    * 
    * @param    type
    *           The class matching the legacy OO type.
    * @param    pname
    *           The legacy property name.
    * @param    getter
    *           Flag indicating to retrieve a getter or setter.
    * @param    isExtent
    *           Flag indicating whether the property is defined as extent, meaning
    *           we have to retrieve the setter method we need.
    * @param    indexed
    *           Flag indicating to retrieve the indexed getter or setter.
    * 
    * @return   The found {@link InternalEntry} or <code>null</code> if not found.
    */
   public static InternalEntry findLegacyAccessor(Class<?> type,
                                                  String   pname,
                                                  boolean  getter,
                                                  boolean  isExtent,
                                                  boolean  indexed)
   {
      registerLegacyClass(type);
      
      int targetSize = (getter ? 0 : 1) + (indexed ? 1 : 0);
      
      LegacyClass cls = classMap.get(type);
      List<InternalEntry> methods = (getter ? cls.getters : cls.setters).get(pname.toLowerCase());
      if (methods != null)
      {
         if (!indexed && methods.size() == 1)
         {
            return methods.get(0);
         }
         
         if (isExtent && !getter)
         {
            for (InternalEntry candidate : methods)
            {
               // match on the method receiving one extent argument
               if (candidate.getParameterList().size() == 1 &&
                   candidate.getParameter(0).isExtent())
               {
                  return candidate;
               }
            }
         }
         else
         {
            for (InternalEntry candidate : methods)
            {
               // we match only the signature, not the type (METHOD/EVENT) or the name
               if (candidate.getParameterListSize() != targetSize)
               {
                  continue;
               }

               // found it
               return candidate;
            }
         }
      }
      
      if (type.isInterface())
      {
         return null;
      }
      
      type = type.getSuperclass();
      
      return _BaseObject_.class.isAssignableFrom(type) 
                ? findLegacyAccessor(type, pname, getter, isExtent, indexed)
                : null;
   }

   /**
    * Returns a list of all the setters associated with this property. Allows for choosing the
    * right one at run-time. Emitted for DYNAMIC-INVOKE API.
    *
    * @param    type
    *           The class matching the legacy OO type.
    * @param    propName
    *           The legacy property name.
    *
    * @return   The list containing all {@link InternalEntry}'s representing the different setter
    *           methods.
    */
   public static List<InternalEntry> findLegacySetters(Class<?> type, String propName)
   {
      registerLegacyClass(type);

      LegacyClass cls = classMap.get(type);
      List<InternalEntry> methods = cls.setters.get(propName.toLowerCase());

      return methods;
   }
   
   /**
    * Check if the given legacy class defines a method with a given name.
    * 
    * @param    pname
    *           The legacy method name.
    * 
    * @return   <code>true</code> if there is a method defined with the given name.
    */
   public static boolean hasLegacyMethod(Class<?> type, String pname)
   {
      registerLegacyClass(type);
      
      List<InternalEntry> methods = classMap.get(type).overloads.get(pname.toLowerCase());
      if (methods != null)
      {
         for (InternalEntry candidate : methods)
         {
            if (candidate.getLegacyName().equalsIgnoreCase(pname))
            {
               return true;
            }
         }
      }
      
      type = type.getSuperclass();
      
      return _BaseObject_.class.isAssignableFrom(type) ? hasLegacyMethod(type, pname) 
                                                       : false;
   }
   
   /**
    * Given a converted class name, find the legacy name for a Java method defined in that program or legacy
    * class.
    * 
    * @param    type
    *           The converted class name.
    * @param    mname
    *           The Java method name.
    *           
    * @return   See above.
    */
   public static String findLegacyEntryName(Class<?> type, String mname)
   {
      registerLegacyClass(type);

      LegacyClass lc = classMap.get(type);
      
      Function<Collection<List<InternalEntry>>, String> searchName = (l) ->
      {
         for (List<InternalEntry> l2 : l)
         {
            for (InternalEntry ie : l2)
            {
               if (ie.jname.equals(mname))
               {
                  return ie.pname;
               }
            }
         }
         
         return null;
      };
      
      String iename = searchName.apply(lc.overloads.values());
      if (iename == null)
      {
         iename = searchName.apply(lc.setters.values());
      }
      if (iename == null)
      {
         iename = searchName.apply(lc.getters.values());
      }
      if (iename == null)
      {
         Function<List<InternalEntry>, String> searchName2 = (l) ->
         {
            for (InternalEntry ie : l)
            {
               if (ie.jname.equals(mname))
               {
                  return ie.pname;
               }
            }
            
            return null;
         };

         iename = searchName2.apply(lc.ctors);
         
         if (iename == null && lc.dtor != null && lc.dtor.jname.equals(mname))
         {
            iename = lc.dtor.pname;
         }
         if (iename == null && lc.staticCtor != null && lc.staticCtor.jname.equals(mname))
         {
            iename = lc.staticCtor.pname;
         }
      }

      return iename;
   }
   
   /**
    * Look into all the methods with the specified legacy name (in the given type) and return the
    * one matching exactly the specified signature.
    * 
    * @param    type
    *           The class matching the legacy OO type.
    * @param    pname
    *           The legacy method name.
    * @param    signature
    *           The legacy signature to match.
    */
   public static InternalEntry findLegacyMethod(Class<?>      type, 
                                                String        pname, 
                                                InternalEntry signature)
   {
      registerLegacyClass(type);
      
      List<InternalEntry> methods = classMap.get(type).overloads.get(pname.toLowerCase());
      if (methods != null)
      {
         l1: for (InternalEntry candidate : methods)
         {
            // we match only the signature, not the type (METHOD/EVENT) or the name
            if (candidate.getParameterListSize() != signature.getParameterListSize()          ||
                !Objects.equals(candidate.getParameterModes(), signature.getParameterModes()) ||
                !Objects.equals(candidate.getExtent(), signature.getExtent())                 ||
                !Objects.equals(candidate.getQualified(), signature.getQualified()))
            {
               continue;
            }
               
            for (int i = 0; i < candidate.getParameterListSize(); i++)
            {
               Parameter pcand = candidate.getParameter(i);
               Parameter psign = signature.getParameter(i);
               if (pcand.getExtent() != psign.getExtent()   || 
                   !pcand.getType().equals(psign.getType()) ||
                   !pcand.getMode().equals(psign.getMode()))
               {
                  continue l1;
               }
               
               // check the qualified OO name
               if (pcand.getQualified() != null && psign.getQualified() != null)
               {
                  Class<?> clsCand = ObjectOps.resolveClass(pcand.getQualified());
                  Class<?> clsSign = ObjectOps.resolveClass(psign.getQualified());
                  String mode = pcand.getMode();
                  if ("INPUT".equals(mode))
                  {
                     if (!clsCand.isAssignableFrom(clsSign))
                     {
                        continue l1;
                     }
                  }
                  else if ("OUTPUT".equals(mode))
                  {
                     if (!clsSign.isAssignableFrom(clsCand))
                     {
                        continue l1;
                     }
                  }
                  else if ("INPUT-OUTPUT".equals(mode))
                  {
                     if (clsCand != clsSign)
                     {
                        continue l1;
                     }
                  }
               }
               else if (!(pcand.getQualified() == null && psign.getQualified() == null))
               {
                  continue l1;
               }
            }
   
            // all equals, return it
            return candidate;
         }
      }
      
      type = type.getSuperclass();
      
      return _BaseObject_.class.isAssignableFrom(type) ? findLegacyMethod(type, pname, signature) 
                                                       : null;
   }

   /**
    * Remove any <code>..</code> entries from the given relative path name.
    *
    * @param    relName
    *           Relative file path.
    *
    * @return   The resolved name.
    */
   public static String removeUpPath(String relName)
   {
      FastStringBuilder bufRelative = new FastStringBuilder(relName.length());
      String[] lstRelative = relName.split(fileSep);
      int eat = 0;
      boolean inContent = false;
      for (int i = lstRelative.length - 1; i >= 0; i--)
      {
         String token = lstRelative[i];
         if (token.equals(".."))
         {
            eat++;
            continue;
         }
         if (eat > 0)
         {
            eat--;
            continue;
         }
         if (eat == 0 && inContent)
         {
            bufRelative.insertLeft(fileSepChar);
         }
         inContent = true;
         bufRelative.insertLeft(token);
      }

      // New absolute name based on relative one but without extra ".."
      return bufRelative.toString();
   }

   /**
    * Convert the given Progress file name into a simple relative path
    * form.  Any leading file separator is removed.  All references
    * to directories <code>&#46;</code> and <code>&#46;&#46;</code> are
    * processed.  Empty path elements are removed.
    * <p>
    * If {@link WorkArea#caseSens} is false, the program name is lower-cased.
    *
    * @param    progName
    *           Input name.
    * @param    caseSens
    *           Case sensitivity of the passed in input name.
    *
    * @return   The relative name.
    */
   public static String canonicalize(String progName, boolean caseSens)
   {
      // honor case-insensitivity
      if (!caseSens)
      {
         progName = progName.toLowerCase();
      }

      FastStringBuilder bufRelative = new FastStringBuilder(progName.length());
      String[] pathTokens = progName.split(fileSep);
      int eat = 0;
      boolean inContent = false;
      for (int i = pathTokens.length - 1; i >= 0; i--)
      {
         String token = pathTokens[i];
         if (token.equals("") && i > 0 || token.equals("."))
         {
            continue;
         }
         else if (token.equals(".."))
         {
            eat++;
            continue;
         }
         if (eat > 0)
         {
            eat--;
            continue;
         }
         if (eat == 0 && inContent)
         {
            bufRelative.insertLeft(fileSepChar);
         }
         inContent = true;
         bufRelative.insertLeft(token);
      }

      // New absolute name based on relative one but without extra ".."
      return bufRelative.toString();
   }

   /**
    * Resolve the execute method names for this legacy class and its hierarchy.
    * 
    * @param    cls
    *           The legacy class for which the method name must be resolved.
    *           
    * @return   The method names, under the form '__qualified_converted_name_execute__'.
    */
   private static Method[] resolveExecuteMethods(Class<?> cls)
   {
      // enums are not tracked and have no execute methods.
      // interfaces have no execute methods.
      if (LegacyEnum.class.isAssignableFrom(cls) || cls.isInterface())
      {
         return new Method[0];
      }
      
      ArrayList<Method> methods = new ArrayList<>();
      while (cls != Object.class)
      {
         Method m = resolveExecuteMethod(cls);
         if (m != null)
         {
            methods.add(m);
         }
         cls = cls.getSuperclass();
      }
      return methods.toArray(new Method[0]);
   }
   
   /**
    * Resolve the name of this legacy class converted 'execute' method.
    * 
    * @param    cls
    *           The legacy class for which the method name must be resolved.
    *           
    * @return   The method name, under the form '__qualified_converted_name_execute__'.
    */
   private static Method resolveExecuteMethod(Class<?> cls)
   {
      // enums are not tracked and have no execute method
      if (LegacyEnum.class.isAssignableFrom(cls))
      {
         return null;
      }
      
      // go through all declared methods and get the one with Type.EXECUTE annotation
      Method[] mthds = cls.getDeclaredMethods();
      for (int i = 0; i < mthds.length; i++)
      {
         Method mthd = mthds[i];
         LegacySignature ls = mthd.getAnnotation(LegacySignature.class);
         if (ls != null && ls.type() == Type.EXECUTE)
         {
            return mthd;
         }
      }
      
      return null;
   }
   
   /**
    * Creates a new SourceNameMapperCache for the given propath value using an
    * AssociatedThread in order to access the current context.
    * If the initial thread that reads the path-mapping nodes from <code>name_map.xml</code>
    * did not complete, the method is short-circuited.
    * 
    * @param   propath
    *          The propath for which the cache will be created.
    * @param   propathHash
    *          The hash code of the propath.
    */
   private static void createSourceCacheEntry(String[] propath, Long propathHash)
   {
      if (!isPathMappingInitialized())
      {
         return;
      }
      
      if (MonitorRunnable.hasIdentifier(propathHash))
      {
         LOG.finer("Previously submitted task identifier was found, skipping the task.");
         return;
      }
      
      Runnable task = new Runnable() 
      {
         @Override
         public void run()
         {
            // check again if there is no other thread doing work (the identifier is pushed when the thread is
            // created not when this 'createSourceCacheEntry' starts, so that check is too early).
            if (MonitorRunnable.hasIdentifier(propathHash))
            {
               LOG.finer("Previously submitted task identifier was found, skipping the task.");
               return;
            }
            
            // Create a map that will use the integer value from the initializationPaths
            // as the key and the result from fastConvertName as the value. This map
            // is stored in the SourceNameMapperCache and reduces String usage by only
            // having a single map of paths instead of mapping the path to the result 
            // obtained.
            LRUCache<Integer, String[]> resolvedNames = CacheManager.createLRUCache(SourceNameMapper.class,
                                                                                    "names",
                                                                                    16384,
                                                                                    null,
                                                                                    CapacityPolicy.LENIENT);
            // TODO: this can terminate when the cache is full, no reason to go through all the paths to
            // just evict them from cache
            for (Map.Entry<String, Integer> entry : initializationPaths.entrySet())
            {
               String path = entry.getKey();
               Integer index = entry.getValue();
               
               String[] res = fastConvertName(path, true);
               resolvedNames.put(index, res);
            }
               
            SourceNameMapperCache mapperCache = new SourceNameMapperCache(propath, resolvedNames);
            synchronized (sourceCache)
            {
               // do not overwrite if some other thread has already computed the cache for this hash
               sourceCache.putIfAbsent(propathHash, mapperCache);
            }
            
            MonitorRunnable.removeIdentifier(propathHash);
         }
      };
      
      MonitorRunnable namedTask = new MonitorRunnable(propathHash, task);
      mappingExecutor.execute(namedTask);
   }
   
   /**
    * Writes an entry for the program name given into the propath cache, but only if one is
    * available. New values are added to {@link #initializationPaths} when writing the cache
    * so that other caches that will be created to directly use the available resource instead
    * of separately caching the value when searching. When a new value is added to the 
    * {@link #initializationPaths} map, it will not add the result of the search to other
    * existent caches, that's why it is necessary to check if the cache doesn't have the
    * path mapped instead of relying on the already available map.
    * 
    * @param   propath
    *          The propath used to find the cache in which the new entry will be added.
    * @param   propathHash
    *          The hash code of the propath.
    * @param   programName
    *          The key for the map in the propath cache.
    * @param   convertResult
    *          The value.
    */
   private static void writeConvertedCache(String[] propath,
                                           Long propathHash,
                                           String programName,
                                           String[] convertResult)
   {
      if (!isPathMappingInitialized())
      {
         return;
      }
      
      SourceNameMapperCache convertedCache;
      synchronized (sourceCache)
      {
         convertedCache = sourceCache.get(propathHash);
      }
         
      if (convertedCache == null)
      {
         return;
      }
      
      Integer mappingIndex = initializationPaths.get(programName);
      
      convertedCache.readLock();
      try
      {
         // Initialization paths added by other caches will not be present in
         // the other caches, since their results are not computed.
         if (mappingIndex != null && convertedCache.hasProgram(mappingIndex))
         {
            return;
         }
         
         // check if propath matches before returning
         String[] cachePropath = convertedCache.getPropath();
         if(!Arrays.equals(propath, cachePropath))
         {
            return;
         }
      }
      finally
      {
         convertedCache.readUnlock();
      }
      
      // A path that was added by another cache must use the existent value
      // from the map and not increment the counter.
      if (mappingIndex == null)
      {
         mappingIndex = MAPPING_COUNTER.getAndIncrement();   
      }
      
      initializationPaths.put(programName, mappingIndex);
      
      convertedCache.writeLock();
      try
      {
         convertedCache.addResolvedProgram(mappingIndex, convertResult);
      }
      finally
      {
         convertedCache.writeUnlock();
      }
   }
   
   /**
    * Convert a Progress file name into Java class name. This is an initial search method
    * which will try to short-circuit the whole search process of {@link #convertName(String, WorkArea)}
    * by using {@link #pathLookups} map of lookup instances. The lookup map contains
    * SinglePathLookup and MultiPathLookup instances.
    * <p>
    * SinglePathLookup looks up the program name based on the propath and the case sensitivity
    * values of the WorkArea. If there are path aliases in use, then it will resolve the program 
    * name and redo the lookup process using each path alias available according to the propath
    * and case sensitivity parameters.
    * <p>
    * MultiPathLookup stores a list of SinglePathLookup instances and will use their lookup method
    * to search for the program name. Multiple matches for the same program name will cause a conflict
    * and in this case, <code>null</code> will be returned.
    * <p>
    * If no match was found by SinglePathLookup/MultiPathLookup, or there is no entry for the
    * unqualified program name in the lookup map, the search will fallback to the old implementation. 
    * 
    * @param   legacyProgName
    *          Progress file name.
    *
    * @return  A {@code String} array with three components: 
    *          <ol>
    *             <li>Java class name for the given Progress file name as first element</li>
    *             <li>The legacy procedure name in second element of the array.</li>
    *             <li>The legacy qualified OO name (if the name represents a legacy class or 
    *                 interface).</li>
    *          </ol>
    *          If no appropriate mapping is present {@code null} is returned.
    */
   private static String[] fastConvertName(String legacyProgName)
   {
      return fastConvertName(legacyProgName, false);
   }
   
   /**
    * Convert a Progress file name into Java class name. This is an initial search method
    * which will try to short-circuit the whole search process of {@link #convertName(String, WorkArea)}
    * by using {@link #pathLookups} map of lookup instances. The lookup map contains
    * SinglePathLookup and MultiPathLookup instances.
    * <p>
    * SinglePathLookup looks up the program name based on the propath and the case sensitivity
    * values of the WorkArea. If there are path aliases in use, then it will resolve the program 
    * name and redo the lookup process using each path alias available according to the propath
    * and case sensitivity parameters.
    * <p>
    * MultiPathLookup stores a list of SinglePathLookup instances and will use their lookup method
    * to search for the program name. Multiple matches for the same program name will cause a conflict
    * and in this case, <code>null</code> will be returned.
    * <p>
    * If no match was found by SinglePathLookup/MultiPathLookup, or there is no entry for the
    * unqualified program name in the lookup map, the search will fallback to the old implementation. 
    * 
    * @param   legacyProgName
    *          Progress file name.
    * @param   avoidCache
    *          <code>false</code> is the default value and allows reading from the cache or creating
    *          new entries. <code>true</code> is only used by the thread creating the source cache
    *          entry to avoid concurrent modification.
    *
    * @return  A {@code String} array with three components: 
    *          <ol>
    *             <li>Java class name for the given Progress file name as first element</li>
    *             <li>The legacy procedure name in second element of the array.</li>
    *             <li>The legacy qualified OO name (if the name represents a legacy class or 
    *                 interface).</li>
    *          </ol>
    *          If no appropriate mapping is present {@code null} is returned.
    */
   private static String[] fastConvertName(String legacyProgName, boolean avoidCache)
   {
      // Lookup the extension of the file and check if it matches any available
      // extension that was saved when the procedure maps were created. If the
      // extension does not match any of the available ones from the set, it should
      // not continue the search any further.
      // Program names without an extension will pass this condition.
      int lastDotIndex = legacyProgName.lastIndexOf(".");
      if (lastDotIndex > 0)
      {
         String extension = legacyProgName.substring(lastDotIndex + 1);
         if (!allowedLegacyExtensions.contains(extension))
         {
            return null;
         }
      }
      
      WorkArea wa = local.get();

      // get rid of the Windows file separator in working strings
      if (wa.isWindows)
      {
         legacyProgName = dblBackslashRegexp.matcher(legacyProgName).replaceAll(fileSep);
      }
      
      // internally, we use the canonicalized paths (lowercased and / for path separator)
      // but for the result, we use the original case and path separator (as 4GL does)
      if (wa.propath == null)
      {
         // just let it know the propath needs to be resolved - as it must be null originally
         String searchPath = EnvironmentOps.getSearchPath().toStringMessage();
         setSearchPath(searchPath);
      }
      
      if (wa.caseSens == null)
      {
         wa.caseSens = EnvironmentOps.getLegacyCaseSensitive();
      }
      
      legacyProgName = canonicalize(legacyProgName, wa.caseSens);
      
      boolean hasCache = isPathMappingInitialized() && !avoidCache;
      if (hasCache)
      {
         SourceNameMapperCache convertedCache;
         synchronized (sourceCache)
         {
            convertedCache = sourceCache.get(wa.propathHash);
         }
         
         if (convertedCache == null)
         {
            hasCache = false;
         }
         else
         {
            convertedCache.readLock();
            try
            {
               // check if propath matches before returning
               String[] cachePropath = convertedCache.getPropath();
               if(!Arrays.equals(wa.propath, cachePropath))
               {
                  hasCache = false;
               }
            }
            finally 
            {
               convertedCache.readUnlock();
            }
               
            if (hasCache)
            {
               Integer index = initializationPaths.get(legacyProgName);
               
               convertedCache.readLock();
               try
               {
                  if (index != null && convertedCache.hasProgram(index))
                  {
                     return convertedCache.getResolvedProgram(index);
                  }
               }
               finally 
               {
                  convertedCache.readUnlock();
               }
            }
         }
      }
      
      // create a new cache for the current propath
      if (!hasCache && !avoidCache)
      {
         createSourceCacheEntry(wa.propath, wa.propathHash);
      }
      
      // lookup the unqualified program name in the map
      int index = legacyProgName.lastIndexOf(fileSepChar);
      String progName = index > 0 ? legacyProgName.substring(index + 1) : legacyProgName;
      PathLookup lookup = pathLookups.get(progName);
      boolean modified = false;
      if (lookup == null)
      {
         // check if we have a r-code file and if so, make it a p-code file and check again
         if (progName.endsWith(".r") || (!wa.caseSens && progName.endsWith(".R")))
         {
            String noExtProgName = progName.substring(0, progName.length() - ".r".length());
            String pCodeName = noExtProgName + (wa.caseSens ? ".p" : ".P");
            progName = pCodeName;
            
            lookup = pathLookups.get(pCodeName);
            if (lookup == null)
            {
               pCodeName = noExtProgName + (wa.caseSens ? ".w" : ".W");
               progName = pCodeName;
               lookup = pathLookups.get(pCodeName);
            }
            modified = lookup != null;
         }
      }
      
      if (lookup != null)
      {
         String currentProgram;
         if (modified)
         {
            // If we have a r-code file and it's p-code version was found in the pathLookups map,
            // we have to lookup the p-code file instead of the current legacyProgName
            String modifiedLegacyProgName = legacyProgName.substring(0, index + 1) + progName;
            currentProgram = lookup.lookupPath(modifiedLegacyProgName, wa.propath, wa.caseSens);
         }
         else 
         {
            currentProgram = lookup.lookupPath(legacyProgName, wa.propath, wa.caseSens);
         }

         if (currentProgram == null)
         {
            String[] result = convertName(legacyProgName, wa);
            if (!avoidCache)
            {
               writeConvertedCache(wa.propath, wa.propathHash, legacyProgName, result);
            }
            return result;
         }
         
         ExternalProgram extProg = wa.caseSens ? p2jMap.get(currentProgram) : p2jMap_ci.get(currentProgram);
         
         // Save the legacyProgName before switching slashes into backslashes, otherwise
         // it will cache the wrong value and not be able to retrieve it since it's different.
         String tmpLegacyName = legacyProgName;
         if (wa.isWindows)
         {
            // 4GL automatically switch all slashes into backslashes on Win OS
            legacyProgName = fileSepRegexp.matcher(legacyProgName).replaceAll("\\\\");
         }
         
         String[] result = new String[] { addPkg(extProg.jname), legacyProgName, extProg.ooname };
         if (!avoidCache)
         {
            writeConvertedCache(wa.propath, wa.propathHash, tmpLegacyName, result);
         }
         return result;
      }
      
      String[] result = convertName(legacyProgName, wa);
      if (!avoidCache)
      {
         writeConvertedCache(wa.propath, wa.propathHash, legacyProgName, result);
      }
      return result;
   }
   
   /**
    * Convert a Progress file name into Java class name. The input name can be an absolute file
    * name, a file name relative to current location or a file name relative to some directory
    * in a given list of directories . 
    * Before processing, the input file name is processed by {@link #canonicalize}.
    * <p>
    * A simple lookup is tried first, taking into account if there is a .r, .p, or .w extension
    * which may have been left off the 4GL statement. If this fails direct check for absolute path 
    * if attempted using one of the roots from {@code legacyRoots}. In multiple project scenario, 
    * the project name is also added as a path token in order to match the structure from
    * {@code name_map.xml}.
    * <p>
    * If this mapping does not exist then each path segment of the list of directories is tried in order. 
    * The given program name is check to see if it is prefixed with the directory name, if so
    * that prefix is removed and a lookup is done with this name. The first match found ends the
    * search.
    * <p>
    * Absolute paths are converted into relative paths based on the existence of a matching path
    * in the legacy PROPATH (see {@link EnvironmentOps#getLegacySearchPath}). To handle additional
    * absolute paths, one must add the appropriate path prefix to the legacy PROPATH and it will
    * be removed here.
    * <p>
    * This same PROPATH processing is how names relative to the current directory can be handled.
    * This is the only way to handle that case since the current directory in the source system is
    * runtime information that is not available here.
    * <p>
    * The legacy PROPATH is the key configuration tool by which one specifies to this method how
    * to remove absolute and current directory relative prefixes.  This means that the legacy
    * PROPATH that is used here may need to be different than the PROPATH on the original system.
    * <p>
    * As a fallback, if no mapping can be found and if the given {@code progName} is a fully
    * qualified Java class name which references a valid Java class, then the {@code progName} is
    * returned unchanged.
    * <p>
    * In Windows it can verify the absolute or relative program name is in current PROPATH 
    * directory set.  This allows to properly map the following program names used in
    * PROGRESS RUN statement:
    * "c:/path/to/project/stcases/uast/rundir/absolute/program.p"
    * "rundir/../../uast/rundir/relative/test11.p."
    * "../uast/rundir/relative/test5.p"
    * <p>
    * If no program name is explicitly found with a <code>.r</code> extension, R-code program 
    * names are looked up using the {@link #p2jNonExtMap} (the program name without the extension).
    * <p> 
    * If {@link #pathAliases} is specified, and the program can be found (as it is or after 
    * removing the <code>.r</code> extension) using the directories list, the lookup falls back to resolving
    * the aliased folders in the program name, via {@link #resolvePathAliases}.
    * 
    * 
    * @param   legacyProgName
    *          Progress file name.
    * @param   wa
    *          WorkArea of the current local context.
    * @param   searchPaths
    *          Paths to use when searching for the Progress file. If null, will search in propath.
    *
    * @return  A {@code String} array with three components: 
    *          <ol>
    *             <li>Java class name for the given Progress file name as first element</li>
    *             <li>The legacy procedure name in second element of the array.</li>
    *             <li>The legacy qualified OO name (if the name represents a legacy class or 
    *                 interface).</li>
    *          </ol>
    *          If no appropriate mapping is present {@code null} is returned.
    */
   private static String[] convertName(String legacyProgName, WorkArea wa, String[] searchPaths)
   {
      Function<String, ExternalProgram> lookupDirectName;
      Function<String, ExternalProgram> lookupCompiledRcode;
      Function<String, ExternalProgram> lookupAliasedDirectName;
      Function<String, ExternalProgram> lookupAliasedCompiledRcode;
      boolean caseSens = wa.caseSens;
      
      // TODO: RUN can target r-code/procedures even if the extension is missing; i.e. RUN foo. will execute
      // foo.r or foo.p, if it exists. There may be PROPATH involvement, as well.
      
      Function<String, Boolean> isRcode = 
         (pname -> pname.endsWith(".r") || (!caseSens && pname.endsWith(".R")));
      boolean legacyRcode = isRcode.apply(legacyProgName);
      Function<String, Boolean> isPcode = 
         (pname -> pname.endsWith(".p") || (!caseSens && pname.endsWith(".P")) || 
                   pname.endsWith(".w") || (!caseSens && pname.endsWith(".W")));
      boolean legacyPcode = isPcode.apply(legacyProgName);
      boolean noExt = !legacyPcode && !legacyRcode;
      boolean hasAliases = pathAliases != null;
      String msg = "Found first match %s for program %s, and additional %s";
      
      lookupDirectName = (pname -> 
      {
         // Check for direct name before looking for noExt name
         ExternalProgram extProg = getExternalProgram(pname);

         if (extProg == null)
         {
            pname = (noExt) ? pname : pname.substring(0, pname.length() - ".p".length());

            // we could use a try, but keeping a match of names without extension to the full 
            // program names (with extension) is easier
            List<ExternalProgram> progs = caseSens ? p2jNonExtMap.get(pname) 
                                                   : p2jNonExtMap_ci.get(pname);
            if (progs != null && !progs.isEmpty())
            {
               extProg = progs.get(0);

               if (LOG.isLoggable(Level.WARNING))
               {
                  for (int i = 1; i < progs.size(); i++)
                  {
                     LOG.log(Level.WARNING, msg, extProg.pname, legacyProgName, progs.get(i).pname);
                  }
               }
            }
         }
         
         return extProg;
      });
      
      lookupCompiledRcode = !legacyRcode ? null : (pname ->
      {
         if (!isRcode.apply(pname))
         {
            return null;
         }
         
         pname = pname.substring(0, pname.length() - ".r".length());
         
         ExternalProgram extProg = null;
         
         // we could use a trie, but keeping a match of names without extension to the full 
         // program names (with extension) is easier
         List<ExternalProgram> progs = caseSens ? p2jNonExtMap.get(pname) 
                                                : p2jNonExtMap_ci.get(pname);
         if (progs != null && !progs.isEmpty())
         {
            extProg = progs.get(0);

            if (LOG.isLoggable(Level.WARNING))
            {
               for (int i = 1; i < progs.size(); i++)
               {
                  LOG.log(Level.WARNING, String.format(msg, 
                                                       extProg.pname, 
                                                       legacyProgName, 
                                                       progs.get(i).pname));
               }
            }
         }
         
         return extProg;
      });
      
      lookupAliasedDirectName = !hasAliases ? null : (pname ->
      {
         String[] aliases = resolvePathAliases(pname);
         
         ExternalProgram extProg = null;
         
         for (String aliasName : aliases)
         {
            ExternalProgram prog = lookupDirectName.apply(aliasName);
            
            if (prog != null)
            {
               if (extProg != null)
               {
                  if (LOG.isLoggable(Level.WARNING))
                  {
                     LOG.log(Level.WARNING, String.format(msg, 
                                                          extProg.pname, 
                                                          legacyProgName, 
                                                          prog.pname));
                  }
               }
               else
               {
                  extProg = prog;
               }
            }
         }
         
         return extProg;
      });
      
      lookupAliasedCompiledRcode = !legacyRcode || !hasAliases ? null : (pname ->
      {
         if (!isRcode.apply(pname))
         {
            return null;
         }
         
         String[] aliases = resolvePathAliases(pname);
         
         ExternalProgram extProg = null;
         
         for (String aliasName : aliases)
         {
            ExternalProgram prog = lookupCompiledRcode.apply(aliasName);
            
            if (prog != null)
            {
               if (extProg != null)
               {
                  LOG.log(Level.WARNING, String.format(msg, 
                                                       extProg.pname, 
                                                       legacyProgName, 
                                                       prog.pname));
               }
               else
               {
                  extProg = prog;
               }
            }
         }
         
         return extProg;
      });
      
      @SuppressWarnings("unchecked")
      Function<String, ExternalProgram>[] lookups = new Function[]
      {
         lookupDirectName,
         lookupCompiledRcode,
         lookupAliasedDirectName,
         lookupAliasedCompiledRcode,
      };
      
      for (Function<String, ExternalProgram> lookup : lookups)
      {
         if (lookup == null)
         {
            continue;
         }

         String[] res = convertName(legacyProgName, lookup, wa, searchPaths);
         if (res != null)
         {
            return res;
         }
      }
      
      return null;
   }
   
   /**
    * Uses {@link #convertName(String, WorkArea, String[])} with the PROPATH segments for the second parameter.
    * Consult the javadoc for the overloaded method with 3 parameters for more information.
    * 
    * @param   legacyProgName
    *          Progress file name.
    * @param   wa
    *          WorkArea of the current local context.
    *
    * @return  A {@code String} array with three components: 
    *          <ol>
    *             <li>Java class name for the given Progress file name as first element</li>
    *             <li>The legacy procedure name in second element of the array.</li>
    *             <li>The legacy qualified OO name (if the name represents a legacy class or 
    *                 interface).</li>
    *          </ol>
    *          If no appropriate mapping is present {@code null} is returned.
    */
   private static String[] convertName(String legacyProgName, WorkArea wa)
   {
      return convertName(legacyProgName, wa, null);
   }
   
   /**
    * Returns the absolute prefix for a given path if it has one, or null otherwise.
    * 
    * @param   path
    *          The path for which we want to find the absolute prefix.
    *
    * @return  The absolute prefix of the path or null if it doesn't have one.
    */
   public static String getAbsolutePrefix(String path)
   {
      String aRoot;
      for (int i = 0; i < legacyRoots.length; i++)
      {
         aRoot = legacyRoots[i];
         if (aRoot.isEmpty())
         {
            // ensure empty entries are ignored
            continue;
         }
         
         if (path.startsWith(aRoot))
         {
            return aRoot;
         }
      }
      
      return null;
   }
   
   /**
    * Remove the absolute prefix from a given path.
    * 
    * @param   absolutePath
    *          The absolute path which we want to transform into a path relative to the root folder.
    *        
    * @param   absolutePrefix
    *          The absolute prefix of the absolute path.
    *        
    * @return  The path relative to the root folder created from absolutePath 
    *          using the {@link #canonicalize(String, boolean)} method.
    */
   public static String removeAbsolutePrefix(String absolutePath, String absolutePrefix)
   {
      WorkArea wa = local.get();
      
      String relName = absolutePath.substring(absolutePrefix.length());
      if (relName.startsWith(fileSep) || relName.startsWith(altFileSep))
      {
         relName = relName.substring(1); // drop the starting fileSep
      }
      
      relName = canonicalize(relName, wa.caseSens);
      
      return relName;
   }
   
   /**
    * Static getter for the file separator.
    * 
    * @return  The {@link #fileSep} static data member.
    */
   public static String getFileSep()
   {
      return fileSep;
   }
   
   /**
    * Static getter for the array of Strings representing the propath paths.
    * 
    * @return  An array of String objects, each String being a path in the propath.
    */
   public static String[] getPropath()
   {
      WorkArea wa = local.get();
      if (wa.propath == null)
      {
         // just let it know the propath needs to be resolved - as it must be null originally
         String searchPath = EnvironmentOps.getSearchPath().toStringMessage();
         setSearchPath(searchPath);
      }
      
      return wa.propath;
   }

   /**
    * Convert a Progress file name into Java class name. The input name can be an absolute file
    * name, a file name relative to current location or a file name relative to some directory
    * in a given list of directories. Before processing, the input file name is processed by {@link #canonicalize}.
    * <p>
    * A simple lookup is tried first. If this fails direct check for absolute path if attempted
    * using one of the roots from {@code legacyRoots}. In multiple project scenario, the project
    * name is also added as a path token in order to match the structure from
    * {@code name_map.xml}.
    * <p>
    * If this mapping does not exist then each path segment of the list of directories is tried in order. 
    * The given program name is check to see if it is prefixed with the directory name, if so
    * that prefix is removed and a lookup is done with this name. The first match found ends the
    * search.
    * <p>
    * Absolute paths are converted into relative paths based on the existence of a matching path
    * in the legacy PROPATH (see {@link EnvironmentOps#getLegacySearchPath}). To handle additional
    * absolute paths, one must add the appropriate path prefix to the legacy PROPATH and it will
    * be removed here.
    * <p>
    * This same PROPATH processing is how names relative to the current directory can be handled.
    * This is the only way to handle that case since the current directory in the source system is
    * runtime information that is not available here.
    * <p>
    * The legacy PROPATH is the key configuration tool by which one specifies to this method how
    * to remove absolute and current directory relative prefixes.  This means that the legacy
    * PROPATH that is used here may need to be different than the PROPATH on the original system.
    * <p>
    * As a fallback, if no mapping can be found and if the given {@code progName} is a fully
    * qualified Java class name which references a valid Java class, then the {@code progName} is
    * returned unchanged.
    * <p>
    * In Windows it can verify the absolute or relative program name is in current PROPATH 
    * directory set.  This allows to properly map the following program names used in
    * PROGRESS RUN statement:
    * "c:/path/to/project/stcases/uast/rundir/absolute/program.p"
    * "rundir/../../uast/rundir/relative/test11.p."
    * "../uast/rundir/relative/test5.p"
    *
    * @param   legacyProgName
    *          Progress file name.
    * @param   lookup
    *          The lookup function used to resolve a program name to a {@link ExternalProgram}.
    * @param   wa
    *          WorkArea of the current local context.
    * @param   searchPaths
    *          Paths to use when searching for the Progress file. If null, will search in propath.
    *
    * @return  A {@code String} array with three components: 
    *          <ol>
    *             <li>Java class name for the given Progress file name as first element</li>
    *             <li>The legacy procedure name in second element of the array.</li>
    *             <li>The legacy qualified OO name (if the name represents a legacy class or 
    *                 interface).</li>
    *          </ol>
    *          If no appropriate mapping is present {@code null} is returned.
    */
   private static String[] convertName(String legacyProgName,
                                       Function<String, ExternalProgram> lookup,
                                       WorkArea wa,
                                       String[] searchPaths)
   {
      if (wa == null)
      {
         wa = local.get();
      }
      if (wa.propath == null)
      {
         // just let it know the propath needs to be resolved - as it must be null originally
         String searchPath = EnvironmentOps.getSearchPath().toStringMessage();
         setSearchPath(searchPath);
      }
      
      if (searchPaths == null)
      {  
         searchPaths = wa.propath;
      }     

      String progName = wa.caseSens ? legacyProgName : legacyProgName.toLowerCase();
      String[] originalPropath = wa.originalPropath;
      
      String retLegacy = legacyProgName;
      ExternalProgram extProg = null;
      
      // the searchPaths list may contain only relative paths (relative to the legacy conversion folder);
      // if the given program name is an absolute path, it will use the 'legacyRoots' array to try 
      // to remove the absolute prefix, so that the remaining is a path which can be checked in the
      // p2jMap.  first found absolute root which allows the name to be resolved in the map will
      // be used
      
      // note: we can't determine in a os-independent way if ANY path is absolute or not.  for
      // this reason, the legacy-system/4gl-src-root directory.xml entry will always contain 
      // absolute path prefixes. if such a prefix matches, then it will be removed and the 
      // resulting path will be checked against the legacyName-to-Java mappings - if there exists 
      // a key matching this path, then this program will be returned.
      
      // case 1. check if the progName represents an absolute filename of a source procedure
      String absolutePrefix = getAbsolutePrefix(progName);
      boolean hasAbsolutePrefix = absolutePrefix != null;
      if(hasAbsolutePrefix)
      {
         String relName = progName.substring(absolutePrefix.length());
         if (relName.startsWith(fileSep) || relName.startsWith(altFileSep))
         {
            relName = relName.substring(1); // drop the starting fileSep
         }
         
         relName = canonicalize(relName, wa.caseSens);
         
         // regardless if there are '..' up paths or not, we must check the relName against
         // our dictionary - don't use the searchPaths here.
         extProg = lookup.apply(relName);
         
         if (extProg == null)
         {
            // one more try, because of file collisions, the files from current project were
            // converted from a 'token' directory of the src-root
            String token = Utils.getProjectToken();
            if (token != null)
            {
               extProg = lookup.apply(token + fileSep + relName);
            }
         }
         
         if (extProg != null)
         {
            // the reported name is the full absolute name
            retLegacy = legacyProgName;
         }
      }

      if (hasAbsolutePrefix && extProg == null)
      {
         // absolute name, not found after removing the absolute prefixes, so fail
         return null;
      }

      // case 2. search via the searchPaths
      if (extProg == null && searchPaths.length != 0)
      {
         // we have determined that the name is not an absolute name; we will use the normal 
         // searchPaths search
         
         for (int i = 0; i < searchPaths.length; i++)
         {
            String aPath = searchPaths[i];
            if (aPath.isEmpty())
            {
               // ensure empty entries are ignored
               continue;
            }

            String relName = aPath.concat(fileSep).concat(progName);            
            relName = removeUpPath(relName);
            String aPathRel = removeUpPath(aPath);
            
            // Check if the program is in searchPaths
            if (relName.startsWith(aPathRel))
            {
               // this is a relative name, computed with a resolved searchPaths prefix; we must not
               // remove the current searchPaths path, as we must match the exact program name as
               // we found it in the (resolved) searchPaths list.  in other words, searchPaths represents 
               // directories relative to the 'abl/' (legacy source deploy) folder or even an
               // absolute folder.

               // check for a match
               extProg = lookup.apply(canonicalize(relName, wa.caseSens));
               
               if (extProg != null)
               {
                  if (!retLegacy.startsWith("."))
                  {
                     String path = originalPropath[i];
                     retLegacy = path +
                                 (path.charAt(path.length() - 1) == fileSepChar ? "" : fileSepChar) +
                                 legacyProgName;
                  }
                  break; // first match exits the loop
               }
            }
         }
      }
      
      // case 3. check the simple form of the name - in this case, just check for an exact match
      if (extProg == null)
      {
         extProg = lookup.apply(canonicalize(progName, wa.caseSens));
      }

      // case 4. fallback processing
      if (extProg == null)
      {
         // enable support for fully qualified Java class names if they are passed in directly
         Boolean isClass = CLASS_LOOKUPS.get(progName);
         if (isClass == null)
         {
            try
            {
               Class.forName(progName);

               // if we are here, then the progName must have been a fully qualified Java class name
               // to a class that exists so we return it unchanged. The legacy name is unknown and
               // unneeded/inexistent.
               CLASS_LOOKUPS.put(progName, Boolean.TRUE);
               return new String[]{progName, null, null};
            }
            catch (ClassNotFoundException cnfe)
            {
               CLASS_LOOKUPS.put(progName, Boolean.FALSE);
               // do nothing, the given program can't be found as a Java class
            }
         }
         else if (isClass == Boolean.TRUE)
         {
            return new String[] {progName, null, null};
         }
      }

      if (extProg == null)
      {
         // search failed. The parameter does not contain a valid procedure name
         return null;
      }
      
      if (wa.isWindows)
      {
         // 4GL automatically switch all slashes into backslashes on Win OS
         retLegacy = fileSepRegexp.matcher(retLegacy).replaceAll("\\\\");
      }
      
      return new String[] { addPkg(extProg.jname), retLegacy, extProg.ooname };
   }
   
   /**
    * Get the {@link ExternalProgram} of the given Legacy 4GL program name.
    *  
    * @param   pname
    *          Legacy 4GL program name.
    *
    * @return  the {@link ExternalProgram} or {@code null} if not a valid pname.
    */
   private static ExternalProgram getExternalProgram(String pname)
   {
      ExternalProgram extProg = null;
      
      if (pname != null)
      {
         boolean caseSens = getLegacyCaseSensitive();
         extProg = caseSens ? p2jMap.get(pname) : p2jMap_ci.get(pname);
      }
      
      return extProg;
   }
   
   /**
    * Notification that the search path has changed - it updates the {@link WorkArea#propath} 
    * value.
    * 
    * @param    searchPath
    *           The new PROPATH value.
    */
   private static void setSearchPath(String searchPath)
   {
      String[][] searchResult;
      synchronized (searchPathCache)
      {
         searchResult = searchPathCache.get(searchPath);
      }
      
      if (searchResult != null)
      {
         WorkArea wa = local.get();
         wa.propath = searchResult[0];
         wa.originalPropath = searchResult[1];
         wa.propathHash = (long) Arrays.hashCode(searchResult[0]);
         return;
      }
      
      String originalSearchPath = searchPath;
      boolean isWindows  = EnvironmentOps.isLegacyPlatformWindows();
      boolean caseSens = getLegacyCaseSensitive();
      
      // get rid of the Windows file separator in working strings
      if (isWindows)
      {
         searchPath = dblBackslashRegexp.matcher(searchPath).replaceAll(fileSep);
      }
      
      // internally, we use the canonicalized paths (lowercased and / for path separator)
      // but for the result, we use the original case and path separator (as 4GL does)
      String[] originalPropath = searchPath.split(EnvironmentOps.PROPATH_SEPARATOR);
      String[] propath;
      
      if (!caseSens)
      {
         propath = new String[originalPropath.length];
         for (int i = 0; i < originalPropath.length; i++)
         {
            propath[i] = originalPropath[i].toLowerCase();
         }
      }
      else
      {
         propath = originalPropath;
      }
      
      // remove the absolute root (if possible) from this entry
      for (int i = 0; i < propath.length; i++)
      {
         String aPath = propath[i];

         for (String aRoot : legacyRoots)
         {
            if (aPath.startsWith(aRoot))
            {
               aPath = aPath.substring(aRoot.length());
               if (aPath.startsWith(fileSep))
               {
                  aPath = aPath.substring(1);
               }
               
               break;
            }
         }
         
         propath[i] = aPath;
      }

      WorkArea wa = local.get();
      
      wa.propath = propath;
      wa.originalPropath = originalPropath;
      wa.propathHash = (long) Arrays.hashCode(propath);
      
      synchronized (searchPathCache)
      {
         searchPathCache.put(originalSearchPath, new String[][] { wa.propath, wa.originalPropath });
      }
   }

   /**
    * Private utility method for locating a parameter of a routine.
    *
    * @param   pname
    *          Legacy 4GL program name.
    * @param   iename
    *          Legacy internal-entry name.
    * @param   func
    *          Flag indicating if this is a function or a procedure.
    * @param   param
    *          0-based index of the parameter to be checked.
    *
    * @return  the requested parameter or {@code null} if parameters do not point to a valid
    *          parameter.
    */
   private static Parameter getParameter(String pname, String iename, boolean func, int param)
   {
      InternalEntry ie = getInternalEntry(pname, iename, func);
      
      if (ie == null || param < 0 || param >= ie.getParameterListSize())
      {
         return null;
      }
      
      return ie.getParameter(param);
   }
   
   /**
    * Given a program name, replace all folder names which have an associated 'source' folder,
    * and return the new path.
    * 
    * @param    pname
    *           The legacy program name.
    *           
    * @return   The resolved program names or empty array if no change was possible.
    */
   private static String[] resolvePathAliases(String pname)
   {
      Map<String,String> pA = getLegacyCaseSensitive() ? pathAliases : pathAliases_ci;
      return resolvePathAliases(pA, pname);
   }
   
   /**
    * Load the path aliases from the directory, into the {@link #pathAliases} map.  This map 
    * contains as key the 'aliased' name which is an alias for a 'source' folder.
    * <p>
    * The directory contains these mappings in the <code>/server/default/path-aliases</code> or
    * <code>/server/&lt;server-id&gt;/path-aliases</code> node.  The child nodes have as class 
    * <code>path-alias</code> and this structure:
    * <code>
    *     &lt;node class="path-alias" name="alias1"&gt;
    *     <br>&nbsp;&nbsp;&nbsp;
    *       &lt;node-attribute name="source" value="src1"/&gt;
    *       <br>&nbsp;&nbsp;&nbsp;
    *       &lt;node-attribute name="alias" value="obj1"/&gt;
    *       <br>
    *     &lt;/node&gt;
    * </code>
    * <p>
    * The <code>alias1</code> name is just an unique ID for each child.
    */
   private static void loadPathAliases()
   {
      DirectoryService ds = DirectoryService.getInstance();
      
      if (ds == null)
      {
         // we are in conversion mode, we can't use path aliases
         
         return;
      }
      
      if (!ds.bind())
         throw new RuntimeException("directory bind failed");
      
      try
      {
         String path = Utils.findDirectoryNodePath(ds, "path-aliases", "container", false);
         if (path == null)
         {
            return;
         }
         
         String[] aliasPaths = ds.enumerateNodes(path);
         if (aliasPaths.length == 0)
         {
            return;
         }
   
         pathAliases = new HashMap<>();
         pathAliases_ci = new HashMap<>();
         for (String aliasPath : aliasPaths)
         {
            aliasPath = path + "/" + aliasPath;
            String source = ds.getNodeString(aliasPath, "source");
            String alias = ds.getNodeString(aliasPath, "alias");
            String source_ci = source.toLowerCase();
            String alias_ci  = alias.toLowerCase();
            
            pathAliases.put(alias, source);
            pathAliases_ci.put(alias_ci, source_ci);
         }
      }
      finally
      {
         ds.unbind();
      }
   }
   
   /**
    * Given this external program (which may be associated with a legacy OO class), register all
    * of its exposed services.
    * 
    * @param    extProg
    *           The external program specification.
    * @param    qname
    *           Fully qualified, converted, Java class name.
    */
   private static void registerServices(ExternalProgram extProg, String qname)
   {
      Class<?> cls = null;
      try
      {
         cls = Class.forName(qname);
      }
      catch (ClassNotFoundException e)
      {
         throw new RuntimeException(e);
      }
      
      boolean foundService = false;
      
      LegacyService[] legacyServices = cls.getAnnotationsByType(LegacyService.class);
      if (legacyServices != null)
      {
         for (LegacyService pls : legacyServices)
         {
            String executionMode = pls.executionMode().toLowerCase();

            if (LegacyService.REST.equalsIgnoreCase(pls.type()))
            {
               if (executionMode.isEmpty())
               {
                  throw new RuntimeException("executionMode must be specified for " + qname);
               }
            }
            
            if (!pls.name().isEmpty())
            {
               switch (pls.type().toUpperCase())
               {
                  case LegacyService.SOAP:
                     SoapHandler.addService(pls, extProg.pname, extProg.main.getMethod());
                     break;

                  case LegacyService.REST:
                     RestHandler.addService(pls, extProg.pname, extProg.main.getMethod());
                     break;
   
                  case LegacyService.WEBHANDLER:
                     WebServiceHandler.addService(pls, (Class) cls);
                     break;
                     
                  default:
                     throw new RuntimeException("Unknown legacy service type " + pls.type() + 
                                                " in " + qname);
               }
               
               foundService = true;
            }
            
            if (pls.name().isEmpty() || 
                (pls.type().equalsIgnoreCase(LegacyService.SOAP) && 
                 executionMode.equalsIgnoreCase(LegacyWebServiceHandler.PERSISTENT)))
            {
               // this is a legacy class or external program, get the methods
               boolean isCls = BaseObject.class.isAssignableFrom(cls);
               boolean isJava = executionMode.equalsIgnoreCase(LegacyWebServiceHandler.JAVA);
               
               Method[] methods = cls.getDeclaredMethods();
               for (Method m : methods)
               {
                  LegacyService[] mLegacyServices = m.getAnnotationsByType(LegacyService.class);
                  if (mLegacyServices != null)
                  {
                     for (LegacyService mls : mLegacyServices)
                     {
                        switch (mls.type().toUpperCase())
                        {
                           case LegacyService.SOAP:
                              if (isJava)
                              {
                                 throw new RuntimeException("Java SOAP services are not supported!");
                              }
                              else if (isCls)
                              {
                                 SoapHandler.addService(pls, (Class) cls, mls, m);
                              }
                              else
                              {
                                 boolean isFunc = (m.getReturnType() != void.class);
                                 InternalEntry ie = (isFunc ? extProg.j2pf : extProg.j2pp).get(m.getName());
                                 SoapHandler.addService(pls, extProg.pname, mls, ie, isFunc);
                              }
                              break;
                              
                           case LegacyService.REST:
                              if (isJava)
                              {
                                 Class<?>[] parametersTypes = m.getParameterTypes();
                                 boolean erasedTypes = (parametersTypes.length > 0) && Arrays.stream(parametersTypes).allMatch(Object.class::equals);
                                 if (!erasedTypes)
                                 {
                                    RestHandler.addJavaService(pls, (Class) cls, mls, m);
                                 }
                              }
                              else if (isCls)
                              {
                                 RestHandler.addService(pls, (Class) cls, mls, m);
                              }
                              else
                              {
                                 boolean isFunc = (m.getReturnType() != void.class);
                                 InternalEntry ie = (isFunc ? extProg.j2pf : extProg.j2pp).get(m.getName());
                                 RestHandler.addService(pls, extProg.pname, mls, ie, isFunc);
                              }
                              break;
                              
                           default:
                              throw new RuntimeException("Unknown legacy service type " + mls.type() + 
                                                         " in " + qname);
                        }
                        
                        foundService = true;
                     }
                  }
               }
            }
         }
      }
      
      if (!foundService && LOG.isLoggable(Level.FINER))
      {
         LOG.log(Level.FINER, "Could not find a LegacyService annotation in " + qname + "!");
      }
   }
   
   /**
    * This method loads all mapping data from a URL which is calculated
    * using the <code>pkgroot</code> and the {@link #NAME_MAP_FILE} with
    * all "." characters converted to file separators and an extra file
    * separator added before the file name portion. This URL is expected to
    * be found in the application's JAR file. Note that this will silently
    * return if the mapping data is not found.  Any file system configuration
    * data will be read from the directory via the {@link EnvironmentOps}
    * helper.
    */
   private static void initMappingData()
   {
      // prevent reinitialization
      if (mappingDataInitialized)
      {
         return;
      }
      // allow only one thread to continue, other threads shall wait
      synchronized (SourceNameMapper.class)
      {
         // prevent recursive calls and reinitialization
         if (mappingDataInitialized || mappingDataInitInProgess)
         {
            return;
         }
         // prevent recursive calls
         mappingDataInitInProgess = true;
         
         if (Configuration.isRuntimeConfig())
         {
            classMap = new ConcurrentHashMap<>(1024);
         }
         
         p2jMap = new HashMap<>(1024);
         p2jMap_ci = new CaseInsensitiveHashMap<>(1024);
         p2jNonExtMap = new HashMap<>(1024);
         p2jNonExtMap_ci = new CaseInsensitiveHashMap<>(1024);
         pathLookups = new HashMap<>();
         j2pMap = new HashMap<>(1024);
         searchPathCache = CacheManager.createLRUCache(SourceNameMapper.class, "search", 64);
         
         // lookup the descriptors for the original file system
         pkgroot = Utils.getDirectoryNodeString(null, "legacy-system/pkgroot", "");
         
         // if the pkgroot is null get the custom package and reset it
         if (pkgroot == null || pkgroot.isEmpty())
         {
            pkgroot = custpkgroot;
            custpkgroot = null;
         }
         
         // this is only a conversion-time configuration value ("basepath")
         proroot = "";
         
         boolean caseSens = getLegacyCaseSensitive();
         String pathSep   = getLegacyPathSeparator();
         
         // at runtime we use legacyRoots - all must be absolute paths
         String srcRoots = Utils.getDirectoryNodeString(null, "legacy-system/4gl-src-root", "");
         if (!caseSens)
         {
            srcRoots = srcRoots.toLowerCase();
         }
         // get rid of the Windows file separator in working strings
         if (EnvironmentOps.isLegacyPlatformWindows())
         {
            srcRoots = dblBackslashRegexp.matcher(srcRoots).replaceAll(fileSep);
         }
         legacyRoots = srcRoots.split(pathSep);
         
         String nameMappingXmlPath = Utils.packageToJarPath(pkgroot) + NAME_MAP_FILE;
         Set<URL> allNameMaps = new HashSet<>();
         ClassLoader systemClassLoader = MultiClassLoader.getClassLoader();
         for (String jarPath : JarUtil.resolveClasspathJars(LOG))
         {
            JarClassLoader loader = JarClassLoader.createJarClassLoader(jarPath, systemClassLoader);
            if (loader.containsResource(nameMappingXmlPath))
            {
               URL resourceUrl = loader.findResource(nameMappingXmlPath);
               if (resourceUrl != null)
               {
                  allNameMaps.add(resourceUrl);
               }
            }
         }
         
         if (allNameMaps.isEmpty())
         {
            if (LOG.isLoggable(Level.SEVERE))
            {
               LOG.log(Level.SEVERE, 
                       "No mapping data exists: " + NAME_MAP_FILE + " not found in jars.\n\t" +
                       "Expected location: " + nameMappingXmlPath);
            }
            
            URL resource = null;
            try
            {
               resource = systemClassLoader.getResource(nameMappingXmlPath);
            }
            catch (Exception e)
            {
               // silently ignore it
            }
            
            if (resource != null)
            {
               LOG.log(Level.SEVERE, "Developper mode: using file-system resource " + resource);
               allNameMaps.add(resource);
            }
            else
            {
               return;
            }
            
         }
         
         allowedLegacyExtensions = new HashSet<>();
         // always allow compiled procedure code files
         allowedLegacyExtensions.add("r");
         allowedLegacyExtensions.add("R");
         
         boolean hasLoadedAnyMap = false;
         for (URL mapUrl : allNameMaps)
         {
            try
            {
               populateInMemoryMaps(mapUrl, nameMappingXmlPath);
               hasLoadedAnyMap = true;
            }
            catch (Exception e)
            {
               LOG.warning("Unable to load name mapping data from " + mapUrl.toExternalForm(), e);
            }
         }
         
         if (!hasLoadedAnyMap)
         {
            throw new RuntimeException("Unable to load any name mapping data.");
         }
         
         loadPathAliases();
         mappingDataInitialized = true;
         mappingDataInitInProgess = false;
      }
   }

   /**
    * Reads the name map xml files from the provided URL and resolves all mappings. Should always get 
    * called in a synchronized block.
    * 
    * @param    mapUrl
    *           The URL of the map name xml file (the file path inside the jar).
    * @param    nameMappingXmlPath
    *           Only the name map xml file path.
    * 
    * @throws   IOException
    *           Map name xml file read exception.
    * @throws   ParserConfigurationException
    *           XML parser configuration exception.
    * @throws   SAXException
    *           XML parser general exception.
    */
   private static void populateInMemoryMaps(URL mapUrl, String nameMappingXmlPath)
   throws IOException,
          ParserConfigurationException,
          SAXException
   {
      Document dom = XmlHelper.parse(mapUrl.openStream());

      // find the root node
      Element root = dom.getDocumentElement();

      if (root == null)
      {
         LOG.warning("Name mapping file " + nameMappingXmlPath + " is empty.");
         return;
      }
      
      populateRestServices(root);

      if (!populateClasses(root))
      {
         LOG.warning("No class mappings in name map " +  mapUrl);
         return;
      }

      populateInitPaths(root, mapUrl);
   }

   /**
    * Resolves and populates in-memory the REST service mappings from the xml root node. Should always get 
    * called in a synchronized block.
    *
    * @param    mapRootNode
    *           The root node of the map name xml file.
    */
   private static void populateRestServices(Element mapRootNode)
   {
      // enumerate all 'rest-service' nodes.
      NodeList restServices = mapRootNode.getElementsByTagName(ELEM_REST_SERVICE);
      if (restServices == null || restServices.getLength() == 0)
      {
         return;
      }
      if (servicePrograms == null)
      {
         servicePrograms = new ArrayList<>();
      }
      for (int i = 0; i < restServices.getLength(); i++)
      {
         Element restService = (Element) restServices.item(i);

         String jname = restService.getAttribute(ATTR_JNAME);
         // create a bogus pname
         String pname = jname + "-fwd-rest-service.p";
         String ooname = restService.getAttribute(OONAME);

         ExternalProgram ep = buildExternalProgram(pname, jname, ooname, restService);
         servicePrograms.add(ep);
      }
   }
   
   /**
    * Resolves and populates in-memory the class mappings from the xml root node. Should always get called 
    * in a synchronized block.
    *
    * @param    mapRootNode
    *           The root node of the map name xml file.
    * 
    * @return   <code>true</code> if the node is successfully read, <code>false</code> otherwise.
    * 
    * @throws   IllegalStateException
    *           On malformed class mapping entry.
    */
   private static boolean populateClasses(Element mapRootNode)
   {
      // enumerate all child nodes (there will be one for each source file that can be mapped)
      NodeList cmaps = mapRootNode.getElementsByTagName(ELEM_CMAP);

      if (cmaps == null)
      {
         return false;
      }

      for (int i = 0; i < cmaps.getLength(); i++)
      {
         Element classMap = (Element) cmaps.item(i);

         String pname = classMap.getAttribute(ATTR_PNAME);
         String jname = classMap.getAttribute(ATTR_JNAME);
         String ooname = classMap.getAttribute(OONAME);

         if (pname == null || pname.length() == 0 || jname == null || jname.length() == 0)
         {
            throw new IllegalStateException("Malformed class mapping entry for " + classMap.toString());
         }

         if (j2pMap != null && j2pMap.containsKey(jname))
         {
            ExternalProgram resolvedProgram = j2pMap.get(jname);
            if (!resolvedProgram.pname.equals(pname) || 
               (!(ooname.isEmpty() && resolvedProgram.ooname == null) && !ooname.equals(resolvedProgram.ooname)))
            {
               LOG.severe("Mapping for class " + jname + " found multiple times with different attributes.");
               System.exit(-1);
            }
            if (LOG.isLoggable(Level.FINE))
            {
               LOG.fine("Mapping for class " + jname + " found multiple times.");
            }
         }

         // store bidirectional mappings for the progress relative
         // filename to/from the java relative file name mapping
         ExternalProgram ep = buildExternalProgram(pname, jname, ooname, classMap);
         boolean withServices = "true".equalsIgnoreCase(classMap.getAttribute(WITH_SERVICES));
         if (withServices)
         {
            if (servicePrograms == null)
            {
               servicePrograms = new ArrayList<>();
            }
            servicePrograms.add(ep);
         }

         // Each program name will create a SinglePathLookup, but if the unqualified
         // name appears multiple times, it will create a new MultiPathLookup instance
         // or use an existing one.
         SinglePathLookup singleLookup = new SinglePathLookup(pname);
         String simpleProgramName = singleLookup.getSimpleProgramName();
         if (!pathLookups.containsKey(simpleProgramName))
         {
            pathLookups.put(simpleProgramName, singleLookup);
         }
         else
         {
            PathLookup pathLookup = pathLookups.get(simpleProgramName);
            if (pathLookup instanceof SinglePathLookup)
            {
               MultiPathLookup multiLookup = new MultiPathLookup();
               multiLookup.references.add((SinglePathLookup) pathLookup);
               multiLookup.references.add(singleLookup);

               pathLookups.put(simpleProgramName, multiLookup);
            }
            else
            {
               ((MultiPathLookup) pathLookup).references.add(singleLookup);
            }
         }

         p2jMap.put(ep.pname, ep);
         p2jMap_ci.put(pname, ep);
         j2pMap.put(ep.jname, ep);
         
         if (ep.pnameExt != null && !allowedLegacyExtensions.contains(ep.pnameExt))
         {
            allowedLegacyExtensions.add(ep.pnameExt);
            allowedLegacyExtensions.add(ep.pnameExt.toLowerCase());
         }

         List<ExternalProgram> samePnameProgs = p2jNonExtMap.get(ep.pnameNoExt);
         if (samePnameProgs == null)
         {
            samePnameProgs = new ArrayList<>(1);
            p2jNonExtMap.put(ep.pnameNoExt, samePnameProgs);
            p2jNonExtMap_ci.put(ep.pnameNoExt, samePnameProgs);
         }
         samePnameProgs.add(ep);
      }
      return true;
   }

   /**
    * Resolves and populates in-memory the initialization paths from the xml root node. Should always get 
    * called in a synchronized block.
    *
    * @param    mapRootNode
    *           The root node of the map name xml file.
    * @param    mapUrl
    *           The URL of the map name xml file (the file path inside the jar). Used for logging.
    */
   private static void populateInitPaths(Element mapRootNode, URL mapUrl)
   {
      if (!PATH_INIT_THREADS_COUNTER.compareAndSet(-1, 1))
      {
         PATH_INIT_THREADS_COUNTER.incrementAndGet();
      }
      
      if (initializationPaths == null)
      {
         initializationPaths = new ConcurrentHashMap<>();
      }

      Runnable task = () ->
      {
         NodeList pmaps = mapRootNode.getElementsByTagName(ELEM_PMAP);

         int plen = pmaps.getLength();
         for (int i = 0; i < plen; i++)
         {
            Element pathEntry = (Element) pmaps.item(i);

            String pathName = pathEntry.getAttribute(ATTR_PATHNAME);
            if (pathName == null || pathName.length() == 0)
            {
               throw new IllegalStateException("Malformed path mapping entry for " + pathEntry.toString() + " found in " + mapUrl);
            }

            if (initializationPaths.containsKey(pathName))
            {
               LOG.log(Level.FINE, 
                       "Path %s already initialized. Skipping element in name map %s.",
                       pathName,
                       mapUrl);
               continue;
            }

            initializationPaths.put(pathName, MAPPING_COUNTER.getAndIncrement());
         }

         sourceCache = CacheManager.createLRUCache(SourceNameMapper.class, "source", 8);
         PATH_INIT_THREADS_COUNTER.decrementAndGet();
      };

      Thread pathMappingThread = new Thread(task);
      pathMappingThread.setDaemon(true);
      pathMappingThread.setName("Path mapping initializer thread for " + mapUrl);
      pathMappingThread.start();
   }

   /**
    * Returns <code>true</code> if all path mapping initializer threads have completed work, 
    * <code>false</code> otherwise.
    * 
    * @return See above.
    */
   private static boolean isPathMappingInitialized()
   {
      // mappingDataInitialized indicates all jars have been processed
      return mappingDataInitialized && PATH_INIT_THREADS_COUNTER.get() == 0;
   }

   /**
    * Collect the {@link ExternalProgram} info contained in the <code>classMap
    * </code> node.
    * 
    * @param    pname
    *           The legacy 4GL program name.
    * @param    jname
    *           The converted class name.
    * @param    ooname
    *           The legacy OO class name.
    * @param    classMap
    *           The DOM node containing the info for this external program.
    *           
    * @return   An {@link ExternalProgram} instance for this DOM node.
    */
   private static ExternalProgram buildExternalProgram(String pname,
                                                       String jname,
                                                       String ooname,
                                                       Element classMap)
   {
      String publishedEvents = classMap.getAttribute(PUBLISHED_EVENTS);
      
      ExternalProgram extProg = new ExternalProgram(pname, jname, publishedEvents);
      extProg.ooname = ooname.isEmpty() ? null : ooname;
      
      // for legacy classes, just the class/interface registry is kept plus any function declarations.
      // the methods are annotated with their signature, so build them from there.
      
      // the legacy classes, the LegacyClass structure is built when the class is first access at runtime
      // for external programs, both LegacySignature and name_map.xml are kept, but they are built on a 
      // lazy approach
      
      // enumerate method mapping nodes
      NodeList mmaps = classMap.getElementsByTagName(ELEM_MMAP);
      
      if (mmaps == null)
      {
         return extProg;
      }
      
      int mlen = mmaps.getLength();
      
      if (mlen > 0)
      {
         extProg.ieMap = new LinkedHashMap<>();
      }
      
      Set<String> defFuncs = new HashSet<>();
      Set<String> forwardFuncs = new HashSet<>();
      
      // process each method mapping
      // 4GL allows same name for a function or procedure, as long as they
      // are not internal. they appear in the internal-entry list, but the
      // get-signature call will see only the function (for this "feature" to
      // work, the procedure must be defined before the function, else 4GL
      // gets compile error).
      for (int j = 0; j < mlen; j++)
      {
         Element methMap = (Element) mmaps.item(j);
         
         String iepname = methMap.getAttribute(ATTR_PNAME);
         String iejname = methMap.getAttribute(ATTR_JNAME);
         
         if (iepname == null || iejname == null || iejname.length() == 0 || iepname.length() == 0)
         {
            String errmsg = "Malformed method mapping entry for " + methMap.toString();
            throw new IllegalStateException(errmsg);
         }
         
         InternalEntry ie = buildMethod(iepname, iejname, methMap);
         ie.setExternalProgram(extProg);
         
         // the FORWARD functions are emitted in the name_map.xml only to preserve the order.
         if (ie.isFunction())
         {
            String forward = methMap.getAttribute("forward");
            if ("true".equalsIgnoreCase(forward))
            {
               forwardFuncs.add(iepname);
            }
            else
            {
               defFuncs.add(iepname);
               
               // if the function already exists as forward, take the parameter
               // names from here and update the forward declaration
               if (forwardFuncs.contains(iepname))
               {
                  if (ie.getParameterListSize() > 0)
                  {
                     InternalEntry ief = extProg.search(iepname, true);
                     ief.setParameterList(ie.getParameterList());
                  }
                  
                  continue;
               }
            }
         }
         
         // store the bidirectional Progress relative proc/function name from/to Java method name
         // mapping
         InternalEntryKey key = new InternalEntryKey(ie.isFunction(), ie.getLegacyName());
         extProg.ieMap.put(key, ie);
         
         Map<String, InternalEntry> p2j = null;
         Map<String, InternalEntry> j2p = null;
         
         if (ie.isFunction())
         {
            if (extProg.p2jf.isEmpty())
            {
               extProg.p2jf = new HashMap<>();
               extProg.j2pf = new HashMap<>();
            }
            p2j = extProg.p2jf;
            j2p = extProg.j2pf;
         }
         else
         {
            if (extProg.p2jp.isEmpty())
            {
               extProg.p2jp = new HashMap<>();
               extProg.j2pp = new HashMap<>();
            }
            p2j = extProg.p2jp;
            j2p = extProg.j2pp;
         }
         p2j.put(iepname.toLowerCase().intern(), ie);
         j2p.put(ie.jname, ie);
      }
      
      // remove all functions defined as forward but without body
      forwardFuncs.removeAll(defFuncs);
      for (String iepname : forwardFuncs)
      {
         InternalEntry ie = extProg.search(iepname, true);
         InternalEntryKey key = new InternalEntryKey(ie.isFunction(), ie.getLegacyName());
         extProg.ieMap.remove(key);
      }
      
      extProg.main = new InternalEntry("", "", InternalEntry.Type.MAIN);
      extProg.main.setExternalProgram(extProg);
      buildParameters(classMap, extProg.main);
      extProg.main.initialize();
      
      return extProg;
   }
   
   /**
    * Collect the {@link InternalEntry} info contained in the <code>methMap</code> node.
    * 
    * @param    pname
    *           The legacy 4GL internal-entry name.
    * @param    jname
    *           The converted internal-entry name.
    * @param    methMap
    *           The DOM node containing the info for this internal-entry.
    *           
    * @return   An {@link InternalEntry} instance for this DOM node.
    */
   private static InternalEntry buildMethod(String pname, String jname, Element methMap)
   {
      String type = methMap.getAttribute(ATTR_TYPE);
      
      if (type == null || type.length() == 0)
      {
         String err = "Missing type attribute for method mapping entry " + methMap.toString();
         throw new IllegalStateException(err);
      }
      
      InternalEntry ie = null;
      
      if (InternalEntry.Type.DLL_ENTRY.toString().equals(type))
      {
         // instantiate the subclass that handles native calls
         ie = new NativeAPIEntry(pname, jname);
      }
      else
      {
         // the common case
         ie = new InternalEntry(pname, jname, type);
      }
      
      buildParameters(methMap, ie);
      
      int len = methMap.getAttributes().getLength();
      
      // add the method's attributes
      for (int i = 0; i < len; i++)
      {
         Node n = methMap.getAttributes().item(i);
         
         String name  = n.getNodeName();
         String value = n.getNodeValue();
         
         ie.putAttribute(name, value);
      }
      
      ie.initialize();
      
      return ie;
   }
   
   /**
    * Build the parameters for the specified internal entry.
    * 
    * @param   node
    *          The current node from where the parameters are read.
    * @param   ie
    *          The internal entry where to save the parameters.
    */
   private static void buildParameters(Element node, InternalEntry ie)
   {
      // this enumerates all parameter nodes under the node instance (on any
      // level)
      NodeList params = node.getElementsByTagName(ELEM_PARAM);
      
      if (params == null)
      {
         return;
      }
      
      int plen = params.getLength();
      
      ArrayList<Parameter> list = new ArrayList<>();
      
      // process each method mapping
      for (int j = 0; j < plen; j++)
      {
         Element param = (Element) params.item(j);
         
         // keep only parameters which belong to the given node
         if (param.getParentNode() != node)
         {
            continue;
         }
         
         String ppname = param.getAttribute(ATTR_PNAME);
         String ptype  = param.getAttribute(ATTR_TYPE);
         String pmode  = param.getAttribute(ATTR_PARAM_MODE);
         
         if (!("TABLE".equalsIgnoreCase(ptype)  ||
               "BUFFER".equalsIgnoreCase(ptype) ||
               "DATASET".equalsIgnoreCase(ptype)) &&
             (ppname == null || ppname.length() == 0))
         {
            String errmsg = "Malformed parameter entry for " + param.toString() +
                            ", in method " + node.toString();
            throw new IllegalStateException(errmsg);
         }
         
         Parameter p = new Parameter(ppname, ptype, pmode);
         
         // add the parameter's attributes
         int attrLen = param.getAttributes().getLength();
         
         for (int i = 0; i < attrLen; i++)
         {
            Node n = param.getAttributes().item(i);
            String attrName = n.getNodeName();
            String attrValue = n.getNodeValue();
            
            p.putAttribute(attrName, attrValue);
         }
         
         p.setQualified(p.getAttribute("qualified"));
         p.setExtent(p.getAttribute("extent"));
         
         list.add(p);
      }
      
      if (plen > 0)
      {
         list.trimToSize();
         ie.setParameterList(list);
      }
   }
   
   /**
    * Build the internal-entries 4GL attribute for the given external program.
    * 
    * @param    extProg
    *           The {@link ExternalProgram} instance.
    * @param    prv
    *           Flag to include or not the private members.
    *           
    * @return   See above.
    */
   private static String buildInternalEntries(ExternalProgram extProg, boolean prv)
   {
      // - PRIVATE members are not visible
      // - prototypes (such as IN SUPER or IN handle functions/procs) appear in this list 
      // - FORWARD functions without implementations do not appear
      // - first are procedures, then functions, then IN SUPER procedures are put last (in
      //   reverse order)
      
      StringBuilder sbSuperProcs = new StringBuilder();
      StringBuilder sbProcs = new StringBuilder();
      StringBuilder sbFuncs = new StringBuilder();
      List<String> superProcs = new LinkedList<>();
      
      for (InternalEntry ie : extProg.ieMap.values())
      {
         StringBuilder sb = null;
         
         if (ie.isPrivate() && !prv)
         {
            continue;
         }
         
         if (ie.isFunction())
         {
            sb = sbFuncs;
         }
         else
         {
            if (ie.isSuper())
            {
               superProcs.add(0, ie.pname);
               continue;
            }
            
            sb = sbProcs;
         }
         
         if (sb.length() > 0)
         {
            sb.append(',');
         }
         
         sb.append(ie.pname);
      }
      
      for (String psuper : superProcs)
      {
         if (sbSuperProcs.length() > 0)
         {
            sbSuperProcs.append(',');
         }
         
         sbSuperProcs.append(psuper);
      }
      
      StringBuilder sb = new StringBuilder();
      
      sb.append(sbProcs);
      
      if (sb.length() > 0 && sbFuncs.length() > 0)
      {
         sb.append(',');
      }
      
      sb.append(sbFuncs);
      
      if (sb.length() > 0 && sbSuperProcs.length() > 0)
      {
         sb.append(',');
      }
      
      sb.append(sbSuperProcs);
      
      return sb.toString();
   }
   
   /**
    * Build a map of legacy method names to their Java {@link Method}, for the given external program.
    * <p>
    * Only methods marked with {@link LegacySignature} are included.
    */
   static void buildLegacyProgram(ExternalProgram extprog)
   {
      Class<?> cls = null;
      try
      {
         cls = Class.forName(addPkg(extprog.jname));
      }
      catch (ClassNotFoundException e)
      {
         throw new RuntimeException(e);
      }

      Method[] methods = cls.getDeclaredMethods();
      for (Method mthd : methods)
      {
         if (!mthd.isSynthetic() && mthd.isAnnotationPresent(LegacySignature.class))
         {
            LegacySignature ls = mthd.getAnnotation(LegacySignature.class);
            
            InternalEntry ie = null;
            
            if (ls.type() == InternalEntry.Type.MAIN)
            {
               ie = extprog.main;
            }
            else if (ls.type() == InternalEntry.Type.FUNCTION)
            {
               ie = extprog.j2pf.get(mthd.getName());
            }
            else if (ls.type() == InternalEntry.Type.PROCEDURE)
            {
               ie = extprog.j2pp.get(mthd.getName());
            }
            
            if (ie != null)
            {
               ie.setMethod(mthd);
            }
         }
      }
  }
   
   /**
    * Build a legacy class entry by resolving all the legacy methods (marked with the 
    * {@link LegacySignature} annotation) and register them.
    * <p>
    * The {@link InternalEntry} will be linked with the defining Java method.
    * 
    * @param    cls
    *           The Java class associated with the legacy 4GL type.
    *           
    * @return   The built legacy class.
    */
   static LegacyClass buildLegacyClass(Class<?> cls)
   {
      LegacyClass lcls = new LegacyClass();

      Method[] methods = cls.getDeclaredMethods();
      for (Method mthd : methods)
      {
         if (!mthd.isSynthetic() && mthd.isAnnotationPresent(LegacySignature.class))
         {
            LegacySignature ls = mthd.getAnnotation(LegacySignature.class);
            if (ls.type() == InternalEntry.Type.METHOD      || 
                ls.type() == InternalEntry.Type.GETTER      ||
                ls.type() == InternalEntry.Type.SETTER)
            {
               InternalEntry ie = buildInternalEntry(ls, mthd.getName());
               ie.setMethod(mthd);
               
               String key = ie.pname.toLowerCase().intern();
               
               Map<String, List<InternalEntry>> target = 
                  ls.type() == InternalEntry.Type.GETTER  
                     ? lcls.getters 
                     : (ls.type() == InternalEntry.Type.SETTER 
                           ? lcls.setters
                           : lcls.overloads);

               List<InternalEntry> overloads = target.get(key);
               if (overloads == null)
               {
                  target.put(key, overloads = new LinkedList<>());
               }
               overloads.add(ie);
            }
            else if (ls.type() == InternalEntry.Type.CONSTRUCTOR)
            {
               InternalEntry ie = buildInternalEntry(ls, mthd.getName());
               ie.setMethod(mthd);
               
               if (Modifier.isStatic(mthd.getModifiers()))
               {
                  if (lcls.staticCtor != null)
                  {
                     throw new IllegalStateException("More than one static constructor defined for " + cls);
                  }
                  lcls.staticCtor = ie;
               }
               else
               {
                  lcls.ctors.add(ie);
               }
            }
            else if (ls.type() == InternalEntry.Type.DESTRUCTOR)
            {
               InternalEntry ie = buildInternalEntry(ls, mthd.getName());
               ie.setMethod(mthd);
               
               if (lcls.dtor != null)
               {
                  throw new IllegalStateException("More than one static constructor defined for " + cls);
               }
               lcls.dtor = ie;
            }
         }
      }
      
      for (Field f : cls.getDeclaredFields())
      {
         if (!f.isSynthetic() && f.isAnnotationPresent(LegacySignature.class))
         {
            LegacySignature sig = f.getAnnotation(LegacySignature.class);
            if (sig.type() == InternalEntry.Type.PROPERTY && sig.extent() != SourceNameMapper.NO_EXTENT)
            {
               lcls.extents.put(sig.name(), sig.extent());
            }
         }
      }

      lcls.overloads = Collections.unmodifiableMap(lcls.overloads);
      lcls.getters = Collections.unmodifiableMap(lcls.getters);
      lcls.setters = Collections.unmodifiableMap(lcls.setters);
      lcls.extents = Collections.unmodifiableMap(lcls.extents);
      
      // build the destructor list
      Class<?> parent = cls;
      while (BaseObject.class.isAssignableFrom(parent))
      {
         LegacyClass pcls;
         if (parent == cls)
         {
            pcls = lcls;
         }
         else
         {
            registerLegacyClass(parent);

            pcls = classMap.get(parent);
         }
         
         if (pcls.dtor != null)
         {
            if (lcls.dtors == null)
            {
               lcls.dtors = new LinkedList<>();
            }
            lcls.dtors.add(0, pcls.dtor);
         }
         
         parent = parent.getSuperclass();
      }
      
      lcls.executeMethods = resolveExecuteMethods(cls);
      if (lcls.executeMethods.length == 0)
      {
         lcls.executeMethods = null;
      }
      
      return lcls;
   }
   
   /**
    * Build an {@link InternalEntry} matching the specified legacy signature.
    * 
    * @param    ls
    *           The signature.
    * @param    jmeth
    *           The converted Java method.
    *           
    * @return   See above.
    */
   public static InternalEntry buildInternalEntry(LegacySignature ls, Method jmeth)
   {
      if (ls == null || jmeth == null) 
      {
         return null;
      }
      
      InternalEntry ie = buildInternalEntry(ls, jmeth.getName());
      ie.setMethod(jmeth);
      
      return ie;
   }
   
   /**
    * Check whether a {@link Class} is either a legacy class that inherits from {@link _BaseObject_} or is  
    * converted class.
    * 
    * @param clazz the {@link Class} to be tested
    * @return See above
    */
   public static boolean isLegacyOrConvertedClass(Class clazz)
   {
      return _BaseObject_.class.isAssignableFrom(clazz) || // is legacy
         SourceNameMapper.getLegacySourceName(clazz.getName()) != null; // is a converted class
   }
   
   /**
    * Build an {@link InternalEntry} matching the specified legacy signature.
    * 
    * @param    ls
    *           The signature.
    * @param    jname
    *           The converted Java name.
    *           
    * @return   See above.
    */
   static InternalEntry buildInternalEntry(LegacySignature ls, String jname)
   {
      String name = (ls.name().isEmpty() ? jname : ls.name());
      InternalEntry ie = new InternalEntry(name, jname, ls.type());
      
      if (ls.extent() != NO_EXTENT)
      {
         ie.putAttribute("extent", Integer.toString(ls.extent()));
      }
      
      if (!ls.qualified().isEmpty())
      {
         ie.putAttribute("qualified", ls.qualified());
      }
      else if ("object".equalsIgnoreCase(ls.returns()))
      {
         ie.putAttribute("qualified", "progress.lang.object");
      }
      
      List<Parameter> parameters = new LinkedList<>();
      for (LegacyParameter lp : ls.parameters())
      {
         parameters.add(buildParameter(lp));
      }
      ie.setParameterList(parameters);
      ie.initialize();
      
      return ie;
   }
   
   /**
    * Build a {@link Parameter} instance from the specified {@link LegacyParameter} annotation.
    * 
    * @param     lp
    *            The parameter signature.
    *          
    * @return    See above.
    */
   private static Parameter buildParameter(LegacyParameter lp)
   {
      Parameter param = new Parameter(lp.name(), lp.type(), lp.mode());
      
      if (!lp.qualified().isEmpty())
      {
         param.setQualified(lp.qualified());
         param.putAttribute("qualified", lp.qualified());
      }
      else if ("object".equalsIgnoreCase(lp.type()))
      {
         param.setQualified("progress.lang.object");
         param.putAttribute("qualified", "progress.lang.object");
      }
      if (lp.extent() != NO_EXTENT)
      {
         param.setExtent(Integer.toString(lp.extent()));
         param.putAttribute("extent", Integer.toString(lp.extent()));
      }
      else
      {
         param.setExtent(null);
      }
      
      if (!lp.table().isEmpty())
      {
         param.putAttribute("table", lp.table());
      }
      
      if (!lp.buffer().isEmpty())
      {
         param.putAttribute("buffer", lp.buffer());
      }
      
      if (!lp.bufferFor().isEmpty())
      {
         param.putAttribute("for", lp.bufferFor());
      }

      if (lp.append())
      {
         param.putAttribute("append", Boolean.toString(lp.append()));
      }
      
      if (lp.bind())
      {
         param.putAttribute("bind", Boolean.toString(lp.bind()));
      }
      
      if (lp.handleTo())
      {
         param.putAttribute("handle_to", Boolean.toString(lp.handleTo()));
      }

      return param;
   }

   /**
    * Construct a full class name (including packages) for the stored package
    * root and the given class name.
    * 
    * @param    name
    *           Class name.
    * 
    * @return   The fully qualified class name.
    */
   private static String addPkg(String name)
   {
      if (name == null)
      {
         return null;
      }
      
      if (pkgroot == null)
      {
         return name;
      }
      
      return pkgroot + "." + name; 
   }
   
   /**
    * Remove the package prefix from the given class name.
    * 
    * @param    name
    *           Class name.
    * 
    * @return   The class name with the root package prefix removed.
    */
   private static String removePkg(String name)
   {
      if (name == null)
      {
         return null;
      }
      
      if (pkgroot == null)
      {
         return name;
      }
      
      if (name.startsWith(pkgroot))
      {
         name = name.substring(pkgroot.length() + 1);
      }
      
      return name;
   }

   /**
    * Worker to get the path separator from the legacy environment. Since we can have multiple
    * environments configured in multiple sessions, We store this in the WorkAre {@link WorkArea#pathSep} 
    * 
    * @return   String
    *           The path separator from the legacy environment.
    */
   private static String getLegacyPathSeparator()
   {
      WorkArea wa = local.get();
      
      if (wa.pathSep == null)
      {
          wa.pathSep = EnvironmentOps.getLegacyPathSeparator();
      }

      return wa.pathSep;
   }

   /**
    * Worker to get the case-sensitivity from the legacy environment. Since we can have multiple
    * environments configured in multiple sessions, We store this in the WorkAre {@link WorkArea#caseSens} 
    * 
    * @return   boolean
    *           The case-sensitivity from the legacy environment.
    */
   private static boolean getLegacyCaseSensitive()
   {
      WorkArea wa = local.get();
      
      if (wa.caseSens == null)
      {
          wa.caseSens = EnvironmentOps.getLegacyCaseSensitive();
      }

      return wa.caseSens;
   }

   /**
    * Container for all legacy class information.
    */
   private static class LegacyClass
   {
      /** A map of overloaded methods to their signatures.  The key is the legacy method name. */
      private Map<String, List<InternalEntry>> overloads = new HashMap<>();
      
      /** 
       * A map of all overloaded methods, including from super-classes.  The key is an intern'ed string, so an
       * identity map can be used.
       */
      private Map<String, List<InternalEntry>> overrides = new IdentityHashMap<>();
      
      /** A list of overloaded constructors to their signatures. */
      private List<InternalEntry> ctors = new LinkedList<>();
      
      /** The static constructor for this legacy type. */
      private InternalEntry staticCtor = null;
      
      /** The destructor for this legacy type. */
      private InternalEntry dtor = null;
      
      /** The resolved list of the destructors (super-class first). */
      private List<InternalEntry> dtors = null;
      
      /** The execute methods for this type and its hierarchy. */
      private Method[] executeMethods = null;

      /** A map of property getters to their signatures.  The key is the legacy property name. */
      private Map<String, List<InternalEntry>> getters = new HashMap<>();
      
      /** A map of property setters to their signatures.  The key is the legacy property name. */
      private Map<String, List<InternalEntry>> setters = new HashMap<>();
      
      /** The map of each property's extent (where it applies). */
      private Map<String, Integer> extents = new CaseInsensitiveHashMap<>();

      /**
       * Get all overloads of this method name; only the overrides of the methods are included (their super-class
       * definitions matching name and parameters will not be included).
       *  
       * @param    type
       *           The legacy {@link com.goldencode.p2j.oo.lang._BaseObject_} sub-class.
       * @param    iename
       *           The legacy name.
       *           
       * @return   list of the overloaded methods, from all super-classes.
       */
      public synchronized List<InternalEntry> getOverrides(Class<?> type, String iename)
      {
         // do not lowercase - the space of possible case-insensitive variations is usually small, and just
         // spend some space to save the list for these cases, instead of lowercasing always.
         String key = iename.intern();
         List<InternalEntry> res = overrides.get(key);
         
         if (res != null)
         {
            return res;
         }
         
         res = new ArrayList<>();
         overrides.put(key, res);
         
         LinkedHashSet<Class<?>> types = new LinkedHashSet<>();
         types.add(type);
         if (type.isInterface())
         {
            Utils.collectInterfaces(type, types);
         }
         else
         {
            Class<?> cls = type;
            while (cls != Object.class)
            {
               types.add(cls);
               cls = cls.getSuperclass();
            }
         }
         
         for (Class<?> cls : types)
         {
            if (!_BaseObject_.class.isAssignableFrom(cls))
            {
               continue;
            }
            
            List<InternalEntry> l = SourceNameMapper.getOverloads(cls, iename);
            if (l != null)
            {
               for (InternalEntry ie : l)
               {
                  boolean ok = true;
                  
                  for (InternalEntry oie : res)
                  {
                     if (oie.getMethod().getName().equals(ie.getMethod().getName()) &&
                         Arrays.equals(oie.getMethod().getParameterTypes(), ie.getMethod().getParameterTypes()))
                     {
                        ok = false;
                        break;
                     }
                  }
                  
                  if (ok)
                  {
                     res.add(ie);
                  }
               }
            }
         }
         
         return res;
      }
   }
   
   /**
    * Container for all external program information.
    */
   static class ExternalProgram
   {
      /** Legacy 4GL name for this external program. */
      final String pname;

      /** The legacy 4GL name for this external program without the extension. */
      final String pnameNoExt;

      /** The extension of the legacy 4GL name for this external program. */
      final String pnameExt;
      
      /** Converted Java name for this external program. */
      final String jname;
      
      /** The named events published by this procedure. */
      final String publishedEvents;
      
      /** The qualified OO name if this is a legacy class or interface. */
      String ooname = null;
      
      /** 
       * List of all internal entries, as they appear in the name_map file.
       * This is needed to provide consistency for the internal-entries 
       * attribute (as a function and a procedure may have the same name,
       * a map can not be used).
       */
      Map<InternalEntryKey, InternalEntry> ieMap = new LinkedHashMap<>();
      
      /**
       * Provide fast access to the java name of a specified legacy function
       * name.
       */
      Map<String, InternalEntry> p2jf = Collections.EMPTY_MAP;
      
      /**
       * Provide fast access to the java name of a specified legacy procedure
       * name.
       */
      Map<String, InternalEntry> p2jp = Collections.EMPTY_MAP;
      
      /**
       * Provide fast access to the legacy name of a specified java function
       * name.
       */
      Map<String, InternalEntry> j2pf = Collections.EMPTY_MAP;
      
      /**
       * Provide fast access to the legacy name of a specified java procedure
       * name.
       */
      Map<String, InternalEntry> j2pp = Collections.EMPTY_MAP;
      
      /** 
       * The <code>internal-entry</code> attribute for this external program,
       * without private members.
       */
      String internalEntries = null;
      
      /** 
       * The <code>internal-entry</code> attribute for this external program,
       * with private members.
       */
      String internalEntriesPrv = null;
      
      /** The internal-entry associated with the MAIN (external) procedure. */
      InternalEntry main = null;
      
      /**
       * Basic c'tor.
       * 
       * @param    pname
       *           Legacy 4GL name for this external program.
       * @param    jname
       *           Converted Java name for this external program.
       * @param    publishedEvents
       *           The named events published by this procedure.
       */
      ExternalProgram(String pname, String jname, String publishedEvents) 
      {
         this.pname = pname.intern();
         this.jname = jname.intern();
         this.publishedEvents = (publishedEvents == null ? "" : publishedEvents.intern());

         int lastDotIdx = pname.lastIndexOf('.');
         pnameNoExt = lastDotIdx > 0 ? pname.substring(0, lastDotIdx) : pname;
         pnameExt = lastDotIdx > 0 ? pname.substring(lastDotIdx + 1) : null;
      }
      
      /**
       * Gets the <code>internal-entry</code> attribute for this external
       * program.  If this is the first access, it initializes it.
       * 
       * @param    prv
       *           Flag to include or not the private members.
       *           
       * @return   See above.
       */
      String getInternalEntries(boolean prv)
      {
         if (prv)
         {
            if (internalEntriesPrv == null)
            {
               internalEntriesPrv = buildInternalEntries(this, prv).intern();
            }
            
            return internalEntriesPrv;
         }
         else
         {
            if (internalEntries == null)
            {
               internalEntries = buildInternalEntries(this, prv).intern();
            }
            
            return internalEntries;
         }
      }
      
      /**
       * Search the {@link #ieMap internal-entry list} for the specified 
       * name, using the <code>function</code> flag to search only for 
       * functions or only for procedures.
       * 
       * @param    pname
       *           Legacy 4GL name for the search internal-entry.  When <code>null</code>, the 
       *           target is always the external program.
       * @param    function
       *           <code>true</code> if search for a function
       * 
       * @return   The {@link InternalEntry} instance, or <code>null</code> if
       *           it was not found.
       */
      InternalEntry search(String pname, boolean function)
      {
         if (pname == null || pname.isEmpty())
         {
            // this is the external program
            return main;
         }
         
         InternalEntryKey key = new InternalEntryKey(function, pname);
         
         return ieMap.get(key);
      }
      
      /**
       * Search the fast-access p2j maps for a function or procedure, using 
       * the <code>function</code> flag.
       * 
       * @param    pname
       *           Legacy 4GL name for the search internal-entry.
       * @param    function
       *           <code>true</code> if search for a function
       * 
       * @return   The {@link InternalEntry} instance, or <code>null</code> if
       *           it was not found.
       */
      InternalEntry searchp(String pname, boolean function)
      {
         Map<String, InternalEntry> p2j = function ? p2jf : p2jp;
         
         return p2j.get(pname.toLowerCase());
      }
      
      /**
       * Search the fast-access j2p maps for a function or procedure. This
       * ignores the IN SUPER and IN HANDLE entries, as it is expected to
       * always find a method with its body in this {@link ExternalProgram}.
       * 
       * @param    jname
       *           Java method name.
       * 
       * @return   The {@link InternalEntry} instance, or <code>null</code> if
       *           it was not found.
       */
      InternalEntry searchLocalj(String jname)
      {
         // in this case, we need only internal entries which body resides
         // in this procedure. so, IN SUPER and IN HANDLE entries are ignored.
         
         InternalEntry ie = j2pf.get(jname);
         if (ie != null && !ie.isInHandle() && !ie.isSuper())
         {
            return ie;
         }
         
         ie = j2pp.get(jname);
         if (ie != null && !ie.isInHandle() && !ie.isSuper())
         {
            return ie;
         }
         
         return null;
      }

      @Override
      public boolean equals(Object o)
      {
         if (this == o)
         {
            return true;
         }
         if (o == null || getClass() != o.getClass())
         {
            return false;
         }
         ExternalProgram that = (ExternalProgram) o;
         return pname.equals(that.pname);
      }

      @Override
      public int hashCode()
      {
         return Objects.hash(pname);
      }
   }
   
   /**
    * Map key for internal entry objects.
    */
   private static class InternalEntryKey
   {
      /** Does entry represent a function? */
      private final boolean function;
      
      /** Normalized (lowercased) legacy entry name */
      private final String name;
      
      /** The cached hash code. */
      private final int hashCode;
      
      /**
       * Constructor.
       * 
       * @param   function
       *          Does entry represent a function?
       * @param   name
       *          Legacy name.
       */
      private InternalEntryKey(boolean function, String name)
      {
         this.function = function;
         this.name = name;

         int result = 17;
         result = 37 * result + (function ? 5 : 3);
         result = 37 * result + StringHelper.hashCodeCaseInsensitive(name);
         this.hashCode = result;
      }
      
      /**
       * Generate a hash code consistent with {@link #equals(Object)}.
       * 
       * @return  Hash code.
       */
      @Override
      public int hashCode()
      {
         return hashCode;
      }
      
      /**
       * Determine equivalence with the given key object.
       * 
       * @return  {@code true} if the internal states of this key and the given key match,
       *          else {@code false}.
       */
      @Override
      public boolean equals(Object o)
      {
         InternalEntryKey that = (InternalEntryKey) o;
         
         return (this.function == that.function && this.name.equalsIgnoreCase(that.name));
      }
   }
   
   /**
    * A cache class that contains the propath and a map of indexes for search-specs mapped to their
    * target procedures (the result obtained from {@link SourceNameMapper#fastConvertName(String)}).
    * 
    * The propath is used for double-checking the cache when it is read.
    */
   private static class SourceNameMapperCache
   {
      /** Propath for which the cache was created */
      private String[] propath;
      
      /** 
       * A map of program indexes mapped to their target procedures.
       * The program index is obtained from {@link SourceNameMapper#initializationPaths} and
       * is used instead of the search-spec to reduce the number of Java Strings being used.
       */
      private LRUCache<Integer, String[]> convertedNames;
      
      /** Lock for read/write operations related to convertedNames. */
      private ReentrantReadWriteLock lock;

      /**
       * Constructor
       * 
       * @param   propath
       *          The propath for which the cache is created.
       * @param   convertedNames
       *          Initial map of indexed search-specs mapped to their target procedure.
       */
      private SourceNameMapperCache(String[] propath, LRUCache<Integer, String[]> convertedNames)
      {
         this.propath = propath;
         this.convertedNames = convertedNames;
         this.lock = new ReentrantReadWriteLock();
      }
      
      /**
       * Returns the propath of the cache.
       * 
       * @return  the propath
       */
      private String[] getPropath() 
      {
         return propath;
      }
      
      /**
       * Check if the given program name has an entry in the converted names map.
       * 
       * @param   programIndex
       *          The index of the program name that is checked.
       *          
       * @return  <code>true</code> if the program was previously converted and has
       *          an entry in the map, <code>false</code> otherwise.
       */
      private boolean hasProgram(Integer programIndex)
      {
         return convertedNames.containsKey(programIndex);
      }
      
      /**
       * Given an index of a program name, check if it is mapped to any target procedure and
       * return it if so.
       * 
       * @param   programIndex
       *          The index of the program name that is searched.
       *          
       * @return  The target procedure or <code>null</code> if there is none;
       */
      private String[] getResolvedProgram(Integer programIndex)
      {
         return convertedNames.get(programIndex);
      }
      
      /**
       * Add a new converted result of a program using the index of the program.
       * 
       * @param   programIndex
       *          The index of the program which is used as key.
       * @param   result
       *          The result obtained from fastConvertName of the program.
       */
      private void addResolvedProgram(Integer programIndex, String[] result)
      {
         convertedNames.put(programIndex, result);
      }
      
      /**
       * Lock read operation.
       */
      private void readLock()
      {
         lock.readLock().lock();
      }
      
      /**
       * Unlock read operation.
       */
      private void readUnlock()
      {
         lock.readLock().unlock();
      }
      
      /**
       * Lock write operation.
       */
      private void writeLock()
      {
         lock.writeLock().lock();
      }
      
      /**
       * Unlock write operation.
       */
      private void writeUnlock()
      {
         lock.writeLock().unlock();
      }
   }
   
   /**
    * A thread factory class that is used to create AssociatedThread(s) for creating source cache entries.
    * The threads will have a lower priority.
    */
   private static class SourceThreadFactory
   implements ThreadFactory
   {
      @Override
      public Thread newThread(Runnable r)
      {
         Thread workThread = new AssociatedThread(r);
         workThread.setName("Source cache entry task");
         workThread.setPriority(4);
         return workThread;
      }
   }
   
   /**
    * A custom Runnable class that monitors identifiers represented by the propath hash code used
    * by the {@link MonitorRunnable#task} to create a source cache entry. This identifier is used
    * to avoid queuing source cache entry threads for the same propath.
    */
   private static class MonitorRunnable
   implements Runnable 
   {
      /** Set of all identifiers for which a source cache entry task was created. */
      private static final Set<Long> identifiers = new HashSet<>();
      
      /** The task that needs to be executed to create a new source cache entry. */
      private final Runnable task;
      
      /**
       * Basic constructor that initializes the class members.
       * 
       * @param   identifier
       *          The propath hash code used by the task.
       * @param   task
       *          The task that needs to be executed to create a new source cache entry.
       */
      private MonitorRunnable(Long identifier, Runnable task)
      {
         synchronized (identifiers)
         {
            identifiers.add(identifier);
         }
         this.task = task;
      }
      
      /**
       * Override of the run method for the Runnable that only runs the task provided when instantiating
       * the object.
       */
      @Override
      public void run()
      {
         task.run();
      }
      
      /**
       * Method that checks if the identifier provided was previously submitted.
       * 
       * @return  {@code true} if there is no identifier submitted, {@code false} otherwise.
       */
      private static boolean hasIdentifier(Long identifier)
      {
         synchronized (identifiers)
         {
            return identifiers.contains(identifier);
         }
      }
      
      /**
       * Method that clears the identifier of a source cache entry after it is created.
       * 
       * @param   identifier
       *          The identifier for the source cache entry that needs to be removed.
       */
      private static void removeIdentifier(Long identifier)
      {
         synchronized (identifiers)
         {
            identifiers.remove(identifier);
         }
      }
   }
   
   /**
    * This interface is implemented by classes which search for a program name
    * representing a Progress procedure file name that can be matched to
    * an existent map table of Java classes.
    */
   private interface PathLookup
   {
      /**
       * Looks up the given program name based on the propath and the case sensitivity
       * values of the WorkArea.
       *
       * @param   legacyProgName
       *          The program name that is searched.
       * @param   propath
       *          The propath of the current context.
       * @param   caseSens
       *          The case sensitivity of the current context.
       *          
       * @return  The program name which represents a Progress procedure file name
       *          if there is a match or <code>null</code> if there is no match.
       */
      public String lookupPath(String legacyProgName, String[] propath, boolean caseSens);
   }
   
   /**
    * A lookup class which stores possible paths of a program and can be used to retrieve
    * the same ExternalProgram for each of those paths. A possible path is either
    * the unqualified program name or any path pre-pended by a propath value.
    */
   private static class SinglePathLookup 
   implements PathLookup
   {
      /** The full path to the program */
      private String programName;
      
      /** The unqualified program name */
      private String simpleProgramName;
      
      /**
       * Constructor which initializes the program name.
       * 
       * @param   programName
       *          The full path of the program.
       */
      public SinglePathLookup(String programName)
      {
         this.programName = programName;
         int fileSepIndex = programName.lastIndexOf(fileSep);
         if (fileSepIndex > 0)
         {
            this.simpleProgramName = programName.substring(fileSepIndex + 1);
         }
         else 
         {
            this.simpleProgramName = programName;
         }
      }
      
      /**
       * Getter for the simpleProgramName property.
       * 
       * @return  The unqualified program name.
       */
      public String getSimpleProgramName()
      {
         return simpleProgramName;
      }
      
      /**
       * Looks up the given program name based on the propath and the case sensitivity
       * values of the WorkArea. It will try to match the program name with the existing
       * full path of the instance to return early. 
       * If there are path aliases in use, then it will try to resolve the program name
       * before trying the same after removing any pre-pended propath.
       * 
       * @param   legacyProgName
       *          The program name that is searched.
       * @param   propath
       *          The propath of the current context.
       * @param   caseSens
       *          The case sensitivity of the current context.
       *
       * @return  The program name which represents a progress procedure file name
       *          if there is a match or <code>null</code> if there is no match.
       */
      public String lookupPath(String legacyProgName, String[] propath, boolean caseSens)
      {
         String answerProgram = caseSens ? programName : programName.toLowerCase(); 
         if (legacyProgName.equals(answerProgram))
         {
            return programName;
         }
         
         if (pathAliases != null)
         {
            String[] aliases = resolvePathAliases(legacyProgName);
            for (int i = 0; i < aliases.length; i++)
            {
               if (aliases[i].equals(answerProgram))
               {
                  return programName;
               }
            }
         }
         
         for (int i = 0; i < propath.length; i++)
         {
            String filterProgram = answerProgram;
            String currentPropath = propath[i];

            // This should be the case already handled by the short-circuit above
            // the only exception is that the empty or "." propath can be found
            // much later in the array.
            if (currentPropath.isEmpty() || currentPropath.equals("."))
            {
               continue;
            }
            
            if (filterProgram.startsWith(currentPropath))
            {
               String possiblePath;
               int propathLength = currentPropath.length();
               if (filterProgram.charAt(propathLength) == fileSepChar)
               {
                  possiblePath = filterProgram.substring(propathLength + 1);
               }
               else 
               {
                  possiblePath = filterProgram.substring(propathLength);
               }
               
               if (possiblePath.equals(legacyProgName))
               {
                  return programName;
               }
               
               if (pathAliases != null)
               {
                  String[] aliases = resolvePathAliases(possiblePath);
                  for (int j = 0; j < aliases.length; j++)
                  {
                     if (aliases[j].equals(legacyProgName))
                     {
                        return programName;
                     }
                  }
               }
            }
         }
         return null;
      }
   }
   
   /**
    * A lookup class which stores multiple SinglePathLookup instances. When looking up
    * a program name, all references are searched for the path and if only ONE match is
    * found then it is returned. Multiple answers cause a conflict and in this case
    * <code>null</code> is returned.
    */
   private static class MultiPathLookup 
   implements PathLookup
   {
      /** A list of lookup instances for the same program name */
      private List<SinglePathLookup> references;
      
      /**
       * Constructor which initializes the list of SinglePathLookup instances.
       */
      public MultiPathLookup()
      {
         references = new ArrayList<>();
      }
      
      /**
       * Returns a match after looking through the list of SinglePathLookup instances.
       * If the is more than one match found, it will short-circuit the lookup and
       * return <code>null</code>.
       * 
       * @param   legacyProgName
       *          The program name that is searched.
       * @param   propath
       *          The propath of the current context.
       * @param   caseSens
       *          The case sensitivity of the current context.
       *
       * @return  The program name which represents a progress procedure file name
       *          if there is only one match found or <code>null</code> if there are
       *          multiple matches or none could be found. 
       */
      public String lookupPath(String legacyProgName, String[] propath, boolean caseSens)
      {
         String result = null;
         for (SinglePathLookup spl : references)
         {
            String possibleResult = spl.lookupPath(legacyProgName, propath, caseSens);
            if (result == null && possibleResult != null)
            {
               result = possibleResult;
               continue;
            }
            
            // The result is already set, so any possible result is a conflict and should return null
            // in order to fallback to the old implementation
            if (possibleResult != null)
            {
               return null;
            }
         }
         return result;
      }
   }
   
   /**
    * Container for context-local data.
    */
   private static class WorkArea
   {
      /** The current context's PROPATH value (prepared, depending on the OS). */
      private String[] propath = null;
      
      /** The current context's PROPATH value (unprepared). */
      private String[] originalPropath = null;
      
      /** Path separator used for the Progress file name data. */
      private String pathSep = null;
   
      /** Case sensitivity flag for the Progress file name data. */
      private Boolean caseSens = null;

      /** Windows flag */
      private boolean isWindows;
      
      /** Hash value of the propath that is computed when it is set. */
      private Long propathHash = null;
   }
}