# TableMapper.java

    /*
    ** Module   : TableMapper.java
    ** Abstract : Table and field name to converted DMO and property name (bidirectional).
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 CA  20131015 Created initial version.
    ** 002 CA  20131021 Added legacy index support.
    ** 003 CA  20131029 mapPermanentDMO must register the mapper for the permanent schema in the
    **                  permanentDBs map.
    ** 004 SVL 20131115 Changes to functions that return DMO class.
    ** 005 SVL 20140106 Fixed mapping logic for temp tables.
    ** 006 OM  20140117 Added support for indexed access to fields of the buffer.
    ** 007 SVL 20140123 Added getPropertyName(Class, int).
    ** 008 VMN 20140122 Added format(), initial(), columnLabel(), label(), mandatory(),
    **                  validateMessage(), validateExpression() support.
    ** 009 ECF 20140301 Added getLegacyFieldToPropertyMap, a helper method optimized to retrieve all
    **                  legacy field name to DMO property name mappings at once.
    ** 010 SVL 20140320 Fixed propertyName(Class<?>, int).
    ** 011 VMN 20140328 Added support for custom denormalization of fields with extent.
    ** 012 VMN 20140409 Changes to getLegacyFieldNameMap() method, return LegacyFieldNameMap.
    ** 013 CA  20140513 Added a weight for the context-local var, to ensure predetermined order during
    **                  context reset.
    ** 014 ECF 20140920 Memory optimizations: intern many strings and re-use LegacyTableInfo objects
    **                  which share a DMO class, instead of creating duplicates.
    ** 015 VMN 20150227 Fixed LegacyFieldInfo instances used in map denorm2j.
    ** 016 ECF 20150313 Refactored to store statically defined table information in shared caches,
    **                  and dynamically defined temp-table information in a context-local cache, to
    **                  conserve memory and avoid redundant mapping work for static temp-tables.
    ** 017 VMN 20150402 Expand using getDenormalizedProperty() for temp-tables.
    ** 018 SVL 20150503 Added getPropertyName(Buffer, String).
    ** 019 ECF 20150810 Denormalized schema fixes.
    ** 020 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 021 ECF 20160225 Changes required by new PropertyHelper implementation. Implemented backing
    **                  logic for NUM-FIELDS attribute.
    ** 022 ECF 20160403 Avoid unnecessary context initialization.
    ** 023 IAS 20160511 Create temporary table LIKE a permanent denormalized one
    ** 024 SVL 20160610 Support for table-level delete validation.
    ** 025 SVL 20160626 Added helper functions for detecting table joins at runtime.
    ** 026 SVL 20160705 Added methods to support INDEX-INFORMATION.
    ** 027 OM  20161014 Normalized field name in getPropertyName() methods.
    ** 028 SVL 20161025 Added support for the quirk which is triggered when an unknown value is
    **                  assigned to LABEL attribute of a buffer field.
    ** 029 ECF 20171025 Expanded LegacyFieldInfo class to include serialization information and added
    **                  public APIs to it; made LegacyFieldInfo class itself public, for use from the
    **                  serial package.
    ** 030 GES 20180418 Integrated ORDER and POSITION values from annotations. Added a method to
    **                  obtain a list of legacy field info in POSITION sequence.
    **     CA  20180511 Added table serialization options: XML-NODE-NAME, NAMESPACE-PREFIX,
    **                  NAMESPACE-URI, SERIALIZE-NAME.
    ** 031 OM  20181018 Removed NPE because dmo was used instead of legacy table name.
    ** 032 ECF 20190219 Fixed lookup of legacy field info for denormalized extent field.
    ** 033 SVL 20190614 Support for code page and LIKE attributes.
    ** 034 OM  20190611 Change accessibility levels. Now the legacy index components can be queried.
    ** 035 AIL 20190722 Added LegacyTriggerInfo and some getters as a support for trigger handling.
    **     SBI 20190728 Added getLegacyFieldHelp.
    **     SBI 20190814 Set the default format for the legacy field format if it is not provided.
    ** 036 CA  20190802 Added getLegacyIndexInfo, to return a string representation of a table's
    **                  indexes, compatible with Java Open Client's expectation.
    **     OM  20190821 Exposed more information about indexes.
    **     OM  20190823 Added help and format field option.
    **     OM  20190831 Added hidden fields and index specific to BEFORE TEMP-TABLES.
    ** 037 OM  20191014 The legacy field list getter allows filtering out hidden fields.
    ** 038 CA  20200110 A small improvement in getAllLegacyFieldInfo.
    ** 039 IAS 20200414 Added DECIMALS attribute support mock.
    ** 040 ECF 20200906 New ORM implementation.
    ** 041 CA  20200910 Fixed the datetime(-tz) field's initial value - it uses ISO8601 format or NOW function.
    **         20200914 Fixed initial value for null fields.
    **     ECF 20200916 Use DmoMeta to retrieve schema name instead of DatabaseManager.getSchemaByInterface.
    **     OM  20200919 Improved access to dmo metadata.
    **     ECF 20200919 Use DmoMeta to map permanent DMO.
    **     OM  20200924 P2JIndexComponent carries multiple information to avoid map lookups for them.
    **     OM  20201001 Improved DMO manipulation performance by caching slow Property annotation access.
    **     OM  20201002 Use DmoMeta cached information instead of map lookups.
    **     SVL 20201111 Support for null LABEL and COLUMN-LABEL.
    **     OM  20201029 Added case sensitive column support.
    **     OM  20201110 TableSerializeOptions are volatile and cannot be cached.
    **     OM  20210122 Temp-table attributes in LegacyFieldInfo can be modified.
    **     OM  20210127 Replaced TableMapper.getLegacyName() (now deprecated) triple map lookup with direct access
    **                  to local dmoMeta.legacyTable.
    **     CA  20210304 Fixed date, datetime and datetimetz literal parsing.
    **     OM  20210309 Do not use DmoMeta as key because temp-tables (both static and dynamic) share the same
    **                  DmoMeta, leading attribute values being leaked from one context to another. The public
    **                  interface use RecordBuffer for fast and accurate access to table meta information.
    **     OM  20210323 Added some missing setters for field attributes.
    **     ECF 20210506 Use RecordBuffer.getDmoInfo() getter instead of direct field access to prevent
    **                  NPE in proxy case.
    **     ECF 20210606 Attempted to provide relief for a memory leak caused by TempTableMapper, until a proper
    **                  solution is available.
    **     CA  20210629 Fixed word indexes when importing a dataset serialized with its schema.
    **     ECF 20211202 Removed get{Field|Extent}Value methods, which did not work correctly for temp-tables.
    **                  This processing is now handled by DmoMeta.
    **     OM  20211214 The static temp-table data was no longer shared and Info data must be removed once the
    **                  table goes out of scope (at the same moment it is dropped from SQL).
    **     OM  20220212 Use ReservedProperty static method for testing reserved properties names.
    **     OM  20220303 Take into account the FORMAT when computing the initial value of logical fields.
    **     IAS 20220617 If field labele is not specifiled return field name.
    **     OM  20220727 FieldId and PropertyId are different for denormalized extent fields.
    **     CA  20221006 Refactored TableMapper for temp-tables to keep a cache of LegacyTableInfo and the mutable
    **                  state in MutableFieldInfo, for the actual TempTable instance. Refs #6825
    **     TJD 20220504 Java 11 compatibility minor changes
    **     CA  20220601 getIndexFieldNames must return lowercased legacy names.
    **     OM  20220914 Added MAX-WIDTH support for CHARACTER fields.
    **     IAS 20221006 Added 'definedFormat' and 'definedLabel'.
    **     TJD 20221018 Allow initialization of Logical from empty string
    **     IAS 20221018 Fixed processing of 'now' and 'today' INITIAL attribute value.
    **     CA  20221031 Added JMX instrumentation for 'mapTemporaryTable'.
    **     IAS 20222204 Added 'indexComponents()' and 'getLegacyIndexes' methods.
    **     IAS 20221111 Minor cleanup of the LegacyFieldInfo c'tor.
    **     SVL 20230108 Improved performance by replacing some "for-each" loops with indexed "for" loops.
    **     CA  20230110 Cache the mutable info at the temp-table and not in a separate map at TableMapper.
    **     SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
    **                  performance.
    **     SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
    **     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
    **     HC  20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
    **                  for performance.
    ** 042 IAS 20230501 Fixed support for the INITIAL-NULL pseudo-attribute.
    ** 043 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 044 AD  20231123 Optimized getPropertyName(Class, int) and fixed propertyNameImpl(Class, int).
    **     AD  20231128 Deleted unused methods, overloads of getPropertyName, getIndexName and getIndexNameImpl.
    ** 045 AI  20231103 Updated return of javaName for denormalized fields to take into account the index.
    **         20231116 Updated javaName to compute based on original, "_" and index for expanded extent fields.
    **         20231117 Update denormalizedProperty to return name of field using getCustomExtentFieldInfo.
    **         20231122 Refactored getDenormalizedProperty() and removed the use of extractPropertyName() to
    **                  use dmoMeta.getFieldInfo instead.
    **         20231127 Removed unused methods.
    **         20321128 Check parameter update for getDenormalizedProperty.
    **         20231205 Removed getPropertyNameImpl and updated getPropertyName(TempTable, int).
    ** 046 CA  20231129 Moved the LegacyTableInfo instance from TableMapper to AbstractTempTable (only for temp-
    **                  tables.
    ** 047 AI  20231218 Removed the use of getFieldInfo when using the legacy nameand replaced with
    **                  dmoMeta.byLegacyName().
    ** 048 HC  20240222 Enabled JMX on FWD Client.
    ** 049 CA  20240318 INITIAL value for a DATETIME(-TZ) field will always use 'mdy' as date format.
    ** 050 OM  20240402 Added support for unloading dynamic tables (removeDynamicTable() method).
    ** 051 OM  20240508 Runtime parsing of date literal takes into account the current date-format.
    **                  Avoid wrapping String literals, when possible.
    ** 052 OM  20240510 Added auto detection for date format, depending on the context and a quick heuristic
    **                  analysis of the text.
    ** 053 OM  20240524 Refined algorithm from H052. It also caused regressions.
    ** 054 CA  20240809 Skip using JMX timers when JMX_DEBUG flag is not set.
    ** 055 ICP 20250129 Used logical constants to leverage cached instances.
    ** 056 PMP 20250314 Fixed getColumnLabel to ensure that if the column label is null, it falls back to
    **                  getLabel, which already handles the legacy name when the label is null.
    */

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

    import java.lang.reflect.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.function.*;
    import java.util.stream.Stream;

    import com.goldencode.p2j.persist.annotation.*;
    import com.goldencode.p2j.persist.annotation.Trigger;
    import com.goldencode.p2j.persist.orm.Property;
    import com.goldencode.util.*;
    import com.goldencode.p2j.util.logging.*;
    import org.apache.commons.lang3.tuple.Pair;
    import com.goldencode.p2j.convert.*;
    import com.goldencode.p2j.jmx.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.persist.serial.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;

    /**
     * This class performs a bi-directional runtime mapping of DMO implementation classes (permanent
     * or temporary) to their associated legacy table name. The mapping for a DMO implementation class
     * is held in a {@link LegacyTableInfo} instance.
     * <p>
     * For physical DBs, the mappings for the permanent DBs (connected at server startup) will be
     * registered at server startup. For permanent DBs connected at application runtime, the mappings
     * will be registered and deregistered at the appropriate DB connection/disconnection times.
     * <p>
     * All the mappings for temporary tables will be registered during application lifetime, as the
     * temp tables get created or dropped.
     * <p>
     * APIs which resolve a legacy name for a converted name may return null, when resolving an index,
     * field or property name; this is to allow checks for synthetic properties or indexes, which do
     * not exist in the legacy code.  In all cases, it is expected that the given table name is for a
     * table which has registered mappings; else, a {@link NullPointerException} will be thrown.
     *
     * @param    <K>
     *           The type of the key used by the primary internal structure of the mapper. For permanent DMOs,
     *           this will be a {@code Class<? extends DataModelObject>}. For temporary DMOs, this will be an
     *           {@link Integer} value with the {@code UNIQUE-ID} identifying the {@link TempTable} resource.
     */
    public abstract class TableMapper<K>
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(TableMapper.class);

       /** Instrumentation for {@link #mapTemporaryTable(TempTable)}. */
       private static final NanoTimer MAP = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmTableMapperMap);

       /** Holds the mappers for all permanent databases by their schema name */
       private static final Map<String, PermanentTableMapper> permanentDBs = new HashMap<>();

       /** Holds the mapper for all statically defined temp tables. */
       private static final ContextLocal<TempTableMapper> staticTempDB =
             new ContextLocal<TempTableMapper>()
             {
                @Override
                public WeightFactor getWeight()
                {
                   return WeightFactor.LEVEL_2;
                }

                @Override
                protected TempTableMapper initialValue()
                {
                   return new TempTableMapper();
                }
             };

       /** Context-local mapper for all dynamic temporary tables. */
       private static final ContextLocal<TempTableMapper> dynamicDB =
             new ContextLocal<TempTableMapper>()
             {
                @Override
                public WeightFactor getWeight()
                {
                   return WeightFactor.LEVEL_2;
                }

                @Override
                protected TempTableMapper initialValue()
                {
                   return new TempTableMapper();
                }

                @Override
                protected void cleanup(TempTableMapper mapper)
                {
                   mapper.clear();
                }
             };

       /**
        * Map containing fields for which the quirk has been triggered. The quirk takes place when an
        * unknown value is assigned to LABEL attribute of a buffer field. It affects values of LABEL
        * attributes for the specific field of <i>all</i> buffers belonging to the specific permanent
        * table. Mapping is "DMO interface class" to "set of property names of the fields affected by
        * the quirk".
        */
       private static final ContextLocal<Map<Class<?>, Set<String>>> contextLabelQuirks = new ContextLocal<>();

       /** The schema name */
       private final String schema;

       /**
        * Create a new mapper for the given schema.
        *
        * @param    schema
        *           The schema name.
        */
       private TableMapper(String schema)
       {
          this.schema = schema;
       }

       /**
        * Get new field name for denormalized legacy field name and index.
        *
        * @param    recBuf
        *           The DMO interface for a permanent DMO.
        * @param    property
        *           The DMO property name.
        * @param    index
        *           zero-based index of this property in denormalization hints or null.
        *
        * @return   New field name.
        */
       public static String getDenormalizedProperty(RecordBuffer recBuf, String property, Integer index)
       {
          if (property == null || index == null || recBuf == null || index < 0)
          {
             return null;
          }

          DmoMeta dmoInfo = recBuf.getDmoInfo();
          List<Property> prop = dmoInfo.getCustomExtentFieldInfo(property);

          return prop == null ? null : prop.get(index).name;
       }

       /**
        * Get the legacy table name for the DMO referenced by the given buffer. This method is needed because the
        * legacy name of a buffer is not always directly accessible from the {@code buf.dmoInfo}:
        * <ul>
        *    <li>the {@code DmoMeta} is shared among multiple DMOs;
        *    <li>name not yet assigned to a static temp table, get it from the mapping.
        * </ul>
        *
        * @param    recBuf
        *           A record buffer instance.
        *
        * @return   The legacy table name.
        */
       public static String getLegacyName(RecordBuffer recBuf)
       {
          if (recBuf.isTemporary())
          {
             TempTable tt = recBuf.getParentTable();
             if (tt == null)
             {
                // buffer not initialized() so the parent table is not yet available. This is a bit of an
                // edge-case (see the comment below)
                return recBuf.getDmoInfo().legacyTable;
             }

             String name = tt.name().getValue();
             if (name != null)
             {
                // return the name of the parent table: this might be different from [buf.dmoInfo.legacyTable]
                // if the [buf] is dynamic a temp-table which had benefited from a [cache] hit to another
                // differently named table (but with same structure) in DynamicTablesHelper.createDynamicDMO(),
                // when was created
                return name;
             }
             else
             {
                // name not yet assigned to a static temp table, get it from the mapping
                TempTableMapper mapper = locateTemp(tt);

                synchronized (mapper)
                {
                   return mapper.legacyName(tt);
                }
             }
          }
          else
          {
             // persistent table case: the [dmoInfo] is not shared
             return recBuf.getDmoInfo().legacyTable;
          }
       }

       /**
        * Get the COLUMN-LABEL field attribute for the property associated with the DMO referenced
        * by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   COLUMN-LABEL field attribute or <code>null</code> if there is no such field.
        */
       public static String getLegacyFieldColumnLabel(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return (legacyFieldInfo == null) ? null : legacyFieldInfo.getColumnLabel(recBuf.getParentTable());
       }

       /**
        * Set the COLUMN-LABEL field attribute for the property associated with the DMO referenced
        * by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        * @param    columnLabel
        *           COLUMN-LABEL field attribute.
        */
       public static void setLegacyFieldColumnLabel(RecordBuffer recBuf, String property, String columnLabel)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          legacyFieldInfo.setColumnLabel(columnLabel.intern(), recBuf.getParentTable());
       }

       /**
        * Get the LABEL field attribute for the property associated with the DMO referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   LABEL field attribute or <code>null</code> if there is no such field.
        */
       public static String getLegacyFieldLabel(RecordBuffer recBuf, String property)
       {
          Map<Class<?>, Set<String>> contextQuirks = contextLabelQuirks.get();
          if (contextQuirks != null)
          {
             Class<? extends DataModelObject> dmoInterface = recBuf.getDMOInterface();
             Set<String> classQuirks = contextQuirks.get(dmoInterface);
             if (classQuirks != null && classQuirks.contains(property))
             {
                return ""; // 4GL quirk
             }
          }

          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          if (legacyFieldInfo == null)
          {
             return null;
          }

          // defaulting to legacy name if label not found in meta information
          return legacyFieldInfo.getLabel(recBuf.getParentTable()) == null
                    ? legacyFieldInfo.legacyName
                    : legacyFieldInfo.getLabel(recBuf.getParentTable());
       }

       /**
        * Set the LABEL field attribute for the property associated with the DMO referenced
        * by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        * @param    label
        *           LABEL field attribute.
        */
       public static void setLegacyFieldLabel(RecordBuffer recBuf, String property, String label)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          legacyFieldInfo.setLabel(label.intern(), recBuf.getParentTable());
       }

       /**
        * Trigger quirk which takes place when an unknown value is assigned to LABEL attribute of a
        * buffer field. It affects values of LABEL attributes for this field of <i>all</i> buffers
        * belonging to the specific permanent table.
        *
        * @param   recBuf
        *          Buffer which has triggered the quirk.
        * @param   property
        *          Property name of the field for which the quirk has triggered.
        */
       public static void setFieldLabelQuirk(RecordBuffer recBuf, String property)
       {
          Map<Class<?>, Set<String>> contextQuirks = contextLabelQuirks.get();
          if (contextQuirks == null)
          {
             contextQuirks = new HashMap<>();
             contextLabelQuirks.set(contextQuirks);
          }

          Class<? extends DataModelObject> dmoInterface = recBuf.getDMOInterface();
          Set<String> classQuirks = contextQuirks.computeIfAbsent(dmoInterface, k -> new HashSet<>());

          classQuirks.add(property);
       }

       /**
        * Get the id of field in the sequence of the fields of the buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   the id of field in the sequence of the fields of the buffer.
        */
       @Deprecated
       public static integer getLegacyFieldId(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return new integer(legacyFieldInfo == null ? null : legacyFieldInfo.fieldId);
       }

       /**
        * Get the LITERAL-QUESTION field attribute for the property associated with the DMO
        * referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   LITERAL-QUESTION field attribute or <code>null</code> if there is no such field.
        */
       public static logical getLegacyFieldLiteralQuestion(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return new logical(legacyFieldInfo == null
                                ? null
                                : legacyFieldInfo.isLiteralQuestion(recBuf.getParentTable()));
       }

       /**
        * Set the LITERAL-QUESTION field attribute for the property associated with the DMO
        * referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        * @param    literalQuestion
        *           LITERAL-QUESTION field attribute.
        */
       public static void setLegacyFieldLiteralQuestion(RecordBuffer recBuf, String property, logical literalQuestion)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          legacyFieldInfo.setLiteralQuestion(literalQuestion.booleanValue(), recBuf.getParentTable());
       }

       /**
        * Get the MANDATORY field attribute for the property associated with the DMO
        * referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   MANDATORY field attribute or <code>null</code> if there is no such field.
        */
       public static logical isLegacyFieldMandatory(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? logical.UNKNOWN : logical.of(legacyFieldInfo.mandatory);
       }

       /**
        * Get the POSITION field attribute for the property associated with the DMO
        * referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   POSITION field attribute or -1 if there is no such field.
        */
       public static int getLegacyFieldPosition(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? -1 : legacyFieldInfo.getPosition();
       }

       /**
        * Get the VALIDATE-EXPRESSION field attribute for the property associated with the DMO
        * referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   VALIDATE-EXPRESSION field attribute or <code>null</code> if there is no such field.
        */
       public static String getLegacyFieldValidateExpression(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? null : legacyFieldInfo.getValidateExpression(recBuf.getParentTable());
       }

       /**
        * Set the VALIDATE-EXPRESSION field attribute for the property associated with the DMO
        * referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        * @param    validateExpression
        *           VALIDATE-EXPRESSION field attribute.
        */
       public static void setLegacyFieldValidateExpression(RecordBuffer recBuf,
                                                           String property,
                                                           String validateExpression)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          legacyFieldInfo.setValidateExpression(validateExpression, recBuf.getParentTable());
       }

       /**
        * Get the VALIDATE-MESSAGE field attribute for the property associated with the DMO
        * referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   VALIDATE-MESSAGE field attribute or <code>null</code> if there is no such field.
        */
       public static String getLegacyFieldValidateMessage(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? null : legacyFieldInfo.getValidateMessage(recBuf.getParentTable());
       }

       /**
        * Set the VALIDATE-MESSAGE field attribute for the property associated with the DMO
        * referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        * @param    validateMessage
        *           VALIDATE-MESSAGE field attribute.
        */
       public static void setLegacyFieldValidateMessage(RecordBuffer recBuf,
                                                        String property,
                                                        String validateMessage)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          legacyFieldInfo.setValidateMessage(validateMessage, recBuf.getParentTable());
       }

       /**
        * Get the FORMAT field attribute for the property associated with the DMO referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   FORMAT field attribute or <code>null</code> if there is no such field.
        */
       public static String getLegacyFieldFormat(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? null : legacyFieldInfo.getFormat(recBuf.getParentTable());
       }

       /**
        * Get the DECIMALS field attribute for the property associated with the DMO referenced by the
        * given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   FORMAT field attribute or <code>null</code> if there is no such field.
        */
       public static int getLegacyFieldDecimals(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? 0 : legacyFieldInfo.getDecimals(recBuf.getParentTable());
       }

       /**
        * Set the FORMAT field attribute for the property associated with the DMO referenced by the
        * given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        * @param    format
        *           FORMAT field attribute.
        */
       public static void setLegacyFieldFormat(RecordBuffer recBuf, String property, String format)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          legacyFieldInfo.setFormat(format.intern(), recBuf.getParentTable());
       }

       /**
        * Get the HELP field attribute for the property associated with the DMO referenced by the
        * given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   HELP field attribute or {@code null} if there is no such field.
        */
       public static String getLegacyFieldHelp(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? null : legacyFieldInfo.getHelp(recBuf.getParentTable());
       }

       /**
        * Set the HELP field attribute for the property associated with the DMO referenced by the
        * given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        * @param    help
        *           HELP field attribute.
        */
       public static void setLegacyFieldHelp(RecordBuffer recBuf, String property, String help)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          legacyFieldInfo.setHelp(help.intern(), recBuf.getParentTable());
       }

       /**
        * Get the INITIAL field attribute for the property associated with the DMO referenced by the
        * given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   INITIAL field attribute or <code>null</code> if there is no such field.
        */
       public static String getLegacyFieldInitial(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? null : legacyFieldInfo.initial;
       }

       /**
        * Get the legacy field name for the property associated with the DMO referenced by the given buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    property
        *           The DMO property name.
        *
        * @return   The legacy field name or {@code null} if there is no such field.
        */
       public static String getLegacyFieldName(RecordBuffer recBuf, String property)
       {
          LegacyFieldInfo legacyFieldInfo = getLegacyFieldInfo(recBuf, property);
          return legacyFieldInfo == null ? null : legacyFieldInfo.legacyName;
       }

       /**
        * Get a string-representation of this buffer's indexes.  This is compatible with the
        * JavaOpenClient encoding of the indexes.
        *
        * @param    tt
        *           The buffer instance.
        *
        * @return   The string representation of the indexes.
        */
       public static String getLegacyIndexInfo(TempTable tt)
       {
          // compute the indexes
          int idxNo = 0;
          TableMapper.LegacyIndexInfo index = TableMapper.getLegacyIndexInfo(tt, idxNo);
          String primaryIndex = "";
          StringBuilder indexes = new StringBuilder();
          while (index != null)
          {
             if (index.isUnique() || index.effectivePrimary)
             {
                StringBuilder indexInfo = new StringBuilder();

                for (String field : index.getComponents())
                {
                   indexInfo.append((indexInfo.length() == 0) ? "" : ",").append(field);
                }
                indexInfo.append(":").append(index.legacyName).append(".");

                if (index.effectivePrimary)
                {
                   primaryIndex = (index.isUnique() ? "1," : "0,") + indexInfo;
                }
                else
                {
                   indexes.append(indexInfo);
                }
             }

             index = TableMapper.getLegacyIndexInfo(tt, ++idxNo);
          }

          indexes.insert(0, primaryIndex);

          return indexes.toString();
       }

       /**
        * Get stream for the indexes associated with the given temporary table.
        *
        * @param    tt
        *           Temp table object.
        * @return   stream for the indexes associated with the given temporary table.
        */
       public static Stream<TableMapper.LegacyIndexInfo> getLegacyIndexes(TempTable tt)
       {
          TempTableMapper mapper = locateTemp(tt);

          synchronized (mapper)
          {
             return mapper.legacyIndexInfo(tt);
          }
       }
       /**
        * Get information about the index associated with the DMO referenced by the given buffer,
        * as a string.
        *
        * @param    recBuf
        *           A buffer instance which specifies the DMO class associated with the target index.
        * @param    indexNum
        *           0-based index number in the index definition order.
        *
        * @return   a string describing the target index or <code>null</code> if there is no index
        *           with the given number. Format of the string is described in
        *           {@link Buffer#indexInformation(int)}.
        */
       public static String getLegacyIndexInfoString(RecordBuffer recBuf, int indexNum)
       {
          LegacyIndexInfo legacyIndexInfo = getLegacyIndexInfo(recBuf, indexNum);
          return legacyIndexInfo == null ? null : legacyIndexInfo.indexInformation();
       }

       /**
        * Get the legacy index name of a converted index, defined by the backing table of the
        * specified buffer.
        *
        * @param    recBuf
        *           A buffer instance.
        * @param    idx
        *           The converted index name.
        *
        * @return   The legacy index name or <code>null</code> if there is no such index defined.
        */
       public static String getLegacyIndexName(RecordBuffer recBuf, String idx)
       {
          if (recBuf.isTemporary())
          {
             return getLegacyIndexName(recBuf.getParentTable(), idx);
          }
          else
          {
             return getLegacyIndexName(recBuf.getDMOInterface(), idx);
          }
       }

       /**
        * Obtain the list of all legacy field information, sorted in ascending order:
        * <ul>
        *    <li>by the ORDER schema attribute, if the parent table is a temp-table which was defined
        *        with LIKE-SEQUENTIAL option;</li>
        *    <li>by the POSITION schema attribute otherwise.</li>
        * </ul>
        *
        * @param   buffer
        *          The buffer for which the field info is needed.  This can be either a
        *          temp-table or a permanent table.
        * @param   noHidden
        *          If {@code true} do not include the hidden BEFORE-BUFFER specific fields.
        *
        * @return  The list, sorted by POSITION (if the temp-table was defined with LIKE-SEQUENTIAL) or ORDER.
        */
       public static ArrayList<LegacyFieldInfo> getLegacyOrderedList(Buffer buffer, boolean noHidden)
       {
          RecordBuffer buf = ((BufferImpl) buffer).buffer();
          LegacyTableInfo tinfo;

          if (buf.isTemporary())
          {
             TempTable tt = ((BufferImpl) buffer).tableHandleNative();
             tinfo = ((AbstractTempTable) tt).tableInfo;
          }
          else
          {
             Class<? extends DataModelObject> dmoIface = buf.getDMOInterface();
             String schema = DmoMetadataManager.getDmoInfo(dmoIface).getSchema();

             synchronized (permanentDBs)
             {
                PermanentTableMapper mapper = locatePerm(schema);
                tinfo = mapper.byKey(dmoIface);
             }
          }

          ArrayList<LegacyFieldInfo> list = new ArrayList<>();
          Collection<LegacyFieldInfo> values = tinfo.ip2j.values();
          if (!noHidden)
          {
             list.addAll(values);
          }
          else
          {
             // filter the hidden fields
             for (LegacyFieldInfo val : values)
             {
                if (!ReservedProperty.isReservedProperty(val.getLegacyName()))
                {
                   list.add(val);
                }
             }
          }

          if (buf.isTemporary())
          {
             list.sort(Comparator.comparingInt(tinfo.likeSequential
                                               ? LegacyFieldInfo::getOrder
                                               : LegacyFieldInfo::getPosition));
          }
          else
          {
             list.sort(Comparator.comparingInt(LegacyFieldInfo::getPosition));
          }
          return list;
       }

       /**
        * Get the schema name for the given DMO, which is the schema concatenated with the legacy table name.
        *
        * @param    dmoIface
        *           The DMO interface for a <b>permanent</b> DMO.
        *
        * @return   The schema name.
        */
       public static String getLegacySchemaName(Class<? extends DataModelObject> dmoIface)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
          return dmoInfo.getSchema() + "." + dmoInfo.legacyTable;
       }

       /**
        * Determine if the permanent table has table-level delete validation.
        *
        * @param    dmoIface
        *           The DMO interface for a permanent DMO which defines the target table.
        *
        * @return   <code>true</code> if the table has table-level delete validation.
        *
        * @throws   IllegalStateException
        *           If a DMO belonging to the {@link DatabaseManager#TEMP_TABLE_SCHEMA} temp schema
        *           is passed as a parameter.
        */
       public static boolean hasValidation(Class<? extends DataModelObject> dmoIface)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
          if (dmoInfo.isTempTable())
          {
             final String msg = "Table-level validation is not available for temporary tables!";
             throw new IllegalArgumentException(msg);
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema());

             return mapper.validation(dmoIface);
          }
       }

       /**
        * Get the legacy field name for the given property, which is defined by a DMO part of a permanent DB.
        *
        * @param    dmoIface
        *           The DMO interface for a <b>permanent</b> DMO.
        * @param    property
        *           The DMO property name.
        *
        * @return   The legacy field name or <code>null</code> if there is no such field.
        *
        * @throws   IllegalStateException
        *           If a DMO belonging to the {@link DatabaseManager#TEMP_TABLE_SCHEMA} temp schema
        *           is passed as a parameter; for these cases, {@link #getLegacyFieldName(TempTable,
        *           String)} or {@link #getLegacyFieldName(RecordBuffer, String)} must be used.
        */
       public static String getLegacyFieldName(Class<? extends DataModelObject> dmoIface, String property)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
          if (dmoInfo.isTempTable())
          {
             final String msg =
                "Legacy field names for a property of a temporary DMO can not be resolved using this API!";
             throw new IllegalArgumentException(msg);
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema());
             return mapper.legacyFieldNameImpl(dmoIface, property);
          }
       }

       /**
        * Get the 4GL name of the extent field represented by the given property.
        *
        * @param   dmoIface
        *          The DMO interface for a permanent DMO.
        * @param   property
        *          The DMO property name
        *
        * @return  The 4GL name of the extent field
        */
       public static String getExtent4GLName(Class<? extends DataModelObject> dmoIface, String property)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
          if (dmoInfo.isTempTable())
          {
             return null;
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema());
             LegacyFieldInfo info = mapper.byKey(dmoIface).extents.get(property);

             return info == null ? property : info.legacyName;
          }
       }

       /**
        * Get the legacy field for the given property, which is defined by a DMO representing a table in a
        * <b>permanent</b> database.
        *
        * @param   dmoInfo
        *          The DMO interface for a permanent DMO.
        * @param   property
        *          The DMO property name.
        *
        * @return  The legacy field info structure of the specified DMO or {@code null} if a table belonging to
        *          the {@link DatabaseManager#TEMP_TABLE_SCHEMA} is used or there is no such field.
        */
       public static LegacyFieldInfo getLegacyFieldInfo(DmoMeta dmoInfo, String property)
       {
          if (dmoInfo.isTempTable())
          {
             // Properties for temporary DMOs can not be resolved using this API!
             return null;
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema());

             // TODO: remove ?: ASA [permanentDBs] is correctly populated
             return mapper == null ? null : mapper.legacyFieldInfoImpl(dmoInfo.getAnnotatedInterface(), property);
          }
       }

       /**
        * Get the legacy fields' list which are defined by a DMO part of a permanent DB.
        *
        * @param    dmoIface
        *           The DMO interface for a permanent DMO.
        *
        * @return   The legacy fields' list if a table belonging to the
        *           {@link DatabaseManager#TEMP_TABLE_SCHEMA} or empty list if it is not.
        */
       public static List<String> getLegacyProperties(Class<? extends DataModelObject> dmoIface)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
          if (dmoInfo.isTempTable())
          {
             // Properties for temporary DMOs can not be resolved using this API!
             return Collections.emptyList();
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema());
             ArrayList<LegacyFieldInfo> lfi = new ArrayList<>(mapper.byKey(dmoIface).j2p.values());
             lfi.sort(Comparator.comparing(f -> f.fieldId));
             List<String> lp = new ArrayList<>(lfi.size());
             String ln = "";
             for (int i = 0; i < lfi.size(); i++)
             {
                LegacyFieldInfo lf = lfi.get(i);
                if (ln.equals(lf.legacyName))
                {
                   continue;
                }
                ln = lf.legacyName;
                lp.add("".equals(lf.original) ? lf.javaName : lf.original);
             }
             return lp;
          }
       }

       /**
        * Get the legacy name of a converted index, which is defined by the backing table for the
        * specified buffer, part of a permanent DB.
        *
        * @param    dmoIface
        *           The DMO interface for a permanent DMO.
        * @param    idx
        *           The converted index name.
        *
        * @return   The legacy index name or <code>null</code> if there is no such index defined.
        *
        * @throws   IllegalStateException
        *           If a DMO belonging to the {@link DatabaseManager#TEMP_TABLE_SCHEMA} temp schema
        *           is passed as a parameter; for these cases, {@link #getLegacyFieldName(TempTable,
        *           String)} or {@link #getLegacyFieldName(RecordBuffer, String)} must be used.
        */
       public static String getLegacyIndexName(Class<? extends DataModelObject> dmoIface, String idx)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
          if (dmoInfo.isTempTable())
          {
             String msg = "Legacy index names for a temporary DMO can not be resolved using this API!";
             throw new IllegalArgumentException(msg);
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema());

             return mapper.legacyIndexNameImpl(dmoIface, idx);
          }
       }

       /**
        * Get the legacy field name for the given {@link TempTable} resource and property.
        *
        * @param    tt
        *           The {@link TempTable} resource.
        * @param    property
        *           The DMO property name.
        *
        * @return   The legacy field name or <code>null</code> if there is no such field.
        */
       public static String getLegacyFieldName(TempTable tt, String property)
       {
          TempTableMapper mapper = locateTemp(tt);

          synchronized (mapper)
          {
             return mapper.legacyFieldInfoImpl(tt, property).legacyName;
          }
       }

       /**
        * Get an ordered list of all {@link TableMapper.LegacyFieldInfo} items for the given
        * temp-table. The items are sorted by field id (i.e., legacy schema order).
        *
        * @param   tt
        *          Temp-table for which to retrieve field information.
        *
        * @return  List of legacy field information.
        */
       public static List<LegacyFieldInfo> getAllLegacyFieldInfo(TempTable tt)
       {
          return ((AbstractTempTable) tt).tableInfo.fieldList;
       }

       /**
        * Get the legacy field for the given {@link TempTable} resource and property.
        *
        * @param    tt
        *           The {@link TempTable} resource.
        * @param    property
        *           The DMO property name.
        *
        * @return   The legacy field or <code>null</code> if there is no such field.
        */
       public static LegacyFieldInfo getLegacyFieldInfo(TempTable tt, String property)
       {
          TempTableMapper mapper = locateTemp(tt);

          synchronized (mapper)
          {
             return mapper.legacyFieldInfoImpl(tt, property);
          }
       }

       /**
        * Get the legacy index name for the given {@link TempTable} resource and converted index name.
        *
        * @param    tt
        *           The {@link TempTable} resource.
        * @param    idx
        *           The converted index name.
        *
        * @return   The legacy index name or <code>null</code> if there is no such index defined.
        */
       public static String getLegacyIndexName(TempTable tt, String idx)
       {
          TempTableMapper mapper = locateTemp(tt);

          synchronized (mapper)
          {
             return mapper.legacyIndexNameImpl(tt, idx);
          }
       }

       /**
        * Resolve the DMO property name for a legacy field name belonging to the specified
        * {@link TempTable} resource.
        *
        * @param    table
        *           The {@link TempTable} resource.
        * @param    field
        *           The legacy field name (not mandatory normalized).
        *
        * @return   The DMO property name associated with the legacy field or {@code null} if
        *           there is no such field.
        */
       public static String getPropertyName(TempTable table, String field)
       {
          DmoMeta dmoInfo = table.getDmoMeta();
          Property property = dmoInfo.byLegacyName(field);
          return property == null ? null : property.name;
       }

       /**
        * Resolve the DMO property name for a field index name belonging to the specified
        * {@link TempTable} resource.
        *
        * @param    table
        *           The {@link TempTable} resource.
        * @param    fieldId
        *           The field index in table declaring sequence.
        *
        * @return   The DMO property name associated with the legacy field or <code>null</code> if
        *           there is no such field.
        */
       public static String getPropertyName(TempTable table, int fieldId)
       {
          DmoMeta dmoInfo = table.getDmoMeta();
          Property property = dmoInfo.getExistingFieldProperty(fieldId);
          return property == null ? null : property.name;
       }

       /**
        * Resolve the number of fields belonging to the specified {@link TempTable} resource.
        *
        * @param    table
        *           The {@link TempTable} resource.
        *
        * @return   The DMO property name associated with the legacy field or <code>null</code> if
        *           there is no such field.
        */
       public static int getNumFields(TempTable table)
       {
          TempTableMapper mapper = locateTemp(table);

          synchronized (mapper)
          {
             return mapper.getNumFieldsImpl(table);
          }
       }

       /**
        * Get the DMO implementation class for the legacy table name (part of the specified schema).
        *
        * @param    schema
        *           The schema name.
        * @param    table
        *           The legacy table name.
        *
        * @return   The DMO implementation class or <code>null</code> if there is no table with the
        *           given name.
        *
        * @throws   IllegalArgumentException
        *           If a table belonging to the {@link DatabaseManager#TEMP_TABLE_SCHEMA} is used.
        */
       public static Class<? extends Record> getDMOClass(String schema, String table)
       {
          if (DatabaseManager.TEMP_TABLE_SCHEMA.equals(schema))
          {
             final String msg = "DMO for a temporary table can not be resolved using this API!";
             throw new IllegalArgumentException(msg);
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(schema);

             return (mapper == null) ? null : mapper.getDMOClass(table);
          }
       }

       /**
        * Get the number of fields belonging to the specified legacy table.
        *
        * @param    schema
        *           The schema name.
        * @param    table
        *           The legacy table name.
        *
        * @return   Number of fields in the table.
        *
        * @throws   IllegalArgumentException
        *           If a table belonging to the {@link DatabaseManager#TEMP_TABLE_SCHEMA} is used.
        */
       public static int getNumFields(String schema, String table)
       {
          if (DatabaseManager.TEMP_TABLE_SCHEMA.equals(schema))
          {
             final String msg = "Properties for temporary DMOs can not be resolved using this API!";
             throw new IllegalArgumentException(msg);
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(schema);
             LegacyTableInfo tableInfo = mapper.byLegacyName(table);

             // return the size of the p2j map, which has one entry per original legacy field, as
             // opposed to the ip2j map, which has one entry per legacy scalar field and one per
             // denormalized (expanded) legacy extent field. The hidden fields are dropped, also.
             return tableInfo.p2j.size() - tableInfo.hiddenFields.size();
          }
       }

       /**
        * Get the DMO property name for the given buffer and legacy field name.
        *
        * @param  recBuff
        *         Buffer which belongs to the target table.
        * @param  legacyFieldName
        *         Legacy name of the target field.
        *
        * @return DMO property name.
        */
       public static String getPropertyName(RecordBuffer recBuff, String legacyFieldName)
       {
          DmoMeta dmoInfo = recBuff.getDmoInfo();
          Property property = dmoInfo.byLegacyName(legacyFieldName);
          return property == null ? null : property.name;
       }

       /**
        * Add the legacy mappings for the specified DMO.
        *
        * @param   dmoInfo
        *          Object which contains metadata about the DMO.
        */
       public static void mapPermanentDMO(DmoMeta dmoInfo)
       {
          String schema = dmoInfo.getSchema();

          // - mapping is kept by schema
          // - permanentDBs are kept in a global map
          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(schema);
             if (mapper == null)
             {
                mapper = new PermanentTableMapper(schema);
                permanentDBs.put(schema,  mapper);
             }

             mapper.mapClass(dmoInfo);
          }
       }

       /**
        * Remove a dynamic permanent DMO from the specified schema.
        *
        * @param   dmoInfo
        *          Object which contains metadata about the DMO.
        */
       static void removeDynamicTable(DmoMeta dmoInfo)
       {
          String schema = dmoInfo.getSchema();

          // - mapping is kept by schema
          // - permanentDBs are kept in a global map
          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(schema);
             if (mapper == null)
             {
                return; // nothing to do here
             }
             mapper.removeTable(dmoInfo.getDmoImplInterface());
          }
       }

       /**
        * Add the legacy mappings for the given temporary table.
        *
        * @param    table
        *           The {@link TempTable} resource.
        */
       static void mapTemporaryTable(TempTable table)
       {
          if (FwdServerJMX.JMX_DEBUG)
          {
             MAP.timer(() ->
             {
                TempTableMapper mapper = locateTemp(table);

                synchronized (mapper)
                {
                   mapper.mapClass(table);
                }
             });
          }
          else
          {
             TempTableMapper mapper = locateTemp(table);

             synchronized (mapper)
             {
                mapper.mapClass(table);
             }
          }
       }

       /**
        * Compose a set of legacy field names which represents the intersection of the fields (by name
        * and type) of the two tables. These fields represent the possible, <em>natural</em> join
        * points between the tables.
        *
        * @param   buf1
        *          The record buffer which represents the first table.
        * @param   buf2
        *          The record buffer which represents the second table.
        *
        * @return  The set of legacy field names which are common between the two tables.
        */
       static Set<String> getCommonFields(RecordBuffer buf1, RecordBuffer buf2)
       {
          Set<String> res = new HashSet<>();

          Map<String, String> legacyToProp1 = getLegacyFieldNameMap(buf1).getLegacyField2Name();
          Map<String, String> legacyToProp2 = getLegacyFieldNameMap(buf2).getLegacyField2Name();
          Set<String> legacy1 = legacyToProp1.keySet();
          Set<String> legacy2 = legacyToProp2.keySet();

          Class<? extends DataModelObject> iface1 = buf1.getDMOInterface();
          Map<String, Method> getters1 = PropertyHelper.legacyGettersByProperty(iface1);
          Map<String, Integer> extents1 = PropertyHelper.extentsByProperty(iface1);

          Class<? extends DataModelObject> iface2 = buf2.getDMOInterface();
          Map<String, Method> getters2 = PropertyHelper.legacyGettersByProperty(iface2);
          Map<String, Integer> extents2 = PropertyHelper.extentsByProperty(iface2);

          for (String legacyField: legacy1)
          {
             if (!legacy2.contains(legacyField))
             {
                continue;
             }

             // Compare property types.
             Method getter1 = getters1.get(legacyToProp1.get(legacyField));
             ParmType parmType1 = ParmType.fromClass(getter1.getReturnType());

             Method getter2 = getters2.get(legacyToProp2.get(legacyField));
             ParmType parmType2 = ParmType.fromClass(getter2.getReturnType());

             if (parmType1 == null || parmType2 == null || parmType1 != parmType2)
             {
                continue;
             }

             // Shouldn't be an extent field.
             Integer extent1 = extents1.get(legacyField);
             Integer extent2 = extents2.get(legacyField);

             if (extent1 != null || extent2 != null)
             {
                continue;
             }

             res.add(legacyField);
          }

          return res;
       }

       /**
        * Given a table and a map of fields which this table class has in common (by name and type)
        * with another table, find the index which can be used to join this table with the other. The
        * index must be unique and must contain only fields which are in the common map. There should
        * be only one such index in the table, but this restriction is not enforced here; once a
        * suitable index is found, it is returned.
        *
        * @param   buffer
        *          The buffer which represents the table to be searched for a suitable index.
        * @param   commonFields
        *          Map of legacy fields which the given table has in common with another one.
        *
        * @return  The unique index which defines the join between this table and another, based upon
        *          fields described by <code>commonFields</code>, or <code>null</code> if no such
        *          index is found.
        */
       static P2JIndex findJoiningIndex(RecordBuffer buffer, Set<String> commonFields)
       throws PersistenceException
       {
          // [indexes] uses orm-style property names, not normalized legacy, and database table/indexes
          Iterator<P2JIndex> indexes = buffer.getDmoInfo().getDatabaseIndexes();

          indexLoop:
          while (indexes.hasNext())
          {
             P2JIndex index = indexes.next();
             if (!index.isUnique())
             {
                continue;
             }

             ArrayList<P2JIndexComponent> comps = index.components();
             for (int i = 0; i < comps.size(); i++)
             {
                P2JIndexComponent component = comps.get(i);
                if (!commonFields.contains(component.getNormalizedName()))
                {
                   continue indexLoop;
                }
             }

             return index;
          }

          return null;
       }

       /**
        * Compose a set of all legacy field names defined for the given index.
        *
        * @param   index
        *          The index whose fields are to be included in the set.
        *
        * @return  Set of legacy field names defined for the given index.
        */
       static Set<String> getIndexFieldNames(P2JIndex index)
       {
          Set<String> res = new HashSet<>();
          ArrayList<P2JIndexComponent> comps = index.components();
          for (int i = 0; i < comps.size(); i++)
          {
             P2JIndexComponent component = comps.get(i);
             res.add(component.getLegacyName().toLowerCase());
          }

          return res;
       }

       /**
        * A helper method to return bi-directional mapping of (lower case) legacy field names to
        * their DMO property counterparts, optimized to gather information about all fields at once.
        *
        * @param   buffer
        *          Record buffer for which the mapping is needed.
        *
        * @return  Bi-directional mapping of legacy field names to property names for the given
        *          buffer.
        */
       static LegacyFieldNameMap getLegacyFieldNameMap(RecordBuffer buffer)
       {
          Map<String, String> legacyField2Name = new HashMap<>();
          Map<String, String> name2LegacyField = new HashMap<>();

          LegacyTableInfo tableInfo;

          if (buffer.isTemporary())
          {
             TempTable tt = buffer.getParentTable();
             tableInfo = ((AbstractTempTable) tt).tableInfo;
          }
          else
          {
             synchronized (permanentDBs)
             {
                PermanentTableMapper mapper = locatePerm(buffer.getSchema());
                try
                {
                   tableInfo = mapper.j2p.get(buffer.getDMOInterface());
                }
                catch (Exception e)
                {
                   LOG.warning("", e);
                   tableInfo = null;
                }
             }
          }

          synchronized (tableInfo)
          {
             // it is assumed this method is only called at a safe time, after the mappings have been
             // initialized, otherwise we will get a NullPointerException here, which indicates a
             // programming error, rather than a runtime error
             for (LegacyFieldInfo fieldInfo : tableInfo.p2j.values())
             {
                String legacyName = fieldInfo.legacyName;
                String legacyLowName = fieldInfo.lcLegacyName;
                String propertyName = (fieldInfo.original.length() > 0
                                       ? fieldInfo.original
                                       : fieldInfo.javaName);
                legacyField2Name.put(legacyLowName, propertyName);
                name2LegacyField.put(propertyName, legacyName);
             }
          }

          return new LegacyFieldNameMap(legacyField2Name, name2LegacyField);
       }

       /**
        * Get the legacy field info for the property associated with the DMO referenced by the given buffer.
        *
        * @param   recBuff
        *          A {@code RecordBuffer} instance.
        * @param    property
        *           The DMO property name.
        *
        * @return  The legacy field info or {@code null} if there is no such field.
        */
       public static LegacyFieldInfo getLegacyFieldInfo(RecordBuffer recBuff, String property)
       {
          if (recBuff.isTemporary())
          {
             return getLegacyFieldInfo(recBuff.getParentTable(), property);
          }
          else
          {
             // permanent database
             return getLegacyFieldInfo(recBuff.getDmoInfo(), property);
          }
       }

       /**
        * Get information about the index associated with the DMO referenced by the given buffer,
        * as a string.
        *
        * @param    recBuf
        *           A buffer instance which specifies the DMO class associated with the target index.
        * @param    indexNum
        *           0-based index number in the index definition order.
        *
        * @return   a string describing the target index or <code>null</code> if there is no index
        *           with the given number. Format of the string is described in
        *           {@link Buffer#indexInformation(int)}.
        */
       static LegacyIndexInfo getLegacyIndexInfo(RecordBuffer recBuf, int indexNum)
       {
          if (recBuf.isTemporary())
          {
             return getLegacyIndexInfo(recBuf.getParentTable(), indexNum);
          }
          else
          {
             return getLegacyIndexInfo(recBuf.getDMOInterface(), indexNum);
          }
       }

       /**
        * Get information about the index associated with the given temporary table, as a string.
        *
        * @param    tt
        *           Temporary table associated with the target index.
        * @param    indexNum
        *           0-based index number in the index definition order.
        *
        * @return   a string describing the target index or <code>null</code> if there is no index
        *           with the given number. Format of the string is described in
        *           {@link Buffer#indexInformation(int)}.
        */
       public static LegacyIndexInfo getLegacyIndexInfo(TempTable tt, int indexNum)
       {
          TempTableMapper mapper = locateTemp(tt);

          synchronized (mapper)
          {
             return mapper.legacyIndexInfo(tt, indexNum);
          }
       }

       /**
        * Get information about the index associated with the given DMO class, as a string.
        *
        * @param    dmoIface
        *           DMO class associated with the target index.
        * @param    indexNum
        *           0-based index number in the index definition order.
        *
        * @return   a string describing the target index or <code>null</code> if there is no index
        *           with the given number. Format of the string is described in
        *           {@link Buffer#indexInformation(int)}.
        */
       static LegacyIndexInfo getLegacyIndexInfo(Class<? extends DataModelObject> dmoIface, int indexNum)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
          if (dmoInfo.isTempTable())
          {
             return null;
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema());

             return mapper.legacyIndexInfo(dmoIface, indexNum);
          }
       }

       /**
        * Get the procedure of a trigger associated with the given DMO class and event, as a string.
        *
        * @param    dmoIface
        *           DMO class associated with the target trigger.
        * @param    event
        *           the type of the event for the trigger.
        *
        * @return   a string describing the trigger procedure or <code>null</code> if there is no trigger
        *           for this kind of event.
        */
       public static String getLegacyTriggerProcedure(Class<? extends DataModelObject> dmoIface,
                                                      String event,
                                                      String property)
       {
          LegacyTriggerInfo triggerInfo =  getLegacyTriggerInfo(dmoIface, event, property);
          return (triggerInfo == null) ? null : triggerInfo.procedure;
       }

       /**
        * Determine if the trigger associated with the given DMO class and event, is overridable.
        *
        * @param    dmoIface
        *           DMO class associated with the target trigger.
        * @param    event
        *           the type of the event for the trigger.
        *
        * @return   a true if the trigger is overridable or false otherwise.
        */
       public static boolean isLegacyTriggerOverrideable(Class<? extends DataModelObject> dmoIface,
                                                         String event,
                                                         String property)
       {
          LegacyTriggerInfo triggerInfo =  getLegacyTriggerInfo(dmoIface, event, property);
          return triggerInfo != null && triggerInfo.overridable;
       }

       /**
        * Get information about the trigger associated with the given DMO class and event.
        *
        * @param    dmoIface
        *           DMO class associated with the target index.
        * @param    event
        *           the type of the event for the trigger.
        *
        * @return   a container describing the trigger or <code>null</code> if there is no trigger
        *           for this kind of event.
        */
       public static LegacyTriggerInfo getLegacyTriggerInfo(Class<? extends DataModelObject> dmoIface,
                                                            String event,
                                                            String property)
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
          if (dmoInfo.isTempTable())
          {
             return null;
          }

          synchronized (permanentDBs)
          {
             PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema());

             // TODO: remove ?: ASA [permanentDBs] is correctly populated
             return mapper == null ? null : mapper.legacyTriggerInfo(dmoIface, event, property);
          }
       }

       /**
        * Locate the appropriate {@link TempTableMapper} for the given temp-table. If it is statically
        * defined, return the shared, static version, else return the context-local version for
        * dynamically defined temp-tables.
        *
        * @param   tt
        *          Temp-table object.
        *
        * @return  Appropriate table mapper as described above.
        */
       private static TempTableMapper locateTemp(TempTable tt)
       {
          return tt._dynamic() ? dynamicDB.get() : staticTempDB.get();
       }

       /**
        * Locate the {@link PermanentTableMapper} instance for the given schema.
        *
        * @param    schema
        *           The schema name.
        *
        * @return   See above.
        */
       private static PermanentTableMapper locatePerm(String schema)
       {
          synchronized (permanentDBs)
          {
             return permanentDBs.get(schema);
          }
       }

       /**
        * Remove the mappings for the specified key.
        *
        * @param    key
        *           The key to be removed.
        */
       abstract void removeTable(K key);

       /**
        * Get the number of fields in the table mapped to the given key. This returns the number of
        * legacy fields, unadjusted for extent field denormalization. Only normal fields are counted,
        * the hidden fields (which start with underscore (_)) are not taken into consideration.
        *
        * @param   legacyKey
        *          Key to which the target table is matched.
        *
        * @return  Number of fields in the table.
        */
       int getNumFieldsImpl(K legacyKey)
       {
          LegacyTableInfo tableInfo = byKey(legacyKey);
          if (tableInfo == null)
          {
             // maybe report this issue
             return 0;
          }

          // return the size of the p2j map, which has one entry per original legacy field, as
          // opposed to the ip2j map, which has one entry per legacy scalar field and one per
          // denormalized (expanded) legacy extent field. The hidden fields are dropped, also.
          return tableInfo.p2j.size() - tableInfo.hiddenFields.size();
       }

       /**
        * Get the converted index name for the specified legacy index. The legacy table info is
        * determined using the specified <code>legacyKey</code>.
        *
        * @param    legacyKey
        *           The key used to identify the {@link LegacyTableInfo}.
        * @param    idx
        *           The legacy index name.
        *
        * @return   The converted index name or <code>null</code> if there is no such index.
        */
       String getIndexNameImpl(K legacyKey, String idx)
       {
          LegacyTableInfo tableInfo = byKey(legacyKey);
          LegacyIndexInfo indexInfo = tableInfo.idxp2j.get(idx.toLowerCase());

          return (indexInfo == null) ? null : indexInfo.convertedName;
       }

       /** Clear all the mappings. */
       abstract void clear();

       /**
        * Get the {@link LegacyTableInfo} instance mapped to a specific {@code key}. The type of key
        * is implementation specific.
        *
        * @return  The specific {@link LegacyTableInfo} instance if one exists.
        */
       abstract LegacyTableInfo byKey(K key);

       /**
        * Mapper for permanent tables.
        */
       private static class PermanentTableMapper
       extends TableMapper<Class<? extends DataModelObject>>
       {
          /** Mapping of DMO interfaces to {@link LegacyTableInfo} instances. */
          private final Map<Class<? extends DataModelObject>, LegacyTableInfo> j2p = new HashMap<>();

          /** Mapping of the legacy table names to {@link LegacyTableInfo} instances. */
          private final Map<String, LegacyTableInfo> p2j = new CaseInsensitiveHashMap<>();

          /**
           * Default c'tor.
           *
           * @param    schema
           *           The schema name.
           */
          private PermanentTableMapper(String schema)
          {
             super(schema);
          }

          /**
           * Map the specified DMO implementation class using the specified {@code legacyKey}.
           * <p>
           * Before calling this method, make sure the passed DMO has a {@link Table} annotation.
           *
           * @param   dmoInfo
           *          Object which contains metadata about the DMO.
           */
          void mapClass(DmoMeta dmoInfo)
          {
             Class<? extends DataModelObject> dmoIface = dmoInfo.getAnnotatedInterface();

             if (j2p.containsKey(dmoIface))
             {
                // already mapped
                return;
             }

             LegacyTableInfo tableInfo = new LegacyTableInfo(dmoInfo);
             String legacyKey = dmoInfo.normalizedLegacyTable;

             j2p.put(dmoIface, tableInfo);
             p2j.put(legacyKey, tableInfo);
          }

          /**
           * Get the legacy index name of converted index, which is defined by the backing table for the
           * specified DMO implementation class.
           *
           * @param    dmoIface
           *           The DMO implementation class.
           * @param    idx
           *           The converted index name.
           *
           * @return   The legacy index name or <code>null</code> if there is no such index defined.
           */
          String legacyIndexNameImpl(Class<? extends DataModelObject> dmoIface, String idx)
          {
             LegacyTableInfo tableInfo = byKey(dmoIface);
             LegacyIndexInfo indexInfo = tableInfo.idxj2p.get(idx);

             return (indexInfo == null ? null : indexInfo.legacyName);
          }

          /**
           * Get the DMO class for the legacy table identified with the specified {@code legacyKey}.
           *
           * @param    legacyKey
           *           The key used to identify the {@link LegacyTableInfo}.
           *
           * @return   The DMO implementation class or {@code null} if there is no class
           *           corresponding the given key.
           */
          Class<? extends Record> getDMOClass(String legacyKey)
          {
             LegacyTableInfo tableInfo = p2j.get(legacyKey);
             return tableInfo == null ? null : tableInfo.dmoClass;
          }

          /**
           * Get the to {@link LegacyTableInfo} instance of a specific table in this mapper using its legacy name.
           *
           * @param   legacyName
           *          The table legacy name;
           *
           * @return  The associated {@code LegacyTableInfo} if one exists.
           */
          LegacyTableInfo byLegacyName(String legacyName)
          {
             return p2j.get(legacyName);
          }

          /**
           * Get the {@link LegacyTableInfo} instance mapped to a specific {@code key}. The type of key
           * is implementation specific.
           *
           * @return  The specific {@link LegacyTableInfo} instance if one exists.
           */
          @Override
          LegacyTableInfo byKey(Class<? extends DataModelObject> key)
          {
             return j2p.get(key);
          }

          /**
           * Determine if the table has table-level delete validation.
           *
           * @param    dmoIface
           *           The DMO interface associated with the given table.
           *
           * @return   <code>true</code> if the table has table-level delete validation.
           */
          boolean validation(Class<? extends DataModelObject> dmoIface)
          {
             LegacyTableInfo tableInfo = byKey(dmoIface);

             return tableInfo.validation;
          }

          /**
           * Get the legacy field name for given property defined by the specified DMO.
           *
           * @param    dmoIface
           *           The DMO interface.
           * @param    property
           *           The DMO property name.
           *
           * @return   The legacy field name or <code>null</code> if there is no such field.
           */
          String legacyFieldNameImpl(Class<? extends DataModelObject> dmoIface, String property)
          {
             LegacyTableInfo tableInfo = byKey(dmoIface);
             LegacyFieldInfo fieldInfo = tableInfo.j2p.get(property);

             return (fieldInfo == null ? null : fieldInfo.legacyName);
          }

          /**
           * Get the DMO property name for a field index belonging to the specified legacy table.
           *
           * @param    dmoIface
           *           The DMO interface for a permanent DMO.
           * @param    fieldId
           *           The field index.
           *
           * @return   The DMO property name associated with the legacy field or <code>null</code> if
           *           there is no such field.
           */
          String propertyNameImpl(Class<? extends DataModelObject> dmoIface, int fieldId)
          {
             LegacyTableInfo tableInfo = byKey(dmoIface);
             LegacyFieldInfo fieldInfo = tableInfo.ip2j.get(fieldId);

             return (fieldInfo == null ? null :
                   fieldInfo.original.isEmpty() ? fieldInfo.javaName : fieldInfo.original);
          }

          /**
           * Get legacy field info for a field from the specified legacy table.
           *
           * @param    dmoIface
           *           The DMO interface for a permanent DMO.
           * @param    property
           *           Property name.
           *
           * @return   legacy field info for a field from the specified legacy table or
           *           <code>null</code> if there is no such field.
           */
          LegacyFieldInfo legacyFieldInfoImpl(Class<? extends DataModelObject> dmoIface, String property)
          {
             LegacyTableInfo tableInfo = byKey(dmoIface);

             LegacyFieldInfo fieldInfo = tableInfo.j2p.get(property);

             // if fieldInfo is null, property may represent the "original" (i.e., pre-normalization,
             // converted property) name of a denormalized extent field
             if (fieldInfo == null)
             {
                Map<Integer, LegacyFieldInfo> dmap = tableInfo.denorm2j.get(property);

                // if the map is null at this point, property is probably a computed column property
                // name, which won't map to legacy info; otherwise, we have a denormalized extent field
                if (dmap != null)
                {
                   // first map value will do for legacy info
                   fieldInfo = dmap.get(1);
                }
             }

             return fieldInfo;
          }

          /**
           * Get information about the index associated with the given DMO class.
           *
           * @param    dmoIface
           *           DMO class associated with the target index.
           * @param    indexNum
           *           0-based index number in the index definition order.
           *
           * @return   an object describing the target index or <code>null</code> if there is no index
           *           with the given number.
           */
          LegacyIndexInfo legacyIndexInfo(Class<? extends DataModelObject> dmoIface, int indexNum)
          {
             LegacyTableInfo tableInfo = byKey(dmoIface);

             return indexNum < 0 || indexNum >= tableInfo.idx.size() ?
                   null :
                   tableInfo.idx.get(indexNum);
          }

          /**
           * Get information about the trigger associated with the given DMO class.
           *
           * @param    dmoIface
           *           DMO class associated with the target index.
           * @param    event
           *           the type of the event for the trigger.
           *
           * @return   a container describing the trigger or <code>null</code> if there is no trigger
           *           for this kind of event
           */
          LegacyTriggerInfo legacyTriggerInfo(Class<? extends DataModelObject> dmoIface,
                                              String event,
                                              String property)
          {
             LegacyTableInfo tableInfo = byKey(dmoIface);
             return tableInfo.trigger.get(Pair.of(event, property));
          }

          /**
           * Remove the mappings for the specified key.
           *
           * @param    dmoIface
           *           The key to be removed.
           */
          @Override
          void removeTable(Class<? extends DataModelObject> dmoIface)
          {
             LegacyTableInfo info = j2p.remove(dmoIface);
             if (info == null)
             {
                // ignore if not found
                return;
             }

             p2j.remove(info.legacyName);
          }

          /** Clear all the mappings. */
          @Override
          void clear()
          {
             j2p.clear();
             p2j.clear();
          }
       }

       /** Mapper for temporary tables. */
       private static class TempTableMapper
       extends TableMapper<TempTable>
       {
          /** A cache of the {@link LegacyTableInfo} instances, for each defined record. */
          private static final Map<Class<? extends Record>, LegacyTableInfo> TT_CACHE = new ConcurrentHashMap<>();

          /**
           * Default constructor.
           */
          TempTableMapper()
          {
             super(DatabaseManager.TEMP_TABLE_SCHEMA);
          }

          /**
           * Register the mappings for the specified temp table.
           *
           * @param    tt
           *           The temp-table which needs to be mapped.
           *
           * @throws   NullPointerException
           *           If the associated DMO class does not have a {@link Table} annotation.
           */
          void mapClass(TempTable tt)
          {
             Class<? extends Record> impl = tt.getDmoMeta().getImplementationClass();

             LegacyTableInfo lti = TT_CACHE.computeIfAbsent(impl, (cls) -> new LegacyTableInfo(tt.getDmoMeta()));

             ((AbstractTempTable) tt).tableInfo = lti;
          }

          /**
           * Get the legacy table name associated with the given temp table.
           *
           * @param    tt
           *           Temp table object.
           *
           * @return   The legacy table name.
           */
          String legacyName(TempTable tt)
          {
             return ((AbstractTempTable) tt).tableInfo.legacyName;
          }

          /**
           * Get the {@link LegacyTableInfo} instance mapped to a specific {@code key}. The type of key
           * is implementation specific.
           *
           * @return  The specific {@link LegacyTableInfo} instance if one exists.
           */
          @Override
          LegacyTableInfo byKey(TempTable key)
          {
             return ((AbstractTempTable) key).tableInfo;
          }

          /**
           * Get the legacy field name for given property defined by the specified temp table.
           *
           * @param    tt
           *           Temp table object.
           * @param    property
           *           The DMO property name.
           *
           * @return   The legacy field name or <code>null</code> if there is no such field.
           */
          private LegacyFieldInfo legacyFieldInfoImpl(TempTable tt, String property)
          {
             LegacyTableInfo tableInfo = ((AbstractTempTable) tt).tableInfo;

             return tableInfo.j2p.get(property);
          }

          /**
           * Get information about the index associated with the given temporary table.
           *
           * @param    tt
           *           Temp table object.
           * @param    indexNum
           *           0-based index number in the index definition order.
           *
           * @return   an object describing the target index or <code>null</code> if there is no index
           *           with the given number.
           */
          private LegacyIndexInfo legacyIndexInfo(TempTable tt, int indexNum)
          {
             LegacyTableInfo tableInfo = ((AbstractTempTable) tt).tableInfo;

             return indexNum < 0 || indexNum >= tableInfo.idx.size() ?
                      null :
                      tableInfo.idx.get(indexNum);
          }

          /**
           * Get stream for the indexes associated with the given temporary table.
           *
           * @param    tt
           *           Temp table object.
           * @return   stream for the indexes associated with the given temporary table.
           */
          private Stream<LegacyIndexInfo> legacyIndexInfo(TempTable tt)
          {
             LegacyTableInfo tableInfo = ((AbstractTempTable) tt).tableInfo;
             return new ArrayList<>(tableInfo.idx).stream();
          }

          /**
           * Get the legacy index name of converted index, which is defined by the specified backing
           * temp table.
           *
           * @param    tt
           *           Temp table object.
           * @param    idx
           *           The converted index name.
           *
           * @return   The legacy index name or <code>null</code> if there is no such index defined.
           */
          private String legacyIndexNameImpl(TempTable tt, String idx)
          {
             LegacyTableInfo tableInfo = ((AbstractTempTable) tt).tableInfo;
             LegacyIndexInfo indexInfo = tableInfo.idxj2p.get(idx);

             return indexInfo.legacyName;
          }

          /**
           * Remove the mappings for the specified key.
           *
           * @param    key
           *           The key to be removed.
           */
          @Override
          void removeTable(TempTable key)
          {
             // no-op
          }

          /** Clear all the mappings. */
          void clear()
          {
             // no-op
          }
       }

       /** A container with the legacy info for a table. */
       static class LegacyTableInfo
       {
          /** Field info mapped by normalized legacy name. */
          private final Map<String, LegacyFieldInfo> p2j = new HashMap<>();

          /**
           * Field info kept by legacy buffer field sequence index in progress.
           * This is needed at runtime by the buffer-field attribute.
           */
          private final Map<Integer, LegacyFieldInfo> ip2j = new HashMap<>();

          /** Field info kept by associated DMO property name. */
          private final Map<String, LegacyFieldInfo> j2p = new HashMap<>();

          /** Field info about extent fields kept by associated DMO property name. */
          private final Map<String, LegacyFieldInfo> extents = new HashMap<>();

          /** Index info kept by legacy index name. */
          private final Map<String, LegacyIndexInfo> idxp2j = new HashMap<>();

          /** Index info kept by converted index name. */
          private final Map<String, LegacyIndexInfo> idxj2p = new HashMap<>();

          /** Index info kept by index number. */
          private final List<LegacyIndexInfo> idx = new ArrayList<>();

          /** Trigger info kept by event type and property. */
          private final Map<Pair<String, String>, LegacyTriggerInfo> trigger = new HashMap<>();

          /** The legacy table name. */
          private final String legacyName;

          /** The set of hidden legacy field names (specific to BEFORE TEMP-TABLES). */
          private final Set<String> hiddenFields = new HashSet<>();

          /** Determines if the table has table-level delete validation. */
          private final boolean validation;

          /** Indexed map of new field names for denormalized legacy field name. */
          private final Map<String, Map<Integer, LegacyFieldInfo>> denorm2j = new HashMap<>();

          /** The DMO interface. */
          private final Class<? extends DataModelObject> dmoIface;

          /** The DMO implementation class. */
          private final Class<? extends Record> dmoClass;

          /** <code>true</code> if the table was defined with LIKE-SEQUENTIAL option. */
          private final boolean likeSequential;

          /** The sorted list of fields. */
          private final List<LegacyFieldInfo> fieldList;

          /**
           * Initialize the legacy info for a table by analyzing the {@code DmoMeta} and reading the
           * {@link Indices} and {@link Triggers} annotation from the DMO interface.
           *
           * @param   dmoInfo
           *          Object which contains metadata about the DMO.
           *
           * @see      #loadFields(DmoMeta)
           */
          private LegacyTableInfo(DmoMeta dmoInfo)
          {
             this.legacyName = dmoInfo.legacyTable;
             this.dmoIface = dmoInfo.getAnnotatedInterface();
             this.dmoClass = dmoInfo.getImplementationClass();
             this.validation = dmoInfo.hasValidation;
             this.likeSequential = dmoInfo.isLikeSequential;
             loadFields(dmoInfo);

             List<LegacyFieldInfo> fieldList = new ArrayList<>(j2p.values());
             Collections.sort(fieldList);
             this.fieldList = Collections.unmodifiableList(fieldList);

             Indices lindexes = dmoIface.getAnnotation(Indices.class);
             if (lindexes != null)
             {
                LegacyIndexInfo firstIndex = null;
                boolean hasPrimaryIndex = false;

                for (Index lidx : lindexes.value())
                {
                   LegacyIndexInfo idxInfo = new LegacyIndexInfo(lidx);
                   if (firstIndex == null)
                   {
                      firstIndex = idxInfo;
                   }

                   if (idxInfo.effectivePrimary)
                   {
                      hasPrimaryIndex = true;
                   }

                   this.idxp2j.put(idxInfo.legacyName.toLowerCase(), idxInfo);
                   this.idxj2p.put(idxInfo.convertedName, idxInfo);
                   this.idx.add(idxInfo);
                }

                // If there is no index with "primary" annotation, make the first index primary.
                if (!hasPrimaryIndex && firstIndex != null)
                {
                   firstIndex.effectivePrimary = true;
                }
             }

             Triggers triggersAnn = dmoIface.getAnnotation(Triggers.class);
             if (triggersAnn != null)
             {
                for (Trigger ltrg : triggersAnn.value())
                {
                   LegacyTriggerInfo trgInfo = new LegacyTriggerInfo(ltrg);
                   String property = null;
                   String field = trgInfo.field;
                   if (!field.isEmpty())
                   {
                      String normalized4glField = TextOps.rightTrim(field).toStringMessage().toLowerCase();
                      LegacyFieldInfo fieldInfo = this.p2j.get(normalized4glField);
                      if (fieldInfo == null)
                      {
                         continue;
                      }
                      property = fieldInfo.original.length() > 0 ? fieldInfo.original : fieldInfo.javaName;
                   }
                   this.trigger.put(Pair.of(trgInfo.event, property), trgInfo);
                }
             }
          }

          /**
           * Load all fields from the specified class and add their mappings.
           *
           * @param    dmoInfo
           *           The DMO data for the interface or a composite class with the extent fields.
           *
           * @throws   NullPointerException
           *           If a DMO property does not have a {@link Property} annotation.
           */
          private void loadFields(DmoMeta dmoInfo)
          {
             for (Iterator<Property> it = dmoInfo.getFields(true); it.hasNext(); )
             {
                Property fieldAnn = it.next();
                Class<?> dataType = fieldAnn._fwdType;

                if (fieldAnn.propId < 0)
                {
                   // ignore P2J-specific DMO properties. all legacy DMO properties have BaseDataType
                   // subclass as their type
                   continue;
                }
                BaseDataType initialValue;
                try
                {
                   if (fieldAnn.initialNull)
                   {
                      initialValue = BaseDataType.generateUnknown(dataType);
                   }
                   else
                   {
                      String text = fieldAnn.initial;

                      if (dataType == date.class)
                      {
                         initialValue = "today".equalsIgnoreCase(text)
                               ? new character(text)
                               : date.parseInitial(text);
                      }
                      else if (dataType == datetime.class)
                      {
                         initialValue = "now".equalsIgnoreCase(text)
                               ? new character(text)
                               : datetime.parseInitial(text);
                      }
                      else if (dataType == datetimetz.class)
                      {
                         initialValue = "now".equalsIgnoreCase(text)
                               ? new character(text)
                               : datetimetz.parseInitial(text);
                      }
                      else if (dataType == logical.class)
                      {
                         String fmt = fieldAnn.format.isEmpty() ? logical.DEFAULT_FORMAT : fieldAnn.format;
                         // in case text is empty during initialization return a default value
                         initialValue = "".equals(text) ? logical.UNKNOWN : new logical(text, fmt);
                      }
                      else
                      {
                         Constructor<BaseDataType> ctor =
                               (Constructor<BaseDataType>) dataType.getConstructor(String.class);
                         initialValue = ctor.newInstance(text);
                      }
                   }
                }
                catch (ReflectiveOperationException e)
                {
                   throw new RuntimeException(
                         "Failed to instantiate default value '" + fieldAnn.initial + "' (" +
                         dataType + ") for field '" + fieldAnn.legacy + "' of " +
                         dmoInfo.getSqlTableName(), e);
                }

                String columnLabel = fieldAnn.columnLabel;
                String legacyName = fieldAnn.legacy;
                String javaName = fieldAnn.name;
                String format = fieldAnn.format;
                String initial = fieldAnn.initialNull ? null : fieldAnn.initial;
                boolean initialNull = fieldAnn.initialNull;
                int decimals = fieldAnn.scale;
                int maxWidth = fieldAnn.width;
                String label = fieldAnn.label;
                boolean mandatory = fieldAnn.mandatory;
                String validateMessage = fieldAnn.validateMessage;
                String validateExpression = fieldAnn.validateExpression;
                int extent = fieldAnn.extent; // TODO: handle denormalized properties?
                int order = fieldAnn.order;
                int position = fieldAnn.position;
                int extentIndex = fieldAnn.index;
                String original = fieldAnn.original;
                boolean serializeHidden = fieldAnn.serializeHidden;
                String serializeName = fieldAnn.serializeName;
                String xmlDataType = fieldAnn.xmlDataType;
                String xmlNodeName = fieldAnn.xmlNodeName;
                String xmlNodeType = fieldAnn.xmlNodeType;
                String like = fieldAnn.like;
                String codePage = fieldAnn.codePage;
                String help = fieldAnn.help;

                Integer fieldId = fieldAnn.id;

                String pnameLowCase = fieldAnn.legacyLower;
                LegacyFieldInfo fieldInfo = p2j.get(pnameLowCase);
                boolean firstSeen = fieldInfo == null;
                if (firstSeen || fieldInfo.fieldId != fieldId)
                {
                   fieldInfo = new LegacyFieldInfo(
                         dmoInfo.tempTable,
                         columnLabel,
                         fieldId,
                         format,
                         help,
                         initial,
                         decimals,
                         javaName,
                         label,
                         legacyName,
                         mandatory,
                         extent,
                         order,
                         position,
                         original,
                         validateMessage,
                         validateExpression,
                         (Class<? extends BaseDataType>) dataType,
                         initialValue,
                         initialNull,
                         serializeHidden,
                         serializeName,
                         xmlDataType,
                         xmlNodeName,
                         xmlNodeType,
                         like,
                         codePage,
                         fieldAnn.caseSensitive);
                }

                if (firstSeen) // for extent field store the first representative info only
                {
                   p2j.put(pnameLowCase, fieldInfo);
                   if (ReservedProperty.isReservedProperty(pnameLowCase))
                   {
                      hiddenFields.add(pnameLowCase);
                   }
                }
                j2p.put(javaName, fieldInfo);
                ip2j.put(fieldId, fieldInfo);

                if (original.length() > 0)
                {
                   Map<Integer, LegacyFieldInfo> javaNames = denorm2j.computeIfAbsent(original,
                                                                                      k -> new HashMap<>());
                   javaNames.put(extentIndex, fieldInfo);
                   if (extents.get(original) == null)
                   {
                      LegacyFieldInfo extentInfo = new LegacyFieldInfo(
                            dmoInfo.tempTable,
                            columnLabel,
                            fieldId,
                            format,
                            help,
                            initial,
                            decimals,
                            original,
                            label,
                            legacyName,
                            mandatory,
                            extent,
                            order,
                            position,
                            "",
                            validateMessage,
                            validateExpression,
                            (Class<? extends BaseDataType>) dataType,
                            initialValue,
                            initialNull,
                            serializeHidden,
                            serializeName,
                            xmlDataType,
                            xmlNodeName,
                            xmlNodeType,
                            like,
                            codePage,
                            fieldAnn.caseSensitive);
                      extents.put(original, extentInfo);
                   }
                }
             }
          }
       }

       /**
        * A container with a index information, holding index attributes and components.
        */
       public static class LegacyIndexInfo
       {
          /** The legacy name of this index. */
          private final String legacyName;

          /** The converted name of this index. */
          private final String convertedName;

          /**
           * Determine if the index is a primary index: the one with the "primary" annotation or, if
           * there is no such one, the first in the definition order.
           */
          private boolean effectivePrimary;

          /** Determine if the index is an unique index. */
          private final boolean unique;

          /** Determine if the index is a word-index index. */
          private final boolean word;

          /** Index components (fields with sorting directions). */
          private final LegacyIndexComponentInfo[] components;

          /**
           * Create a new index information container, holding index attributes and components.
           *
           * @param index
           *        Source index annotation generated at conversion stage.
           */
          LegacyIndexInfo(Index index)
          {
             legacyName = index.legacy();
             convertedName = index.name();
             effectivePrimary = index.primary() && !index.word();
             unique = index.unique() && !index.word();
             word = index.word();

             IndexComponent[] components = index.components();
             int size = components.length;
             this.components = new LegacyIndexComponentInfo[size];
             Boolean descending = null;
             for (int i = 0; i < size; i++)
             {
                this.components[i] = new LegacyIndexComponentInfo(components[i]);
                if (descending == null)
                {
                   descending = this.components[i].effectiveDescending;
                }
             }
          }

          /**
           * Get a stream for the index components.
           *
           * @return iterator for the index components.
           */
          public Stream<LegacyIndexComponentInfo> indexComponents()
          {
             return Arrays.stream(components);
          }
          /**
           * Returns index information as a string. Format of this string is described in
           * {@link Buffer#indexInformation(int)}
           *
           * @return see above.
           */
          public String indexInformation()
          {
             StringBuilder sb = new StringBuilder(legacyName).append(",");
             sb.append(unique ? "1" : "0").append(",");
             sb.append(effectivePrimary ? "1" : "0").append(",");
             sb.append(word ? "1" : "0");

             for (LegacyIndexComponentInfo component : components)
             {
                sb.append(",").append(component.fieldLegacyName);
                sb.append(",").append(component.effectiveDescending ? "1" : "0");
             }

             return sb.toString();
          }

          /**
           * Checks whether the index is an unique index.
           *
           * @return  {@code true} if this is an unique index.
           */
          public boolean isUnique()
          {
             return unique;
          }

          /**
           * Checks whether the index is a word index.
           *
           * @return  {@code true} if this is a word index.
           */
          public boolean isWord()
          {
             return word;
          }

          /**
           * Retrieve the list of the field components of this index.
           *
           * @return  the list of the fields this index is composed of.
           */
          public String[] getComponents()
          {
             String[] comps = new String[components.length];
             for (int i = 0; i < components.length; i++)
             {
                comps[i] = components[i].fieldLegacyName;
             }
             return comps;
          }

          /**
           * Gets the legacy name of the index.
           *
           * @return  the legacy name of the index.
           */
          public String getLegacyName()
          {
             return legacyName;
          }

          /**
           * Checks whether this is an effective primary index.
           *
           * @return  {@code true} when this is an effective primary index.
           */
          public boolean isEffectivePrimary()
          {
             return effectivePrimary;
          }

          /**
           * Obtain the sort direction of the components returned with {@link #getComponents()}
           * method. {@code true} value of a component means it is sorted ascending.
           *
           * @return the sort direction of the components returned with {@link #getComponents()}
           *          method.
           */
          public boolean[] getComponentsDirection()
          {
             boolean[] comps = new boolean[components.length];
             for (int i = 0; i < components.length; i++)
             {
                comps[i] = !components[i].effectiveDescending;
             }
             return comps;
          }
       }

       /** A container with an index component information. */
       static class LegacyIndexComponentInfo
       {
          /** The legacy name of the field participating in the index. */
          public final String fieldLegacyName;

          /** Determines if this component has descending sorting. */
          public final boolean effectiveDescending;

          /**
           * Create a new index component information container, holding field name and sorting
           * direction.
           *
           * @param component
           *        Source index component annotation generated at conversion stage.
           *
           */
          LegacyIndexComponentInfo(IndexComponent component)
          {
             fieldLegacyName = component.legacy();
             effectiveDescending = component.descending();
          }
       }

       /**
        * A container with a trigger information.
        */
       private static class LegacyTriggerInfo
       {
          /** Determine the event type of the trigger. */
          private final String event;

          /** Determine if the trigger is overridable.*/
          private final boolean overridable;

          /** Determine the trigger procedure. */
          private final String procedure;

          /** Determine the trigger's field. Only for assign triggers, otherwise empty.*/
          private final String field;

          /**
           * Create a new trigger information container, holding the event type, the overridable property and the procedure.
           *
           * @param trigger
           *        Source trigger annotation generated at conversion stage.
           */
          LegacyTriggerInfo(Trigger trigger)
          {
             event = trigger.event();
             overridable = trigger.overridable();
             procedure = trigger.procedure();
             field = trigger.field();
          }
       }

       /**
        * A container for all the field state which can be mutable; this is decoupled from the
        * {@link LegacyFieldInfo}, to allow caching of those instances, and each {@link TempTable} instance will
        * have its own {@link MutableFieldInfo} instances, for each field.
        * <p>
        * When accessing the mutable state via {@link LegacyFieldInfo}, the {@link TempTable} instance is passed
        * as a parameter, and if non-null, it will resolve and delegate the call to the proper
        * {@link MutableFieldInfo} instance.
        */
       static class MutableFieldInfo
       {
          /** COLUMN-LABEL attribute of this field. */
          private String columnLabel;

          /** DECIMALS attribute of this field. */
          private int decimals;

          /** FORMAT attribute of this field. */
          private String format;

          /** HELP attribute of this field. */
          private String help;

          /** LABEL attribute of this field. */
          private String label;

          /** LITERAL-QUESTION attribute of this field. */
          private boolean literalQuestion = false;

          /** VALIDATE-EXPRESSION attribute of this field. */
          private String validateExpression;

          /** VALIDATE-MESSAGE attribute of this field. */
          private String validateMessage;

          /** Options needed when serializing temp-table data to/from external media */
          private final SerializeOptions serializeOptions;

          /**
           * Initialize this mutable info from the field's definition.
           *
           * @param    field
           *           The field's definition as it appears at the temp-table.
           */
          public MutableFieldInfo(LegacyFieldInfo field)
          {
             this.columnLabel = field.columnLabel;
             this.decimals = field.decimals;
             this.format = field.format;
             this.help = field.help;
             this.label = field.label;
             this.literalQuestion = field.literalQuestion;
             this.validateExpression = field.validateExpression;
             this.validateMessage = field.validateMessage;
             this.serializeOptions = new SerializeOptions(field.serializeOptions);
          }

          /**
           * Get the {@link #columnLabel}.
           *
           * @return   See above.
           */
          public String getColumnLabel()
          {
             return columnLabel;
          }

          /**
           * Set the {@link #columnLabel}.
           *
           * @param    columnLabel
           *           The new column label value.
           */
          public void setColumnLabel(String columnLabel)
          {
             this.columnLabel = columnLabel;
          }

          /**
           * Get the {@link #decimals}.
           *
           * @return   See above.
           */
          public int getDecimals()
          {
             return decimals;
          }

          /**
           * Set the {@link #decimals}.
           *
           * @param    decimals
           *           The new decimals value.
           */
          public void setDecimals(int decimals)
          {
             this.decimals = decimals;
          }

          /**
           * Get the {@link #format}.
           *
           * @return   See above.
           */
          public String getFormat()
          {
             return format;
          }

          /**
           * Set the {@link #format}.
           *
           * @param    format
           *           The new format value.
           */
          public void setFormat(String format)
          {
             this.format = format;
          }

          /**
           * Get the {@link #help}.
           *
           * @return   See above.
           */
          public String getHelp()
          {
             return help;
          }

          /**
           * Set the {@link #help}.
           *
           * @param    help
           *           The new help value.
           */
          public void setHelp(String help)
          {
             this.help = help;
          }

          /**
           * Get the {@link #label}.
           *
           * @return   See above.
           */
          public String getLabel()
          {
             return label;
          }

          /**
           * Set the {@link #label}.
           *
           * @param    label
           *           The new label value.
           */
          public void setLabel(String label)
          {
             this.label = label;
          }

          /**
           * Get the {@link #literalQuestion}.
           *
           * @return   See above.
           */
          public boolean isLiteralQuestion()
          {
             return literalQuestion;
          }

          /**
           * Set the {@link #literalQuestion}.
           *
           * @param    literalQuestion
           *           The new literal question value.
           */
          public void setLiteralQuestion(boolean literalQuestion)
          {
             this.literalQuestion = literalQuestion;
          }

          /**
           * Get the {@link #validateExpression}.
           *
           * @return   See above.
           */
          public String getValidateExpression()
          {
             return validateExpression;
          }

          /**
           * Set the {@link #validateExpression}.
           *
           * @param    validateExpression
           *           The new validate expression value.
           */
          public void setValidateExpression(String validateExpression)
          {
             this.validateExpression = validateExpression;
          }

          /**
           * Get the {@link #validateMessage}.
           *
           * @return   See above.
           */
          public String getValidateMessage()
          {
             return validateMessage;
          }

          /**
           * Set the {@link #validateMessage}.
           *
           * @param    validateMessage
           *           The new validate message value.
           */
          public void setValidateMessage(String validateMessage)
          {
             this.validateMessage = validateMessage;
          }

          /**
           * Get the {@link #serializeOptions}.
           *
           * @return   See above.
           */
          public SerializeOptions getSerializeOptions()
          {
             return serializeOptions;
          }
       }

       /**
        * A container with a field's legacy and DMO property name and field attributes.
        */
       public static class LegacyFieldInfo
       implements Comparable<LegacyFieldInfo>
       {
          /** COLUMN-LABEL attribute of this field. */
          private String columnLabel;

          /**
           * The id of this field in the sequence of the fields of the buffer. It is needed by the
           * dynamic <code>bufferField(int)</code> attribute of the <code>BufferImpl</code> for
           * emulating the <code>BUFFER-FIELD(integer)</code> buffer handle attribute.
           */
          private final Integer fieldId;

          /** FORMAT attribute of this field. */
          private String format;

          /** Defined FORMAT attribute of this field. */
          private String definedFormat;

          /** HELP attribute of this field. */
          private String help;

          /** INITIAL attribute of this field. */
          private final String initial;

          /** INITIAL-NULL pseudo- attribute of this field. */
          private final boolean initialNull;

          /** DECIMALS attribute of this field. */
          private int decimals;

          /** The associated DMO property name. */
          private final String javaName;

          /** LABEL attribute of this field. */
          private String label;

          /** The legacy name of this field. */
          private final String legacyName;

          /** Lowercased version of the legacy name. */
          private final String lcLegacyName;

          /** EXTENT attribute of the field (from the legacy schema). */
          private final int extent;

          /** ORDER attribute of the field (from the legacy schema). */
          private final int order;

          /** POSITION attribute of the field (from the legacy schema). */
          private final int position;

          /** LITERAL-QUESTION attribute of this field. */
          private boolean literalQuestion = false;

          /** MANDATORY attribute of this field. */
          public final boolean mandatory;

          /** Original property name (not exposed in DMO) for denormalized field with extent. */
          public final String original;

          /** VALIDATE-MESSAGE attribute of this field. */
          private String validateMessage;

          /** VALIDATE-EXPRESSION attribute of this field. */
          private String validateExpression;

          /** Field data type. */
          private final Class<? extends BaseDataType> dataType;

          /** The initial value for this field. */
          private final BaseDataType initialValue;

          /** Options needed when serializing temp-table data to/from external media */
          private final SerializeOptions serializeOptions;

          /**
           * Fully qualified name of the source field from which the field definitions were copied
           * using LIKE option.
           */
          private final String like;

           /** Code page of the CLOB field. */
          private final String codePage;

          /** Flags case-sensitive character fields. */
          private final boolean caseSensitive;

          /** Flags mutable objects. */
          private final boolean mutable;

          /**
           * Create a new field information container, holding the legacy and converted names and field
           * attributes.
           *
           * @param   mutable
           *          If this structure is mutable. This is the case of temp-tables, which allow some of theirs
           *          field properties to be changed programmatically, at runtime. Only the mutable attributes
           *          have setters and their implementation must check this flag.
           * @param   columnLabel
           *          COLUMN-LABEL attribute.
           * @param   fieldId
           *          The id of field in the sequence of the fields of the buffer.
           * @param   format
           *          FORMAT attribute.
           * @param   help
           *          HELP attribute.
           * @param   initial
           *          INITIAL attribute.
           * @param   javaName
           *          The field's associated DMO property name.
           * @param   label
           *          LABEL attribute.
           * @param   legacyName
           *          The field's legacy name.
           * @param   mandatory
           *          MANDATORY attribute.
           * @param   extent
           *          EXTENT attribute from the schema (this is the original extent value without any
           *          regard for normalization/denormalization).
           * @param   order
           *          ORDER attribute.
           * @param   position
           *          POSITION attribute.
           * @param   original
           *          Original property name (actually not exposed in DMO) for denormalized field
           *          with extent.
           * @param   validateMessage
           *          VALIDATE-MESSAGE attribute.
           * @param   validateExpression
           *          VALIDATE-EXPRESSION attribute.
           * @param   dataType
           *          The field data type.
           * @param   initialValue
           *          The value of the field when the record is first created.
           * @param   serializeHidden
           *          {@code true} to omit this field from serialized output, else {@code false}.
           * @param   serializeName
           *          Name of field in serialized output.
           * @param   xmlDataType
           *          XML schema data type of field.
           * @param   xmlNodeName
           *          Name of element or attribute representing field in XML output.
           * @param   xmlNodeType
           *          Node type of field in XML output (valid values are "ELEMENT" and "ATTRIBUTE").
           * @param   like
           *          Fully qualified name of the source field from which the field definitions were
           *          copied using LIKE option.
           * @param   codePage
           *          Code page of the CLOB field.
           * @param   caseSensitive
           *          Flags case-sensitive character fields.
           */
          private LegacyFieldInfo(boolean mutable,
                                  String columnLabel,
                                  Integer fieldId,
                                  String format,
                                  String help,
                                  String initial,
                                  int decimals,
                                  String javaName,
                                  String label,
                                  String legacyName,
                                  boolean mandatory,
                                  int extent,
                                  int order,
                                  int position,
                                  String original,
                                  String validateMessage,
                                  String validateExpression,
                                  Class<? extends BaseDataType> dataType,
                                  BaseDataType initialValue,
                                  boolean initialNull,
                                  boolean serializeHidden,
                                  String serializeName,
                                  String xmlDataType,
                                  String xmlNodeName,
                                  String xmlNodeType,
                                  String like,
                                  String codePage,
                                  boolean caseSensitive)
          {
             this.mutable = mutable;
             this.columnLabel = columnLabel == null ? null : columnLabel.intern();
             this.fieldId = fieldId;
             this.help = help.intern();
             this.initial = initial == null ? null : initial.intern();
             this.initialNull = initialNull;
             this.decimals = decimals;
             this.javaName = javaName.intern();
             this.label = label == null ? null : label.intern();
             this.legacyName = legacyName.intern();
             this.lcLegacyName = this.legacyName.toLowerCase().intern();
             this.mandatory = mandatory;
             this.extent = extent;
             this.order = order;
             this.position = position;
             this.original = original.intern();
             this.validateMessage = validateMessage;
             this.validateExpression = validateExpression;
             this.caseSensitive = caseSensitive;

             String defaultFormat;

             if (dataType != null)
             {
                BaseDataType defValue = BaseDataType.generateDefault(dataType);
                defaultFormat = defValue.defaultFormatString();
             }
             else
             {
                defaultFormat = "";
             }

             if (format != null && !format.isEmpty())
             {
                this.format = format.intern();
                this.definedFormat = this.format;
             }
             else
             {
                this.format = defaultFormat;
                this.definedFormat = null;
             }

             this.dataType = dataType;
             this.initialValue = initialValue;
             this.serializeOptions = new SerializeOptions(serializeHidden,
                                                          serializeName,
                                                          xmlDataType,
                                                          xmlNodeName,
                                                          xmlNodeType);
             this.like = like;
             this.codePage = codePage;
             this.help = help;
          }

          /**
           * Get the column label.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  Column label.
           */
          public String getColumnLabel(TempTable tt)
          {
             MutableFieldInfo info = getMutableInfo(tt, false);

             String colLabel = (info == null ? columnLabel : info.columnLabel);

             return colLabel != null ? colLabel : getLabel(tt);
          }

          /**
           * Set the column label.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           * @param   columnLabel
           *          The new column label.
           */
          public void setColumnLabel(String columnLabel, TempTable tt)
          {
             if (!mutable)
             {
                throw new IllegalStateException("Object not mutable");
             }

             MutableFieldInfo info = getMutableInfo(tt, true);

             if (info == null)
             {
                this.columnLabel = columnLabel;
             }
             else
             {
                info.columnLabel = columnLabel;
             }
          }

          /**
           * Get the format string.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  Format string.
           */
          public String getFormat(TempTable tt)
          {
             MutableFieldInfo info = getMutableInfo(tt, false);

             return info == null ? format : info.format;
          }

          /**
           * Get the defined format string.
           *
           * @return  Defined format string.
           */
          public String getDefinedFormat()
          {
             return definedFormat;
          }

          /**
           * Set the format string.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           * @param   format
           *          The new format string.
           */
          public void setFormat(String format, TempTable tt)
          {
             if (!mutable)
             {
                throw new IllegalStateException("Object not mutable");
             }

             MutableFieldInfo info = getMutableInfo(tt, true);

             if (info == null)
             {
                this.format = format;
             }
             else
             {
                info.format = format;
             }
          }

          /**
           * Get the help string.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  Help string.
           */
          public String getHelp(TempTable tt)
          {
             MutableFieldInfo info = getMutableInfo(tt, false);

             return info == null ? help : info.help;
          }

          /**
           * Set the help string.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           * @param   help
           *          The new help string.
           */
          public void setHelp(String help, TempTable tt)
          {
             if (!mutable)
             {
                throw new IllegalStateException("Object not mutable");
             }

             MutableFieldInfo info = getMutableInfo(tt, true);

             if (info == null)
             {
                this.help = help;
             }
             else
             {
                info.help = help;
             }
          }

          /**
           * Get the label.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  Label.
           */
          public String getLabel(TempTable tt)
          {
             MutableFieldInfo info = getMutableInfo(tt, false);
             String lbl = (info == null ? label : info.label);

             return lbl == null ? legacyName : lbl;
          }

          /**
           * Get the defined label.
           *
           * @return  Defined label.
           */
          public String getDefinedLabel()
          {
             return label;
          }

          /**
           * Set the label.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @param   label
           *          The new abel.
           */
          public void setLabel(String label, TempTable tt)
          {
             if (!mutable)
             {
                throw new IllegalStateException("Object not mutable");
             }

             MutableFieldInfo info = getMutableInfo(tt, true);

             if (info == null)
             {
                this.label = label;
             }
             else
             {
                info.label = label;
             }
          }

          /**
           * Get the literal-question value.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  Literal-question value.
           */
          public boolean isLiteralQuestion(TempTable tt)
          {
             MutableFieldInfo info = getMutableInfo(tt, false);

             return info == null ? literalQuestion : info.literalQuestion;
          }

          /**
           * Set the literal-question value.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           * @param   literalQuestion
           *          The new value of {@code LITERAL-QUESTION} attribute.
           */
          public void setLiteralQuestion(boolean literalQuestion, TempTable tt)
          {
             if (!mutable)
             {
                throw new IllegalStateException("Object not mutable");
             }

             MutableFieldInfo info = getMutableInfo(tt, true);

             if (info == null)
             {
                this.literalQuestion = literalQuestion;
             }
             else
             {
                info.literalQuestion = literalQuestion;
             }
          }

          /**
           * Get the validate message, if any.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  Validate message.
           */
          public String getValidateMessage(TempTable tt)
          {
             MutableFieldInfo info = getMutableInfo(tt, false);

             return info == null ? validateMessage : info.validateMessage;
          }

          /**
           * Set the validate message attribute to a new value.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           * @param   validateMessage
           *          The new validate message.
           */
          public void setValidateMessage(String validateMessage, TempTable tt)
          {
             if (!mutable)
             {
                throw new IllegalStateException("Object not mutable");
             }

             MutableFieldInfo info = getMutableInfo(tt, true);

             if (info == null)
             {
                this.validateMessage = validateMessage;
             }
             else
             {
                info.validateMessage = validateMessage;
             }
          }

          /**
           * Get the validate expression, if any.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  Validate expression.
           */
          public String getValidateExpression(TempTable tt)
          {
             MutableFieldInfo info = getMutableInfo(tt, false);

             return info == null ? validateExpression : info.validateExpression;
          }

          /**
           * Get the validate expression, if any.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           * @param   validateExpression
           *          The new validate expression.
           */
          public void setValidateExpression(String validateExpression, TempTable tt)
          {
             if (!mutable)
             {
                throw new IllegalStateException("Object not mutable");
             }

             MutableFieldInfo info = getMutableInfo(tt, true);

             if (info == null)
             {
                this.validateExpression = validateExpression;
             }
             else
             {
                info.validateExpression = validateExpression;
             }
          }

          /**
           * Get the field's ID, which also represents its natural order.
           *
           * @return  Field id.
           */
          public Integer getFieldId()
          {
             return fieldId;
          }

          /**
           * Get the schema's string representation of the field's initial value.
           *
           * @return  Initial value string.
           */
          public String getInitial()
          {
             return initial;
          }

          /**
           * Get the field's corresponding DMO property name.
           *
           * @return  DMO property name.
           */
          public String getJavaName()
          {
             return javaName;
          }

          /**
           * Get the legacy name.
           *
           * @return  Legacy name.
           */
          public String getLegacyName()
          {
             return legacyName;
          }

          /**
           * Get the legacy name.
           *
           * @return  lowercased legacy name.
           */
          public String getLcLegacyName()
          {
             return lcLegacyName;
          }

          /**
           * Indicate whether this field is mandatory (i.e., non-null).
           *
           * @return  {@code true} if mandatory; else {@code false}.
           */
          public boolean isMandatory()
          {
             return mandatory;
          }

          /**
           * Get the EXTENT attribute.
           *
           * @return  The value of the EXTENT attribute from the original schema.
           */
          public int getExtent()
          {
             return extent;
          }

          /**
           * Get the ORDER attribute.
           *
           * @return  The value of the ORDER attribute from the original schema.
           */
          public int getOrder()
          {
             return order;
          }

          /**
           * Get the POSITION attribute, if it is set, otherwise return the value of the ORDER
           * attribute.  This is a fallback since some schemata are not exported with the
           * POSITION attribute and temp-tables that are explicitly defined do not have the
           * POSITION. In both of these cases, the 4GL would use the ORDER as the substitute
           * value.
           *
           * @return  The value of the POSITION attribute from the original schema.
           */
          public int getPosition()
          {
             return position > 0 ? position : fieldId + 1;
          }

          /**
           * Get the field's original name (will differ from legacy name, if extent field was
           * denormalized).
           *
           * @return  Original legacy name.
           */
          public String getOriginal()
          {
             return original;
          }

          /**
           * Get the field's data-type as a Java class instance.
           *
           * @return   Data type of the field.
           */
          public Class<? extends BaseDataType> getDataType()
          {
             return dataType;
          }

          /**
           * Get the field's initial value as actual data.
           *
           * @return  Initial value.
           */
          public BaseDataType getInitialValue()
          {
             return initialValue;
          }

          /**
           * Get the value of the INITIAL-NULL pseudo-attribute.
           *
           * @return the value of the INITIAL-NULL pseudo-attribute.
           */
          public boolean isInitialNull()
          {
             return initialNull;
          }

          /**
           * Get all serialize options defined for the field (only meaningful for temp-table fields).
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  Serialize options.
           */
          public SerializeOptions getSerializeOptions(TempTable tt)
          {
             // this is assumed as setter because the returned value is mutable
             MutableFieldInfo info = getMutableInfo(tt, true);

             return info == null ? serializeOptions : info.serializeOptions;
          }

          /**
           * Get fully qualified name of the source field from which the field definitions were copied
           * using LIKE option.
           *
           * @return see above.
           */
          public String getLike()
          {
             return like;
          }

          /**
           * Get code page of the CLOB field.
           *
           * @return code page of the CLOB field.
           */
          public String getCodePage()
          {
             return codePage;
          }

          /**
           * Checks whether this is a case-sensitive character field.
           *
           * @return  {@code true} only if this is a case-sensitive character field.
           */
          public boolean isCaseSensitive()
          {
             return caseSensitive;
          }

          /**
           * Obtain the precision for decimal fields.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           *
           * @return  the precision for decimal fields.
           */
          public int getDecimals(TempTable tt)
          {
             MutableFieldInfo info = getMutableInfo(tt, false);

             return info == null ? decimals : info.decimals;
          }

          /**
           * Sets the precision for decimal fields. Only for mutable objects.
           *
           * @param   tt
           *          The {@link TempTable} resource.
           * @param   decimals
           *          The new precision for decimal fields.
           *
           * @throws  IllegalStateException
           *          if the object is not declared as mutable.
           */
          public void setDecimals(int decimals, TempTable tt)
          {
             if (!mutable)
             {
                throw new IllegalStateException("Object not mutable");
             }

             MutableFieldInfo info = getMutableInfo(tt, true);

             if (info == null)
             {
                this.decimals = decimals;
             }
             else
             {
                info.decimals = decimals;
             }
          }

          /**
           * Compare this field info object to another, based on its natural order. Natural order is
           * determined by field IDs.
           *
           * @return  &gt; 1 if this field sorts ahead of the given field;
           *          &lt; 1 if this field sorts behind the given field;
           *          0 if they sort equivalently.
           */
          @Override
          public int compareTo(LegacyFieldInfo o)
          {
             return this.fieldId - o.fieldId;
          }

          /**
           * Obtain a short sreing representation of the object used in debugging.
           *
           * @return  a short sreing representation of the object used in debugging.
           */
          @Override
          public String toString()
          {
             StringBuilder sb = new StringBuilder("LFI{");
             sb.append(legacyName).append("/").append(javaName);
             sb.append("(").append(fieldId).append(")").append(":").append(dataType.getSimpleName());
             if (extent != 0)
             {
                sb.append("[").append(extent).append("]");
             }
             sb.append('}');
             return sb.toString();
          }

          /**
           * Get the {@link MutableFieldInfo} associated with the field in the given {@link TempTable} instance.
           *
           *
           * @param   tt
           *          The {@link TempTable} resource.
           * @param   setter
           *          Flag indicating if this call is associated with a setter, which will change the value.  In
           *          this case, if not already built, the {@link MutableFieldInfo} will be constructed.
           *          Otherwise, the default value from {@link LegacyTableInfo#p2j field} will be returned.
           *
           * @return  The instance from {@link AbstractTempTable#fieldInfo}, for this temp-table.
           */
          private MutableFieldInfo getMutableInfo(TempTable tt, boolean setter)
          {
             if (tt == null)
             {
                return null;
             }

             BiFunction<TempTable, String, MutableFieldInfo> mutate = !setter ? null : (tempTable, fieldName) ->
             {
                Class<? extends Record> implClass = tempTable.getDmoMeta().getImplementationClass();
                LegacyTableInfo lti = TempTableMapper.TT_CACHE.get(implClass);
                LegacyFieldInfo field = lti.p2j.get(fieldName);
                return new MutableFieldInfo(field);
             };

             return ((AbstractTempTable) tt).getMutableInfo(lcLegacyName, setter, mutate);
          }
       }
    }