TableHints.java

/*
** Module   : TableHints.java
** Abstract : manages hints read from a UAST hints file about a schema table
**
** Copyright (c) 2004-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050713   @21702 Created initial version. Contains info to override name conversions
**                           at the table and field level, and to define new associations between
**                           tables, or to provide supplemental data for existing associations.
** 002 NVS 20050816   @22154 Added DROP attribute and DISPOSITION element definition and
**                           processing. These define how the table or table fields are imported.
**                           Added methods to query DROP attribute for table and fields, as well
**                           as DISPOSITIONs.
** 003 NVS 20050817   @22159 Changed methods returning dispositions from Set to List as these are
**                           readily accepted by putAnnotation() method.
** 004 ECF 20060828   @29081 Added 'encoded' attribute to field node. Indicates whether a
**                           character field must be stored base-64 encoded (and decoded on read).
** 005 ECF 20070303   @32266 Added 'temp' attribute to table node. This specifies an explicit
**                           name to apply to a temp/work-table defined like the permanent
**                           table for which the hint applies.
** 006 SVL 20080326   @37684 Added support for additional ("synthetic") indexes loaded from the
**                           hints file.
** 007 VMN 20131114          Added support for custom conversion of fields with extents.
** 008 VMN 20140221          Using ELEM_EXTENT_NAME instead of ELEM_EXTENT_NAMES for custom
**                           conversion of fields with extents. Added ExtentHintField support.
** 009 VMN 20140501          Added support for literal replacements.
** 010 OM  20140407          Added support for optional SQL length specified in hints file.
**                           Generified collections.
** 011 IAS 20160619          Added support for the "read-only" hint 
** 012 OM  20200108          Added support for "dirty-read" hint/attribute.
** 013 OM  20200925          Added support for generating denormalized extent fields in dynamic temp-table
**                           conversion.
**     IAS 20210219          Added custom extent adjustment.
**     ECF 20220528          Added table attribute to disable match phrase replacement.
**     OM  20220722          The indices in denormalized fields are separated by one underscore.
** 014 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 015 SVL 20230516          Support for dirty-intra-read annotation.
** 016 DDF 20230620          Replaced static initialization of values from the directory configuration with
**                           a method called at server bootstrap.
**     DDF 20230627          Used a standard handling for directory binding.
*/

/*
** 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.util.*;
import java.util.logging.*;

import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.util.*;
import org.w3c.dom.*;

/**
 * Describes a variety of hint information for a single schema table.
 */
public class TableHints
{
   /** Field element of table and association elements */
   private static final String ELEM_FIELD = "field";
   
   /** Phrase element to define a customized name conversion */
   private static final String ELEM_PHRASE = "phrase";
   
   /** Disposition element to define a type of import processing */
   private static final String ELEM_DISP = "disposition";
   
   /** Relational association between this table and another */
   private static final String ELEM_ASSOCIATION = "association";
   
   /** Synthetic index element */
   private static final String ELEM_INDEX = "index";
   
   /** Field element of index */
   private static final String ELEM_INDEX_FIELD = "index-field";
   
   /** Element to define custom processing table fields with extent */
   private static final String ELEM_CUSTOM_EXTENT = "custom-extent";
   
   /** Element to define custom naming conversion of extent field */
   private static final String ELEM_EXTENT_NAME = "extent-field";
   
   /** Name attribute of table and field elements */
   private static final String ATTR_NAME = "name";
   
   /** Read-only attribute of table */
   private static final String ATTR_READ_ONLY = "read-only";
   
   /** {@code dirty-read} attribute of table. */
   private static final String ATTR_DIRTY_READ = "dirty-read";

   /** {@code dirty-intra-read} attribute of table. */
   private static final String ATTR_DIRTY_INTRA_READ = "dirty-intra-read";
   
   /** Drop attribute of table and field elements */
   private static final String ATTR_DROP = "drop";
   
   /** Temp-table raw (pre-conversion) name attribute */
   private static final String ATTR_TEMP = "temp";
   
   /** Encoded attribute of field element */
   private static final String ATTR_ENCODED = "encoded";
   
   /** Unique attribute of index element */
   private static final String ATTR_UNIQUE = "unique";
   
   /** Direction attribute of index field element */
   private static final String ATTR_DIRECTION = "direction";
   
   /** Type attribute of association element */
   private static final String ATTR_TYPE = "type";
   
   /** Attribute to disable match phrase replacement, except for invalid names (table and all fields) */
   private static final String ATTR_NO_REPLACE = "no-phrase-replace";
   
