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