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