   /** Match attribute of phrase element */
   private static final String ATTR_MATCH = "match";
   
   /** Replace attribute of phrase element */
   private static final String ATTR_REPLACE = "replace";
   
   /** No-inherit attribute of phrase element */
   private static final String ATTR_NO_INHERIT = "no-inherit";
   
   /** References attribute of phrase element */
   private static final String ATTR_REFERENCES = "references";
   
   /** Foreign attribute of association elements */
   private static final String ATTR_FOREIGN = "foreign";
   
   /** Dmo attribute of table element */
   private static final String ATTR_DMO = "dmo";
   
   /** Sql-table attribute of table element */
   private static final String ATTR_SQL_TABLE = "sql-table";
   
   /** Property attribute of field element */
   private static final String ATTR_PROPERTY = "property";
   
   /** Sql-column attribute of field element */
   private static final String ATTR_SQL_COLUMN = "sql-column";
   
   /** SQL-size attribute to define a SQL size hint for character fields */
   private static final String ATTR_SQL_SIZE = "sql-size";
   
   /** Key under which join fields are stored in an association map */
   private static final String KEY_FIELDS = "fields";
   
   /** Key under which index fields are stored in the indexes map */
   private static final String KEY_INDEX_FIELDS = "index-fields";
   
   /** Value of type for many-to-one association */
   private static final String TYPE_MANY_TO_ONE = "many-to-one";
   
   /** Value of type for one-to-many association */
   private static final String TYPE_ONE_TO_MANY = "one-to-many";
   
   /** Value of type for one-to-one association */
   private static final String TYPE_ONE_TO_ONE = "one-to-one";
   
   /** Maximum size of a character field in Progress is about 32K. */
   private static final int MAX_SQL_SIZE = 32 * 1024 * 1024;

   /** Logger */
   private static final ConversionStatus LOG = ConversionStatus.get(TableHints.class);
   
   /**
    * Flag for denormalized extent properties in dynamic temp-tables. When enabled from directory, 
    * {@code "persistence/denormalize-dynamic-temp-tables"}, the extent fields will NOT use secondary tables
    * for SQL storage. <p> 
    * Default is disabled. 
    */
   private static boolean denormalizeDynamicTempTables = false;
   
   /** Table escape attribute, null means that attribute is not set. */
   private Boolean escaped = null;
   
   /** Field escape attributes. */
   private final Map<String, Boolean> escapedFields = new HashMap<>();
   
   /**
    * Dmo conversion replacement for the table. Specifies the exact DMO class name to generate
    * for the Java DMO interface (and implementation class, with an appended "Impl")
    */
   private String dmo;
   
   /**
    * SQL table conversion replacement for the table. Specifies the exact table name to
    * generate for the DDL for this table
    */
   private String sqlTable = null;
   
   /**
    * DMO properties conversion replacements for the table. Specifies the exact DMO property
    * name to generate for the Java DMO interface and implementation class getter/setter methods.
    */
   private final Map<String, String> properties = new HashMap<>();
   
   /**
    * SQL columns conversion replacements for the table. Specifies the exact column name to
    * generate for the DDL for specified field
    */
   private final Map<String, String> sqlColumns = new HashMap<>();
   
   /** Name conversion replacements for the table inherited by fields */
   private final Map<String, String> inheritPhrases = new HashMap<>();
   
   /** Name conversion replacements for the table not inherited by fields */
   private final Map<String, String> noInheritPhrases = new HashMap<>();
   
   /** Name conversion replacements for fields within table */
   private final Map<String, Map<String, String>> fieldPhrases = new HashMap<>();
   
   /**
    * SQL sizes for (character) fields extracted from .hints file.
    * If some fields from this table are not in this map the maximum SQL size is used when
    * declaring the DDL for creating current SQL table.
    */
   private final Map<String, Integer> sqlSizes = new HashMap<>();
   
   /** Fields which require base-64 encoding for storage */
   private final Set<String> encodedFields = new HashSet<>();
   
   /** Association definitions for this table */
   private final Map<Object, Map<String, Object>> associations = new HashMap<>();
   
   /** Table dispositions for this table */
   private final Set<String> tableDisp = new HashSet<>();
   
   /** Field drop flags for this table */
   private final Set<String> fieldDrop = new HashSet<>();
   
   /** Field dispositions for this table */
   private final Map<String, Set<String>> fieldDisp = new HashMap<>();
   
   /** Map containing information about synthetic indexes and their fields */
   private final Map<String, Map<String, Object>> indexes = new HashMap<>();
   
