# P2OLookup.java

    /*
    ** Module   : P2OLookup.java
    ** Abstract : Helper class which loads P2O ASTs and enables lookup access
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ---------------------------------- Description ----------------------------------
    ** 001 ECF 20051013   @23034 Created initial version. Lazily loads P2O
    **                           ASTs and enables lookups of Java names for
    **                           legacy Progress table and field names.
    **                           This first implementation supports fixed
    **                           tables only (not temp- or work-tables).
    **                           Also exposes index information by table.
    ** 002 ECF 20051017   @23054 Change to P2JIndex construction parameters.
    **                           Use historical index name instead of
    **                           converted name.
    ** 003 ECF 20051023   @23095 Added javaPropertyName() convenience method.
    **                           Looks up a Java property name for a given
    **                           legacy field name, optionally qualifying it
    **                           with a buffer name.
    ** 004 ECF 20051027   @23176 Added 'ignore-case' processing for index
    **                           components. Takes new annotation in P2O AST
    **                           into account when creating indexes.
    ** 005 ECF 20051102   @23181 Added legacyFieldNames method. Retrieves set
    **                           of legacy field names for a Progress table,
    **                           in display order.
    ** 006 ECF 20051121   @23423 Added isValidatable method. Indicates whether
    **                           a schema field must invoke validation
    **                           processing before it may be saved to its DMO.
    ** 007 ECF 20060202   @24252 Added support for temp/work-table schema
    **                           data. These data are refreshed by pattern
    **                           workers each time a new source file AST is
    **                           visited.
    ** 008 ECF 20060227   @24818 Minor fix for field-level validation support.
    **                           Made check for validatable annotation safer.
    ** 009 ECF 20060408   @25429 Added isDMOInterface() method. Indicates
    **                           whether a given class name collides with any
    **                           known DMO interface name. Temp table DMOs not
    **                           currently handled.
    ** 010 ECF 20061026   @30795 Added isForeignReference() method. Indicates
    **                           whether a given table is referenced by the
    **                           foreign key of any other table.
    ** 011 ECF 20061101   @30882 Added dumpTableDependencies() method. Debug
    **                           method to print tree of dependencies for a
    **                           table.
    ** 012 ECF 20070628   @35342 Minor optimization. Replaced StringBuffer
    **                           with StringBuilder.
    ** 013 ECF 20080304   @37468 Modified to support changes in P2JIndex.
    ** 014 SVL 20080326   @37683 Synthetic indexes are ignored in the
    **                           initializeIndexes().
    ** 015 GES 20090424   @41953 Import change.
    ** 016 GES 20090429   @42051 Match package and class name changes.
    ** 017 GES 20090515   @42220 Moved to AstManager from AstRegistry/AstPersister.
    ** 018 GES 20090518   @42398 Import change.
    ** 019 LMR 20110106          Fixes to reflect renaming of load() method in
    **                           Configuration class to getSchemaConfig() and
    **                           removal of its 'key' parameter.
    ** 020 ECF 20130130          Add metadata to P2OLookup data structures, under each primary schema
    **                           loaded. Implemented generics.
    ** 021 OM  20130305          Fix java path for metadata tables. Added message when a table name
    **                           conflict is detected.
    ** 022 ECF 20131013          Don't add synthetic legacy fields to legacy field name map.
    ** 023 CA  20131028          All the static info is kept context-local, for runtime query
    **                           conversion support.  All filenames must be normalized.
    ** 024 OM  20131030          Added support for checking if a schema contains a certain dmo iface.
    ** 025 VMN 20140328          Added support for denormalized fields with extent.
    ** 026 SVL 20140329          loadAst loads data from a resource in runtime mode.
    ** 027 OM  20140424          Added CASE_SENSITIVE constant used for dynamic temp-tables
    **                           generation.
    ** 028 ECF 20140921          Memory optimization: intern many duplicated strings.
    ** 029 OM  20150212          Forced lowercase legacy access for javaName accessors while saving
    **                           the original legacy names for later extraction.
    **                           Simplified String operations. Update code to Java 7.
    ** 030 ECF 20150304          Refactor storage of P2OLookup instances to keep permanent database
    **                           info in a static, shared data structure, to save memory. These data
    **                           structures, which can be quite large, were being duplicated across
    **                           all contexts.
    ** 031 VMN 20150313          Fixed denormalized field assignments.
    ** 032 ECF 20150715          Replace StringBuffer with StringBuilder.
    ** 033 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 034 ECF 20160907          Added initialize method to allow lookups to be primed at server
    **                           startup.
    ** 035 ECF 20180305          Added isReadOnly API to check for a read-only table.
    ** 036 ECF 20180908          Added support for unused index and field reporting.
    ** 037 CA  20190508          Buffers must be looked in the class hierarchy, not only in the
    **                           current class (thus we need to recursively load .p2o files).
    ** 038 CA  20190724          Fixed case sensitivity of javaPackage, validatables and readOnlyTables
    **                           (meta tables are lowercased).
    ** 039 GES 20190620          Added multiple inheritance support (for OO interface defs).
    ** 040 OM  20200806          Fixed NPE.
    ** 041 OM  20200924          P2JIndexComponent carries multiple information to avoid map lookups for them.
    **     CA  20201008          Added API to get an iterator over the property ASTs.  The propsMap is no longer
    **                           cleared for normal conversion - just for runtime conversion.
    **     OM  20210711          In case of buffer parameters, use the super .Buf interface name to allow
    **                           calls with 'sibling' tables. Stored that in the new [javaInterfaceMap].
    **     OM  20210716          Fixed population of [javaInterfaceMap]. Local optimizations.
    **     OM  20210908          [javaPackage] uses computed java name instead of legacy name.
    **     IAS 20211928          Added isNullable method.
    **     ECF 20211108          Format cleanup.
    **     OM  20211020          Added _DATASOURCE_ROWID property.
    **     OM  20220212          Use ReservedProperty static method for testing reserved properties names.
    **     OM  20220330          Moved database conversion artifacts to ${cvtpath} folder.
    **     CA  20230110          Let 'getImmediateChild' work with a set, if an array is used for the types.
    **     OM  20230111          Prioritize the _temp database in case of dynamic conversion.
    ** 042 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 043 OM  20240318          Special handling of dynamic tables in mutable permanent databases.
    ** 044 OM  20240416          Replaced string literal with SchemaDictionary.MUTABLE_ANNOTATION constant.
    ** 045 RAA 20240821          Added file name for indeterminate sorting queries report.
    **     RAA 20240906          Added writeHeaderToIndeterminateSortFile() method.
    ** 046 OM  20241128          Multi-tenant conversion support: added getTenantType() method.
    ** 047 DDF 20240205          Added NON_NULLABLE annotation to index component.
    */

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

    import java.io.*;
    import java.util.*;
    import java.util.function.*;
    import java.util.logging.*;

    import com.goldencode.ast.*;
    import com.goldencode.p2j.convert.*;
    import com.goldencode.util.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.uast.*;

    /**
     * A helper class which exposes information stored in P2O ASTs for access
     * during source conversion processing. Information is exposed via static
     * methods, which resolve lookups using individual instances of this class.
     * The following data currently is exposed:
     * <ul>
     *   <li>Java DMO class names (given legacy table names)
     *   <li>Java DMO property names (given legacy field names)
     *   <li>Java DMO package names (given legacy table names)
     *   <li>Index descriptors (given legacy table names)
     *   <li>Whether a field has schema-level validation configured (given legacy
     *       field names)
     *   <li>Whether a DMO interface exists in any schema (given a Java class
     *       name)
     *   <li>Whether a database table is referenced by the foreign key of any
     *       other table (given the legacy name of the table which is the
     *       potential referent)
     * </ul>
     *
     * @see  P2JIndex
     * @see  P2JIndexComponent
     */
    public final class P2OLookup
    implements DataModelTokenTypes
    {
       /** Name of schema dictionary configuration */
       public static final String CFG_NAME = "schema-dict";

       /** File extension appended to schema and source filenames */
       public static final String P2O_POSTFIX = ".p2o";

       /** Key for "package" database AST annotation */
       public static final String PACKAGE = "package";

       /** Key for "historical" class/property AST annotation */
       public static final String HISTORICAL = "historical";

       /** Key for "label" class/property AST annotation */
       public static final String LABEL = "label";

       /** Key for "col_lab" property AST annotation */
       public static final String COLUMN_LABEL = "col_lab";

       /** Key for "format" class/property AST annotation */
       public static final String FORMAT = "format";

       /** Key for "datatype" property AST annotation */
       public static final String DATATYPE = "datatype";

       /** Key for "extent" property AST annotation */
       public static final String EXTENT = "extent";

       /** Key for "validatable" property AST annotation */
       public static final String VALIDATABLE = "validatable";

       /** Key for non-nullable property AST annotation */
       public static final String NON_NULLABLE = "not-null";

       /** Key for "decimals" property AST annotation */
       public static final String DECIMALS = "decimals";

       /** Key for "read-only" class AST annotation */
       public static final String READ_ONLY = "read-only";

       /** Key for "table" class AST annotation */
       public static final String TABLE = "table";

       /** Key for "unique" index AST annotation */
       public static final String UNIQUE = "unique";

       /** Key for "primary" index AST annotation */
       public static final String PRIMARY = "primary";

       /** Key for "synthetic" index AST annotation */
       public static final String SYNTHETIC = "synthetic";

       /** Key for "word" index AST annotation */
       public static final String WORD = "word";

       /** Key for "refid" index column AST annotation */
       public static final String REFID = "refid";

       /** Key for "descend" index column AST annotation */
       public static final String DESCEND = "descend";

       /** Key for "ignore-case" index column AST annotation */
       public static final String IGNORE_CASE = "ignore-case";

       /** Key for "case-sensitive" annotation in data model AST */
       public static final String CASE_SENSITIVE = "case-sensitive";

       /** Key for "abbreviated" index column AST annotation */
       public static final String ABBREVIATED = "abbreviated";

       /** Key for "foreign" relation AST annotation */
       public static final String FOREIGN = "foreign";

       /** Key for "references" relation AST annotation */
       public static final String REFERENCES = "references";

       /** File name for unindexed query report */
       static final String UNINDEXED_QUERIES_FILE = "unindexed_queries.txt";

       /** File name for indeterminate sorting queries report */
       static final String INDETERMINATE_SORT_QUERIES_FILE = "indeterminate_sort_queries.csv";

       /** File name for unused index report */
       private static final String UNUSED_INDEX_FILE = "unused_database_indices.txt";

       /** File name for unused field report */
       private static final String UNUSED_FIELD_FILE = "unreferenced_database_fields.txt";

       /** Key for current temp table schema in schema map */
       private static final String TEMP_TABLE_KEY = null;

       /** The types used by {@link #initializeRelations(String, Aast)}. */
       private static final Set<Integer> types = new HashSet<>(Arrays.asList(ONE_TO_MANY, ONE_TO_ONE));

       /** Container with user-specific data. */
       private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
       {
          @Override
          protected WorkArea initialValue()
          {
             return new WorkArea();
          }
       };

       /** Shared map of permanent database schema names to <code>P2OLookup</code> instances */
       private static final Map<String, P2OLookup> permSchemaMap = new HashMap<>();

       /** Stores the list of meta tables */
       private final Set<String> historicalMetaTables = new HashSet<>();

       /** Map of AST IDs to class ASTs */
       private final Map<Long, Aast> classByIDMap = new HashMap<>();

       /** Map of historical table/field names to Java class/property names */
       private final Map<String, Map<Long, String>> javaNameMap = new LinkedHashMap<>();

       /** Map of historical table/field names to Java super-interface names (for temp-table only). */
       private final Map<String, String> javaInterfaceMap = new LinkedHashMap<>();

       /**
        * Map with normalized to original names of table/field names. To save memory, this map
        * contains only the entries that are not in lowercase (normalized).
        * If an entry is a key in {@code javaNameMap} but not here, then the name is already the
        * original form.
        */
       private final Map<String, String> originalNamesMap = new HashMap<>();

       /** Set of historical fully qualified denormalized properties of fields with custom extent */
       private final Set<String> denormalizedProperties = new HashSet<>();

       /** Map of fully qualified, normalized, historical table names to index descriptors */
       private final Map<String, List<P2JIndex>> indexMap = new HashMap<>();

       /** Mutable index information for unused index reporting */
       private final TableResourceMap mutableIndexMap = new TableResourceMap();

       /** Mutable field information for unused field reporting */
       private final TableResourceMap mutableFieldMap = new TableResourceMap();

       /** Map of historical table names to sets of referencing tables */
       private final Map<String, Set<Long>> foreignReferenceMap = new HashMap<>();

       /** Map of property AST unique IDs to the AST nodes. */
       private final Map<Long, Aast> propsMap = new HashMap<>();

       /** Set of schema names of validatable fields */
       private final Set<String> validatables = new HashSet<>();

       /** Set of schema names of non-nullable fields */
       private final Set<String> nonnullables = new HashSet<>();

       /** Set of schema names of read-only tables */
       private final Set<String> readOnlyTables = new HashSet<>();

       /** Set of all unqualified DMO Java interface names */
       private final Set<String> javaNames = new HashSet<>();

       /**
        * The tenant table type. Only for multi-tenant tables. Mapped values:
        * 0 = single-tenant (not store here),
        * 1 = multi-tenant,
        * 2 = multi-tenant-with-default.
        */
       private final Map<String, Integer> multiTenant = new HashMap<>();

       /** Logger */
       private static final ConversionStatus LOG = ConversionStatus.get(P2OLookup.class);

       /** Java package associated with this lookup instance */
       private String javaPackage = null;

       /** Java package associated with this lookup instance metadata */
       private String metaPackage = null;

       /** The P2O information in any super-class (interfaces can have multiple). */
       private P2OLookup[] parents = null;

       /**
        * Constructor which initializes this object with the information in the
        * P2O AST associated with <code>schema</code>.
        *
        * @param   schema
        *          Database schema name which determines which P2O AST to load.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       private P2OLookup(String schema)
       throws SchemaException
       {
          initialize(schema);
          propsMap.clear();
       }

       /**
        * Attempts to locate the {@code P2OLookup} for an existing dynamic or temporary schema. The AST tree of
        * {@code filename} is loaded and the {@code dynamic} flag from its root is analyzed for deciding whether
        * it is dynamic or not. In the first case, the existing {@code P20Lookup} instance is returned if one
        * exist. Otherwise, the application defaults to temp-table mode and a new {@code P2OLookup} instance
        * is created and returned.
        *
        * @param   filename
        *          Name of file containing the P2O AST to load.  File may or may not exist.
        * @param   clsDef
        *          The associated class definition for this file, otherwise {@code null}.
        *
        * @return  A {@code P2OLookup} instance as described above.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       private static P2OLookup dynamicGet(String filename, ClassDefinition clsDef)
       throws SchemaException
       {
          if (AstManager.get().isExistingAst(filename))
          {
             Aast root = AstManager.get().loadTree(filename);
             if (root.isAnnotation(SchemaDictionary.MUTABLE_ANNOTATION))
             {
                P2OLookup dynamicInstance = getInstance(root.getText(), 0);
                if (dynamicInstance != null)
                {
                   dynamicInstance.initialize(root.getText(), root, false);
                   return dynamicInstance;
                }
             }
          }
          return new P2OLookup(TEMP_TABLE_KEY, filename, clsDef);
       }

       /**
        * Constructor which initializes this object with the information in the
        * P2O AST stored in <code>filename</code>.  This variant is intended to
        * be used to load temp/work-table schema information.  If there is no
        * file with name <code>filename</code>, we still update the static
        * schema map, in order to replace the <code>P2OLookup</code> instance
        * (if any) already associated with <code>schema</code>.  This ensures
        * that we are not using out-of-date temp table schema information.
        *
        * @param   schema
        *          Database schema name to which this instance is mapped.
        * @param   filename
        *          Name of file containing the P2O AST to load.  File may or may not exist.
        * @param   clsDef
        *          The associated class definition for this file, otherwise <code>null</code>.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       private P2OLookup(String schema, String filename, ClassDefinition clsDef)
       throws SchemaException
       {
          if (AstManager.get().isExistingAst(filename))
          {
             try
             {
                initialize(schema, AstManager.get().loadTree(filename), false);
             }
             catch (AstException exc)
             {
                throw new SchemaException(
                      "Error loading P2O AST for database schema:  " + schema,
                      exc);
             }
             // do not clear this map for full conversion
             if (Configuration.isRuntimeConfig())
             {
                propsMap.clear();
             }
          }

          if (clsDef != null)
          {
             ClassDefinition[] pars = clsDef.getParents();

             if (pars != null)
             {
                ArrayList<P2OLookup> parr = new ArrayList<>();
                SchemaException[]    exc  = new SchemaException[1];

                Consumer<ClassDefinition> consumer = (ClassDefinition cls) ->
                {
                   if (exc[0] == null && !cls.isBuiltIn())
                   {
                      String parentFile = cls.getFilename();
                      parentFile += P2O_POSTFIX;
                      parentFile = Configuration.normalizeFilename(parentFile);
                      try
                      {
                         parr.add(new P2OLookup(schema, parentFile, cls));
                      }
                      catch (SchemaException e)
                      {
                         exc[0] = e;
                      }
                   }
                };

                clsDef.processParentGraph(consumer);

                if (exc[0] != null)
                {
                   throw exc[0];
                }
                else
                {
                   parents = parr.toArray(new P2OLookup[0]);
                }
             }
          }

          WorkArea wa = locate();
          wa.tempP2OLookup = this;
       }

       /**
        * Initialize a {@code P2OLookup} instance for the schema of each primary database managed by
        * this server.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static void initialize()
       throws SchemaException
       {
          for (Database database : DatabaseManager.getManagedDatabases())
          {
             if (database.isPrimary())
             {
                String schema = DatabaseManager.getSchema(database);
                getInstance(schema, EntityName.DATABASE);
             }
          }
       }

       /**
        * Attempt to load the P2O AST stored in <code>filename</code>, which must
        * represent a temp/work-table schema.  This method should be invoked by
        * conversion service pattern workers which need to load temporary schema
        * information upon loading a new source file AST.  A new instance of this
        * class is created as a result, and is stored in the schema map, under
        * the temp table schema key.  If this method is invoked more than once in
        * succession with the same file name, only the first invocation triggers
        * the P2O AST to be loaded.
        * <p>
        * If no file exists with the name <code>filename</code>, an empty
        * instance of this class is stored in the schema map under the temp table
        * schema key.  This ensures that an out-of-date <code>P2OLookup</code>
        * instance is not maintained under the temp table key beyond it useful
        * life.
        *
        * @param   filename
        *          Name of file containing the P2O AST to load.  File may or may
        *          not exist, but this value may not be <code>null</code>.
        * @param   clsDef
        *          The associated class definition for this file, otherwise <code>null</code>.
        *
        * @throws  RuntimeException
        *          if there is any error loading or parsing the P2O AST data for
        *          the given file;  non-existence of the file is not considered
        *          an error.
        */
       public static void loadTemporarySchema(String filename, ClassDefinition clsDef)
       {
          filename += P2O_POSTFIX;
          filename = Configuration.normalizeFilename(filename);

          WorkArea wa = locate();

          if (!filename.equals(wa.lastTempFilename))
          {
             try
             {
                P2OLookup.dynamicGet(filename, clsDef);
             }
             catch (SchemaException exc)
             {
                throw new RuntimeException(exc);
             }

             wa.lastTempFilename = filename;
          }
       }

       /**
        * Report whether the given class name is the unqualified name of any DMO
        * interface, in any schema tracked by the current P2J configuration.
        *
        * @param   name
        *          An unqualified class name.
        *
        * @return  <code>true</code> if <code>name</code> collides with any DMO
        *          interface in any schema, else <code>false</code>.
        *
        * @throws  SchemaException
        *          if there is an error loading schema names from configuration.
        */
       public static boolean isDMOInterface(String name)
       throws SchemaException
       {
          Iterator<String> iter = schemaNames();
          while (iter.hasNext())
          {
             String schema = iter.next();
             if (isDMOInterface(name, schema))
             {
                return true;
             }
          }

          return false;
       }

       /**
        * Report whether the given class name is the unqualified name of a DMO interface, in the
        * <code>schema</code> tracked by the current P2J configuration.
        *
        * @param   name
        *          An unqualified class name.
        * @param   schema
        *          The name of the schema to look into.
        *
        * @return  <code>true</code> if <code>name</code> is present as java name of a table from
        *          the specified <code>schema</code> and <code>false</code> if <code>schema</code>
        *          does not exist or it does not contain such interface.
        *
        * @throws  SchemaException
        *          if there is an error loading schema names from configuration.
        */
       public static boolean isDMOInterface(String name, String schema)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(schema, EntityName.DATABASE);

          return lookup.containsDMOInterface(name);
       }

       /**
        * Report whether the given, historical table will be mapped in the
        * converted application to a database table which represents a foreign
        * reference of another table's foreign key.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name (not mandatory normalized).
        *
        * @return  <code>true</code> if the given table represents the foreign
        *          end of a one-to-one or a one-to-many relation between two
        *          tables.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static boolean isForeignReference(String historical)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.TABLE);

          return lookup.containsForeignReference(historical);
       }

       /**
        * Dump an exhaustive tree of nested table dependencies.  A dependency is
        * a foreign key relation from the dependent table to the "parent" table.
        * The tree is printed to stdout.  Each indent indicates a dependency to
        * the next outer level.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name.
        *
        * @throws  SchemaException
        *          if there is any error loading the AST from persistence.
        */
       public static void dumpTableDependencies(String historical)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.TABLE);
          lookup.dumpTableDependencies(historical, 0);
       }

       /**
        * Retrieve the Java package name of the DMO associated with the given
        * legacy Progress table name.
        *
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a table.
        *
        * @return  Corresponding Java package name, or <code>null</code> if no
        *          associated DMO or package name was found.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static String javaPackage(String historical)
       throws SchemaException
       {
          P2OLookup inst = getInstance(historical, EntityName.TABLE);

          return inst.getJavaPackage(historical);
       }

       /**
        * Retrieve a Java class or variable name, based upon the legacy Progress
        * schema name of the corresponding table or field, respectively.
        *
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a table or
        *          field.
        * @param   type
        *          <code>EntityName</code> constant to indicate whether
        *          <code>historical</code> represents a table or a field.
        *
        * @return  Corresponding, unqualified, Java class or variable name.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence;
        *          if the table or field portion of <code>historical</code>
        *          cannot be resolved to a corresponding Java name.
        */
       public static String javaName(String historical, int type)
       throws SchemaException
       {
          return javaName(historical, type, null);
       }

       /**
        * In the case of temp-tables, look up the name of a Java DMO super interface, given its legacy name.
        *
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a temp-table.
        *
        * @return  Corresponding, unqualified, Java super interface.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence;
        *          if the table or field portion of {@code historical} cannot be resolved to a corresponding Java
        *          name.
        */
       public static String javaInterfaceName(String historical)
       throws SchemaException
       {
          String normalized = historical.toLowerCase();
          return getInstance(normalized, EntityName.TABLE).javaInterfaceMap.get(normalized);
       }

       /**
        * Retrieve a Java class or variable name, based upon the legacy Progress schema name of the
        * corresponding table or field, respectively.
        *
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a table or field (may not be
        *          normalized).
        * @param   type
        *          <code>EntityName</code> constant to indicate whether <code>historical</code>
        *          represents a table or a field.
        * @param   index
        *          Zero-based index of custom property name for denormalized field with extent,
        *          otherwise null.
        *
        * @return  Corresponding, unqualified, Java class or variable name.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence;
        *          if the table or field portion of <code>historical</code> cannot be resolved to a
        *          corresponding Java name.
        */
       public static String javaName(String historical, int type, Long index)
       throws SchemaException
       {
          String normalized = historical.toLowerCase();
          String name = null;

          if (Configuration.isRuntimeConfig())
          {
             P2OLookup tempP2OLookup = locate().tempP2OLookup;
             if (tempP2OLookup != null)
             {
                // prioritize the _temp database in case of dynamic conversion. Statistically, the runtime will
                // process objects from _temp database. Also, this will allow non-standard field names to be
                // processed (for example with names containig "." character).
                name = tempP2OLookup.getJavaName(normalized, index);
             }

             // if not found (name stays null), the full P2OLookup instance lookup is performed
          }

          if (name == null)
          {
             name = getInstance(normalized, type).getJavaName(normalized, index);
          }

          if (name == null)
          {
             // attempt to identify special fields of dataset (before) temp-tables
             String unqualified = normalized;
             int k = unqualified.lastIndexOf('.');
             if (k > 0)
             {
                unqualified = unqualified.substring(k + 1);
             }
             if (ReservedProperty.isReservedProperty(unqualified))
             {
                switch (unqualified)
                {
                   case Buffer.__ERROR_FLAG__:        return Buffer.ERROR_FLAG_FIELD;
                   case Buffer.__ORIGIN_ROWID__:      return Buffer.ORIGIN_ROWID_FIELD;
                   case Buffer.__DATA_SOURCE_ROWID__: return Buffer.DATA_SOURCE_ROWID_FIELD;
                   case Buffer.__ERROR_STRING__:      return Buffer.ERROR_STRING_FIELD;
                   case Buffer.__AFTER_ROWID__:       return Buffer.AFTER_ROWID_FIELD;
                   case Buffer.__ROW_STATE__:         return Buffer.ROW_STATE_FIELD;
                }
             }
             // if no match, throw error:

             StringBuilder buffer = new StringBuilder("No java name found for legacy name:  ");
             buffer.append(historical);
             if (index != null)
             {
                buffer.append(", index = ");
                buffer.append(index);
             }
             throw new SchemaException(buffer.toString());
          }

          return name;
       }

       /**
        * Retrieve the full set of fully qualified, legacy field names for a
        * legacy Progress table.  The set is guaranteed to iterate in the default
        * display order defined for the original, Progress schema for that table.
        *
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a table.
        * @param   includeHidden
        *          If {@code true} the hidden fields are also included.
        *
        * @return  Set of fully qualified, legacy field names.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence;
        *          if {@code historical} does not represent a known table.
        */
       public static Set<String> legacyFieldNames(String historical, boolean includeHidden)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.TABLE);
          Set<String> names = lookup.getLegacyFieldNames(historical, includeHidden);
          if (names.isEmpty())
          {
             throw new SchemaException("No table found for legacy name:  " + historical);
          }

          return names;
       }

       /**
        * Retrieve a Java property name, based upon the legacy Progress schema
        * name of the corresponding table or field, respectively.  If specified,
        * the property name will be qualified with a buffer name, suitable for
        * insertion into an HQL where clause or order by clause.  The convention
        * for the buffer name qualifier is to use the short DMO class name, with
        * the first letter lowercased.
        *
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a table or
        *          field.
        * @param   qualified
        *          <code>true</code> if property name should be qualified with a
        *          buffer name;  <code>false</code> if it should remain
        *          unqualified.
        *
        * @return  Corresponding Java property name.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence;
        *          if the table or field portion of <code>historical</code>
        *          cannot be resolved to a corresponding Java name.
        */
       public static String javaPropertyName(String historical, boolean qualified)
       throws SchemaException
       {
          return javaPropertyName(historical, qualified, null);
       }

       /**
        * Retrieve a Java property name, based upon the legacy Progress schema
        * name of the corresponding table or field, respectively.  If specified,
        * the property name will be qualified with a buffer name, suitable for
        * insertion into an HQL where clause or order by clause.  The convention
        * for the buffer name qualifier is to use the short DMO class name, with
        * the first letter lowercased.
        *
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a table or
        *          field.
        * @param   qualified
        *          <code>true</code> if property name should be qualified with a
        *          buffer name;  <code>false</code> if it should remain
        *          unqualified.
        * @param   index
        *          Zero-based index of custom property name for denormalized field with extent,
        *          otherwise null.
        *
        * @return  Corresponding Java property name.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence;
        *          if the table or field portion of <code>historical</code>
        *          cannot be resolved to a corresponding Java name.
        */
       public static String javaPropertyName(String historical, boolean qualified, Long index)
       throws SchemaException
       {
          String property = javaName(historical, EntityName.FIELD, index);
          if (!qualified)
          {
             return property;
          }

          historical = historical.substring(0, historical.lastIndexOf("."));
          String className = javaName(historical, EntityName.TABLE);

          return StringHelper.changeCase(className, false) + '.' + property;
       }

       /**
        * Obtain the multi-tenant table type. The values are:
        *    0 = single-tenant (not store here),
        *    1 = multi-tenant,
        *    2 = multi-tenant-with-default.
        *
        * @param   table
        *          The table name.
        *
        * @return  The table multi-tenant type, as described above.
        *
        * @throws  SchemaException
        *          In case of error accessing internal data.
        */
       public static int getTenantType(String table)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(table, EntityName.TABLE);
          Integer mt = lookup.multiTenant.get(table);
          return mt == null ? 0 : mt;
       }

       /**
        * The given index in the given table has been referenced in business logic. Remove it from
        * the mutable index resource map.
        *
        * @param   table
        *          Qualified, normalized, legacy table name.
        * @param   index
        *          Legacy name of index which was referenced.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static void indexReferenced(String table, String index)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(table, EntityName.TABLE);
          lookup.processIndexedReferenced(table, index);
       }

       /**
        * The given field has been referenced in business logic. Remove it from the mutable field
        * resource map.
        *
        * @param   field
        *          Legacy name (normalized schemaname) of field which was referenced.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static void fieldReferenced(String field)
       throws SchemaException
       {
          EntityName eName = new EntityName(EntityName.FIELD, field);
          String database = eName.getDatabase();
          String table = (database != null ? database + '.' : "") + eName.getTable();
          P2OLookup lookup = getInstance(table, EntityName.TABLE);
          lookup.processFieldReferenced(table, eName);
       }

       /**
        * Delete previous database resource report files, if any.
        */
       public static void deleteDatabaseResourceReports()
       {
          String[] names = new String[]
          {
             UNUSED_INDEX_FILE,
             UNUSED_FIELD_FILE,
             UNINDEXED_QUERIES_FILE,
             INDETERMINATE_SORT_QUERIES_FILE,
          };

          for (String next : names)
          {
             File file = new File(next);
             if (file.exists() && file.isFile())
             {
                file.delete();
             }
          }
       }

       /**
        * Write the header for the indeterminate sorting queries report.
        */
       public static void writeHeaderToIndeterminateSortFile()
       throws IOException
       {
          String sep = System.lineSeparator();
          String header = "File, Line number, Table, Chosen index, Risk, Query";
          FileWriter fw = new FileWriter(P2OLookup.INDETERMINATE_SORT_QUERIES_FILE, true);
          try (BufferedWriter bw = new BufferedWriter(fw))
          {
             bw.write(header + sep);
             bw.close();
          }
       }

       /**
        * Emit content into the unused index report.
        *
        * @param   temp
        *          {@code true} if content relates to temp-tables; {@code false} for permanent
        *          tables.
        * @throws  IOException
        *          if there is an error writing the report.
        */
       public static void unusedIndexReport(boolean temp)
       throws IOException
       {
          unusedResourceReport(UNUSED_INDEX_FILE, (lookup) -> lookup.mutableIndexMap, temp);
       }

       /**
        * Emit content into the unused field report.
        *
        * @param   temp
        *          {@code true} if content relates to temp-tables; {@code false} for permanent
        *          tables.
        * @throws  IOException
        *          if there is an error writing the report.
        */
       public static void unusedFieldReport(boolean temp)
       throws IOException
       {
          unusedResourceReport(UNUSED_FIELD_FILE, (lookup) -> lookup.mutableFieldMap, temp);
       }

       /**
        * Get an iterator on all the index descriptors associated with the given,
        * legacy table. There is no guaranteed order of this iteration; it is
        * dictated by the order in which in which the indexes were read from the
        * exported Progress schema.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name (may not be normalized).
        *
        * @return  Iterator on all indexes associated with the table named
        *          <code>historical</code>.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static Iterator<P2JIndex> indexes(String historical)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.TABLE);
          List<P2JIndex> indexList = lookup.getIndexes(historical);

          if (indexList == null)
          {
             throw new SchemaException("Table '" + historical + "' not recognized");
          }

          return indexList.iterator();
       }

       /**
        * Get an iterator on all the property ASTs associated with the given, legacy table. There is no guaranteed
        * order of this iteration; it is dictated by the order in which in which the properties were read from the
        * exported Progress schema.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name (may not be normalized).
        *
        * @return  Iterator on all properties associated with the table named <code>historical</code>.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static Iterator<Aast> properties(String historical)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.TABLE);
          List<Aast> propList = lookup.getProperties(historical);

          if (propList == null || propList.isEmpty())
          {
             throw new SchemaException("Table '" + historical + "' not recognized");
          }

          propList.sort(Comparator.comparingLong(c -> (Long) c.getAnnotation("fieldid")));

          return propList.iterator();
       }

       /**
        * Compose a bean-like getter/setter method name from the Java DMO
        * property associated with the specified, legacy, Progress field name.
        * The name of the property is capitalized and prepended with
        * <code>prefix</code>.
        *
        * @param   prefix
        *          Verb to prepend to the capitalized property name; typically,
        *          "get", "is", or "set".
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a database
        *          field.
        *
        * @return  Method name as described above.
        *
        * @throws  SchemaException
        *          if this request results in the loading of a persisted P2O AST
        *          and there is any error loading P2J schema configuration data
        *          or loading the AST from persistence;  if the field portion of
        *          <code>historical</code> cannot be resolved to a corresponding
        *          Java name.
        */
       public static String beanMethodName(String prefix, String historical)
       throws SchemaException
       {
          return beanMethodName(prefix, historical, null);
       }

       /**
        * Compose a bean-like getter/setter method name from the Java DMO
        * property associated with the specified, legacy, Progress field name.
        * The name of the property is capitalized and prepended with
        * <code>prefix</code>.
        *
        * @param   prefix
        *          Verb to prepend to the capitalized property name; typically,
        *          "get", "is", or "set".
        * @param   historical
        *          Fully qualified, legacy, Progress schema name for a database field.
        * @param   index
        *          Zero-based index of custom property name for denormalized field with extent,
        *          otherwise null.
        *
        * @return  Method name as described above.
        *
        * @throws  SchemaException
        *          if this request results in the loading of a persisted P2O AST
        *          and there is any error loading P2J schema configuration data
        *          or loading the AST from persistence;  if the field portion of
        *          <code>historical</code> cannot be resolved to a corresponding
        *          Java name.
        */
       public static String beanMethodName(String prefix, String historical, Long index)
       throws SchemaException
       {
          StringBuilder buf = new StringBuilder(prefix);
          String prop = javaName(historical, EntityName.FIELD, index);
          buf.append(StringHelper.changeCase(prop, true));

          return buf.toString();
       }

       /**
        * Check if given historical fully qualified property is denormalized.
        *
        * @param   historical
        *          Historical fully qualified property name.
        *
        * @return  <code>true</code> if given property is denormalized.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static boolean isDenormalizedProperty(String historical)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.FIELD);

          return lookup.containsDenormalizedProperty(historical);
       }

       /**
        * Indicate whether a DMO property can be validated at runtime, given its
        * legacy schema name.  Possible forms of validation include:
        * <ul>
        *   <li>mandatory (non-nullable) constraint;
        *   <li>participation in a unique constraint;
        *   <li>explicit validation method (converted from a Progress validation
        *       expression).
        * </ul>
        *
        * @param   historical
        *          Fully qualified, legacy Progress field name.
        *
        * @return  <code>true</code> if the field should be validated at runtime,
        *          else <code>false</code>.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static boolean isValidatable(String historical)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.FIELD);

          return lookup.containsValidatable(historical);
       }

       /**
        * Indicate whether a DMO property is nullable.
        *
        * @param   historical
        *          Fully qualified, legacy Progress field name.
        *
        * @return  <code>true</code> if the field is nullable, else <code>false</code>.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static boolean isNullable(String historical)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.FIELD);

          return lookup.containsNullable(historical);
       }

       /**
        * Indicate whether a DMO class represents a read-only table, given its schema name.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name.
        *
        * @return  {@code true} if the table is read-only, else {@code false}.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       public static boolean isReadOnly(String historical)
       throws SchemaException
       {
          P2OLookup lookup = getInstance(historical, EntityName.TABLE);

          return lookup.containsReadOnly(historical);
       }

       /**
        * Get an iterator on all schema names managed by the current P2J
        * configuration.
        *
        * @return  Iterator on all known schema names.
        *
        * @throws  SchemaException
        *          if there is an error loading schema names from configuration.
        */
       private static Iterator<String> schemaNames()
       throws SchemaException
       {
          WorkArea wa = locate();

          if (wa.schemaNames == null)
          {
             wa.schemaNames = new HashSet<>();
             SchemaConfig config = Configuration.getSchemaConfig();
             String meta = config.getMetadata().getName();
             Iterator<String> iter = config.databases();
             while (iter.hasNext())
             {
                String next = iter.next();
                if (!next.equals(meta))
                {
                   wa.schemaNames.add(next);
                }
             }
          }

          return wa.schemaNames.iterator();
       }

       /**
        * Emit content into an unused database resource report.
        *
        * @param   file
        *          Name of the file into which to emit content.
        * @param   mapFunc
        *          Function to return a {@code TableResourceMap}, given a {@code P2OLookup} object.
        * @param   temp
        *          {@code true} if content relates to temp-tables; {@code false} for permanent
        *          tables.
        * @throws  IOException
        *          if there is an error writing the report.
        */
       private static void unusedResourceReport(String file,
                                                Function<P2OLookup, TableResourceMap> mapFunc,
                                                boolean temp)
       throws IOException
       {
          WorkArea wa = null;
          TableResourceMap trm = null;

          // quick out for frequently invoked temp-table case
          if (temp)
          {
             wa = locate();
             P2OLookup lookup = wa.tempP2OLookup;
             trm = mapFunc.apply(lookup);
             if (trm.isEmpty())
             {
                return;
             }
          }

          FileWriter fw = new FileWriter(file, true);

          try (BufferedWriter bw = new BufferedWriter(fw))
          {
             if (temp)
             {
                String p2oFile = wa.lastTempFilename;
                int pos = p2oFile.lastIndexOf('.');
                p2oFile = p2oFile.substring(0, pos);
                String heading = "Source File:  " + p2oFile;
                trm.writeContent(bw, heading);
             }
             else
             {
                synchronized (permSchemaMap)
                {
                   for (Map.Entry<String, P2OLookup> entry : permSchemaMap.entrySet())
                   {
                      String schema = entry.getKey();
                      String heading = "Schema:  " + schema;
                      P2OLookup lookup = entry.getValue();
                      trm = mapFunc.apply(lookup);
                      trm.writeContent(fw, heading);
                   }
                }
             }
          }
       }

       /**
        * Get the context-local {@link WorkArea} instance.
        *
        * @return   See above.
        */
       private static WorkArea locate()
       {
          return local.get();
       }

       /**
        * Get or create a <code>P2OLookup</code> instance, given a schema name or
        * a fully qualified, legacy table or field name. If the specified schema
        * has not been processed previously, a new instance is created and
        * cached;  otherwise, a cached instance is returned.
        *
        * @param   historical
        *          Schema name or fully qualified, legacy table or field name (may not be normalized).
        * @param   type
        *          <code>EntityName</code> type, indicating whether the name
        *          <code>historical</code> represents a schema, table, or field.
        *
        * @return  A <code>P2OLookup</code> instance.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       private static P2OLookup getInstance(String historical, int type)
       throws SchemaException
       {
          EntityName eName = new EntityName(type, historical.toLowerCase());
          String schema = eName.getDatabase();

          if (schema == TEMP_TABLE_KEY)
          {
             WorkArea wa = locate();

             return wa.tempP2OLookup;
          }

          synchronized (permSchemaMap)
          {
             P2OLookup lookup = permSchemaMap.get(schema);
             if (lookup == null)
             {
                lookup = new P2OLookup(schema);
                permSchemaMap.put(schema, lookup);
             }

             return lookup;
          }
       }

       /**
        * Get the Java package name for the DMO interface classes associated with
        * this lookup object's schema.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name (not mandatory normalized).
        *
        * @return  DMO interface package for this schema.
        */
       private String getJavaPackage(String historical)
       {
          if (javaNameMap.containsKey(historical))
          {
             boolean meta = historicalMetaTables.contains(historical);
             return meta ? metaPackage : javaPackage;
          }

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                String pkg = parent.getJavaPackage(historical);

                if (pkg != null)
                   return pkg;
             }
          }

          return null;
       }

       /**
        * Report whether the given class name is the unqualified name of any DMO
        * interface in the schema represented by this lookup object.
        *
        * @param   name
        *          An unqualified class name.
        *
        * @return  <code>true</code> if <code>name</code> collides with any DMO
        *          interface in this schema, else <code>false</code>.
        */
       private boolean containsDMOInterface(String name)
       {
          if (javaNames.contains(name))
             return true;

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                if (parent.containsDMOInterface(name))
                   return true;
             }
          }

          return false;
       }

       /**
        * Check if given historical fully qualified property is denormalized.
        *
        * @param   historical
        *          Historical fully qualified property name.
        *
        * @return  <code>true</code> if given property is denormalized.
        */
       private boolean containsDenormalizedProperty(String historical)
       {
          if (denormalizedProperties.contains(historical))
             return true;

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                if (parent.containsDenormalizedProperty(historical))
                   return true;
             }
          }

          return false;
       }

       /**
        * Report whether the given, historical table will be mapped in the
        * converted application to a database table which represents a foreign
        * reference of another table's foreign key.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name (not mandatory normalized).
        *
        * @return  <code>true</code> if the given table represents the foreign
        *          end of a one-to-one or a one-to-many relation between two
        *          tables.
        */
       private boolean containsForeignReference(String historical)
       {
          if (foreignReferenceMap.containsKey(historical.toLowerCase()))
             return true;

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                if (parent.containsForeignReference(historical))
                   return true;
             }
          }

          return false;
       }

       /**
        * Indicate whether a DMO class represents a read-only table, given its schema name.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name.
        *
        * @return  {@code true} if the table is read-only, else {@code false}.
        */
       private boolean containsReadOnly(String historical)
       {
          if (readOnlyTables.contains(historical))
             return true;

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                if (parent.containsReadOnly(historical))
                   return true;
             }
          }

          return false;
       }

       /**
        * Indicate whether a DMO property can be validated at runtime, given its
        * legacy schema name.  Possible forms of validation include:
        * <ul>
        *   <li>mandatory (non-nullable) constraint;
        *   <li>participation in a unique constraint;
        *   <li>explicit validation method (converted from a Progress validation
        *       expression).
        * </ul>
        *
        * @param   historical
        *          Fully qualified, legacy Progress field name.
        *
        * @return  <code>true</code> if the field should be validated at runtime,
        *          else <code>false</code>.
        */
       private boolean containsValidatable(String historical)
       {
          if (validatables.contains(historical))
             return true;

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                if (parent.containsValidatable(historical))
                   return true;
             }
          }

          return false;
       }

       /**
        * Indicate whether a DMO property is nullable.
        *
        * @param   historical
        *          Fully qualified, legacy Progress field name.
        *
        * @return  <code>true</code> if the field is nullable, else <code>false</code>.
        */
       private boolean containsNullable(String historical)
       {
          return !nonnullables.contains(historical);
       }

       /**
        * The given index in the given table has been referenced in business logic. Remove it from
        * the mutable index resource map.
        *
        * @param   table
        *          Qualified, normalized, legacy table name.
        * @param   index
        *          Legacy name of index which was referenced.
        */
       private void processIndexedReferenced(String table, String index)
       {
          if (mutableIndexMap.remove(table, index))
          {
             return;
          }

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                parent.processIndexedReferenced(table, index);
             }
          }
       }

       /**
        * The given field has been referenced in business logic. Remove it from the mutable field
        * resource map.
        *
        * @param   table
        *          Qualified, normalized, legacy table name.
        * @param   eName
        *          Legacy name (normalized schemaname) of the field which was referenced.
        */
       private void processFieldReferenced(String table, EntityName eName)
       {
          if (mutableFieldMap.remove(table, eName.getField()))
          {
             return;
          }

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                parent.processFieldReferenced(table, eName);
             }
          }
       }

       /**
        * Get the converted, Java name of the class or property associated with
        * the given, legacy table or field name (respectively).
        *
        * @param   historical
        *          Fully qualified, legacy table or field name. Must be normalized (in lowercase).
        *
        * @return  Unqualified, Java class name (for a table) or property name
        *          (for a field).
        */
       private String getJavaName(String historical)
       {
          return getJavaName(historical, null);
       }

       /**
        * Get the converted, Java name of the class or property associated with
        * the given, legacy table or field name (respectively).
        *
        * @param   historical
        *          Fully qualified, legacy table or field name. Must be normalized (in lowercase).
        * @param   index
        *          Zero-based index of custom property name for denormalized field with extent,
        *          otherwise null.
        *
        * @return  Unqualified, Java class name (for a table) or property name
        *          (for a field).
        */
       private String getJavaName(String historical, Long index)
       {
          String name = javaNameMap.containsKey(historical)
                ? javaNameMap.get(historical).get(index == null ? null : index + 1)
                : null;

          if (name != null)
          {
             return name;
          }

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                name = parent.getJavaName(historical, index);

                if (name != null)
                {
                   return name;
                }
             }
          }

          return null;
       }

       /**
        * Get the set of legacy field names for a particular legacy table.
        *
        * @param   historical
        *          Fully qualified, legacy table or field name (not necessarily normalized).
        * @param   includeHidden
        *          If {@code true} the hidden fields are also included.
        *
        * @return  Set of legacy field names for the given table.
        */
       private Set<String> getLegacyFieldNames(String historical, boolean includeHidden)
       {
          Set<String> names = new LinkedHashSet<>();
          String test = historical.toLowerCase() + ".";
          for (String next : javaNameMap.keySet())
          {
             if (next.startsWith(test))
             {
                // prepare to return the original historical name
                String original = originalNamesMap.get(next);
                original = (original == null) ? next : original;
                boolean isHidden = false;
                int k = original.lastIndexOf('.');
                if (k > 0)
                {
                   isHidden = ReservedProperty.isReservedProperty(original.substring(k + 1));
                }
                if (includeHidden || !isHidden)
                {
                   names.add(original);
                }
             }
          }

          if (names.isEmpty() && parents != null)
          {
             for (P2OLookup parent : parents)
             {
                names = parent.getLegacyFieldNames(historical, includeHidden);

                if (!names.isEmpty())
                {
                   break;
                }
             }
          }

          return names;
       }

       /**
        * Get the full list of index descriptors associated with a Progress table.
        *
        * @param   historical
        *          Fully qualified, legacy table name (not necessarily normalized).
        *
        * @return  A list of index descriptors associated with the table named {@code historical}. The list may be
        *          empty, but will not be {@code null}.
        */
       private List<P2JIndex> getIndexes(String historical)
       {
          List<P2JIndex> res = indexMap.get(historical.toLowerCase());

          if (res != null)
          {
             return res;
          }

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                res = parent.getIndexes(historical);

                if (res != null)
                   return res;
             }
          }

          return null;
       }

       /**
        * Get the full list of index descriptors associated with a Progress table.
        *
        * @param   historical
        *          Fully qualified, legacy table name (not necessarily normalized).
        *
        * @return  A list of index descriptors associated with the table named {@code historical}. The list may be
        *          empty, but will not be {@code null}.
        */
       private List<Aast> getProperties(String historical)
       {
          List<Aast> res = new ArrayList<>();

          for (Aast ast : propsMap.values())
          {
             String hist = (String) ast.getParent().getAnnotation("historical");
             if (historical.equalsIgnoreCase(hist))
             {
                res.add(ast);
             }
          }

          if (!res.isEmpty())
          {
             return res;
          }

          if (parents != null)
          {
             for (P2OLookup parent : parents)
             {
                res = parent.getProperties(historical);

                if (!res.isEmpty())
                {
                   return res;
                }
             }
          }

          return null;
       }

       /**
        * Initialize this <code>P2OLookup</code> instance by loading the P2O AST associated with the
        * specified schema, walking it, and extracting and organizing the relevant information.
        * Currently, initialization does the following:
        * <ul>
        *   <li>maps fully qualified, legacy schema table and field names to their converted
        *       (unqualified) Java equivalents;
        *   <li>maps a table's fully qualified, legacy schema table name to a list of index
        *       descriptors defined for that table.
        * </ul>
        * <p>
        * After the primary schema is walked, we also walk the metadata schema, loading all of its
        * elements under the primary schema. This ensures all metadata references are resolved to the
        * correct database.
        *
        * @param   schema
        *          Name of database schema associated with the target P2O AST.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       private void initialize(String schema)
       throws SchemaException
       {
          // load and initialize the primary schema and then the metadata schema, with the latter
          // being initialized under the former, so that references to metadata elements will match
          // the correct database
          Aast root = null;
          String[] schemas = null;
          SchemaConfig.Metadata meta = Configuration.getSchemaConfig().getMetadata();

          if (meta != null && !meta.getTables().isEmpty())
          {
             schemas = new String[] { schema, meta.getName() };
          }
          else
          {
             schemas = new String[] { schema };
          }

          for (String next : schemas)
          {
             try
             {
                root = loadAst(next);
             }
             catch (Exception exc)
             {
                throw new SchemaException("Error loading P2O AST for database schema:  " + next, exc);
             }

             initialize(schema, root, next != schema); // OK to compare pointers, the value is stored in array
          }
       }

       /**
        * Initialize this <code>P2OLookup</code> instance by walking the given
        * P2O AST and extracting and organizing the relevant information.
        * Currently, initialization does the following:
        * <ul>
        *   <li>maps fully qualified, legacy schema table and field names to
        *       their converted (unqualified) Java equivalents;
        *   <li>maps a table's fully qualified, legacy schema table name to a
        *       list of index descriptors defined for that table.
        * </ul>
        *
        * @param   schema
        *          Name of database schema associated with the target P2O AST.
        * @param   root
        *          Root node of P2O AST to inspect.
        * @param   meta
        *          Flags the schema as metadata information.
        *
        * @throws  SchemaException
        *          if there is any error loading P2J schema configuration data;
        *          if there is any error loading the AST from persistence.
        */
       private void initialize(String schema, Aast root, boolean meta)
       throws SchemaException
       {
          // map the Java package to the database (i.e., schema) name
          if (meta)
          {
             metaPackage = (root.getAnnotation(PACKAGE) + "._meta").intern();
          }
          else
          {
             javaPackage = (root.getAnnotation(PACKAGE) + "." +
                            (schema == null ? SchemaDictionary.TEMP_TABLE_DB : root.getText())).intern();
          }

          // Walk the class and property nodes, mapping each fully qualified, historical schema name to an
          // unqualified, converted Java name.
          // The Java names of classes are capitalized; those of properties are not.
          Aast nextClass = root.getImmediateChild(CLASS, null);
          while (nextClass != null)
          {
             classByIDMap.put(nextClass.getId(), nextClass);

             String ifaceName = nextClass.getText().intern();

             // store the name in a set for quick tests later by isDMOInterface()
             if (!javaNames.add(ifaceName))
             {
                LOG.log(Level.INFO,"Duplicate dmo interface name detected: [" + ifaceName + "]");
             }

             StringBuilder buf = new StringBuilder();
             if (schema != null)
             {
                buf.append(schema).append(".");
             }
             buf.append(nextClass.getAnnotation(HISTORICAL));
             String histTable = buf.toString().intern();
             String histTableNorm = histTable.toLowerCase();
             addJavaName(histTable, histTableNorm, ifaceName, null, null);

             if (nextClass.isAnnotation("multi-tenant"))
             {
                multiTenant.put(histTableNorm, nextClass.isAnnotation("no-default-area") ? 1 : 2);
             }

             // save the super-interface for later use
             if (nextClass.isAnnotation("tt_interface"))
             {
                // only in case of temp tables, the super-interface will be used as the 'wider' parameter type
                javaInterfaceMap.put(histTableNorm, (String) nextClass.getAnnotation("tt_interface"));
             }

             // store the meta tables, if the case
             if (meta)
             {
                historicalMetaTables.add(histTableNorm);
             }

             // add to set of read-only tables if annotation is present
             if (nextClass.isAnnotation(READ_ONLY) && (Boolean) nextClass.getAnnotation(READ_ONLY))
             {
                readOnlyTables.add(histTableNorm);
             }

             boolean runtime = Configuration.isRuntimeConfig();

             // retrieve first property in this class
             Aast nextProp = nextClass.getImmediateChild(PROPERTY, null);
             while (nextProp != null)
             {
                String histField = (String) nextProp.getAnnotation(HISTORICAL);

                // compose fully qualified, legacy field name, normalized and interned
                String legacyName = (histTable + '.' + histField).toLowerCase().intern();

                // map legacy field name to Java property name
                if (!nextProp.isAnnotation(SYNTHETIC))
                {
                   // add field to mutable field map
                   if (!runtime && !meta)
                   {
                      mutableFieldMap.add(histTableNorm, histField.toLowerCase());
                   }

                   if (nextProp.isAnnotation("customextent"))
                   {
                      denormalizedProperties.add(legacyName);
                      long index = (Long)nextProp.getAnnotation("customextent");
                      String denormalizedPropertyName =
                            (String) nextProp.getAnnotation("denormalizedpropertyname");
                      addJavaName(legacyName, legacyName, nextProp.getText(), index, denormalizedPropertyName);
                   }
                   else
                   {
                      addJavaName(legacyName, legacyName, nextProp.getText(), null, null);
                   }
                }

                // add to set of validatable properties if annotation is present
                if (nextProp.isAnnotation(VALIDATABLE) &&
                      (Boolean) nextProp.getAnnotation(VALIDATABLE))
                {
                   validatables.add(legacyName);
                }

                if (nextProp.isAnnotation(NON_NULLABLE ) && (Boolean) nextProp.getAnnotation(NON_NULLABLE))
                {
                   nonnullables.add(legacyName);
                }

                // store property AST by ID for reference later during initialization
                propsMap.put(nextProp.getId(), nextProp);

                // Retrieve next property in this class.
                nextProp = nextClass.getImmediateChild(PROPERTY, nextProp);
             }

             // process indexes
             initializeIndexes(histTable, nextClass, runtime, meta);

             // process relations
             initializeRelations(histTable, nextClass);

             // retrieve next class in this schema
             nextClass = root.getImmediateChild(CLASS, nextClass);
          }
       }

       /**
        * Adds property name to javaNameMap where the key is legacy field name and value is map
        * based on nullable index. Index is not null only for denormalized field with extent, in
        * this case index corresponds to denormalization table hint.
        *
        * @param   originalName
        *          Legacy field name (not necessarily normalized).
        * @param   normalizedName
        *          Legacy field name (mandatory normalized and interned).
        * @param   propertyName
        *          Property name.
        * @param   index
        *          Null-based index of custom property name for denormalized field with extent, otherwise null.
        * @param   denormalizedPropertyName
        *          Denormalized property name for denormalized field with extent, otherwise null.
        */
       private void addJavaName(String originalName,
                                String normalizedName,
                                String propertyName,
                                Long index,
                                String denormalizedPropertyName)
       {
          propertyName = propertyName.intern();

          Map<Long, String> customProperties;
          if (javaNameMap.containsKey(normalizedName))
          {
             customProperties = javaNameMap.get(normalizedName);
          }
          else
          {
             customProperties = new HashMap<>();
             if (index != null && denormalizedPropertyName != null)
             {
                customProperties.put(null, denormalizedPropertyName.intern());
             }
             javaNameMap.put(normalizedName, customProperties);
          }
          customProperties.put(index, propertyName);

          // save the original name for later access only if different than the normalized
          if (!normalizedName.equals(originalName))
          {
             originalNamesMap.put(normalizedName, originalName.intern());
          }
       }

       /**
        * Compose a list of index descriptors for the index children of the
        * given P2O class node and map it to the given, legacy table name.
        *
        * @param   historical
        *          Fully qualified, legacy table name (not necessarily normalized).
        * @param   classAst
        *          P2O AST node describing a DMO; the direct parent of all P2O index nodes.
        * @param   runtime
        *          {@code true} if this is a runtime configuration; {@code false} if this is a static
        *          conversion configuration.
        * @param   meta
        *          Flags the schema as metadata information.
        */
       private void initializeIndexes(String historical, Aast classAst, boolean runtime, boolean meta)
       {
          String table = historical.toLowerCase().intern();
          List<P2JIndex> indexList = new ArrayList<>();
          Aast nextIndex = classAst.getImmediateChild(INDEX, null);
          while (nextIndex != null)
          {
             if (!nextIndex.isAnnotation(SYNTHETIC))
             {
                String indexName = ((String) nextIndex.getAnnotation(HISTORICAL)).intern();
                P2JIndex indexDef = new P2JIndex(
                      (String) classAst.getAnnotation(TABLE),
                      indexName,
                      nextIndex.isAnnotation(UNIQUE),
                      nextIndex.isAnnotation(PRIMARY),
                      nextIndex.isAnnotation(WORD));
                indexList.add(indexDef);

                if (!runtime && !meta)
                {
                   mutableIndexMap.add(table, indexName);
                }

                // Retrieve first component in the index.
                Aast nextCol = nextIndex.getImmediateChild(INDEX_COL, null);
                while (nextCol != null)
                {
                   // Retrieve the property referenced by the index column.
                   Long refid = (Long) nextCol.getAnnotation(REFID);
                   Aast propAst = propsMap.get(refid);

                   // Add a component to the index descriptor.
                   String idxFldName = historical + '.' + propAst.getAnnotation(HISTORICAL);
                   indexDef.addComponent(
                         idxFldName.intern(), null, null,
                         nextCol.isAnnotation(DESCEND),
                         "character".equals(propAst.getAnnotation(DATATYPE)),
                         nextCol.isAnnotation(IGNORE_CASE),
                         nextCol.isAnnotation(ABBREVIATED),
                         nextCol.isAnnotation(NON_NULLABLE));

                   // Retrieve next component in the index.
                   nextCol = nextIndex.getImmediateChild(INDEX_COL, nextCol);
                }
             }

             // Retrieve next index in this class.
             nextIndex = classAst.getImmediateChild(INDEX, nextIndex);
          }

          indexMap.put(table, indexList);
       }

       /**
        * Compose and store the set of tables which refer to a database table via
        * a foreign key, given the referent table's P2O class node and its legacy
        * name.  The referring tables are actually stored as a set AST IDs, which
        * is mapped it to the given, legacy table name.
        *
        * @param   historical
        *          Fully qualified, legacy table name (may not be normalized).
        * @param   classAst
        *          P2O AST node describing a DMO; the direct parent of all P2O relation nodes.
        */
       private void initializeRelations(String historical, Aast classAst)
       {
          historical = historical.toLowerCase().intern();

          Aast nextRel = null;
          while ((nextRel = classAst.getImmediateChild(types, nextRel)) != null)
          {
             boolean referent = false;
             switch (nextRel.getType())
             {
                case ONE_TO_MANY:
                   referent = true;
                   break;
                case ONE_TO_ONE:
                   referent = !(Boolean) nextRel.getAnnotation(FOREIGN);
                   break;
             }

             if (referent)
             {
                // Add AST ID to set of referring tables for this referent table.
                Set<Long> referrers = foreignReferenceMap.get(historical);
                if (referrers == null)
                {
                   referrers = new LinkedHashSet<>();
                   foreignReferenceMap.put(historical, referrers);
                }
                referrers.add((Long) nextRel.getAnnotation(REFERENCES));
             }
          }
       }

       /**
        * Load the persisted, P2O AST associated with the specified database
        * schema.
        *
        * @param   database
        *          Logical name of database schema associated with the P2O
        *          AST to be loaded.
        *
        * @return  P2O AST associated with <code>database</code>.
        *
        * @throws  ConfigurationException
        *          if there is any error loading P2J schema configuration data.
        * @throws  AstException
        *          if there is any error loading the AST from persistence.
        */
       private Aast loadAst(String database)
       throws ConfigurationException,
             AstException
       {
          SchemaConfig config = Configuration.getSchemaConfig();
          boolean runtime = Configuration.isRuntimeConfig();
          String path;
          if (runtime)
          {
             path = config.getSchemaResource(database, P2O_POSTFIX);
             if (path == null)
             {
                throw new ConfigurationException("Cannot find " + P2O_POSTFIX + " for database " + database);
             }
          }
          else
          {
             File file = config.getSchemaFile(database, P2O_POSTFIX);
             path = Configuration.normalizeFilename(file.getAbsolutePath());
          }

          return AstManager.get().loadTree(path);
       }

       /**
        * Dump an exhaustive tree of nested table dependencies.  A dependency is
        * a foreign key relation from the dependent table to the "parent" table.
        * The tree is printed to stdout.  Each indent indicates a dependency to
        * the next outer level.
        *
        * @param   historical
        *          Fully qualified, legacy Progress table name.
        * @param   indent
        *          Level of indent to apply when printing this table.
        */
       private void dumpTableDependencies(String historical, int indent)
       {
          StringBuilder buf = new StringBuilder();
          StringHelper.repeatChar(' ', indent * 3, buf);
          buf.append(historical);
          buf.append(" (");
          buf.append(getJavaName(historical));
          buf.append(")");

          LOG.log(Level.INFO, buf.toString());

          Set<Long> referrers = foreignReferenceMap.get(historical);
          if (referrers != null)
          {
             for (Long id : referrers)
             {
                Aast classAst = classByIDMap.get(id);
                if (classAst != null)
                {
                   StringBuilder hstBuf = new StringBuilder();
                   hstBuf.append(classAst.getParent().getText());
                   hstBuf.append(".");
                   String hist = (String) classAst.getAnnotation(HISTORICAL);
                   hstBuf.append(hist);
                   dumpTableDependencies(hstBuf.toString(), indent + 1);
                }
             }
          }
       }

       /** Container with user-specific data. */
       private static class WorkArea
       {
          /** Current temp-table <code>P2OLookup</code> instance */
          private P2OLookup tempP2OLookup = null;

          /** Set of all known schema names from P2J conversion configuration */
          private Set<String> schemaNames = null;

          /** Name of last temp-table P2O AST file to be loaded */
          private String lastTempFilename = null;
       }

       /**
        * Test harness for property name lookup feature.  Expects no arguments.
        *
        * @param   args
        *          Not used.
        */
       public static void main(String[] args)
       {
          try
          {
             BufferedReader in = new BufferedReader(
                   new InputStreamReader(System.in));
             while (true)
             {
                LOG.log(Level.INFO, "Enter test data or ':q' to quit:  ");
                String input = in.readLine();
                if (":q".equalsIgnoreCase(input))
                {
                   LOG.log(Level.INFO, "Goodbye.");
                   break;
                }

                try
                {
    //               LOG.info("Qualified java property name:  " +
    //                  P2OLookup.javaPropertyName(input, true));
    //               LOG.info("Legacy field names:  " +
    //                  P2OLookup.legacyFieldNames(input));
    //               LOG.infon("Is DMO interface ---> " +
    //                  P2OLookup.isDMOInterface(input));
    //               LOG.info("Is referenced by foreign key ---> " +
    //                  P2OLookup.isForeignReference(input));
                   LOG.log(Level.INFO, "Table Dependencies:");
                   P2OLookup.dumpTableDependencies(input.toLowerCase());
                }
                catch (SchemaException exc)
                {
                   LOG.log(Level.SEVERE, "", exc);
                }
             }
          }
          catch (Exception exc)
          {
             LOG.log(Level.SEVERE, "", exc);
          }
       }
    }