# FWDDataObject.java

    /*
    ** Module   : FWDDataObject.java
    ** Abstract : A proxy to be able to track changes in a DataObject instance.
    **
    ** Copyright (c) 2019-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 CA  20191119 Created initial version.
    ** 002 OM  20201231 Added support for REJECTED attribute.
    ** 003 CA  20210910 Changes to allow support for before-table returned from the remote request via an
    **                  OUTPUT/INPUT-OUTPUT table or dataset.
    **     CA  20210917 Javadoc fixes.
    **     OM  20211020 Added _DATASOURCE_ROWID property.
    **     CA  20220104 Added support for denormalized extent fields, for .NET client compatibility.
    **     CA  20220215 Fixed denormalized extent when the field is referenced by its integer index.
    **     CA  20220216 Fixed issues with denormalized extent field names: they can collide with other legacy
    **                  field names so the changes ensure they are unique.
    **     CA  20220428 Log only the first set state, when tracking changes.
    **     CA  20220604 Replaced the FWD proxy with the Java proxy, to allow easy usage from Tomcat.
    **     CA  20221116 Added 'unknownValuesForFields', a flag which when set, will force the DataObject.get to
    **                  return null even for Java native types - without this flag, DataObject.get will return
    **                  i.e. 0 or false for int/boolean native types.
    ** 004 CA  20230221 Encapsulated the field and relation metadata in their own classes, to avoid the DataObject
    **                  overhead and increase performance.  Other performance optimizations.
    ** 005 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
    **                  default methods cause the Java metaspace to spike one order of magnitude.
    ** 006 AS  20250217 Resolve a normalized extent field case insensitively.
    ** 007 AS  20250227 Use the same property resolving logic for both normalized and denormalized extents.
    ** 008 AP  20250411 Refactored invoke method to better handle both normalized and denormalized extents.
    **                  For denormalized extents added indexed access to each element
    **     CA  20250424 'forcedNormalized' is used by FWD runtime, so normalized access needs to be allowed even
    **                  when using denormalized extents.
    **     CA  20250424 .NET compatibiltiy still uses normalized extent fields when transferring them.
    */

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

    import java.lang.reflect.*;
    import java.util.*;

    import org.apache.tuscany.sdo.util.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.orm.BaseRecord;

    import commonj.sdo.*;

    /**
     * Proxy wrapper for a {@link DataObject} instance.  This is required to be able to track
     * before-table changes for a SDO row.
     */
    public class FWDDataObject
    implements FWDDataObjectType,
               InvocationHandler
    {
       /** The wrapped row. */
       private DataObject row;

       /** The id, as used by {@link BaseRecord#primaryKey()}. */
       private Long id;

       /** The origin-rowid, as used by {@link TempRecord#_originRowid()}. */
       private Long originRowid;

       /** The data-source-rowid, as used by {@link TempRecord#_datasourceRowid()}. */
       private Long dataSourceRowid;

       /** The peer-rowid, as used by {@link TempRecord#_peerRowid()}. */
       private Long peerRowid;

       /** The row-state, as used by {@link TempRecord#_rowState()}. */
       private Integer rowState;

       /** The error-string, as used by {@link TempRecord#_errorString()}. */
       private String errorString;

       /** The error-flag, as used by {@link TempRecord#_errorFlags()}. */
       private Integer errorFlag;

       /** Flag indicating if changes are being tracked. */
       private boolean logging = false;

       /** The source table metadata used to create this {@link #row}. */
       private DataObject tableMetaData;

       /** Flag indicating if extents are normalized. */
       private boolean normalizedExtent;

       /**
        * When <code>true</code>, this will force the resolution of normalized field names, as we are within
        * the FWD runtime, to re-create a row incoming from the remote side (and this always uses normalized
        * fields).
        * <p>
        * This flag is used only when {@link #normalizedExtent} flag is <code>false</code> (we are in denormalized
        * extent fields mode), so the FWD runtime can access the extent field directly by their legacy name,
        * and not in the denormalized mode.
        */
       private boolean forceNormalizedFields = false;

       /** A bit-set with the properties on which a 'set' method has been called. */
       private BitSet setProps = null;

       /** Mapping of the properties to their index. */
       private Map<String, Integer> propToIndex = null;

       /**
        * Proxy handle which dispatches method calls to the wrapped {@link #row}.
        * <p>
        * Certain APIs are intercepted to allow tracking changes.
        */
       @Override
       public Object invoke(Object proxy, Method method, Object[] args)
       throws Throwable
       {
          if (method.getDeclaringClass().isAssignableFrom(FWDDataObjectType.class))
          {
             return method.invoke(this, args);
          }

          String mname = method.getName();
          switch (mname)
          {
             case "delete":
                if (isLogging())
                {
                   DataSetSDOHelper.logDeleted((FWDDataObjectType) proxy);
                }
                DataObjectUtil.delete((DataObject) proxy);
                return null;
             case "equals":
                return this.equals(args[0]);
             case "hashCode":
                return this.hashCode();
          }

          if (normalizedExtent && DataSetSDOHelper.isUnknownValuesForFields())
          {
             // this is currently done only for normalized extent
             if (setProps == null)
             {
                List props = row.getInstanceProperties();
                setProps = new BitSet(props.size());
                propToIndex = new HashMap<>();

                for (int i = 0; i < props.size(); i++)
                {
                   Property prop = (Property) props.get(i);
                   propToIndex.put(prop.getName(), i);
                }
             }

             Integer idx = null;
             if (mname.equals("get") || mname.equals("isSet") || mname.equals("unset") || mname.startsWith("set"))
             {
                idx = getIndex(args[0]);
             }

             if (idx != null)
             {
                switch (mname)
                {
                   case "get":
                      if (!row.isSet(idx))
                      {
                         return null;
                      }

                      break;

                   case "isSet":
                      return setProps.get(idx);

                   case "unset":
                      setProps.clear(idx);
                      break;

                   default:
                      // a setter method
                      setProps.set(idx);
                      break;
                }
             }
          }

          if (isLogging() && mname.startsWith("set") && rowState == null)
          {
             DataSetSDOHelper.logChanged((FWDDataObjectType) proxy);
          }

          // denormalized extent case:
          // mechanism to get the exact index in row if the input is String, then we fallback to the default
          // implementation

          // normalized extent case:
          // mechanism to get the field name ignoring case sensitivity, then we fallback to the default
          // implementation

          // the FWD runtime will require to access the extent fields by their legacy name, even when we are in
          // denormalized mode.  the 'forceNormalizedFields' flag is used to indicate this 'access from
          // FWD runtime mode', so the extent fields can be accessed directly by their name.

          if (!(args != null     &&
                args.length >= 1 &&
                (mname.startsWith("set") ||
                 mname.equals("unset")   ||
                 mname.startsWith("get") ||
                 mname.equals("isSet"))))
          {
             return method.invoke(row, args);
          }

          if (!normalizedExtent)
          {
             LegacyFieldMetaData fieldMeta;

             if (forceNormalizedFields   ||
                 mname.equals("setList") ||
                 mname.equals("getList") ||
                 (mname.equals("set") && args[1] instanceof List))
             {
                if (args[0] instanceof Property)
                {
                   // direct access, 'they know what they are doing'
                   return method.invoke(row, args);
                }

                // forbid access to entire extent if denormalized access
                if (!forceNormalizedFields)
                {
                   throw new IllegalArgumentException("Access to the entire denormalized extent is not allowed");
                }

                if (args[0] instanceof String &&
                    ((String) args[0]).startsWith(DataSetSDOHelper.DENORMALIZED_PREFIX))
                {
                   throw new IllegalArgumentException(
                      "Access is configured for normalized, but denormalized field provided: " + args[0]);
                }

                // find the start position of this field
                Object arg0 = args[0];
                List<LegacyFieldMetaData> fields = DataSetSDOHelper.getFieldMetaData(tableMetaData, false);
                LegacyFieldMetaData field = null;
                for (int i = 0; i < fields.size(); i++)
                {
                   LegacyFieldMetaData f = fields.get(i);
                   String fname = f.extentField == null ? f.name : f.extentField;
                   if (arg0 instanceof String && ((String) arg0).equalsIgnoreCase(fname))
                   {
                      field = f;
                      break;
                   }
                   // f.idx is 1-based, but DataObject receives 0-based
                   else if (arg0 instanceof Integer && ((Integer) arg0).equals(f.idx - 1))
                   {
                      field = f;
                      break;
                   }
                }

                if (field == null)
                {
                   throw new IllegalArgumentException("Could not find field for " + args[0] + " in " +
                                                      DataSetSDOHelper.getTableName(tableMetaData));
                }
                int fieldPos = fields.indexOf(field);

                if (mname.startsWith("set"))
                {
                   if (field.extentField != null)
                   {
                      // second argument is a list
                      List l = (List) args[1];
                      for (int i = 0; i < l.size(); i++)
                      {
                         arg0 = args[0] instanceof String
                                   ? DataSetSDOHelper.buildDenormalizedFieldName(field.extentField, i + 1)
                                   : fields.indexOf(field) + i;
                         if (arg0 instanceof String)
                         {
                            row.set((String) arg0, l.get(i));
                         }
                         else
                         {
                            row.set((Integer) arg0, l.get(i));
                         }
                      }
                   }
                   else
                   {
                      // normal field access
                      arg0 = args[0] instanceof String ? field.name : fieldPos;
                      return method.invoke(row, arg0, args[1]);
                   }
                   return null;
                }
                else if (mname.startsWith("get"))
                {
                   if (field.extentField != null)
                   {
                      // return a list with the extent field content
                      List res = new ArrayList<>();
                      for (int i = fieldPos, idx = 1; i < fields.size(); i++, idx++)
                      {
                         LegacyFieldMetaData fnew = fields.get(i);
                         if (!field.extentField.equalsIgnoreCase(fnew.extentField))
                         {
                            break;
                         }
                         arg0 = args[0] instanceof String
                                   ? DataSetSDOHelper.buildDenormalizedFieldName(field.extentField, idx)
                                   : i;
                         Object val = arg0 instanceof String ? row.get((String) arg0) : row.get((Integer) arg0);
                         res.add(val);
                      }
                      return res;
                   }
                   else
                   {
                      // normal field access
                      arg0 = args[0] instanceof String ? field.name : fieldPos;
                      return method.invoke(row, arg0);
                   }
                }
                else if (mname.startsWith("isSet"))
                {
                   if (field.extentField != null)
                   {
                      for (int i = fieldPos, idx = 1; i < fields.size(); i++, idx++)
                      {
                         LegacyFieldMetaData fnew = fields.get(i);
                         if (!field.extentField.equalsIgnoreCase(fnew.extentField))
                         {
                            break;
                         }
                         arg0 = args[0] instanceof String
                                   ? DataSetSDOHelper.buildDenormalizedFieldName(field.extentField, idx)
                                   : i;
                         if (arg0 instanceof String)
                         {
                            return row.isSet((String) arg0);
                         }
                         else
                         {
                            return row.isSet((Integer) arg0);
                         }
                      }
                      return false;
                   }
                   else
                   {
                      // normal field access
                      arg0 = args[0] instanceof String ? field.name : fieldPos;
                      return method.invoke(row, arg0);
                   }
                }
                else if (mname.startsWith("unset"))
                {
                   if (field.extentField != null)
                   {
                      for (int i = fieldPos, idx = 1; i < fields.size(); i++, idx++)
                      {
                         LegacyFieldMetaData fnew = fields.get(i);
                         if (!field.extentField.equalsIgnoreCase(fnew.extentField))
                         {
                            break;
                         }
                         arg0 = args[0] instanceof String
                                   ? DataSetSDOHelper.buildDenormalizedFieldName(field.extentField, idx)
                                   : i;
                         if (arg0 instanceof String)
                         {
                            row.unset((String) arg0);
                         }
                         else
                         {
                            row.unset((Integer) arg0);
                         }
                      }
                      return null;
                   }
                   else
                   {
                      // normal field access
                      arg0 = args[0] instanceof String ? field.name : fieldPos;
                      return method.invoke(row, arg0);
                   }
                }

                throw new IllegalArgumentException("Could not invoke " + mname + " with normalized mode.");
             }

             if (args[0] instanceof String)
             {
                // access single element from extent using name
                String fieldName = (String) args[0];
                // allow failure only for setters
                boolean failNotFound = (mname.startsWith("set") || mname.equals("unset"));

                fieldMeta = DataSetSDOHelper.getFieldMetaData(tableMetaData,
                                                              fieldName,
                                                              forceNormalizedFields,
                                                              failNotFound);
                if (fieldMeta != null)
                {
                   String extentField = fieldMeta.getExtentField();
                   int extentIndex = fieldMeta.getExtentIndex() != null ? fieldMeta.getExtentIndex() : -1;

                   if (extentIndex > 0)
                   {
                      // calculate the real extent from the legacy field.
                      args[0] = DataSetSDOHelper.buildDenormalizedFieldName(extentField, extentIndex);
                   }
                   else
                   {
                      // we are on the denormalized field, ensure the call uses the proper name
                      args[0] = fieldMeta.getName();
                   }
                }
             }

             return method.invoke(row, args);
          }

          // we are in the normalized case
          if (args[0] instanceof String)
          {
             String fieldName = (String) args[0];

             // allow failure only for setters
             boolean failNotFound = (mname.startsWith("set") || mname.equals("unset"));

             LegacyFieldMetaData fieldMeta = DataSetSDOHelper.getFieldMetaData(tableMetaData,
                                                                               fieldName,
                                                                               forceNormalizedFields,
                                                                               failNotFound);
             if (fieldMeta != null)
             {
                // we are on the normalized field, ensure the call uses the proper name
                args[0] = fieldMeta.getName();
             }
          }

          return method.invoke(row, args);
       }

       /**
        * Set the state of the {@link #forceNormalizedFields} flag.
        *
        * @param    forceNormalizedFields
        *           When <code>true</code>, normalized field names are assumed always.
        */
       @Override
       public void setForceNormalizedFields(boolean forceNormalizedFields)
       {
          this.forceNormalizedFields = forceNormalizedFields;
       }

       /**
        * Start tracking changes in this record.
        */
       @Override
       public void beginLogging()
       {
          this.originRowid = null;
          this.peerRowid = null;
          this.rowState = null;
          this.errorString = null;
          this.errorFlag = null;

          this.logging = true;
       }

       /**
        * Stop tracking changes in this record.
        */
       @Override
       public void endLogging()
       {
          this.logging = false;
       }

       /**
        * Set the source table metadata used to create this {@link #row}.
        *
        * @param    tableMetaData
        *           The source table metadata.
        */
       @Override
       public void setTableMetaData(DataObject tableMetaData)
       {
          this.tableMetaData = tableMetaData;
          this.normalizedExtent = tableMetaData.getBoolean("normalizedExtent");
       }

       /**
        * Set the {@link #row}.
        *
        * @param    row
        *           The wrapped {@link DataObject} instance.
        */
       @Override
       public void setRow(DataObject row)
       {
          this.row = row;
       }

       /**
        * Get the {@link #row}.
        *
        * @return   See above.
        */
       @Override
       public DataObject getRow()
       {
          return row;
       }

       /**
        * Get the identifier (primary key) for this data-object.
        *
        * @return   The {@link #id}.
        */
       @Override
       public Long primaryKey()
       {
          return id;
       }

       /**
        * Set the identifier (primary key) for this data-object.
        *
        * @param   id
        *          Unique identifier to be associated with this object.
        */
       @Override
       public void primaryKey(Long id)
       {
          this.id = id;
       }

       /**
        * Gets the rowid (as {@code Long} value) of the peer record. Exclusively, for a BEFORE-TABLE
        * record this is the {@code after-rowid} and {@code before-rowid} for AFTER-TABLE record.
        *
        * @return  The rowid value of the peer record or {@code null} if there is none.
        *
        * @see     Buffer#__AFTER_ROWID__
        */
       @Override
       public Long _peerRowid()
       {
          return peerRowid;
       }

       /**
        * Sets the rowid (as {@code Long} value) of the peer record. Exclusively, for a BEFORE-TABLE
        * record this is the {@code after-rowid} and {@code before-rowid} for AFTER-TABLE record.
        *
        * @param   rowid
        *          The new long rowid value of the peer record or {@code null}.
        *
        * @see     Buffer#__AFTER_ROWID__
        */
       @Override
       public void _peerRowid(Long rowid)
       {
          this.peerRowid = rowid;
       }

       /**
        * Gets the current value of the {@code ROW-STATE} property for this record. Valid values
        * are defined as constants in {@link Buffer} interface. Additionally, {@code null} value is
        * returned if property is not available.
        *
        * @return  The current value of the {@code ROW-STATUS} property for this record as
        *          explained above.
        *
        * @see     Buffer#__ROW_STATE__
        */
       @Override
       public Integer _rowState()
       {
          return rowState;
       }

       /**
        * Sets the value of the {@code ROW-STATUS} property for this record. Valid values are defined
        * as constants in {@link Buffer} interface. Additionally {@code null} value is allowed if
        * property is not available.
        *
        * @param  state
        *         The new value of the {@code ROW-STATE} property for this record as explained above.
        *
        * @see     Buffer#__ROW_STATE__
        */
       @Override
       public void _rowState(Integer state)
       {
          this.rowState = state;
       }

       /**
        * Gets the current value of the {@code ORIGIN-ROWID} property for this record. This property
        * of a record in a buffer of a CHANGE DATASET points to original record of the change.
        * Additionally, {@code null} value is returned if property is not available.
        *
        * @return  The current value of the {@code ORIGIN-ROWID} property for this record as
        *          explained above.
        *
        * @see     Buffer#__ORIGIN_ROWID__
        */
       @Override
       public Long _originRowid()
       {
          return originRowid;
       }

       /**
        * Sets the current value of the {@code ORIGIN-ROWID} property for this record. This property
        * of a record in a buffer of a CHANGE DATASET points to original record of the change.
        * Additionally, {@code null} value is returned if property is not available.
        *
        * @param  rowid
        *         The new value of the {@code ORIGIN-ROWID} property for this record as explained
        *         above.
        *
        * @see     Buffer#__ORIGIN_ROWID__
        */
       @Override
       public void _originRowid(Long rowid)
       {
          this.originRowid = rowid;
       }

       /**
        * Gets the current value of the {@code DATA_SOURCE_ROWID} property for this record. This property of a
        * record in a buffer of a CHANGE DATASET points to original record of the change. Additionally,
        * {@code null} value is returned if property is not available.
        *
        * @return  The current value of the {@code DATA_SOURCE_ROWID} property for this record as explained above.
        *
        * @see     Buffer#__DATA_SOURCE_ROWID__
        */
       @Override
       public Long _datasourceRowid()
       {
          return dataSourceRowid;
       }

       /**
        * Sets the current value of the {@code DATA_SOURCE_ROWID} property for this record. This property of a
        * record in a buffer of a CHANGE DATASET points to original record of the change. Additionally,
        * {@code null} value is returned if property is not available.
        *
        * @param   rowid
        *          The new value of the {@code DATA_SOURCE_ROWID} property for this record as explained above.
        *
        * @see     Buffer#__DATA_SOURCE_ROWID__
        */
       @Override
       public void _datasourceRowid(Long rowid)
       {
          this.dataSourceRowid = rowid;
       }

       /**
        * Gets the current value of the {@code __error-flag__} for this record.
        *
        * @return  the current value of the {@code __error-flag__} for this record.
        *
        * @see     Buffer#__ERROR_FLAG__
        */
       @Override
       public Integer _errorFlags()
       {
          return errorFlag;
       }

       /**
        * Sets the current value of the {@code __error-flag__} for this record.
        *
        * @param   flag
        *          the current value of the {@code __error-flag__} for this record.
        *
        * @see     Buffer#__ERROR_FLAG__
        */
       @Override
       public void _errorFlags(Integer flag)
       {
          this.errorFlag = flag;
       }

       /**
        * Gets the current value of the {@code __error-string__} for this record.
        *
        * @return  the current value of the {@code __error-string__} for this record.
        *
        * @see     Buffer#__ERROR_STRING__
        */
       @Override
       public String _errorString()
       {
          return errorString;
       }

       /**
        * Sets the current value of the {@code __error-string__} for this record.
        *
        * @param   error
        *          the current value of the {@code __error-string__} for this record.
        *
        * @see     Buffer#__ERROR_STRING__
        */
       @Override
       public void _errorString(String error)
       {
          this.errorString = error;
       }

       /**
        * Check if this instance is the same as the specified {@link FWDDataObject} or
        * {@link DataObject} instance.
        *
        * @param    obj
        *           The other instance.
        *
        * @return    See above.
        */
       @Override
       public boolean equals(Object obj)
       {
          if (obj instanceof FWDDataObjectType)
          {
             return row.equals(((FWDDataObjectType) obj).getRow());
          }

          if (obj instanceof DataObject)
          {
             return row.equals(obj);
          }

          return super.equals(obj);
       }

       /**
        * Compute this object's hashcode, the same as the {@link #row}'s hashcode.
        *
        * @return   See above.
        */
       @Override
       public int hashCode()
       {
          return row.hashCode();
       }

       /**
        * Log an event that this row is newly created.
        */
       @Override
       public void rowCreated()
       {
          if (isLogging())
          {
             DataSetSDOHelper.logCreated(this);
          }
       }

       /**
        * Check if logging is enabled.
        *
        * @return   <code>true</code> if {@link #logging} is set and the row is already added to the table.
        */
       @Override
       public boolean isLogging()
       {
          return logging && row.getDataGraph() != null;
       }

       /**
        * Get an array with this record's meta information.
        *
        * @return   See above.
        */
       @Override
       public Object[] toArray()
       {
          return TempRecord.asArray(this);
       }

       /**
        * Resolve the index argument to its numeric value.
        *
        * @param    idx
        *           The index, as a numeric value, {@link String} or {@link Property}.
        *
        * @return   The property index as resolved from {@link #propToIndex}.
        */
       private Integer getIndex(Object idx)
       {
          if (idx instanceof Integer)
          {
             return (Integer) idx;
          }
          else if (idx instanceof Property)
          {
             return propToIndex.get(((Property) idx).getName());
          }
          else
          {
             return propToIndex.get((String) idx);
          }
       }
    }