   /** Map containing information about custom conversion of fields with extents*/
   private Map<String, List <ExtentHintField>> customExtents = null;
   
   /** Name of table described by this hint instance */
   private final String name;
   
   /** Pre-conversion name of temp-table based upon underlying table */
   private final String tempTableName;
   
   /** Flag indicating that match phrase replacements are disabled (except the standard ones) */
   private final boolean noPhraseReplace;
   
   /** Table drop flag for this table */
   private final boolean tableDrop;
   
   /** Flag indicating that the table is read-only */  
   private final boolean readOnly;
   
   /** Flag indicating that the table needs to be tracked by the dirty share manager across sessions. */
   private final boolean dirtyRead;

   /**
    * Flag indicating that the table needs to be tracked by the dirty share manager within the same session.
    */
   private final boolean dirtyIntraRead;
   
   /**
    * Method called at server bootstrap that initializes values from the directory
    * configuration. Until this method is called, default values are used.
    */
   public static void bootstrap()
   {
      DirectoryService ds = DirectoryService.getInstance();
      if (ds != null)
      {
         if (!ds.bind())
         {
            throw new RuntimeException("Directory bind failed");
         }
         
         try
         {
            // configure denormalized extent in dynamic temp-tables, default is disabled, i.e. the extent
            // fields will use secondary tables for SQL storage
            String path = "persistence/denormalize-dynamic-temp-tables";
            denormalizeDynamicTempTables = Utils.getDirectoryNodeBoolean(ds, path, false, false);
         }
         finally
         {
            ds.unbind();
         }
      }
   }
   
   /**
    * Check whether the extent properties in dynamic temp-tables should be denormalized. When enabled from
    * directory, {@code "persistence/denormalize-dynamic-temp-tables"}, the extent fields will NOT use
    * secondary tables for SQL storage. <p>
    *
    * @return  {@code true} if the extent properties in dynamic temp-tables should be denormalized.
    */
   public static boolean isDenormalizeDynamicTempTables()
   {
      return denormalizeDynamicTempTables;
   }
   
