# TempTableSchema.java

    /*
    ** Module   : TempTableSchema.java
    ** Abstract : Temp-table schema used for serialization, based on internal structure of the table.
    **
    ** Copyright (c) 2017-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20171020 Created initial version.
    ** 002 CA  20180511 Added XML-NODE-NAME table option support.
    ** 003 ECF 20190120 Refactored out XML-specific features into separate XmlTempTableSchema class.
    ** 004 OM  20190711 Added support for BEFORE-TABLEs and default field values.
    ** 005 OM  20190823 Added help and format field option.
    **     CA  20190826 Column lookup must be case-insensitive (as 4GL is case-insensitive).
    ** 006 ECF 20200906 New ORM implementation.
    ** 007 OM  20201029 Added case sensitive column support. Added detection for changed fields.
    **     OM  20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to local
    **                  dmoMeta.legacyTable.
    **     OM  20210309 Do not use DmoMeta as key in TableMapper because temp-tables may share the same
    **                  DmoMeta instance.
    **     OM  20210329 Added Column API to allow extraction of legacy-name, xml-node-name and serialize-name
    **                  independently from the precomputed name.
    **     CA  20210510 Fixed JSON and XML serialization when XML-NODE-NAME/SERIALIZE-NAME field options are set.
    **     OM  20211102 WRITE-XML was not using XML-NODE-NAME for the temp-table.
    **     OM  20220120 Added getters for LABEL and COLUMN-LABEL attributes.
    **     OM  20220212 Use ReservedProperty static method for testing reserved properties names.
    **     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
    **     CA  20220520 Added support for TEXT and ATTRIBUTE XML-NODE-TYPE values.
    **     OM  20220525 Fixed case-sensitivity for column lookup.
    **     IAS 20221006 Added ' getCodePage()', 'getDefinedFormat()', and 'getDefinedLabel()' .
    **     IAS 20221111 Added ' buffer', 'xmlNodeName', 'nsURI', and 'nsPrefix' fields. This is
    **                  required for tables in the the DATASET parameters' support since they are
    **                  processed differently from the standalone tables.
    ** 008 IAS 20230415 Fixed support for SERIALIZE-HIDDEN.
    ** 009 IAS 20230501 Fixed support for the INITIAL-NULL pseudo-attribute.
    ** 010 CA  20240320 Avoid BDTs within internal FWD runtime.  Replaced iterators with Java 'for', where it
    **                  applies.
    */

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

    import java.lang.reflect.*;
    import java.util.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;

    /**
     * The schema used for serialization of a temp-table, based on the internal definition of the
     * table itself. Names used for serialization are legacy names as defined by the original 4GL
     * code, not derived by conversion. Only legacy fields not marked SERIALIZE-HIDDEN are included
     * in the schema.
     */
    public class TempTableSchema
    {
       /* RecordBuffer on the temp-table */
       private final RecordBuffer buffer;
       /** Legacy name of the temp-table */
       private final String name;

       /** XML serialization name of the temp-table */
       private final String xmlName;
       /** XML node name of the temp-table */
       private final String xmlNodeName;

       /** XML namespace URI of the temp-table */
       private final String nsURI;

       /** XML prefix of the temp-table */
       private final String nsPrefix;

       /** Schema information needed for serialization of all columns/fields, in the DMO order. */
       private final ArrayList<Column> columnList = new ArrayList<>();

       /** The columns, mapped for quick case-insensitive lookup. */
       private final LinkedHashMap<String, Column> columnMap = new LinkedHashMap<>();

       /** The indexed of the DMO to be serialized. */
       private final List<TableMapper.LegacyIndexInfo> indexes = new ArrayList<>();

       /** List of column schema objects whose data is to be serialized as XML attributes */
       private final List<Column> attributeColumns;

       /** List of column schema objects whose data is to be serialized as XML elements */
       private final List<Column> elementColumns;

       /** List of column schema objects whose data is to be serialized as XML text */
       private final List<Column> textColumns;

       /** Flags the serialized {@code BEFORE-BUFFERS}. */
       private final boolean beforeImage;

       /** Flags the no-undoable temp-tables {@code NO-UNDO} option. */
       private final boolean noUndo;

       /**
        * Constructor which gathers table and field data from a temp-table buffer.
        *
        * @param   recBuf
        *          RecordBuffer on the temp-table.
        * @param   json
        *          Flag indicating if this is for JSON (when <code>true</code>) or XML (when <code>false</code>).
        */
       public TempTableSchema(RecordBuffer recBuf, boolean json)
       {
          BufferImpl buf = (BufferImpl) recBuf.getDMOProxy();

          // the table name is used for serialization, regardless of the individual buffer name
          this.buffer = recBuf;
          this.name = TableMapper.getLegacyName(recBuf);
          this.xmlName = buf.getSerializeName().toJavaType();

          Class<? extends DataModelObject> dmoIface = recBuf.getDMOInterface();
          Map<String, Method> getterMap = PropertyHelper.allGettersByProperty(dmoIface);
          Map<String, Method> setterMap = PropertyHelper.allSettersByProperty(dmoIface);

          TempTable tt = recBuf.getParentTable();
          this.xmlNodeName = tt.getXmlNodeName().toJavaType();
          this.nsURI = tt.namespaceURI().isUnknown() ? "" : tt.namespaceURI().toJavaType();
          this.nsPrefix = tt.namespacePrefix().toJavaType();
          List<TableMapper.LegacyFieldInfo> fields = TableMapper.getAllLegacyFieldInfo(tt);
          Iterator<TableMapper.LegacyFieldInfo> iter = fields.iterator();
          this.noUndo = !tt.canUndo().toJavaType();

          // indexes:
          int k = 0;
          while (true)
          {
             TableMapper.LegacyIndexInfo legacyIndexInfo = TableMapper.getLegacyIndexInfo(tt, k);
             if (legacyIndexInfo != null)
             {
                indexes.add(legacyIndexInfo);
                ++k;
             }
             else
             {
                break;
             }
          }

          DmoMeta meta = recBuf.getDmoInfo();

          // Construct column schema data object; TableMapper.getAllLegacyFieldInfo returns field
          // info objects in the correct order for serialization
          Column lastCol = null;
          for (int i = 0; iter.hasNext(); i++)
          {
             TableMapper.LegacyFieldInfo info = iter.next();

             // add column to schema if it should be serialized
    //         if (info.getSerializeOptions(tt).isSerializeHidden())
    //         {
    //            continue;
    //         }
             int offset = meta.byLegacyName(info.getLcLegacyName()).getMeta().getOffset();
             Column col = new Column(i + 1, getterMap, setterMap, info, offset, recBuf);
             String cname = json ? col.getSerializeName() : col.getXmlNodeName();

             if (lastCol == null || !lastCol.legacyName.equalsIgnoreCase(col.legacyName))
             {
                // exclude extent column duplicates
                columnList.add(col);

                cname = cname.toLowerCase();
                if (!columnMap.containsKey(cname))
                {
                   columnMap.put(cname, col);
                }
             }
             lastCol = col;
          }

          this.beforeImage = buf.isBeforeBuffer();
          this.attributeColumns = filterColumns(columnList, SerializeOptions.XmlNodeType.ATTRIBUTE);
          this.elementColumns = filterColumns(columnList, SerializeOptions.XmlNodeType.ELEMENT);
          this.textColumns = filterColumns(columnList, SerializeOptions.XmlNodeType.TEXT);
       }

       /**
        * Filter the given list of column schema objects by the given XML node type.
        *
        * @param   columns
        *          Collection of column schema objects to be filtered.
        * @param   nodeType
        *          Node type which forms the filter criterion.
        *
        * @return  A list of column schema objects whose node type matches the given node type.
        *          The list may be empty, but will not be {@code null}.
        */
       private static List<Column> filterColumns(Collection<Column> columns,
                                                 SerializeOptions.XmlNodeType nodeType)
       {
          List<Column> list = null;

          for (Column column : columns)
          {
             if (!column.isSerializeHidden() && column.getNodeType() == nodeType)
             {
                if (list == null)
                {
                   list = new ArrayList<>();
                }
                list.add(column);
             }
          }

          return list == null ? Collections.emptyList() : list;
       }

       /**
        * Get the RecordBuffer on the temp-table.
        *
        * @return RecordBuffer on the temp-table.
        */
       public RecordBuffer buffer()
       {
          return buffer;
       }

       /**
        * Get the XML node name of the temp-table.
        *
        * @return XML node name of the temp-table.
        */
       public String xmlNodeName()
       {
          return xmlNodeName;
       }

       /**
        * Get the XML namespace URI of the temp-table.
        *
        * @return XML namespace URI of the temp-table.
        */
       public String namespaceURI()
       {
          return nsURI;
       }

       /**
        * Get the XML prefix of the temp-table.
        *
        * @return  XML prefix of the temp-table.
        */
       public String namespacePrefix()
       {
          return nsPrefix;
       }

       /**
        * Get the legacy name of this temp-table.
        *
        * @return  Legacy table name.
        */
       public String getName()
       {
          return name;
       }

       /**
        * Return the name for this table.
        *
        * @return  See above.
        */
       public String getTableName()
       {
          return name;
       }

       /**
        * Return the XML serialization name for this table.
        *
        * @return  See above.
        */
       public String getXmlName()
       {
          return xmlName;
       }

       /**
        * Checks whether the temp-table was defined as NO-UNDO.
        *
        * @return  {@code true} if this is a NO-UNDO temp-table.
        */
       public boolean isNoUndo()
       {
          return noUndo;
       }

       /**
        * Get the column schema information associated with the given serialization name. Note: the lookup is
        * performed in case-insensitive mode.
        *
        * @param   name
        *          Name used in serialization output for column data.
        *
        * @return  Column schema information.
        */
       public Column getColumn(String name)
       {
          return columnMap.get(name.toLowerCase());
       }

       /**
        * Obtain an iterator of the indexes of the DMO of this object.
        *
        * @return  an iterator for the indexes of the DMO.
        */
       public Iterator<TableMapper.LegacyIndexInfo> getIndexes()
       {
          return indexes.iterator();
       }

       /**
        * Get an object to iterate over column schema information objects.
        *
        * @return  An iterable object on column schema info objects.
        */
       public ArrayList<Column> columns()
       {
          return columnList;
       }

       /**
        * Get an iterable collection of column schema objects whose data is to be serialized as XML
        * attributes.
        *
        * @return  see above.
        */
       public Iterable<Column> attributeColumns()
       {
          return attributeColumns;
       }

       /**
        * Get an iterable collection of column schema objects whose data is to be serialized as XML
        * elements.
        *
        * @return  see above.
        */
       public Iterable<Column> elementColumns()
       {
          return elementColumns;
       }

       /**
        * Get an iterable collection of column schema objects whose data is to be serialized as XML text.
        *
        * @return  see above.
        */
       public Iterable<Column> textColumns()
       {
          return textColumns;
       }

       /**
        * Check whether the target is a before-table.
        *
        * @return  {@code true} if the target is a before-table.
        */
       public boolean isBeforeImage()
       {
          return beforeImage;
       }

       /**
        * Schema information needed to serialize/deserialize a field/column.
        */
       static class Column
       {
          /** The offset in the record's data array. */
          public final int offset;

          /** Legacy name of field. */
          private final String legacyName;

          /** Extent of an array field (null for a scalar field) */
          private final Integer extent;

          /** Order in which column is serialized */
          private final int order;

          /** Name of element or attribute in serialized form (may differ from legacy field name) */
          private final String precomputedName;

          /** The field's XML-NODE-NAME option value. */
          private final String xmlNodeName;

          /** The field's SERIALIZE-NAME option value. */
          private final String serializeName;

          /** The field's SERIALIZE-HIDDEN option value. */
          private final boolean serializeHidden;

          /** Method to get field data from buffer */
          private final Method getter;

          /** Method to set field data into buffer */
          private final Method setter;

          /** Data wrapper type of field */
          private final Class<? extends BaseDataType> type;

          /** The initial value as an instance of the appropriate type of the INIT option for this column. */
          private final BaseDataType initialValue;

          /** Field info object to which we delegate all getters. */
          private final TableMapper.LegacyFieldInfo lfi;

          /** The {@link TempTable} instance. */
          private TempTable tt;

          /**
           * Constructor.
           *
           * @param   order
           *          Order of column in serialization output.
           * @param   getterMap
           *          Map of java property names to getter methods.
           * @param   setterMap
           *          Map of java property names to setter methods.
           * @param   lfi
           *          Field info object.
           * @param   offset
           *          The offset in the record's data array.
           * @param   buf
           *          The buffer instance.
           */
          Column(int order,
                 Map<String, Method> getterMap,
                 Map<String, Method> setterMap,
                 TableMapper.LegacyFieldInfo lfi,
                 int          offset,
                 RecordBuffer buf)
          {
             Class<? extends DataModelObject> dmoInterface = buf.getDMOInterface();
             this.tt = buf.getParentTable();

             this.offset = offset;

             this.lfi = lfi;
             SerializeOptions opts = lfi.getSerializeOptions(tt);

             String serName = opts.getSerializeName();
             if ("".equals(serName))
             {
                serName = null;
             }
             serializeName = serName;
             serializeHidden = opts.isSerializeHidden();

             String nodeName = opts.getXmlNodeName();
             if ("".equals(nodeName))
             {
                nodeName = null;
             }
             xmlNodeName = nodeName;

             this.legacyName = lfi.getLegacyName();
             this.order = order;

             // pre-compute the name used in serialized output is the legacy name of the field, unless overridden
             // by the SERIALIZE-NAME attribute, unless overridden by the XML-NODE-NAME attribute
             this.precomputedName = (nodeName != null) ? nodeName : (serName != null ? serName : legacyName);

             String javaName = lfi.getJavaName();
             this.extent = lfi.getExtent();
             if (extent == 0 || lfi.getOriginal().isEmpty())
             {
                this.getter = getterMap.get(javaName);
                this.setter = setterMap.get(javaName);
             }
             else
             {
                String originalName = lfi.getOriginal();
                originalName = Character.toUpperCase(originalName.charAt(0)) + originalName.substring(1);
                Method[] allMethods = dmoInterface.getDeclaredMethods();
                Method setterMeth = null;
                Method getterMeth = null;
                for (Method method : allMethods)
                {
                   if (method.getName().equals("set" + originalName) &&
                       method.getReturnType() == Void.TYPE           &&
                       method.getParameterCount() == 2               &&
                       method.getParameterTypes()[0] == Integer.TYPE)
                   {
                      setterMeth = method;
                   }
                   else if (method.getParameterCount() == 1               &&
                            method.getParameterTypes()[0] == Integer.TYPE &&
                            (method.getName().equals("get" + originalName) ||
                             method.getName().equals("is" + originalName)))
                   {
                      getterMeth = method;
                   }
                   if (setterMeth != null && getterMeth != null)
                   {
                      break;
                   }
                }
                this.setter = setterMeth;
                this.getter = getterMeth;
             }

             this.type = (Class<? extends BaseDataType>) getter.getReturnType();
             this.initialValue = lfi.getInitialValue();
          }

          /**
           * Constructor used for BEFORE TEMP-TABLE columns. It allows to set the internal values directly.
           *
           * @param   legacyName
           *          The fields's name (legacy name).
           * @param   name
           *          Name of element or attribute in serialized form (may differ from legacy field name).
           * @param   initialValue
           *          The initial value as an instance of the appropriate type of the INIT option for this column.
           */
          public Column(String legacyName, String name, BaseDataType initialValue)
          {
             this.offset = -1; // not used for reserved properties
             this.lfi = null;
             this.legacyName = legacyName;
             this.extent = 0;
             this.order = 0;
             this.precomputedName = name;
             this.xmlNodeName = null;
             this.serializeName = null;
             this.serializeHidden = false;
             this.getter = null;
             this.setter = null;
             this.type = initialValue.getClass();
             this.initialValue = initialValue;
          }

          /**
           * Get the column's legacy field name.
           *
           * @return  Legacy field name.
           */
          public String getFieldName()
          {
             return legacyName;
          }

          /**
           * Get the column's XML-NODE-NAME name, used for XML (de)serialization.
           *
           * @return    The field name used for (de)serialization.
           */
          public String getXmlNodeName()
          {
             if (lfi == null)
             {
                // BEFORE-TABLE special fields
                return precomputedName;
             }

             // defaulting to SERIALIZE-NAME if not set, and legacy NAME if this is not set, either
             return xmlNodeName == null ? serializeName == null ? legacyName : serializeName : xmlNodeName;
          }

          /**
           * Get the column's legacy SERIALIZE-NAME, used for JSON (de)serialization.
           *
           * @return    The field name used for (de)serialization.
           */
          public String getSerializeName()
          {
             if (lfi == null)
             {
                // BEFORE-TABLE special fields
                return precomputedName;
             }

             return serializeName == null ? legacyName : serializeName; // defaulting to legacy NAME if not set
          }

          /**
           * Get the column's legacy SERIALIZE-HIDDEN attribute value
           *
           * @return    The column's legacy SERIALIZE-HIDDEN attribute value.
           */
          public boolean isSerializeHidden()
          {
             return serializeHidden;
          }

          /**
           * Indicate whether column can be null.
           *
           * @return  {@code true} if nillable, else {@code false}.
           */
          public boolean isNillable()
          {
             return lfi == null || !lfi.mandatory;
          }

          /**
           * Get extent of column, if any.
           *
           * @return  Extent value for an array column, {@code null} for a scalar column.
           */
          public Integer getExtent()
          {
             return extent;
          }

          /**
           * Get the order in which this column is serialized.
           *
           * @return  Order.
           */
          public int getOrder()
          {
             return order;
          }

          /**
           * Get the XML node type (element or attribute) for this column.
           *
           * @return  Node type.
           */
          public SerializeOptions.XmlNodeType getNodeType()
          {
             return lfi == null
                   ? SerializeOptions.XmlNodeType.ELEMENT
                   : lfi.getSerializeOptions(tt).getXmlNodeType();
          }

          /**
           * Get field codepage.
           *
           * @return  Field codepage.
           */
          public String getCodePage()
          {
             return lfi == null ? null : lfi.getCodePage();
          }

          /**
           * Get the DMO getter method for this column's value.
           *
           * @return  Getter method.
           */
          public Method getGetter()
          {
             return getter;
          }

          /**
           * Get the DMO setter method for this column's value.
           *
           * @return  Setter method.
           */
          public Method getSetter()
          {
             return setter;
          }

          /**
           * Get the data type of the field.
           *
           * @return  Data type.
           */
          public Class<? extends BaseDataType> getType()
          {
             return type;
          }

          /**
           * Get the XML data type of the field.
           *
           * @return  XML data type.
           */
          public String getXmlDataType()
          {
             return lfi == null ? null : lfi.getSerializeOptions(tt).getXmlDataType();
          }

          /**
           * Obtain the initial value for this column (field).
           *
           * @return  the initial value for this column if any is declared.
           */
          public String getInitial()
          {
             return lfi == null ? null : lfi.getInitial();
          }

          /**
           * Get the value of the INITIAL-NULL pseudo-attribute.
           *
           * @return the value of the INITIAL-NULL pseudo-attribute.
           */
          public boolean isInitialNull()
          {
             return lfi == null ? false : lfi.isInitialNull();
          }

          /**
           * Obtain the format for this column if one was set.
           *
           * @return  the format for this column.
           */
          public String getFormat()
          {
             return lfi == null ? null : lfi.getFormat(tt);
          }

          /**
           * Obtain the defined format for this column if one was set.
           *
           * @return  the defined format for this column.
           */
          public String getDefinedFormat()
          {
             return lfi == null ? null : lfi.getDefinedFormat();
          }

          /**
           * Obtain the help message for this column if one was set.
           *
           * @return  the help for this column.
           */
          public String getHelp()
          {
             return lfi == null ? null : lfi.getHelp(tt);
          }

          /**
           * Test whether this is a BEFORE TEMP-TABLE hidden field. The hidden fields start with
           * double underscore ({@code __}).
           *
           * @return  {@code true} if this is a BEFORE temp-table hidden field.
           */
          public boolean isHidden()
          {
             return ReservedProperty.isReservedProperty(legacyName);
          }

          /**
           * 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 lfi != null && lfi.isCaseSensitive();
          }

          /**
           * Obtain the precision for decimal fields.
           *
           * @return  the precision for decimal fields.
           */
          public int getDecimals()
          {
             return lfi == null ? 0 : lfi.getDecimals(tt);
          }

          /**
           * Get the {@code COLUMN-LABEL} attribute of this column.
           *
           * @return  The {@code COLUMN-LABEL} attribute of this column.
           */
          public String getColumnLabel()
          {
             return lfi == null ? null : lfi.getColumnLabel(tt);
          }

          /**
           * Get the {@code LABEL} attribute of this column.
           *
           * @return  The {@code LABEL} attribute of this column.
           */
          public String getLabel()
          {
             return lfi == null ? null : lfi.getLabel(tt);
          }

          /**
           * Get the defined {@code LABEL} attribute of this column.
           *
           * @return  The defined {@code LABEL} attribute of this column.
           */
          public String getDefinedLabel()
          {
             return lfi == null ? null : lfi.getDefinedLabel();
          }

          /**
           * Test whether a value is different than the initial value for this column.
           *
           * @param   datum
           *          The value to be tested.
           *
           * @return  {@code true} if the value is different.
           */
          public boolean isChanged(BaseDataType datum)
          {
             boolean initUnknown = initialValue == null || initialValue.isUnknown();
             if (datum.isUnknown() && initUnknown)
             {
                return false; // both unknowns
             }

             if (datum.isUnknown() != initUnknown)
             {
                return true; // only one of them is unknown
             }

             return !datum.equals(initialValue);
          }
       }
    }