PropertyReference.java
/*
** Module : PropertyReference.java
** Abstract : OO property reference which allows deferred read/write access
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA 20190524 Created initial version.
** 002 CA 20190604 Added missing javadocs.
** 003 CA 20190620 Fixed initialization order.
** 004 CA 20190724 When calling the setter, the value must be converted to the property's type,
** if is not the same type.
** 005 CA 20210609 Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly
** initialized at the method's execution, and not at the caller's arguments.
** 006 CA 20240215 Fixed issues using CALL property/field references as arguments, associated with a
** DATASET/TABLE-HANDLE parameter.
** 007 CA 20240520 Fixed validation of the DYNAMIC-PROPERTY arguments for statement and function.
** 008 PBB 20250509 Modified implementation of DYNAMIC-PROPERTY function to introduce support for
** DYNAMIC-INVOKE builtin function that returns or might return an extent.
** 009 PBB 20250522 Added support for DYNAMIC-PROPERTY statement (setter) mostly related to extents.
*/
/*
** 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 com.goldencode.p2j.oo.lang.*;
/**
* Dynamic access to a legacy property.
*/
public final class PropertyReference
implements Resolvable,
Accessor
{
/** The fully qualified legacy class name for this property, in case of static properties. */
private String className = null;
/** The object instance, in case of instance properties. */
private object<? extends _BaseObject_> instance = null;
/** The legacy property name to access. */
private String property = null;
/** The index, in case of extent properties. */
private int64 index = null;
/** The property's extent. */
private Integer extent = null;
/** The legacy type to which the property belongs. */
private Class<? extends _BaseObject_> type;
/** The property's data type. */
private Class<? extends BaseDataType> propType;
/** The associated getter for the property. */
private InternalEntry defaultGetter = null;
/** The associated setter for the property. */
private InternalEntry defaultSetter = null;
/** The list of all associated setters for the property */
private List<InternalEntry> settersList = null;
/** In case of an instance property, the legacy reference. */
private _BaseObject_ reference;
/**
* Constructor.
*
* @param className
* The class name in case of static property access.
* @param property
* Name of class property member.
*/
public PropertyReference(String className, String property)
{
this.className = className;
this.property = property;
init();
}
/**
* Constructor.
*
* @param instance
* The class instance, for instance property access.
* @param property
* Name of class property member.
*/
public PropertyReference(object<? extends _BaseObject_> instance, String property)
{
this.instance = new object<>(instance);
this.property = property;
init();
}
/**
* Constructor.
*
* @param className
* The class name in case of static property access.
* @param property
* Name of class property member.
* @param index
* Index to pass to property's getter method at resolve time.
*/
public PropertyReference(String className, String property, NumberType index)
{
this.className = className;
this.property = property;
this.index = new int64(index);
init();
}
/**
* Constructor.
*
* @param className
* The class name in case of static property access.
* @param property
* Name of class property member.
* @param index
* Index to pass to property's getter method at resolve time.
*/
public PropertyReference(String className, String property, int index)
{
this.className = className;
this.property = property;
this.index = new int64(index);
init();
}
/**
* Constructor.
*
* @param instance
* The class instance, for instance property access.
* @param property
* Name of class property member.
* @param index
* Index to pass to property's getter method at resolve time.
*/
public PropertyReference(object<? extends _BaseObject_> instance, String property, int index)
{
this.instance = new object<>(instance);
this.property = property;
this.index = new int64(index);
init();
}
/**
* Constructor.
*
* @param instance
* The class instance, for instance property access.
* @param property
* Name of class property member.
* @param index
* Index to pass to property's getter method at resolve time.
*/
public PropertyReference(object<? extends _BaseObject_> instance, String property, NumberType index)
{
this.instance = new object<>(instance);
this.property = property;
this.index = new int64(index);
init();
}
/**
* Get the property accessor index.
*
* @return The {@link #index}.
*/
public Integer getIndex()
{
return index.isUnknown() ? null : index.toJavaIntegerType();
}
/**
* Get the property's extent.
*
* @return The {@link #extent}.
*/
public Integer getPropertyExtent()
{
return extent;
}
/**
* Resolve the property's type, the legacy (default) getter and setter,
* and the list of all the setters.
*/
private void init()
{
this.type = className == null ? instance.ref.getClass() : ObjectOps.resolveClass(className);
this.reference = instance == null ? null : instance.ref();
this.extent = SourceNameMapper.findLegacyPropertyExtent(type, property);
this.defaultGetter = SourceNameMapper.findLegacyAccessor(type, property, true, extent != null, index != null);
this.defaultSetter = SourceNameMapper.findLegacyAccessor(type, property, false, extent != null, index != null);
if (this.extent != null)
{
// Save all the setters for an extent property for choosing the right one at runtime
this.settersList = SourceNameMapper.findLegacySetters(type, property);
}
if (this.defaultSetter == null && index != null && extent == null)
{
// defaultSetter calls with a specified index for a non-extent property will ignore the index
this.defaultSetter = SourceNameMapper.findLegacyAccessor(type, property, false, false, false);
this.index = null;
}
if (this.defaultSetter != null)
{
this.defaultSetter.getMethod().setAccessible(true);
}
if (this.defaultGetter != null)
{
this.defaultGetter.getMethod().setAccessible(true);
this.propType = (Class<? extends BaseDataType>) this.defaultGetter.getMethod().getReturnType();
}
else if (this.defaultSetter != null)
{
Parameter p = this.defaultSetter.getParameter(0);
this.propType = BaseDataType.fromTypeName(p.getType());
}
}
/**
* Get a copy of the referenced object's current runtime value (this does not return the
* original instance itself).
*
* @return The copy of the current value.
*/
@Override
public BaseDataType get()
{
OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();
try
{
Object result = (index == null ? defaultGetter.getMethod().invoke(reference)
: defaultGetter.getMethod().invoke(reference, index));
if (result.getClass().isArray())
{
String errMsg = "An array was specified in an expression, " +
"on the right-hand side of an assignment, " +
"or as a parameter when no array is appropriate or expected";
ErrorManager.recordOrThrowError(361, errMsg, true);
return unknown.UNKNOWN;
}
return (BaseDataType) result;
}
catch (InvocationTargetException exc)
{
// TODO: ignore ? Check for errors?
return BaseDataType.generateUnknown(propType);
}
catch (IllegalAccessException exc)
{
// Represents programming or JVM configuration error.
throw new RuntimeException(exc);
}
finally
{
TransactionManager.registerOutputParameterAssigner(opa);
}
}
/**
* Get a copy of the referenced object's current runtime extent value (this does not return the
* original instance itself).
*
* @return The copy of the current extent value.
*/
public BaseDataType[] getExtent()
{
OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();
try
{
return (BaseDataType[]) (index == null ? defaultGetter.getMethod().invoke(reference)
: defaultGetter.getMethod().invoke(reference, index));
}
catch (Exception e)
{
// Represents programming or JVM configuration error.
throw new RuntimeException(e);
}
finally
{
TransactionManager.registerOutputParameterAssigner(opa);
}
}
/**
* Set the referenced object's current runtime value to the given value.
*
* @param val
* The current value to be assigned into the referenced object.
*/
@Override
public void set(BaseDataType val)
{
try
{
if (extent != null)
{
if (index == null)
{
Object currentState = defaultGetter.getMethod().invoke(reference);
if (extent == SourceNameMapper.DYNAMIC_EXTENT && Array.getLength(currentState) == 0)
{
String errMsg = "Invalid assignment to an unfixed indeterminate extent";
ErrorManager.recordOrThrowError(11389, errMsg, false);
}
for (InternalEntry ie : this.settersList)
{
if (ie.getParameterList().size() == 1 && !ie.getParameter(0).isExtent())
{
ie.getMethod().invoke(reference, val);
return;
}
}
String errMsg = "No proper setter found for '%s' extent property of %s class.";
throw new RuntimeException(String.format(errMsg,
this.property,
this.className != null ?
this.className :
this.instance.type().getName()));
}
else
{
for (InternalEntry ie : this.settersList)
{
if (ie.getParameterList().size() == 2)
{
ie.getMethod().invoke(reference, val, index);
}
}
}
}
else if (val.getClass() != propType)
{
// TODO: Assigning a non-extent value to an extent property
BaseDataType v2 = BaseDataType.generateDefault(propType);
v2.assign(val);
val = v2;
}
if (index == null)
{
this.defaultSetter.getMethod().invoke(reference, val);
}
else
{
this.defaultSetter.getMethod().invoke(reference, val, index);
}
}
catch (InvocationTargetException exc)
{
// TODO: ignore ? Check for errors?
}
catch (IllegalAccessException exc)
{
// Represents programming or JVM configuration error.
throw new RuntimeException(exc);
}
}
/**
* Set the referenced object's current runtime extent value to the given extent value.
*
* @param source
* The extent values to be assigned to the referenced extent object.
*/
public void setExtent(BaseDataType[] source)
{
if (extent == null || (extent != source.length && extent != SourceNameMapper.DYNAMIC_EXTENT))
{
String errMsg = "Whole-array assignment target and source must have the same extent " +
"unless the target is indeterminate";
ErrorManager.recordOrThrowError(14905, errMsg, false);
return;
}
if (source.length > 0 && source.getClass().getComponentType() == propType.getComponentType())
{
try
{
Object currentState = defaultGetter.getMethod().invoke(reference);
if (extent == SourceNameMapper.DYNAMIC_EXTENT &&
Array.getLength(currentState) > 0 &&
Array.getLength(currentState) != source.length)
{
String errMsg = String.format("Indeterminate extent is already fixed to a dimension" +
" of %d", Array.getLength(currentState));
ErrorManager.recordOrThrowError(13738, errMsg, false);
return;
}
defaultSetter.getMethod().invoke(reference, (Object) source);
}
catch (InvocationTargetException e)
{
// TODO: ignore ? Check for errors?
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
}
else
{
String[] errMsgs = new String[]
{
"Incompatible datatypes found during runtime conversion",
"Could not convert data for array assignment"
};
ErrorManager.recordOrThrowError(new int[] { 5729, 14907 }, errMsgs, false);
}
}
/**
* Resolve this object to its current runtime value.
*
* @return Resolved value.
*/
@Override
public BaseDataType resolve()
{
return get();
}
/**
* Report the return type expected from resolving the object.
*
* @return Class indicating the type which will be returned by the
* object. Must be a variant of {@link BaseDataType}.
*/
@Override
public Class getType()
{
return propType;
}
/**
* Wrap a class property (used as an OUTPUT or INPUT-OUTPUT argument), so that it can be
* updated when the call returns.
*
* @param io
* Flag indicating the argument is INPUT-OUTPUT or just OUTPUT.
*
* @return A new BDT value, initialized depending on the argument mode.
*/
public <T extends BaseDataType> T wrap(boolean io)
{
BaseDataType init = io ? resolve() : null;
T wrapper = (T) OutputParameter.createVariable(propType,
init,
get(),
(bdt) -> new PropertyAssigner(bdt));
return wrapper;
}
/**
* Get the {@link #instance} for resolving this property.
*
* @return The {@link #instance} field.
*/
object<? extends _BaseObject_> getInstance()
{
return instance;
}
/**
* A simple assigner to update the backing class property, when the call returns.
*/
private class PropertyAssigner
extends AbstractSimpleParameter
{
/**
* Create a new instance using the variable reference as argument.
*/
protected PropertyAssigner(BaseDataType variable)
{
super(variable);
}
/**
* Assign the current value of the parameter variable back to the associated class property.
*/
@Override
protected void assign()
{
set(outputValue);
}
}
}