   /**
    * Constructor which configures hint from an XML element.
    *
    * @param   table
    *          XML element describing the table hint. Corresponds with
    *          <code>table</code> tag within the hints file.
    */
   public TableHints(Element table)
   {
      NodeList list;
      int len;
      
      name = table.getAttribute(ATTR_NAME);
      readOnly = "true".equalsIgnoreCase(table.getAttribute(ATTR_READ_ONLY));
      dirtyRead = "true".equalsIgnoreCase(table.getAttribute(ATTR_DIRTY_READ));
      dirtyIntraRead = "true".equalsIgnoreCase(table.getAttribute(ATTR_DIRTY_INTRA_READ));
      tableDrop = "true".equalsIgnoreCase(table.getAttribute(ATTR_DROP));
      escaped = "true".equalsIgnoreCase(table.getAttribute(HintsConstants.ATTR_ESCAPE));
      
      // Read temp table raw name.
      String ttName = table.getAttribute(ATTR_TEMP);
      tempTableName = (ttName == null || ttName.trim().length() == 0) ? null : ttName;
      
      // Read table dmo.
      if (table.hasAttribute(ATTR_DMO))
      {
         dmo = table.getAttribute(ATTR_DMO);
      }
      
      // Read table sql-table attribute.
      if (table.hasAttribute(ATTR_SQL_TABLE))
      {
         sqlTable = table.getAttribute(ATTR_SQL_TABLE);
      }
      
      // Read property and sql-column field attributes.
      list = table.getElementsByTagName(ELEM_FIELD);
      len = list.getLength();
      for (int i = 0; i < len; i++)
      {
         Element field = (Element) list.item(i);
         String fieldName = field.getAttribute(ATTR_NAME);
         
         if (field.hasAttribute(HintsConstants.ATTR_ESCAPE))
         {
            escapedFields.put(fieldName,
                  "true".equalsIgnoreCase(field.getAttribute(HintsConstants.ATTR_ESCAPE)));
         }
         
         if (field.hasAttribute(ATTR_PROPERTY))
         {
            properties.put(fieldName, field.getAttribute(ATTR_PROPERTY));
         }
         
         if (field.hasAttribute(ATTR_SQL_COLUMN))
         {
            sqlColumns.put(fieldName, field.getAttribute(ATTR_SQL_COLUMN));
         }
      }
      
      // check for no-phrase-replace attribute at the table level, which disables match phrase replacement
      // for the table and all its fields and indices (even if phrase elements appear in the table element)
      this.noPhraseReplace = "true".equalsIgnoreCase(table.getAttribute(ATTR_NO_REPLACE));
      
      // Read table and field phrases.
      list = table.getElementsByTagName(ELEM_PHRASE);
      len = list.getLength();
      for (int i = 0; i < len; i++)
      {
         Element phrase = (Element) list.item(i);
         Element parent = (Element) phrase.getParentNode();
         Map<String, String> map;
         
         // Is parent a field element...
         if (parent.getTagName().equals(ELEM_FIELD))
         {
            String fieldName = parent.getAttribute(ATTR_NAME);
            map = fieldPhrases.get(fieldName);
            if (getProperty(fieldName) == null)
            {
               if (map == null)
               {
                  map = new HashMap<>();
                  fieldPhrases.put(fieldName, map);
               }
            }
            else
            {
               if (map != null && map.size() > 0)
               {
                  LOG.log(Level.WARNING,
                          "Ignoring phrase hints for table " + name + ", " +  "field " + fieldName + ", " +
                             "because at least one of sql-column or property attributes is set!");
                  map = null;
               }
            }
         }
         // ...or a table element?
         else
         {
            map = "true".equals(phrase.getAttribute(ATTR_NO_INHERIT))
                  ? noInheritPhrases
                  : inheritPhrases;
            if (getDmo() != null && map.size() > 0)
            {
               LOG.log(Level.WARNING, "Ignoring phrase hints for table " + name +
                              " because at least one of sql-table or dmo attributes is set!");
               map = null;
            }
         }
         
         if (map != null)
         {
            map.put(phrase.getAttribute(ATTR_MATCH), phrase.getAttribute(ATTR_REPLACE));
         }
      }
      
      // Read field SQL-SIZE hints. At this moment only for character will be handled,
      // but sql sizes for other types can/will be added based on necessities.
      // Syntax: to specify the SQL-SIZE, use the 'sql-size' attribute of the 'field' sub-element
      //         of a 'table' element. The value of the attribute is SQL length in datatype
      //         items (eg.: char(n) will take n bytes but nchar(n) will use 2*n bytes).
      list = table.getElementsByTagName(ELEM_FIELD);
      len = list.getLength();
      for (int i = 0; i < len; i++)
      {
         Element field = (Element) list.item(i);
         
         if (!field.hasAttribute(ATTR_SQL_SIZE))
         {
            continue;
         }
         
         String fieldSize = field.getAttribute(ATTR_SQL_SIZE);
         int sqlSize = -1;
         try
         {
            sqlSize = Integer.parseInt(fieldSize);
         }
         catch (NumberFormatException e)
         {
            // drop this exception, warning will be printed below because sqlSize remains -1.
         }
         
         if (sqlSize > 0 && sqlSize < MAX_SQL_SIZE)
         {
            sqlSizes.put(field.getAttribute(ATTR_NAME), sqlSize);
         }
         // otherwise print some warning ?
      }
      
      // Retrieve associations for table.
      list = table.getElementsByTagName(ELEM_ASSOCIATION);
      len = list.getLength();
      for (int i = 0; i < len; i++)
      {
         int size;
         Element assoc = (Element) list.item(i);
         
         // Store attributes in a map, each of which defines the properties
         // of an association and a simple key/value string pair.
         NamedNodeMap attrs = assoc.getAttributes();
         Map<String, Object> map = new HashMap<>();
         size = attrs.getLength();
         for (int j = 0; j < size; j++)
         {
            Attr attr = (Attr) attrs.item(j);
            map.put(attr.getName(), attr.getValue());
         }
         
         // Handle fields which define the join specially. These are
         // collected into a set and stored under the key "fields" in the
         // associations map.
         NodeList fields = assoc.getElementsByTagName(ELEM_FIELD);
         size = fields.getLength();
         Set<String> set = null;
         for (int j = 0; j < size; j++)
         {
            Element field = (Element) fields.item(i);
            if (set == null)
            {
               set = new HashSet<>();
            }
            set.add(field.getAttribute(ATTR_NAME));
         }
         if (set != null)
         {
            map.put(KEY_FIELDS, set);
         }
         
         // Map associations by the name of the table they reference.
         associations.put(map.get(ATTR_REFERENCES), map);
      }
      
      // Read field drops and encoded fields.
      list = table.getElementsByTagName(ELEM_FIELD);
      len = list.getLength();
      for (int i = 0; i < len; i++)
      {
         Element field = (Element) list.item(i);
         String fieldName = field.getAttribute(ATTR_NAME);
         
         if ("true".equalsIgnoreCase(field.getAttribute(ATTR_DROP)))
         {
            fieldDrop.add(fieldName);
         }
         
         if ("true".equalsIgnoreCase(field.getAttribute(ATTR_ENCODED)))
         {
            encodedFields.add(fieldName);
         }
      }
      
      // Read table and field dispositions.
      list = table.getElementsByTagName(ELEM_DISP);
      len = list.getLength();
      for (int i = 0; i < len; i++)
      {
         Element disp = (Element) list.item(i);
         Set<String> set;
         Element parent = (Element) disp.getParentNode();
         
         // Is parent a field element...
         if (parent.getTagName().equals(ELEM_FIELD))
         {
            String fieldName = parent.getAttribute(ATTR_NAME);
            set = fieldDisp.get(fieldName);
            if (set == null)
            {
               set = new HashSet<>();
               fieldDisp.put(fieldName, set);
            }
         }
         // ...or a table element?
         else
         {
            set = tableDisp;
         }
         
         set.add(disp.getAttribute(ATTR_NAME));
      }
      
      /*
       * Read custom extent elements into the map like field-name -> list of extent property names.
       *
       * Using this hint for fields with extent:
       *
       * a) customExtents == null
       * It means that this table has no extent fields with custom conversion.
       *
       * b) customExtents.get(field-name) == null
       * It means that this field has no custom conversion.
       *
       * c) customExtents.get(field-name) returns empty list
       * It means that this field has custom conversion with default naming conversion
       * (e.g., field(1)-->field_1, field(2)-->field2, ... field(N)-->fieldN).
       *
       * d) customExtents.get(field-name) returns non-empty list
       * It means that this field has custom conversion with configurable, depending on customer
       * preferences (e.g., allowing individual indexes to be named explicitly, as in,
       * hours(1)-->monday_hours, hours(2)-->tuesday_hours, etc.).
       */
      list = table.getElementsByTagName(ELEM_CUSTOM_EXTENT);
      len = list.getLength();
      if (len > 0)
      {
         customExtents = new HashMap<>();
         for (int i = 0; i < len; i++)
         {
            Element extent = (Element) list.item(i);
            NodeList fieldsList = extent.getElementsByTagName(ELEM_FIELD);
            int fieldsListLen = fieldsList.getLength();
            if (fieldsListLen == 0)
            {
               // it overrides custom extents for separate fields
               customExtents.clear();
               break;
            }
            else
            {
               for (int k = 0; k < fieldsListLen; k++)
               {
                  Element field = (Element) fieldsList.item(k);
                  NodeList extentNodes = field.getElementsByTagName(ELEM_EXTENT_NAME);
                  List<ExtentHintField> extentHintFields = new ArrayList<>();
                  for (int j = 0; j < extentNodes.getLength(); j ++)
                  {
                     Element extentHintField = (Element) extentNodes.item(j);
                     extentHintFields.add(new ExtentHintField(
                           extentHintField.getAttribute("name"),
                           extentHintField.getAttribute("label"),
                           extentHintField.getAttribute("description")));
                  }
                  customExtents.put(field.getAttribute("name"), extentHintFields);
               }
            }
         }
      }
      
      if (customExtents == null && denormalizeDynamicTempTables && Configuration.isRuntimeConfig())
      {
         customExtents = new HashMap<>();
      }
      
      /*
       * Read table synthetic indexes into the map like
       * index-name -> 'unique' -> true/false
       *               'index-fields' -> field-name -> 'direction' -> asc/desc
       *                                 field-name2 -> ...
       * index-name2 -> ...
       */ 
      list = table.getElementsByTagName(ELEM_INDEX);
      len = list.getLength();
      for (int i = 0; i < len; i++)
      {
         Element index = (Element) list.item(i);
         String indexName = index.getAttribute(ATTR_NAME);
         
         Map<String, Object> map = new HashMap<>();
         map.put(ATTR_UNIQUE, index.getAttribute(ATTR_UNIQUE));
         
         NodeList fields = index.getElementsByTagName(ELEM_INDEX_FIELD);
         Map<String, Map<String, String>> fieldsMap = new HashMap<>();
         int fieldsLen = fields.getLength();
         for (int j = 0; j < fieldsLen; j++)
         {
            Element field = (Element) fields.item(j);
            String fieldName = field.getAttribute(ATTR_NAME);
            
            Map<String, String> propertiesMap = new HashMap<>();
            propertiesMap.put(ATTR_DIRECTION, field.getAttribute(ATTR_DIRECTION));
            
            fieldsMap.put(fieldName, propertiesMap);
         }
         
         map.put(KEY_INDEX_FIELDS, fieldsMap);
         
         indexes.put(indexName, map);
      }
   }
   
