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

}