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