   /**
    * Get the name of the table described by this hint.
    *
    * @return  Table name.
    */
   public String getName()
   {
      return name;
   }
   
   /**
    * Get the pre-conversion name of a temp table which is based upon the
    * underlying table.
    *
    * @return  Raw temp table name to be passed to the name converter, or
    *          <code>null</code> if no explicit temp-table name exists.
    */
   public String getTempTableName()
   {
      return tempTableName;
   }
   
   /**
    * Get the custom DMO conversion replacement for the table.
    *
    * @return  Replacement DMO name for this table. Taken from &quot;dmo&quot; attribute or from
    *          &quot;sql-table&quot; attribute and <code>null</code> if property hint is not
    *          specified in any of these attributes.
    */
   public String getDmo()
   {
      return dmo == null ? sqlTable : dmo;
   }
   
   /**
    * Get the custom replacement name used for sql table conversion.
    *
    * @return  Replacement name for this table.  Taken from &quot;sql-table&quot; attribute or from
    *          &quot;dmo&quot; attribute and <code>null</code> if property hint is not
    *          specified in any of these attributes.
    */
   public String getSqlTable()
   {
      return sqlTable == null ? dmo : sqlTable;
   }
   
   /**
    * Get the custom name used for conversion of DMO property for the specified field in this
    * table.
    *
    * @param   field
    *          Name of the field for retrieve &quot;property&quot; attribute or
    *          &quot;sqlColumn&quot; attribute.
    *
    * @return  Custom name used for conversion of DMO property for the specified field in this
    *          table. Taken from &quot;property&quot; attribute or from &quot;sql-column&quot;
    *          attribute and <code>null</code> if property hint is not specified in any of
    *          these attributes.
    */
   public String getProperty(String field)
   {
      String property = properties.get(field);
      return property == null ? sqlColumns.get(field) : property;
   }
   
