# UastHints.java

    /*
    ** Module   : UastHints.java
    ** Abstract : reads a UAST hints file and cleanly exposes the contents
    **
    ** Copyright (c) 2005-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 GES 20050204   @19572 Created initial version. This supports
    **                           reading an XML file that "lives" in the same
    **                           location (and basename) as the associated
    **                           Progress 4GL source file to which it refers.
    **                           The hints file is the source file's basename
    **                           + ".hints".  At this time the only hint
    **                           supported is a database hint (to load a list
    **                           of databases into the schema dictionary
    **                           before parsing starts).
    ** 002 GES 20050207   @19622 Added runtime preprocessor arguments
    **                           support.
    ** 003 GES 20050210   @19751 Added alias support and the capability to
    **                           preload the variable/widget dictionaries.
    ** 004 GES 20050221   @19907 Moved token type to symbolic name lookup
    **                           helper to the ProgressParser which is a
    **                           more natural place for it.
    ** 005 GES 20050314   @20325 Added support for generic UAST hints that
    **                           must be uniquely named (within a given file)
    **                           and the value can be one of the following
    **                           data types: boolean, long, double, string,
    **                           string array. Each uniquely named value
    **                           can be obtained using the name as a key.
    ** 006 GES 20050316   @20401 Added a method to get a generic UAST hint's
    **                           class name (data type) and a mechanism to
    **                           detect if a specific generic hint exists
    **                           or not.
    ** 007 GES 20050331   @20562 Minor modification to match change in
    **                           Variable constructor.
    ** 008 ECF 20050421   @20810 Import p2j.xml package to use XmlHelper.
    ** 009 ECF 20050713   @21699 Added schema hints. Schema hints are used to
    **                           override name conversions at the table and
    **                           field level, and to define new associations
    **                           between tables, or to provide supplemental
    **                           data for existing associations.
    ** 010 SIY 20050823   @22197 Fixed loading of multiple UAST hints.
    ** 011 GES 20070313   @32398 Added support for various file-level propath
    **                           customizations.
    ** 012 GES 20070321   @32501 Protect from a null ptr when the source file
    **                           has no pathing.
    ** 013 GES 20070322   @32549 Added support for directory level hints.
    ** 014 GES 20070403   @32716 Added support for ignoring backslash as an
    **                           escape sequence in the preprocessor.
    ** 015 GES 20070423   @33207 Added support for overriding the marker
    **                           character in the preprocessor.
    ** 016 GES 20070717   @34581 Honor ${SYMBOL} style substitutions in the
    **                           propath. This allows these entries to act
    **                           like those in the project level config file.
    ** 017 GES 20070816   @34891 Added support for overriding the BATCH-MODE,
    **                           OPSYS and WINDOW-SYSTEM built-in variables
    **                           of the preprocessor. The other preprocessor
    **                           flags now support a global override in the
    **                           project configuration.
    ** 018 GES 20070904   @34951 Added E4GL preprocessing hints support.
    ** 019 GES 20070919   @35139 Added class name support to variable hints.
    ** 020 GES 20080324   @37667 Changed preprocessor arguments to encode
    **                           each one as a separate XML element instead
    **                           of the more cumbersome encoded string.
    ** 021 ECF 20080704   @39146 Minor memory optimization. Use Boolean
    **                           constants instead of instantiating new
    **                           Boolean objects.
    ** 022 GES 20081008   @40357 Directory hints are honored at any level of
    **                           parent directory from the most specific
    **                           directory and being loaded in turn from each
    **                           successive enclosing parent until reaching
    **                           the top-level base path.
    ** 023 GES 20090429   @42066 Match package and class name changes.
    ** 024 GES 20090515   @42232 Import change.
    ** 025 GES 20110623          Cleanups.
    ** 026 GES 20111014          Documentation fix.
    ** 027 CA  20140424          Allow empty-array string hints.
    ** 028 VMN 20140427          Added support for literal replacements.
    ** 029 IAS 20160404          Configuration.substituteParms() is not static now.
    ** 030 ECF 20160407          Added processing related to call graph hints.
    ** 031 IAS 20160619          Added support for the "read-only" hint
    ** 032 GES 20170303          Added preprocessor debug option.
    ** 033 HC  20170708          Added source-charset uast preprocessor hint.
    ** 032 CA  20180321          Added support for PROCESS-ARCHITECTURE preprocessor var.
    ** 033 GES 20190227          Ensure that the sourceCharset is not null.
    ** 034 OM  20200108          Added support for "dirty-read" hint/attribute.
    ** 035 GES 20200626          Try to detect the current JVM's pointer size to be the default process
    **                           architecture.
    ** 036 RFB 20200929          Use the Configuration default charset, not the JVM default charset.  Ref #4859.
    **     IAS 20210219          Added factory method and cache.
    **     OM  20211122          Added hints-based support for 'unloading' schema.
    **     TJD 20220504          Java 11 compatibility related minor changes
    ** 039 VVT 20230318          Unit test configuration support. See #6237.
    ** 040 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 041 SVL 20230516          Support for dirty-intra-read annotation.
    */

    /*
    ** 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.uast;

    import java.io.*;
    import java.nio.charset.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.logging.*;

    import com.goldencode.p2j.convert.*;
    import org.w3c.dom.*;
    import com.goldencode.ast.*;
    import com.goldencode.util.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.schema.*;

    /**
     * Loads hints information from a UAST hints XML file into memory and
     * exposes the data in an easily accessible format. The purpose of this class
     * is to separate the hints persistence mechanism (i.e., XML) from the user
     * of these hints.
     * <p>
     * There are 2 levels of hints files:
     * <p>
     * <ol>
     *    <li> Directory - hints that apply to all source files loaded from that
     *         specific path or from any paths which are enclosed.
     *    <li> File - hints that apply only to the exact source file being loaded.
     * </ol>
     * <p>
     * The directory hints are loaded first and then file-specific hints are
     * loaded.  This allows generic (directory level) processing to be overridden
     * by more specific values at the file level.
     * <p>
     * Directory hints are loaded in the order found.  The search starts in the
     * base path (top-level source directory) and then walks downward to each
     * more deeply nested directory until the directory containing the file is
     * searched. This means that all directory hints files that exist in that
     * path will be loaded (the search does not stop when the first directory
     * hints file is found). Also note that any value in a more specific
     * directory may override a more enclosing directory hint.
     * <p>
     * Note that the format of both type of hints files are the same and there is
     * no provision for removing a hint that is specified in a previously loaded
     * hints file.  Each specific type of hint handles this differently. Sometimes
     * the hint will work as an addition, but in other cases the hint acts as a
     * replacement.  Here is the guide:
     * <p>
     * <pre>
     * Hint Type                    Edit Type
     * -------------------------    -------------------------
     * database                     adds to end of the list
     * alias                        adds to end of the list
     * variable                     adds to end of the list
     * preprocessor args            replacement
     * preprocessor unix escapes    replacement
     * preprocessor marker          replacement
     * preprocessor batch mode      replacement
     * preprocessor opsys           replacement
     * preprocessor window system   replacement
     * preprocessor debug           replacement
     * propath                      replacement
     * schema hints                 replacement
     * callgraph rootlist           replacement
     * callgraph include-spec       replacement
     * embedded 4GL preprocessor    replacement
     * generic uast                 replacement
     * </pre>
     * <p>
     * Once constructed, the hints file has been loaded into memory and the
     * contents are available via simple 'getter' method calls such as
     * {@link #getDatabases}. This class hides the storage mechanism and the
     * file format from the callers.  It also handles any conversion from the
     * string oriented XML format to the proper data type of the value in
     * question.  Generally, if the hints file doesn't exist or if the value
     * being queried doesn't exist in that hints file, the result from a
     * lookup will be <code>null</code>.
     * <p>
     * There is support for generic UAST hints that must be uniquely named
     * (within a given file) and the value can be one of the following data
     * types: boolean, long, double, string, string array. Each uniquely named
     * value can be obtained using the name as a key.
     * <p>
     * The following is the structure of the XML file:
     * <p>
     * <ul>
     *    <li> hints (root element)
     *    <ul>
     *       <li> database (element, can be 0 or more)
     *       <ul>
     *          <li> name (attribute, the name of the database to load into the
     *               schema dictionary before parsing starts)
     *       </ul>
     *       <li> alias (element, can be 0 or more)
     *       <ul>
     *          <li> name (attribute, the name of the alias to create in the
     *               schema dictionary before parsing starts)
     *          <li> database (attribute, the name of the database to associated
     *               with the alias in the schema dictionary before parsing
     *               starts)
     *       </ul>
     *       <li> variable (element, can be 0 or more)
     *       <ul>
     *          <li> name (attribute, the name of the variable or widget to add
     *               to the variable/widget dictionary before parsing starts)
     *          <li> type (attribute, the non-negative integer token type of
     *               the variable/widget)
     *          <li> classname (optional attribute, the fully qualified name of
     *               the class of an variable that represents an object instance)
     *       </ul>
     *       <li> preprocessor (element, can be 0 or 1)
     *       <ul>
     *          <li> argument (element, can be 0 or 1)
     *          <ul>
     *             <li> name (optional attribute, the name of the argument)
     *             <li> pos (optional attribute, the positional number of the
     *                  argument, which will be used to create a name for the
     *                  argument)
     *             <li> one of either the name or pos attributes must be present to
     *                  allow this value to be added to the arguments map
     *             <li> value (attribute, the exact string encoding of the
     *                  specific argument to be passed to the preprocessor)
     *          </ul>
     *          <li> unix-escapes (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, the string "true" or "false") - honors
     *                  a system-wide default
     *          </ul>
     *          <li> marker (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, a base-10 integer between 0 and 255
     *                  inclusive) - honors a system-wide default
     *          </ul>
     *          <li> batch (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, the string "true" or "false") - honors
     *                  a system-wide default
     *          </ul>
     *          <li> opsys (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, the exact string encoding the operating
     *                  system to be returned by the preprocessor's OPSYS
     *                  built-in) - honors a system-wide default
     *          </ul>
     *          <li> winsys (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, the exact string encoding the window
     *                  system to be returned by the preprocessor's WINDOW-SYSTEM
     *                  built-in) - honors a system-wide default
     *          </ul>
     *          <li> debug (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, the string "true" or "false") - honors
     *                  a system-wide default
     *          </ul>
     *       </ul>
     *       <li> propath (element, can be 0 or 1)
     *       <ul>
     *          <li> type (required attribute), must be one of the following
     *               text strings
     *          <ul>
     *             <li> replace
     *             <li> prepend
     *             <li> append
     *             <li> dynamic-prepend
     *             <li> dynamic-append
     *          </ul>
     *          <li> value (required attribute for types replace, prepend and
     *               append, ignored otherwise; encodes the exact string to
     *               be used as the replacement propath, to be prepended to the
     *               propath or to be appended to the propath), parameters from
     *               the project configuration can be referenced as substitutions
     *               to allow the path values to be made project-relative (use
     *               something like "${P2J_HOME}/my/src/path:")
     *       </ul>
     *       <li> schema (element, can be 0 or 1)
     *       <ul>
     *          <li> table (element, can be 1 or more)
     *          <ul>
     *             <li> name (required attribute, name of the database table)
     *             <li> field (element, can be 0 or more)
     *             <ul>
     *                <li> name (required attribute, name of the database table)
     *                <li> phrase (element, can be 0 or more)
     *                <ul>
     *                   <li> match (required attribute, text to match within the
     *                        field name being converted)
     *                   <li> replace (required attribute, text to substitute
     *                        for matched text within the field name being
     *                        converted)
     *                </ul>
     *             </ul>
     *             <li> phrase (element, can be 0 or more)
     *             <ul>
     *                <li> match (required attribute, text to match within the
     *                     table/field name being converted)
     *                <li> replace (required attribute, text to substitute for
     *                     matched text within the field/table name being
     *                     converted)
     *                <li> no-inherit (optional attribute, true to apply this
     *                     match phrase to only the table name, but not the field
     *                     children; if false or missing, the phrase is inherited
     *                     by all fields of the table)
     *             </ul>
     *             <li> association (element, can be 0 or more)
     *             <ul>
     *                <li> type (required attribute, type of association between
     *                     this table and another: one-to-one, many-to-one, or
     *                     one-to-many)
     *                <li> references (required attribute, name of table
     *                     referenced by this association)
     *                <li> foreign (optional attribute, true if this table
     *                     represents the foreign key end of a one-to-one
     *                     association)
     *                <li> field (element, can be 0 or more)
     *                <ul>
     *                   <li> name (required attribute, name of field in <i>this
     *                        table</i> which is used to join the tables in this
     *                        association)
     *                   <li> references (required attribute, name of field in
     *                        <i>the referenced table</i> which is used to join
     *                        the tables in this association)
     *                </ul>
     *             </ul>
     *          </ul>
     *       </ul>
     *       <li> callgraph (element, can be 0 or 1)
     *       <ul>
     *          <li> rootlist (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, the path to a rootlist XML file)
     *          </ul>
     *          <li> include-spec (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, a specification for included files)
     *          </ul>
     *       </ul>
     *       <li> e4gl (embedded 4GL preprocessor element, can be 0 or 1)
     *       <ul>
     *          <li> wsoptions (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, the exact string encoding all webspeed
     *                  options to be passed to the preprocessor for this file)
     *                  - honors a system-wide default
     *          </ul>
     *          <li> mode (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, the string name of the preprocessor
     *                  compatibility mode) - honors a system-wide default
     *          </ul>
     *          <li> output (element, can be 0 or 1)
     *          <ul>
     *             <li> value (attribute, file name of the 4GL source file that
     *                  will be generated from the HTML input file)
     *          </ul>
     *       </ul>
     *       <li> uast
     *       <ul>
     *          <li> name (attribute, the unique name of the hint which will be
     *               used as a key)
     *          <li> datatype (attribute, the name of the value's data type), can
     *               be one of the following (case is ignored):
     *          <ul>
     *             <li> string
     *             <li> string[]
     *             <li> long
     *             <li> double
     *             <li> boolean
     *          </ul>
     *          <li> value (attribute, the exact string encoding the value of the
     *               hint, except when the datatype is string array)
     *          <li> array-val (element, 1 per array entry when the type is
     *               string array, otherwise it is absent)
     *             <ul>
     *                <li> value (attribute, the exact string encoding the value
     *                     of the entry in the array)
     *             </ul>
     *       </ul>
     *    </ul>
     * </ul>
     */
    public class UastHints
    implements HintsConstants
    {
       /** Logger */
       private static final ConversionStatus LOG = ConversionStatus.get(UastHints.class);

       /** UastHints cache by file name */
       private static final ConcurrentMap<String, UastHints> HINTS = new ConcurrentHashMap<>();

       /** No PROPATH modification is specified for this file. */
       public static final int PATH_MOD_NONE = 0;

       /** Replace the PROPATH with the associated string. */
       public static final int PATH_MOD_REPLACE = 1;

       /** Prepend the PROPATH with the associated string. */
       public static final int PATH_MOD_PREPEND = 2;

       /** Append the PROPATH with the associated string. */
       public static final int PATH_MOD_APPEND = 3;

       /** Prepend the file's containing directory to the PROPATH. */
       public static final int PATH_MOD_DYN_PREPEND = 4;

       /** Append the file's containing directory to the PROPATH. */
       public static final int PATH_MOD_DYN_APPEND = 5;

       /**
        * The standard suffix appended to a source code filename to create a
        * unique filename for storing hints.
        */
       public static final String HINTS_SUFFIX = ".hints";

       /** The standard name for the directory level hints file. */
       public static final String DIR_HINTS = "directory" + HINTS_SUFFIX;

       /** Best efforts attempt to detect the JVM word/pointer size, may be different from the system value. */
       private static final int DEFAULT_ARCH;

       /** The list of databases that must be connected before parsing. */
       private Set<String> databases = null;

       /**
        * The list of databases that must be hidden before parsing. This list is set up for source files which
        * might encounter naming conflicts when these databases are active. If a database occurs in both
        * {@code databases} and {@code bannedDatabases}, the <b>latter</b> takes precedence.
        */
       private Set<String> bannedDatabases = null;

       /** Preprocessor arguments (positional or named) in name + value pairs. */
       private Map<String, String> arguments = null;

       static
       {
          String arch = System.getProperty("os.arch");

          DEFAULT_ARCH = (arch != null && arch.contains("64")) ? 64 : 32;
       }

       /**
        * The preprocessor should honor the backslash character as an escape
        * sequence (this corresponds to how the preprocessor works on UNIX).
        * This value defaults to <code>true</code>.
        */
       private boolean unixEscapes = defaultUnixEscapes();

       /** Sets the internal character used for preprocessor synchronization. */
       private char marker = defaultMarker();

       /** BATCH-MODE preprocessor built-in variable value. */
       private boolean batch = defaultBatch();

       /** OPSYS preprocessor built-in variable value. */
       private String opsys = defaultOpsys();

       /** WINDOW-SYSTEM preprocessor built-in variable value. */
       private String winsys = defaultWinsys();

       /** PROCESS-ARCHITECTURE preprocessor built-in variable value. */
       private int arch = defaultArch();

       /** Preprocessor debug flag. */
       private boolean preprocDebug = defaultPreprocDebug();

       /** WSOPTIONS E4GL preprocessor value. */
       private String wsoptions = Configuration.getParameter(ELEM_WSOPTIONS);

       /** E4GL preprocessor compatibility mode. */
       private String mode = Configuration.getParameter(ELEM_MODE);

       /** E4GL preprocessor output filename. */
       private String output = null;

       /**
        * The set of all aliases and the corresponding database they reference.
        * Used to create aliases in the schema dictionary before parsing starts.
        */
       private ArrayList<String[]> aliases = null;

       /**
        * The set of all variables/widgets to be added to the global variable
        * dictionary or widget dictionaries before parsing begins.
        */
       private Variable[] variables = null;

       /** Schema escape attribute, null means that attribute is not set. */
       private Boolean escaped = null;

       /** Map of schema table hints, keyed by table name */
       private Map<String, TableHints> tableHints = null;

       /** Path to a call graph root list XML file */
       private String rootList = null;

       /** Call graph included file spec */
       private String includeSpec = null;

       /** Stores the mapping of a unique key to a generic UAST hint. */
       private final Map<String, Object> uast = new HashMap<>();

       /** Encodes any PROPATH customization specified for this file. */
       private int pathModType = PATH_MOD_NONE;

       /** Encodes the text to use for PROPATH customization. */
       private String pathModTxt = null;

       /** Path to the source directory. */
       private String sourceDir = null;

       /** Path to the top-level source directory. */
       private String basePath = null;

       /** The source charset. */
       private Charset sourceCharset = Charset.forName(Configuration.getParameter(SOURCE_CHARSET));

       /** Unit test legacy type: valid values are "OEUnit" and "ABLUnit", default is "ABLUnit" */
       private String unitTestType = defaultUnitTestType();

       /**
        * Get a UastHints instance for the file.
        * @param sourceName
        *        The Progress 4GL source program's filename with which this
        *         hints file is associated.
        * @return UastHints instance for the file.
        */
       public static UastHints uastHints(String sourceName)
       {
          return HINTS.computeIfAbsent(sourceName, UastHints::new);
       }

       /**
        * Constructs an instance, loads the associated hints file if it exists
        * and parses the contents.
        *
        * @param    sourceName
        *           The Progress 4GL source program's filename with which this
        *           hints file is associated.
        */
       public UastHints(String sourceName)
       {
          try
          {
             File hints = new File(sourceName + HINTS_SUFFIX);
             initPaths(hints);

             Iterator<String> iter = walkDirectoryTree();

             // load each directory hints file in turn from less nested to more nested
             while (iter.hasNext())
             {
                loadHints(new File(iter.next()));
             }

             // second pass to parse the file-specific hints and overwrite or set config values
             loadHints(hints);
          }
          catch (AstException excpt)
          {
             LOG.log(Level.WARNING,"", excpt);
          }
       }

       /**
        * Provides a standard way to get the default value for the unix-escapes value.
        *
        * @return   <code>true</code> if UNIX escapes should be honored.
        */
       public static boolean defaultUnixEscapes()
       {
          return Boolean.parseBoolean(Configuration.getParameter(ELEM_ESC, "true"));
       }

       /**
        * Provides a standard way to get the default value for the marker value.
        *
        * @return   The default character to use as a stream-synchronization character.
        */
       public static char defaultMarker()
       {
          return (char) Integer.parseInt(Configuration.getParameter(ELEM_MARKER, "1"));
       }

       /**
        * Provides a standard way to get the default value for the batch value.
        *
        * @return   <code>true</code> if batch mode is active in the preprocessor.
        */
       public static boolean defaultBatch()
       {
          return Boolean.parseBoolean(Configuration.getParameter(ELEM_BATCH, "false"));
       }

       /**
        * Provides a standard way to get the default value for the preprocessor debug value.
        *
        * @return   <code>true</code> if debug mode is active in the preprocessor.
        */
       public static boolean defaultPreprocDebug()
       {
          return Boolean.parseBoolean(Configuration.getParameter("preproc-debug", "false"));
       }

       /**
        * Provides a standard way to get the default value for the opsys value.
        *
        * @return   The default operating system descriptor.
        */
       public static String defaultOpsys()
       {
          return Configuration.getParameter(ELEM_OPSYS);
       }

       /**
        * Provides a standard way to get the default value for the winsys value.
        *
        * @return   The default windowing system descriptor.
        */
       public static String defaultWinsys()
       {
          return Configuration.getParameter(ELEM_WINSYS);
       }

       /**
        * Access the default value for the process architecture (word/pointer size in bits) for the current
        * JVM. This may be different from the system architecture.  For example, if the JVM is 32-bit then
        * 32 will be returned, even if running on a 64-bit system.
        *
        * @return   The default process architecture for this JVM.
        */
       public static int defaultArch()
       {
          String arch = Configuration.getParameter(ELEM_ARCH, null);

          return (arch != null) ? Integer.parseInt(arch) : DEFAULT_ARCH;
       }

       /**
        * Provides a standard way to get the default value for the unit test
        * legacy type: valid values are "OEUnit" and "ABLUnit", no default.
        *
        * @return   The default unit test legacy type, may be {@code null}
        */
       public static String defaultUnitTestType()
       {
          return Configuration.getParameter(ELEM_UNITTESTTYPE, "ABLUnit");
       }

       /**
        * Get a list of all database names that must be connected before the ProgressParser can execute.
        *
        * @return  Array of database names or {@code null} if only the default databases are required.
        */
       public Set<String> getDatabases()
       {
          return databases;
       }

       /**
        * Get a set of banned database names which must be 'unloaded' before processing the current source file.
        *
        * @return  A set of database names or {@code null} if only the default databases are required.
        */
       public Set<String> getBannedDatabases()
       {
          return bannedDatabases;
       }

       /**
        * Get the list of aliases (and associated database names) that must be
        * created in the schema dictionary.
        *
        * @return  List of String[] or <code>null</code> if no aliases are
        *          required.
        */
       public List<String[]> getAliases()
       {
          return aliases;
       }

       /**
        * Get a list of all variables and widgets that must be added to the
        * variable and widget dictionaries before the ProgressParser can execute.
        *
        * @return  Array of variables or <code>null</code> if no variables need
        *          to be added.
        */
       public Variable[] getVariables()
       {
          return variables;
       }

       /**
        * Get the type code that specifies the file-specific customization that
        * should be honored for the <code>PROPATH</code>. One of the well known
        * (public) constants of this class will be returned.
        *
        * @return   Type code.
        */
       public int getPathModType()
       {
          return pathModType;
       }

       /**
        * Get the text to use for <code>PROPATH</code> customization.
        *
        * @return   Text string to replace, prepend or append to the
        *           <code>PROPATH</code> or if no replacement text was explicitly
        *           specified, then the parent directory of the associated
        *           source file will be returned.
        */
       public String getPathModTxt()
       {
          return pathModTxt;
       }

       /**
        * Get the parent directory of the source file.
        *
        * @return   The parent directory of the associated source file.
        */
       public String getSourceDir()
       {
          return sourceDir;
       }

       /**
        * Get the runtime arguments that must be passed to the preprocessor or
        * <code>null</code> if no runtime arguments exist.
        *
        * @return  Preprocessor arguments or <code>null</code> if no arguments
        *          are required.
        */
       public Map<String, String> getArguments()
       {
          return arguments;
       }

       /**
        * Gets the preprocessor's escape sequence setting.
        *
        * @return   <code>true</code> if the preprocessor should honor UNIX
        *           escape sequences (backslash).
        */
       public boolean isUnixEscapes()
       {
          return unixEscapes;
       }

       /**
        * Gets the character to use for the preprocessor's internal stream/state
        * synchronization mechanism.
        *
        * @return   The marker character to use or <code>'\001'</code> if no
        *           marker has been specified.
        */
       public char getMarker()
       {
          return marker;
       }

       /**
        * Gets the preprocessor's BATCH-MODE setting.
        *
        * @return   <code>true</code> if the preprocessor should be in batch mode.
        */
       public boolean isBatchMode()
       {
          return batch;
       }

       /**
        * Get any override for the preprocessor's OPSYS setting.
        *
        * @return   OPSYS override or <code>null</code> if not specified.
        */
       public String getOpsys()
       {
          return opsys;
       }

       /**
        * Get any override for the preprocessor's WINDOW-SYSTEM setting.
        *
        * @return   WINDOW-SYSTEM override or <code>null</code> if not specified.
        */
       public String getWindowSystem()
       {
          return winsys;
       }

       /**
        * Get any override for the preprocessor's PROCESS-ARCHITECTURE setting.
        *
        * @return   PROCESS-ARCHITECTURE override or <code>null</code> if not specified.
        */
       public int getProcessArchitecture()
       {
          return arch;
       }

       /**
        * Gets the preprocessor's debug flag.
        *
        * @return   <code>true</code> if the preprocessor should be in debug mode.
        */
       public boolean isPreprocDebug()
       {
          return preprocDebug;
       }

       /**
        * Get any override for the E4GL preprocessor's WSOPTIONS setting.
        *
        * @return   WSOPTIONS override or <code>null</code> if not specified.
        */
       public String getWSOptions()
       {
          return wsoptions;
       }

       /**
        * Get any override for the E4GL preprocessor's compatibility mode.
        *
        * @return   Compatibility mode name or <code>null</code> if not specified.
        */
       public String getE4GLMode()
       {
          return mode;
       }

       /**
        * Get any override for the E4GL preprocessor's output file name.
        *
        * @return   Output file name or <code>null</code> if not specified.
        */
       public String getE4GLOutputFile()
       {
          return output;
       }

       /**
        * Get unit test legacy type. Valid values are "OEUnit" and "ABLUnit", no default.
        *
        * @return see above
        */
       public String getUnitTestType()
       {
          return unitTestType;
       }

       /**
        * Get an unmodifiable map of schema table hints, keyed by table name.
        *
        * @return  The map of table hints, or <code>null</code> if none have
        *          been defined in the current hints file.
        */
       public Map<String, TableHints> getTableHints()
       {
          if (tableHints == null)
          {
             return null;
          }

          return Collections.unmodifiableMap(tableHints);
       }

       /**
        * Get an unmodifiable map of schema table hints, keyed by table name.
        *
        * @return  The map of table hints, or emptyMap if none have
        *          been defined in the current hints file.
        */
       public Map<String, TableHints> getTableHintsOrEmpty()
       {
             return tableHints == null ? Collections.emptyMap():
                                         Collections.unmodifiableMap(tableHints);
       }

       /**
        * Get the path to a call-graph root list XML file.
        *
        * @return  Path to a root list file or <code>null</code> if not defined in hints file.
        */
       public String getRootList()
       {
          return rootList;
       }

       /**
        * Get a file specification of included call-graph files.
        *
        * @return  Include spec or <code>null</code> if not defined in hints file.
        */
       public String getIncludeSpec()
       {
          return includeSpec;
       }

       /**
        * Get a UAST hint using a uniquely named key, accessing a string or
        * <code>null</code> if no such hint exists.
        *
        * @param    key
        *           Unique name for the hint, which is used as a key.
        *
        * @return   The named string or <code>null</code>.
        */
       public String getUastString(String key)
       {
          return (String) uast.get(key);
       }

       /**
        * Get a UAST hint using a uniquely named key, accessing a string array or
        * <code>null</code> if no such hint exists.
        *
        * @param    key
        *           Unique name for the hint, which is used as a key.
        *
        * @return   The named string array or <code>null</code>.
        */
       public String[] getUastStringArray(String key)
       {
          return (String[]) uast.get(key);
       }

       /**
        * Get a UAST hint using a uniquely named key, accessing a long or
        * <code>null</code> if no such hint exists.
        *
        * @param    key
        *           Unique name for the hint, which is used as a key.
        *
        * @return   The named long or <code>null</code>.
        */
       public Long getUastLong(String key)
       {
          return (Long) uast.get(key);
       }

       /**
        * Get a UAST hint using a uniquely named key, accessing a double or
        * <code>null</code> if no such hint exists.
        *
        * @param    key
        *           Unique name for the hint, which is used as a key.
        *
        * @return   The named double or <code>null</code>.
        */
       public Double getUastDouble(String key)
       {
          return (Double) uast.get(key);
       }

       /**
        * Get a UAST hint using a uniquely named key, accessing a boolean or
        * <code>null</code> if no such hint exists.
        *
        * @param    key
        *           Unique name for the hint, which is used as a key.
        *
        * @return   The named boolean or <code>null</code>.
        */
       public Boolean getUastBoolean(String key)
       {
          return (Boolean) uast.get(key);
       }

       /**
        * Get all the hint keys which have the specified prefix.
        *
        * @param    prefix
        *           The key prefix to match.
        *
        * @return   An array with the hint keys.
        */
       public String[] getUastHints(String prefix)
       {
          List<String> hintKeys = new ArrayList<>();

          for (String key : uast.keySet())
          {
             if (key.equals(prefix) || key.startsWith(prefix + "_"))
             {
                hintKeys.add(key);
             }
          }

          return hintKeys.toArray(new String[0]);
       }

       /**
        * Get the Java class name for a UAST hint using a uniquely named key,
        * or <code>null</code> if no such hint exists.
        *
        * @param    key
        *           Unique name for the hint, which is used as a key.
        *
        * @return   The class name or <code>null</code>.
        */
       public String getUastType(String key)
       {
          Object o = uast.get(key);

          if (o == null)
             return null;

          return o.getClass().getName();
       }

       /**
        * Checks if field should be escaped.
        *
        * @param    table
        *           The legacy table name.
        * @param    field
        *           The legacy field name.
        *
        * @return   value of "escape" hint for field if it is set, otherwise "escape" hint for
        *           table or schema.
        */
       public boolean isFieldEscaped(String table, String field)
       {
          if (tableHints != null && tableHints.containsKey(table))
          {
             TableHints tableHint = tableHints.get(table);
             Boolean fieldEscaped = tableHint.isFieldEscaped(field);
             if (fieldEscaped != null)
             {
                return fieldEscaped;
             }
          }
          return isSchemaEscaped();
       }

       /**
        * Checks if table should be escaped.
        *
        * @param    table
        *           The legacy table name.
        *
        * @return   value of "escape" hint for table if hint is set, otherwise "escape" hint for
        *           schema.
        */
       public boolean isTableEscaped(String table)
       {
          if (tableHints != null && tableHints.containsKey(table))
          {
             TableHints tableHint = tableHints.get(table);
             Boolean tableEscaped = tableHint.isEscaped();
             if (tableEscaped != null)
             {
                return tableEscaped;
             }
          }
          return isSchemaEscaped();
       }

       /**
        * Checks if table is read-only.
        *
        * @param    table
        *           The legacy table name.
        *
        * @return   value of "read-only" hint for table if hint is set, otherwise {@code false}.
        */
       public boolean isTableReadOnly(String table)
       {
          if (tableHints != null )
          {
             TableHints tableHint = tableHints.get(table);
             return tableHint != null  && tableHint.isReadOnly();
          }
          return false;
       }

       /**
        * Checks if table is dirty-read (that is, it needs to be tracked by the dirty share manager across
        * sessions).
        *
        * @param    table
        *           The legacy table name.
        *
        * @return   value of "dirty-read" hint for table if hint is set, otherwise {@code false}.
        */
       public boolean isTableDirtyRead(String table)
       {
          if (tableHints != null )
          {
             TableHints tableHint = tableHints.get(table);
             return tableHint != null  && tableHint.isDirtyRead();
          }
          return false;
       }

       /**
        * Checks if table is dirty-intra-read (that is, it needs to be tracked by the dirty share manager within
        * the same session).
        *
        * @param    table
        *           The legacy table name.
        *
        * @return   value of "dirty-intra-read" hint for table if hint is set, otherwise {@code false}.
        */
       public boolean isTableDirtyIntraRead(String table)
       {
          if (tableHints != null )
          {
             TableHints tableHint = tableHints.get(table);
             return tableHint != null  && tableHint.isDirtyIntraRead();
          }
          return false;
       }

       /**
        * Report whether the database schema name for this schema should be escaped.
        *
        * @return  <code>true</code> if schema name should be escaped, <code>false</code> if schema
        *          name should not be escaped, <code>null</code> if attribute &quot;escape&quot; is
        *          set not set for this schema.
        */
       public boolean isSchemaEscaped()
       {
          return escaped != null && escaped;
       }

       /**
        * Checks if a UAST hint exists under a uniquely named key.
        *
        * @param    key
        *           Unique name for the hint, which is used as a key.
        *
        * @return   <code>true</code> if a hint exists under that name.
        */
       public boolean isUastHint(String key)
       {
          return (uast.get(key) != null);
       }

       /**
        * Returns the source charset hint value. The returned value is <code>null</code> when
        * no charset is explicitly defined in the hint file.
        *
        * @return  see above.
        */
       public Charset getSourceCharset()
       {
          return sourceCharset;
       }

       /**
        * Returns the containing directory for the given file or "." if the
        * file doesn't exist AND ALSO has no pathing included in the original
        * name.  If the calculated parent directory does not exist, then "."
        * will be returned.
        *
        * @param    file
        *           The file object to inspect.
        *
        * @return   The containing directory or "." on failure.
        */
       private static String containingDirectory(File file)
       {
          String result = ".";

          File parent = file.getParentFile();

          // parent will not be null except if file is both non-existent AND was
          // created without any pathing info

          if (parent != null && parent.exists())
          {
             try
             {
                // store the full path
                result = parent.getCanonicalPath();
             }

             catch (IOException ioe)
             {
                // should not happen (famous last words)
             }
          }

          return result;
       }

       /**
        * Read XML hints file and create a list of all databases to be connected and one with databases to be
        * ignored.
        *
        * @param    root
        *           The root element of the XML document.
        */
       private void readDatabases(Element root)
       throws Exception
       {
          // create the list of databases
          NodeList dbNodes = root.getElementsByTagName(ELEM_DATABASE);
          int      len     = dbNodes.getLength();

          for (int i = 0; i < len; i++)
          {
             Element elemDb = (Element) dbNodes.item(i);
             String load = elemDb.getAttribute(ATTR_LOAD);
             if (load != null && (load.equalsIgnoreCase("no") || load.equalsIgnoreCase("false")))
             {
                // only if LOAD is expressly set to false/no
                if (bannedDatabases == null)
                {
                   bannedDatabases = new HashSet<>();
                }
                bannedDatabases.add(elemDb.getAttribute(ATTR_NAME));
             }
             else
             {
                // the default action if not set or failed to parse as negative
                if (databases == null)
                {
                   databases = new HashSet<>();
                }
                databases.add(elemDb.getAttribute(ATTR_NAME));
             }
          }
       }

       /**
        * Read XML hints file and create a list of string arrays (each with
        * 2 elements) representing the set of all alias names and the associated
        * database name for each one.
        *
        * @param    root
        *           The root element of the XML document.
        */
       private void readAliases(Element root)
       throws Exception
       {
          // enable multiple passes
          ArrayList<String[]> als = (aliases == null) ? new ArrayList<>() : aliases;

          // create the list of aliases
          NodeList aliasNodes = root.getElementsByTagName(ELEM_ALIAS);
          int      len        = aliasNodes.getLength();

          for (int i = 0; i < len; i++)
          {
             Element elemAlias = (Element) aliasNodes.item(i);

             String  name = elemAlias.getAttribute(ATTR_NAME);
             String  db   = elemAlias.getAttribute(ATTR_DB);

             // we always add to the global scope
             als.add(new String[] {name, db});
          }

          // store our list of aliases if this is the first time
          if (aliases == null && len > 0)
          {
             aliases = als;
          }
       }

       /**
        * Read XML hints file and create an array of <code>Variable</code>
        * objects representing the set of all variables/widgets to add to the
        * associated dictionaries.  Note that all variables are always flagged
        * to be loaded in the global namespace.
        *
        * @param    root
        *           The root element of the XML document.
        */
       private void readVariables(Element root)
       throws Exception
       {
          ArrayList<Variable> vars = new ArrayList<>();

          // enable multiple passes
          if (variables != null)
          {
             vars.addAll(Arrays.asList(variables));
          }

          // create the list of variables
          NodeList varNodes = root.getElementsByTagName(ELEM_VAR);
          int      len      = varNodes.getLength();

          for (int i = 0; i < len; i++)
          {
             Element elemVar = (Element) varNodes.item(i);

             String  name = elemVar.getAttribute(ATTR_NAME);
             String  s    = elemVar.getAttribute(ATTR_TYPE);
             String  cls  = elemVar.getAttribute(ATTR_CLASSNAME); // OK if null
             int     type = ProgressParser.lookupTokenType(s);

             vars.add(new Variable(name, type, cls));
          }

          // store our list of variables
          if (len > 0)
          {
             variables = vars.toArray(new Variable[0]);
          }
       }

       /**
        * Read XML hints file and initialize the path modification members based
        * on the first <code>propath</code> node found.
        *
        * @param    root
        *           The root element of the XML document.
        */
       private void readPathMod(Element root)
       throws Exception
       {
          // create the list of variables
          NodeList ppNodes = root.getElementsByTagName(ELEM_PROPATH);

          if (ppNodes.getLength() > 0)
          {
             Element elemVar = (Element) ppNodes.item(0);

             String type = elemVar.getAttribute(ATTR_TYPE);

             if ("replace".equalsIgnoreCase(type))
             {
                pathModType = PATH_MOD_REPLACE;
             }
             else if ("prepend".equalsIgnoreCase(type))
             {
                pathModType = PATH_MOD_PREPEND;
             }
             else if ("append".equalsIgnoreCase(type))
             {
                pathModType = PATH_MOD_APPEND;
             }
             else if ("dynamic-prepend".equalsIgnoreCase(type))
             {
                pathModType = PATH_MOD_DYN_PREPEND;
             }
             else if ("dynamic-append".equalsIgnoreCase(type))
             {
                pathModType = PATH_MOD_DYN_APPEND;
             }

             if (pathModType == PATH_MOD_REPLACE ||
                 pathModType == PATH_MOD_PREPEND ||
                 pathModType == PATH_MOD_APPEND)
             {
                // set hard coded/explicit override
                pathModTxt = elemVar.getAttribute(ATTR_VAL);

                // handle any substitutions like ${P2J_HOME}
                pathModTxt = Configuration.getInstance().substituteParms(pathModTxt);
             }

             if (pathModType == PATH_MOD_DYN_PREPEND ||
                 pathModType == PATH_MOD_DYN_APPEND)
             {
                // set default propath customization text
                pathModTxt = sourceDir;
             }
          }
       }

       /**
        * Read XML hints file and create a string representing the encoded
        * runtime preprocessor arguments and other preprocessor hints.
        *
        * @param    root
        *           The root element of the XML document.
        */
       private void readPreproc(Element root)
       throws Exception
       {
          NodeList ppNodes = root.getElementsByTagName(ELEM_PREPROC);

          if (ppNodes.getLength() == 1)
          {
             Element  preproc       = (Element) ppNodes.item(0);
             NodeList argNodes      = preproc.getElementsByTagName(ELEM_ARG);
             NodeList escNodes      = preproc.getElementsByTagName(ELEM_ESC);
             NodeList markNodes     = preproc.getElementsByTagName(ELEM_MARKER);
             NodeList batchNodes    = preproc.getElementsByTagName(ELEM_BATCH);
             NodeList opsysNodes    = preproc.getElementsByTagName(ELEM_OPSYS);
             NodeList winsysNodes   = preproc.getElementsByTagName(ELEM_WINSYS);
             NodeList archNodes     = preproc.getElementsByTagName(ELEM_ARCH);
             NodeList debugNodes    = preproc.getElementsByTagName(ELEM_DEBUG);
             NodeList charsetNodes  = preproc.getElementsByTagName(SOURCE_CHARSET);

             int alen = argNodes.getLength();

             if (alen > 0)
             {
                arguments = new LinkedHashMap<>();

                for (int j = 0; j < alen; j++)
                {
                   Element args = (Element) argNodes.item(j);
                   String  val  = args.getAttribute(ATTR_VAL);

                   if (val != null)
                   {
                      String name = null;
                      String pos  = args.getAttribute(ATTR_POS);

                      if (pos != null)
                      {
                         // positional arg

                         // confirm this is a valid number (parsing issues will
                         // be propagated to the caller)
                         Integer.parseInt(pos);

                         // it is valid, just concatenate it into a valid name
                         name = "{" + pos;
                      }
                      else
                      {
                         // named arg
                         name = args.getAttribute(ATTR_NAME);
                      }

                      if (name != null)
                      {
                         arguments.put(name, val);
                      }
                   }
                }
             }

             if (escNodes.getLength() == 1)
             {
                Element esc = (Element) escNodes.item(0);
                unixEscapes = "true".equals(esc.getAttribute(ATTR_VAL));
             }

             if (markNodes.getLength() == 1)
             {
                Element mark = (Element) markNodes.item(0);
                marker = (char) Integer.parseInt(mark.getAttribute(ATTR_VAL));
             }

             if (batchNodes.getLength() == 1)
             {
                Element bnode = (Element) batchNodes.item(0);
                batch = Boolean.parseBoolean(bnode.getAttribute(ATTR_VAL));
             }

             if (opsysNodes.getLength() == 1)
             {
                Element onode = (Element) opsysNodes.item(0);
                opsys = onode.getAttribute(ATTR_VAL);
             }

             if (winsysNodes.getLength() == 1)
             {
                Element wnode = (Element) winsysNodes.item(0);
                winsys = wnode.getAttribute(ATTR_VAL);
             }

             if (archNodes.getLength() == 1)
             {
                Element wnode = (Element) archNodes.item(0);
                arch = Integer.parseInt(wnode.getAttribute(ATTR_VAL));
             }

             if (debugNodes.getLength() == 1)
             {
                Element dnode = (Element) debugNodes.item(0);
                preprocDebug = Boolean.parseBoolean(dnode.getAttribute(ATTR_VAL));
             }

             if (charsetNodes.getLength() == 1)
             {
                Element node = (Element) charsetNodes.item(0);
                sourceCharset = Charset.forName(node.getAttribute(ATTR_VAL));
             }
          }
       }

       /**
        * Process sub-hierarchy which contains schema-related hints, if any.
        * An instance of {@link com.goldencode.p2j.schema.TableHints TableHints}
        * is created for each <code>table</code> element parsed within the
        * <code>schema</code> section of the document. These are mapped for
        * later lookup by table name.
        *
        * @param   root
        *          The root element of the XML document.
        */
       private void readSchemaTables(Element root)
       {
          // get schema hints sub-hierarchy, if any
          NodeList list = root.getElementsByTagName(ELEM_SCHEMA);

          if (list.getLength() == 0)
          {
             return;
          }

          // there should not be more than one schema element
          Element schema = (Element) list.item(0);

          // read schema escape attribute
          if (schema.hasAttribute(ATTR_ESCAPE))
          {
             escaped = "true".equalsIgnoreCase(schema.getAttribute(ATTR_ESCAPE));
          }

          // enable multiple passes
          if (tableHints == null)
          {
             tableHints = new HashMap<>();
          }

          // create a TableHints instance for each table element found
          list = schema.getElementsByTagName(ELEM_TABLE);
          int len = list.getLength();

          for (int i = 0; i < len; i++)
          {
             TableHints next = new TableHints((Element) list.item(i));
             tableHints.put(next.getName(), next);
          }
       }

       /**
        * Read XML hints file and create a string representing the encoded
        * runtime preprocessor arguments and other preprocessor hints.
        *
        * @param    root
        *           The root element of the XML document.
        */
       private void readCallGraph(Element root)
       throws Exception
       {
          NodeList cgNodes = root.getElementsByTagName(ELEM_CALLGRAPH);

          if (cgNodes.getLength() == 1)
          {
             Element  callgraph = (Element) cgNodes.item(0);
             NodeList rlNodes   = callgraph.getElementsByTagName(ELEM_ROOTLIST);
             NodeList isNodes   = callgraph.getElementsByTagName(ELEM_INCLUDE_SPEC);

             if (rlNodes.getLength() == 1)
             {
                Element elem = (Element) rlNodes.item(0);
                rootList = elem.getAttribute(ATTR_VAL);
             }

             if (isNodes.getLength() == 1)
             {
                Element elem = (Element) isNodes.item(0);
                includeSpec = elem.getAttribute(ATTR_VAL);
             }
          }
       }

       /**
        * Read XML hints file and create a string representing the encoded
        * E4GL preprocessor hints.
        *
        * @param    root
        *           The root element of the XML document.
        */
       private void readE4GL(Element root)
       throws Exception
       {
          NodeList e4glNodes = root.getElementsByTagName(ELEM_E4GL);

          if (e4glNodes.getLength() == 1)
          {
             Element  e4gl        = (Element) e4glNodes.item(0);
             NodeList optNodes    = e4gl.getElementsByTagName(ELEM_WSOPTIONS);
             NodeList modeNodes   = e4gl.getElementsByTagName(ELEM_MODE);
             NodeList outNodes    = e4gl.getElementsByTagName(ELEM_OUTPUT);

             if (optNodes.getLength() == 1)
             {
                Element opt = (Element) optNodes.item(0);
                String wsoptions = opt.getAttribute(ATTR_VAL);

                if (wsoptions != null)
                {
                   this.wsoptions = wsoptions;
                }
             }

             if (modeNodes.getLength() == 1)
             {
                Element mod = (Element) modeNodes.item(0);
                String mode = mod.getAttribute(ATTR_VAL);

                if (mode != null)
                {
                   this.mode = mode;
                }
             }

             if (outNodes.getLength() == 1)
             {
                Element out = (Element) outNodes.item(0);
                String output = out.getAttribute(ATTR_VAL);

                if (output != null)
                {
                   this.output = output;
                }
             }
          }
       }

       /**
        * Read XML hints file and create a map of objects representing the set of
        * all generic UAST hints, each element accessed by a unique name.
        *
        * @param    root
        *           The root element of the XML document.
        */
       private void readUastGeneric(Element root)
       throws Exception
       {
          // create the list of generic UAST hints
          NodeList nodes = root.getElementsByTagName(ELEM_UAST);
          int      len   = nodes.getLength();

          for (int i = 0; i < len; i++)
          {
             Element elem = (Element) nodes.item(i);

             String  key = elem.getAttribute(ATTR_NAME);
             String  cls = elem.getAttribute(ATTR_DTYPE);

             // process an array of strings if we have one
             if ("String[]".equalsIgnoreCase(cls))
             {
                NodeList vals  = elem.getElementsByTagName(ELEM_ARRAY);
                int      vlen  = vals.getLength();

                ArrayList<String> strings = new ArrayList<>();

                for (int j = 0; j < vlen; j++)
                {
                   Element e = (Element) vals.item(j);

                   strings.add(e.getAttribute(ATTR_VAL));
                }

                // allow zero-length arrays.
                uast.put(key, strings.toArray(new String[0]));
             }
             else
             {
                String  val  = elem.getAttribute(ATTR_VAL);

                // instantiate the Object wrapper for the annotation based on the
                // stored class name, then store the annotation in the AST
                if ("String".equalsIgnoreCase(cls))
                {
                   uast.put(key, val);
                }
                else if ("Long".equalsIgnoreCase(cls))
                {
                   uast.put(key, Long.valueOf(val));
                }
                else if ("Boolean".equalsIgnoreCase(cls))
                {
                   uast.put(key, Boolean.valueOf(val));
                }
                else if ("Double".equalsIgnoreCase(cls))
                {
                   uast.put(key, Double.valueOf(val));
                }
                else
                {
                   throw new AstException("Invalid UAST hint data type " + cls);
                }
             }
          }
       }

       /**
        * Load UAST hints from an XML document associated with the specified
        * filename.
        *
        * @param   hintsFile
        *          The hints file to read.
        *
        * @throws  AstException
        *          An unknown or unexpected failure.
        */
       private void loadHints(File hintsFile)
       throws AstException
       {
          // is this a valid file?
          if (!hintsFile.exists() || !hintsFile.isFile())
          {
             return;
          }

          try
          {
             Document  dom  = XmlHelper.parse(hintsFile);
             Element   root = dom.getDocumentElement();

             readUnitTestType(root);
             readDatabases(root);
             readAliases(root);
             readVariables(root);
             readPreproc(root);
             readPathMod(root);
             readSchemaTables(root);
             readCallGraph(root);
             readE4GL(root);
             readUastGeneric(root);
          }

          catch (Exception exc)
          {
             throw new AstException("Error loading hints file: " + hintsFile.getAbsolutePath(), exc);
          }
       }

       /**
        * Unit test type from XML hints file
        *
        * @param    root
        *           The root element of the XML document.
         */
       private void readUnitTestType(Element root)
       throws Exception
       {
          final NodeList ppNodes = root.getElementsByTagName(ELEM_UNITTESTTYPE);

          switch (ppNodes.getLength())
          {
             case 1:
             {
                final String attVal = ((Element) ppNodes.item(0)).getAttribute("type");
                switch(attVal)
                {
                   case "OEUnit":
                   case "ABLUnit":
                      unitTestType = attVal;
                      return;
                   default:
                      throw new AstException("Invalid value for " + ELEM_UNITTESTTYPE
                               + ": only \"OEUnit\" and \"ABLUnit\" allowed.");
                }
             }
             case 0:
                return;

             default:
                throw new AstException("More than one element " + ELEM_UNITTESTTYPE);
          }
       }

       /**
        * Initialize the base path and source path for this file.
        *
        * @param    hints
        *           The file-level hints.
        */
       private void initPaths(File hints)
       {
          // find the top level source directory
          basePath = Configuration.getParameter(ELEM_BASEPATH);

          if (basePath != null)
          {
             basePath = containingDirectory(new File(basePath));
          }

          // find the most deeply nested containing directory
          sourceDir = containingDirectory(hints);
       }

       /**
        * Provides an iterator that allows one to walk downward through each
        * containing path of the current file, from less nested to more nested.
        * The first directory in the list will be the configured base path of
        * the project and the last directory will be that which contains the
        * current file.
        *
        * @return   An iterator to the list of directories.
        */
       private Iterator<String> walkDirectoryTree()
       {
          LinkedList<String> list = new LinkedList<>();
          String srcDir = sourceDir;

          // walk up the directory hierarchy adding each directory in turn
          // (from most deeply nested to least deeply nested) into the list
          while (srcDir != null && srcDir.length() > 0)
          {
             // calc the directory level hints filename and store it in the list
             list.add(srcDir + File.separatorChar + DIR_HINTS);

             // find the next most enclosing directory
             int idx = srcDir.lastIndexOf(File.separatorChar);

             if (idx < 0)
             {
                break;
             }

             srcDir = srcDir.substring(0, idx);

             // end at the top-level directory
             if (basePath != null && basePath.equals(srcDir))
             {
                break;
             }
          }

          // the dirs are listed in most specific to least specific order so
          // we return an iterator that walks backward
          return list.descendingIterator();
       }

    }