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