   /**
    * Get the custom name used for conversion of sql column for the specified field in this
    * table.
    *
    * @param   field
    *          Name of the field for retrieve &quot;sqlColumn&quot; attribute or
    *          &quot;property&quot; attribute.
    *
    * @return  Custom name used for conversion of sql column for the specified field in this
    *          table. Taken from &quot;sql-column&quot; attribute or from &quot;property&quot;
    *          attribute and <code>null</code> if property hint is not specified in any of
    *          these attributes.
    */
   public String getSqlColumn(String field)
   {
      String sqlColumn = sqlColumns.get(field);
      return sqlColumn == null ? properties.get(field) : sqlColumn;
   }
   
   /**
    * Indicate whether this hint disables match phrase replacement (except for the standard replacements
    * which ensure a valid/legal name).
    * 
    * @return  {@code true} if replacements are disabled, else {@code false}.
    */
   public boolean isNoPhraseReplace()
   {
      return noPhraseReplace;
   }
   
   /**
    * Get the custom replacement map (if any) used for name conversion for
    * this table. This is a combination of all inheritable and
    * non-inheritable phrases for this table.
    *
    * @return  Replacement map for this table, which may be empty if no
    *          phrase hints are defined, but will not be <code>null</code>.
    */
   public Map<String, String> getNameReplacements()
   {
      Map<String, String> map = new HashMap<>();
      map.putAll(inheritPhrases);
      map.putAll(noInheritPhrases);
      
      return map;
   }
   
   /**
    * Get the custom replacement map (if any) used for name conversion for
    * the specified field in this table. This is a combination of all
    * inheritable phrases for this table and the phrases defined specifically
    * for the named field.
    *
    * @param   field
    *          Name of the field for which to retrieve replacement phrases.
    *
    * @return  Replacement map for this table, which may be empty if no
    *          phrase hints are defined, but will not be <code>null</code>.
    */
   public Map<String, String> getNameReplacements(String field)
   {
      Map<String, String> map = new HashMap<>();
      map.putAll(inheritPhrases);
      Map<String, String> fieldMap = fieldPhrases.get(field);
      if (fieldMap != null)
      {
         map.putAll(fieldMap);
      }
      
      return map;
   }
   
   /**
    * Get the SQL size hint for a (character) field. If not defined, <code>null</code> is
    * returned.
    *
    * @param   field
    *          Name of the field for which to retrieve sql size hint.
    *
    * @return  The SQL size hint for requested field, or <code>null</code> if not defined.
    */
   public Integer getSqLSizeHint(String field)
   {
      return sqlSizes.get(field);
   }
   
