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