   /**
    * Report whether the specified field must be base-64 encoded for data
    * storage (and decoded upon retrieval).
    *
    * @param   field
    *          Name of the field to test for the &quot;encoded&quot;
    *          attribute.
    *
    * @return  <code>true</code> if data must be base-64 encoded/decoded.
    */
   public boolean isFieldEncoded(String field)
   {
      return encodedFields.contains(field);
   }

   /**
    * Report whether the database column name for specified field should be escaped.
    *
    * @param   field
    *          Name of the field to test for escaping.
    *
    * @return  <code>true</code> if column should be escaped, <code>false</code> if column
    *          should not be escaped, <code>null</code> if attribute &quot;escape&quot; is set
    *          neither for field nor for table.
    */
   public Boolean isFieldEscaped(String field)
   {
      if (escapedFields.containsKey(field))
      {
         return escapedFields.get(field);
      }
      return escaped;
   }

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

   /**
    * Get an iterator on the names of all tables referenced by relational
    * associations from this table, in no particular order.
    *
    * @return  Iterator on referenced table names.
    */
   public Iterator<?> associatedTables()
   {
      return associations.keySet().iterator();
   }
   
   /**
    * Get the schema parser token type associated with the association (if
    * any) described by this hint between this table and the table it
    * references. Valid token types are:
    * <ul>
    * <li><code>MANY_TO_ONE</code>
    * <li><code>ONE_TO_MANY</code>
    * <li><code>ONE_TO_ONE</code>
    * </ul>
    *
    * @param   references
    *          Name of table which the association references.
    *
    * @return  One of the above token types, or <code>-1</code> if either the
    *          association does not exist, or an unrecognized association
    *          type was found.
    */
   public int getAssociationType(String references)
   {
      int ttype = -1;
      Map<String, Object> map = associations.get(references);
      if (map != null)
      {
         String type = (String) map.get(ATTR_TYPE);
         switch (type)
         {
            case TYPE_MANY_TO_ONE:
               ttype = SchemaParserTokenTypes.MANY_TO_ONE;
               break;
            case TYPE_ONE_TO_MANY:
               ttype = SchemaParserTokenTypes.ONE_TO_MANY;
               break;
            case TYPE_ONE_TO_ONE:
               ttype = SchemaParserTokenTypes.ONE_TO_ONE;
               break;
         }
      }
      
      return ttype;
   }
   
   /**
    * Indicate whether the table described by this hint represents the
    * foreign key end of an association with the specified table. This is
    * only relevant for one-to-one associations.
    *
    * @param   references
    *          Name of table which the association references.
    *
    * @return  <code>true</code> if this table contains a foreign key for
    *          the referenced table. <code>false</code> if not, or if no
    *          association was found with table <code>references</code>.
    */
   public boolean isForeignAssociation(String references)
   {
      Map<String, Object> map = associations.get(references);
      
      return (map != null && "true".equals(map.get(ATTR_FOREIGN)));
   }

   /**
    * Adjust custom extents' list
    * @param  fieldName
    *         field name
    * @param  extentSize
    *         extent size
    * @return adjusted custom extents' list
    */
   public List<ExtentHintField> adjustCustomExtents(String fieldName, int extentSize)
   {
      List<ExtentHintField> fhints = getCustomFieldExtent(fieldName);
      if (fhints == null)
      {
         return fhints;
      }
      
      int nf = fhints.size();
      if (nf >= extentSize)
      {
         return fhints;
      }
      
      String base = (nf == 0)
                    ? ClassDefinition.nconvert.convert(fieldName, NameConverter.TYPE_COLUMN)
                    : fhints.get(nf - 1).getName();
      
      while (fhints.size() < extentSize)
      {
         fhints.add(new ExtentHintField(String.format("%s_%d", base, fhints.size() + 1), "", ""));
      }
      
      if (nf == 0)
      {
         customExtents.put(fieldName, fhints);
      }
      return fhints;
   }
   /**
    * Get the list of extent property names for given field with extent:
    * several property names in case of custom extent conversion.
    *
    * @param   fieldName
    *          Field name.
    *
    * @return  list of property names.
    */
   public List<ExtentHintField> getCustomFieldExtent(String fieldName)
   {
      if (customExtents == null)
      {
         return null;
      }
      else if (customExtents.size() == 0)
      {
         return new ArrayList<>();
      }
      else
      {
         return customExtents.get(fieldName);
      }
   }
   
   /**
    * Get the set of field names which are used to form a relational
    * association between the table described by this hint and the table
    * it references.
    *
    * @param   references
    *          Name of table which the association references.
    *
    * @return  Set of fields, or <code>null</code> if this hint does not
    *          describe the referenced association or describes only the
    *          basic attributes of the association and not the joining
    *          fields.
    */
   public Set<?> getAssociatedFields(String references)
   {
      Map<String, Object> map = associations.get(references);
      
      return (Set<?>) (map == null ? null : map.get(KEY_FIELDS));
   }
   
   /**
    * Indicate whether the table described by this hint is to be dropped 
    * during the database import.
    *
    * @return  <code>true</code> if this table is to be dropped.
    */
   public boolean isTableDropped()
   {
      return tableDrop;
   }
   
   /**
    * Indicate whether the table described by this hint is read-only 
    *
    * @return  {@code true} if this table is read-only.
    */
   public boolean isReadOnly()
   {
      return readOnly;
   }
   
   /**
    * Indicate whether the table described by this hint needs to be tracked by the dirty share
    * manager across sessions.
    *
    * @return  {@code true} if this table is dirty-read.
    */
   public boolean isDirtyRead()
   {
      return dirtyRead;
   }

   /**
    * Indicate whether the table described by this hint needs to be tracked by the dirty share
    * manager within the same session.
    *
    * @return  {@code true} if this table is dirty-intra-read.
    */
   public boolean isDirtyIntraRead()
   {
      return dirtyIntraRead;
   }
   
   /**
    * Indicate whether the field of the table described by this hint is  
    * to be dropped during the database import.
    *
    * @param field
    *        field name to query.
    *
    * @return  <code>true</code> if this table is to be dropped.
    */
   public boolean isFieldDropped(String field)
   {
      return fieldDrop.contains(field);
   }
   
   /**
    * Get the list of table dispositions defined for the table described 
    * by this hint. 
    *
    * @return  List of dispositions, possibly empty.
    */
   public List<String> getTableDispositions()
   {
      List<String> list = new LinkedList<>();
      list.addAll(tableDisp);
      return list;
   }
   
   /**
    * Get the list of field dispositions defined for the table described by this hint. 
    *
    * @param   field
    *          field name to get the dispositions for.
    *
    * @return  List of dispositions, possibly empty.
    */
   public List<String> getFieldDispositions(String field)
   {
      List<String> list = new LinkedList<>();
      
      Set<String> set = fieldDisp.get(field);
      if (set != null)
      {
         list.addAll(set);
      }
      
      return list;
   }
   
   /**
    * Get the set of synthetic index names.
    *
    * @return Set of index names, possibly empty.
    */
   public Set<String> getIndexNames()
   {
      return indexes.keySet();
   }
   
   /**
    * Determine whether the specified field in the specified index is sorted
    * in the descending order.
    *
    * @param  indexName
    *         The name of the index that contains the required field.
    *
    * @param  fieldName
    *         Field name to query.
    *
    * @return <code>true</code> if the required field is sorted in the
    *         descending order. <code>false</code> otherwise.
    */
   public boolean isIndexFieldDescending(String indexName, String fieldName)
   {
      Map<String, Object> indexMap = indexes.get(indexName);
      Map<?,?> fieldMap = (Map<?,?>) ((Map<?,?>)indexMap.get(KEY_INDEX_FIELDS)).get(fieldName);
      return "desc".equalsIgnoreCase((String)fieldMap.get(ATTR_DIRECTION)) ||
             "descend".equalsIgnoreCase((String)fieldMap.get(ATTR_DIRECTION));
   }
   
   /**
    * Determine whether the specified index is unique. 
    *
    * @param  indexName
    *         Index to query.
    *
    * @return <code>true</code> if the required index is unique.
    *         <code>false</code> otherwise.
    */
   public boolean isIndexUnique(String indexName)
   {
      Map<String, Object> indexMap = indexes.get(indexName);
      return "true".equalsIgnoreCase((String)indexMap.get(ATTR_UNIQUE));
   }
   
   /**
    * Get the set of field names of the specified index.
    *
    * @param  indexName
    *         The name of the index to get the field names for.
    *
    * @return Set of index field names, possibly empty.
    */
   public Set<?> getIndexFieldNames(String indexName)
   {
      Map<String, Object> indexMap = indexes.get(indexName);
      return ((Map<?,?>)indexMap.get(KEY_INDEX_FIELDS)).keySet();
   }
}