FieldReference.java
/*
** Module : FieldReference.java
** Abstract : Buffer field reference which allows deferred read/write access
**
** Copyright (c) 2004-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051112 @23388 Created initial version. Resolves a buffer
** field reference value at runtime. Used as a
** query substitution parameter for field
** references which can only be resolved as the
** query is executing and has partial results.
** 002 ECF 20051216 @23756 Added getValue method. Returns resolved value
** but reports a warning to the UI (via the
** error manager) if resolution fails.
** 003 ECF 20060104 @23822 Implement Resolvable interface. Allows this
** class to be used generically with util APIs.
** 004 ECF 20060202 @24223 Implement Accessor interface. Field access is
** now read/write instead of read-only.
** 005 ECF 20060307 @25015 Added new constructor and get method variant.
** These permit late binding of a DMO instance.
** 006 ECF 20060323 @25226 Added support for case-insensitive field
** references used as query substitution
** parameters. New constructor variants accept
** an uppercase flag.
** 007 GES 20060907 @29285 Setting an extent field was broken. The index
** had to be the first parameter instead of the
** second.
** 008 ECF 20060927 @29970 Added special exception handling in set() and
** getObject(). ConditionException wrapped in an
** InvocationTargetExcpetion is unwrapped and
** rethrown.
** 009 ECF 20061027 @30749 Replaced Object with DataModelObject for DMO
** arguments to methods/constructors. Required
** for compile-time type safety.
** 010 ECF 20070416 @33022 Integrated user interrupt handling.
** 011 ECF 20080528 @38480 Integrated generics.
** 012 ECF 20080606 @38645 Fixed FieldReference(Class<?>, String) c'tor
** variant. It was not properly setting the
** uppercase instance variable in the case of
** references to case-insensitive properties.
** Also added support for the reserved 'id'
** property in getGetter() method.
** 013 CA 20080815 @39458 Support API change in DBUtils.
** 014 ECF 20090701 @43217 Enabled lazy initialization of backing record
** buffer. Database instance variable is extracted
** from buffer lazily, rather than at construction.
** 015 SVL 20090723 @43342 Added getPropertyName() function.
** 016 ECF 20090819 @43695 Downgraded some log messages from ERROR to DEBUG.
** 017 GES 20090828 @43799 Don't unconditionally shift into warning mode as
** that would disable warning mode on exit from the
** getter even if it had already been enabled.
** 018 AIL 20121206 Update from character to TextOps.
** 019 VMN 20130207 Extended from FieldInterface and added support for attributes
** BUFFER-VALUE (KW_BUF_VAL)
** COLUMN-LABEL (KW_COL_LAB)
** EXTENT (KW_EXTENT)
** FORMAT (KW_FORMAT)
** STRING-VALUE (KW_STR_VAL)
** 020 VMN 20130213 Added support for attributes
** BUFFER-NAME (KW_BUF_NAME),
** LITERAL-QUESTION (KW_LIT_QSTN)
** VALIDATE-MESSAGE (KW_VAL_MSG)
** MANDATORY (KW_MAND)
** VALIDATE-EXPRESSION (KW_VAL_EXPR)
** CURRENT-ITERATION (KW_CUR_ITER)
** 021 CA 20130215 Added dynamic EXTENT and EXTENT parameter support.
** 022 VMN 20130217 Corrected for attribute
** CURRENT-ITERATION (KW_CUR_ITER)
** WIDTH-CHARS (KW_WIDTH_C)
** 023 VMN 20130219 Added support for attributes
** POSITION (KW_POS)
** 024 CA 20130224 Added support for UNIQUE-ID, AMD-DATA, Sizeable and CommonField
** attributes. Fixed CHANGE-VALUE. Added Deletable support.
** 025 OM 20130219 Changed return type of value (BUFFER-VALUE)
** 026 OM 20130304 Refactored isValid and isUnknown of WrappedResource to valid
** and unknown.
** 027 OM 20130304 Added missing overloaded method for value().
** 028 CA 20130315 Added missing API for STRING-VALUE.
** 029 ECF 20130417 Moved initial() method from BufferImpl.
** 030 CA 20130603 resourceDelete must return a boolean value.
** 031 SVL 20130822 Implemented extent-related functions and value() function. Added
** getParentBuffer.
** 032 HC 20131215 Implemented ADM-DATA and UNIQUE-ID attribute.
** 033 VMN 20140123 Implemented extent(), initial(), stringValue(), getFormat(),
** setFormat(), columnLabel(), changeColumnLabel(), getColumnLabel(),
** setColumnLabel(), bufferName(), getDbName(), getTable(),
** position(), getDataType(), valid(), setValue() for non BaseDataType
** values.
** 034 OM 20140122 Fixed value() and value(int) to return unknown value when they are
** called for EXTENT, resp. non-EXTENT fields.
** Added Long support for changeValue(), minor aesthetic changes.
** 035 SVL 20140205 Fixed value(int).
** 036 EVL 20140217 Fixed javadoc comments to be able to compile in Windows 1252 CP.
** 037 ECF 20140318 Added logic to compensate for instance of unknown being passed to
** changeValue(). Replaced Apache commons logging with J2SE logging.
** Added error handling for BUFFER-VALUE() to validate index values
** passed to value(NumberType) and changeValue(NumberType) (0 is a valid
** index for scalar fields). Added widening conversion in changeValue
** for polymorphic BaseDataType arguments. Needs additional work to
** exactly mimic 4GL errors.
** 038 ECF 20140415 Set NAME attribute to its default value the first time name() is
** invoked.
** 039 HC 20140613 Fixed extent fields runtime support.
** 040 HC 20141207 Implemented runtime support for widget coordinate attributes.
** 041 ECF 20150315 Major refactoring; moved all code related to BUFFER-FIELD support to
** BufferFieldImpl. This class is now only the worker which interacts
** with the DMO to get and set property values.
** 042 SVL 20150503 Added getIndex, getProperty.
** 043 ECF 20150715 Added APIs in support of compound query optimization.
** 044 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 045 ECF 20160224 Rework to better support denormalized extent fields. Removed
** redundant method and extent caches.
** 046 EVL 20160325 Javadoc fixes.
** 047 SVL 20160314 Added getTemp methods.
** 048 OM 20160613 Fixed character trimming.
** 049 SVL 20160811 Some methods are public now.
** 050 HC 20170830 Modified conversion of EXPORT to expand extent fields dynamically.
** This should prevent the Java source file size to get out of control
** for large extents.
** 051 ECF 20180201 Store proxy separately from DMO.
** 052 OM 20180301 Special handling for rowid/recid functions converted into surrogate
** [id] fields when encountered as substitution expressions.
** ECF 20180305 Fix for regression with cases where property is id field, but dmo is
** a Persistable instance instead of a DMO proxy.
** 053 OM 20180614 Added support for bulk-accessors of extent fields.
** 054 SVL 20180528 Added getFullFieldName().
** OM 20180627 Implemented equals() and hashCode(). Added index value for extent
** fields in string returned by getFullFieldName().
** 055 CA 20190829 Virtual properties (starting with '_') are looked in the
** TempTableRecord interface.
** OM 20190831 Added hidden fields and index specific to BEFORE TEMP-TABLES.
** 056 CA 20191009 In case of bound buffers, the DMO property must be translated.
** 057 RFB 20200117 We need a new constructor to allow us to pass in 'bulk'. This allows
** us the ability to use the multi assignment for un-subscripted field
** references in embedded assignment.
** RFB 20200120 Update the constructor to not impede a non-negative index when passed
** in. Updated javadoc to be more accurate.
** RFB 20200122 Had a type mismatch in the previous update.
** 058 ECF 20200906 New ORM implementation.
** 059 CA 20200924 Replaced Method.invoke with ReflectASM.
** SVL 20201009 Index can be represented by an expression. Scalar index is stored as Integer
** rather as NumberType.
** OM 20201012 Force use locally cached meta information instead of map lookup.
** OM 20210328 Improve support for CODEPAGE of CLOB fields.
** OM 20210404 CLOB fields are created and returned with their CP set. The getObject() method
** was too late to set their CP.
** 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.
** CA 20210628 Indexed extent fields must resolve proper getter/setter - the problem was used
** when IMPORT was targeting an extent field.
** SVL 20210922 In getFullFieldName use legacy name instead of DMO alias as the latter is
** uniquely generated for dynamic buffers (see
** BufferManager.generateUniqueBufferName).
** TJD 20220504 Upgrade do Java 11 minor changes.
** OM 20221208 Small changes in method declarations.
** EVL 20221222 Adding new method to get legacy field name.
** 060 OM 20230303 Proper identification of bulk mode in FieldReference constructor.
** 061 OM 20230307 The original extent and property name were lost when denormalizing extent fields.
** 062 OM 20230404 Improved toString() representation.
** 063 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 064 RAA 20230519 Made the computing of getter, setter and extent lazy.
** Added buffer instance for getParentBuffer.
** RAA 20230519 Added flags for getter, setter and extent to check which one has already been
** computed.
** RAA 20230523 Done some code cleanup.
** RAA 20230525 Creating a new BaseDataType instance is now done using BaseDataTypeFactory.
** 065 CA 20230330 Fixed NPE - 'buffer' can't be used standalone, use 'getParentBuffer()' getter.
** 066 EAB 20240429 Checked for unknownValue method if the getter is for a primary key. If it is,
** the function needs to return a value of type "rowid".
*/
/*
** 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;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.p2j.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;
/**
* A readable, writable reference to a buffer field, used in cases where the
* field needs to be accessed at some arbitrary point in time after it is
* referenced in application code. For example, an instance of this class
* would be used as a substitution parameter in a {@link CompoundQuery}, when
* an inner loop where clause of a multi-table query refers to a field within
* a buffer retrieved in an outer loop.
* <p>
* This class provides both read and write capability to the backing field,
* insofar as the DMO has both an accessor (getter) and a mutator (setter)
* method defined for the specified property. Both indexed and non-indexed
* versions of these methods are supported. It is assumed that the methods
* have bean-like signatures. Specifically, the following, public signature
* patterns are assumed:
* <p>
* <dl>
* <dt>Simple Getter:</dt>
* <dd>
* <code><BaseDataType subclass> get<Property>()</code>
* </dd>
* <dt>Indexed Getter:</dt>
* <dd>
* <code><BaseDataType subclass> get<Property>(int)</code>
* </dd>
* <dt>Simple Setter:</dt>
* <dd>
* <code>void set<Property>(<BaseDataType subclass>)</code>
* </dd>
* <dt>Indexed Setter:</dt>
* <dd>
* <code>void set<Property>(<BaseDataType subclass>, int)</code>
* </dd>
* </dl>
* <p>
*/
public final class FieldReference
implements Resolvable,
Accessor
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(FieldReference.class.getName());
/**
* The special {@code id} field. It is used for {@code rowid} / {@code recid} of the record and
* does not exist as real field in P4GL, only in converted SQL database. For this reason it is
* handled a bit different. Its value is implemented as objects extending {@code Record}
* and not the DMO as other fields.
*/
private static final String ID = DatabaseManager.PRIMARY_KEY;
/** Property name */
private final String property;
/** Method used to resolve this field reference at query execution time */
private Method getter;
/** Method used to resolve this field reference at query execution time */
private Method setter;
/** This field's extent (if an array field), else <code>null</code> */
private Integer extent;
/** Optional index, used with a DMO indexed getter method */
private final Integer index;
/** Optional index expression, used with a DMO indexed getter method */
private final Supplier<NumberType> indexExpr;
/** Saved legacy field name. */
private String legacyName = null;
/** The associated database */
private Database database = null;
/** DMO or DMO proxy instance; the referent of the referencing method */
private Object dmo = null;
/** DMO proxy, if available; may be {@code null} */
private DataModelObject proxy = null;
/** Flag to force resolved character result to uppercase/right-trim */
private boolean uppercase = false;
/**
* When the reference is an EXTENT field and {@code index} is {@code null} the bulk getter/setter
* will be used.
*/
private Boolean bulk = false;
/** The record buffer which contains the referent DMO. */
private RecordBuffer buffer = null;
/** Interface of the target DMO. */
private final Class<? extends DataModelObject> dmoIface;
/**
* Marks this as a reference to {@code id} special field.
*
* @see #ID
*/
private final boolean isIdAccessor;
/** Flag that marks whether the extent field has already been computed or not. */
private boolean isExtentComputed = false;
/** Flag that marks whether the getter field has already been computed or not. */
private boolean isGetterComputed = false;
/** Flag that marks whether the setter field has already been computed or not. */
private boolean isSetterComputed = false;
/** Flag that marks whether the bulk field has already been computed or not. */
private boolean isBulkComputed = false;
/**
* Create a new field reference given a DMO proxy and property name. A
* non-indexed getter method will be used to resolve the field.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
*/
public FieldReference(DataModelObject dmo, String property)
{
this(dmo, property, false, null, null, true);
}
/**
* Create a new field reference given a DMO proxy and property name. A
* non-indexed getter method will be used to resolve the field.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param uppercase
* <code>true</code> to force a <code>character</code> value
* returned by {@link #getObject} to be uppercased/right-trimmed;
* <code>false</code> to leave the result unchanged.
*/
public FieldReference(DataModelObject dmo, String property, boolean uppercase)
{
this(dmo, property, uppercase, null, null, true);
}
/**
* Create a new field reference given a DMO proxy, a property name, and
* an index for the property. An indexed getter method will be used to
* resolve the field.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param index
* Index to pass to property's getter method at resolve time.
*/
public FieldReference(DataModelObject dmo, String property, int index)
{
this(dmo, property, false, new integer(index), null, false);
}
/**
* Create a new field reference given a DMO proxy, a property name, and
* an index expression for the property. An indexed getter method will be used to
* resolve the field.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param indexExpr
* Index expression which is evaluated to pass to property's getter method at resolve time.
*/
public FieldReference(DataModelObject dmo, String property, Supplier<NumberType> indexExpr)
{
this(dmo, property, false, null, indexExpr, false);
}
/**
* Create a new field reference given a DMO proxy, a property name, and
* an index for the property. An indexed getter method will be used to
* resolve the field.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param uppercase
* <code>true</code> to force a <code>character</code> value
* returned by {@link #getObject} to be uppercased/right-trimmed;
* <code>false</code> to leave the result unchanged.
* @param index
* Index to pass to property's getter method at resolve time.
*/
public FieldReference(DataModelObject dmo, String property, boolean uppercase, int index)
{
this(dmo, property, uppercase, new integer(index), null, false);
}
/**
* Create a new field reference given a DMO proxy, a property name, and
* an index for the property. An indexed getter method will be used to
* resolve the field.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param index
* Index to pass to property's getter method at resolve time.
*
* @throws IllegalArgumentException
* if field reference is not recognized.
*/
public FieldReference(DataModelObject dmo, String property, NumberType index)
{
this(dmo, property, false, index, null, false);
}
/**
* Create a new field reference given a DMO proxy, a property name, and an index for the
* property. An indexed getter method will be used to resolve the field.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param uppercase
* {@code true} to force a {@code character} value returned by {@link #getObject} to
* be uppercased/right-trimmed; {@code false} to leave the result unchanged.
* @param index
* Index to pass to property's getter method at resolve time.
*
* @throws IllegalArgumentException
* if field reference is not recognized.
*/
public FieldReference(DataModelObject dmo,
String property,
boolean uppercase,
NumberType index)
{
this(dmo, property, uppercase, index, null, false);
}
/**
* Create a new field reference given a DMO proxy, a property name, and an index expression for the
* property. An indexed getter method will be used to resolve the field.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param uppercase
* {@code true} to force a {@code character} value returned by {@link #getObject} to
* be uppercased/right-trimmed; {@code false} to leave the result unchanged.
* @param indexExpr
* Index expression which is evaluated to pass to property's getter method at resolve time.
*
* @throws IllegalArgumentException
* if field reference is not recognized.
*/
public FieldReference(DataModelObject dmo,
String property,
boolean uppercase,
Supplier<NumberType> indexExpr)
{
this(dmo, property, uppercase, null, indexExpr, false);
}
/**
* Create a new field reference given a DMO proxy, a property name, an index for the
* property, and a flag indicating whether or not to use bulk. Allows for explicit use
* of {@code bulk}. This constructor does not support forcing a {@code character} value
* returned by {@link #getObject} to be uppercased/right-trimmed;
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param index
* index which, when negative, will be passed as null to constructor.
* @param bulk
* When true the bulk getter/setter will be used.
*
* @throws IllegalArgumentException
* if field reference is not recognized.
*/
public FieldReference(DataModelObject dmo,
String property,
int index,
boolean bulk)
{
this(dmo, property, false, index < 0 ? null : new integer(index), null, bulk);
}
/**
* Create a new field reference given a DMO proxy, a property name, an index expression for the
* property, and a flag indicating whether or not to use bulk. Allows for explicit use
* of {@code bulk}. This constructor does not support forcing a {@code character} value
* returned by {@link #getObject} to be uppercased/right-trimmed;
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param indexExpr
* Index expression which is evaluated to pass to property's getter method at resolve time.
* @param bulk
* When true the bulk getter/setter will be used.
*
* @throws IllegalArgumentException
* if field reference is not recognized.
*/
public FieldReference(DataModelObject dmo,
String property,
Supplier<NumberType> indexExpr,
boolean bulk)
{
this(dmo, property, false, null, indexExpr, bulk);
}
/**
* Create a new field reference given a DMO proxy, a property name, an index for the
* property, and a flag indicating whether or not to use bulk. Allows for explicit use
* of {@code bulk}.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param uppercase
* {@code true} to force a {@code character} value returned by {@link #getObject} to
* be uppercased/right-trimmed; {@code false} to leave the result unchanged.
* @param index
* index which, when negative, will be passed as null to constructor.
* @param bulk
* When true the bulk getter/setter will be used.
*
* @throws IllegalArgumentException
* if field reference is not recognized.
*/
public FieldReference(DataModelObject dmo,
String property,
boolean uppercase,
NumberType index,
boolean bulk)
{
this(dmo, property, uppercase, index, null, bulk);
}
/**
* Create a new field reference given a DMO proxy, a property name, an index expression for the
* property, and a flag indicating whether or not to use bulk. Allows for explicit use
* of {@code bulk}.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param uppercase
* {@code true} to force a {@code character} value returned by {@link #getObject} to
* be uppercased/right-trimmed; {@code false} to leave the result unchanged.
* @param indexExpr
* Index expression which is evaluated to pass to property's getter method at resolve time.
* @param bulk
* When true the bulk getter/setter will be used.
*
* @throws IllegalArgumentException
* if field reference is not recognized.
*/
public FieldReference(DataModelObject dmo,
String property,
boolean uppercase,
Supplier<NumberType> indexExpr,
boolean bulk)
{
this(dmo, property, uppercase, null, indexExpr, bulk);
}
/**
* Create a new field reference given a DMO proxy, a property name, and an index / index expression for the
* property. For extent fields, an indexed getter method will be used to resolve the field
* unless {@code bulk} is {@code true} and {@code index} is {@code null}.
*
* @param dmo
* DMO proxy.
* @param property
* Target property name.
* @param uppercase
* {@code true} to force a {@code character} value returned by {@link #getObject} to
* be uppercased/right-trimmed; {@code false} to leave the result unchanged.
* @param index
* Index to pass to property's getter method at resolve time. Can be <code>null</code> if there is
* no index or <code>indexExpr</code> is specified.
* @param indexExpr
* Index expression which is evaluated to pass to property's getter method at resolve time. Not
* used is <code>index</code> is not <code>null</code>.
* @param bulk
* When the reference is an EXTENT field and {@code index} is {@code null} the bulk
* getter/setter will be used.
*
* @throws IllegalArgumentException
* if field reference is not recognized.
*/
public FieldReference(DataModelObject dmo,
String property,
boolean uppercase,
NumberType index,
Supplier<NumberType> indexExpr,
boolean bulk)
{
RecordBuffer definition = ((BufferReference) dmo).definition();
RecordBuffer buffer = RecordBuffer.get(dmo);
if (definition != buffer)
{
property = definition.getBoundPropertyMappings().get(property);
dmo = buffer.getDMOProxy();
}
this.dmo = dmo;
this.proxy = dmo;
this.property = property;
this.dmoIface = buffer.getDMOInterface();
// get getter/setter either for an extent component or extent field
this.extent = null;
this.bulk = bulk;
// We mark this as false because there are more conditions that need to be taken into consideration.
this.isBulkComputed = false;
this.getter = null;
this.isIdAccessor = ID.equals(property);
this.setter = null;
this.index = index != null ? index.intValue() : null;
this.indexExpr = index != null ? null : indexExpr;
this.uppercase = uppercase;
}
/**
* Create a new field reference given a DMO interface and property name.
* A non-indexed getter method will be used to resolve the field. A DMO
* instance must be provided at resolution time, using the {@link
* #get(Record)} variant of the <code>get</code> method.
* <p>
* Note that the created instance can be used to read the related field only.
*
* @param database
* The associated database.
* @param dmoIface
* Interface of the target DMO.
* @param property
* Target property name
*/
FieldReference(Database database, Class<? extends DataModelObject> dmoIface, String property)
{
this.database = database;
this.property = property;
this.dmoIface = dmoIface;
this.getter = null;
this.isIdAccessor = ID.equals(property);
this.extent = null;
this.setter = null;
this.index = null;
this.indexExpr = null;
this.isSetterComputed = true;
this.isBulkComputed = true;
this.isExtentComputed = true;
Property propMeta = DmoMetadataManager.getDmoInfo(dmoIface).getFieldInfo(property);
if (propMeta._isCharacter && !propMeta.caseSensitive)
{
this.uppercase = true;
}
}
/**
* Specialized copy constructor which copies a non-indexed field reference and applies an
* index to the copy.
*
* @param fieldRef
* Original, non-indexed field reference. Although the field is non-indexed, this
* should be a reference to an extent field.
* @param index
* Zero-based index to be used as the subscript into the extent field represented by the copy.
* Use {@code null} for bulk access.
*/
public FieldReference(FieldReference fieldRef, Integer index)
{
this(fieldRef, index, fieldRef.extent, fieldRef.property);
}
/**
* Specialized copy constructor which copies a non-indexed field reference and applies an
* index to the copy.
*
* @param fieldRef
* Original, non-indexed field reference. Although the field is non-indexed, this
* should be a reference to an extent field.
* @param index
* Zero-based index to be used as the subscript into the extent field represented by the copy.
* Use {@code null} for bulk access.
* @param origExtent
* The original value of the extent. In case of denormalized fields the {@code fieldRef} might
* lose this value.
* @param origPropName
* The original property name. In case of denormalized fields the {@code fieldRef} might
* lose this value.
*/
public FieldReference(FieldReference fieldRef, Integer index, Integer origExtent, String origPropName)
{
RecordBuffer buffer = RecordBuffer.get((DataModelObject) fieldRef.dmo);
this.bulk = (origExtent != null) && (index == null);
this.isBulkComputed = true;
this.dmoIface = buffer.getDMOInterface();
this.database = fieldRef.database;
this.property = (origPropName == null || origPropName.isEmpty()) ? fieldRef.property : origPropName;
this.getter = null;
this.isIdAccessor = ID.equals(property);
this.setter = null;
this.uppercase = fieldRef.uppercase;
this.index = index;
this.indexExpr = null;
this.extent = origExtent;
this.dmo = fieldRef.dmo;
this.proxy = fieldRef.proxy;
this.legacyName = fieldRef.legacyName;
}
/**
* Create a new field reference given a property name and a DMO proxy. This method is for
* internal package use. The referenced field can only be read.
*
* @param buffer
* Record buffer which backs this field reference's DMO proxy.
* @param property
* Target property name.
*/
FieldReference(RecordBuffer buffer, String property)
{
this(buffer.getDatabase(), buffer.getDMOInterface(), property); // TODO: OM: caching Database obj?
this.proxy = buffer.getDMOProxy();
}
/**
* Convenience method to produce a <code>BaseDataType</code> instance
* initialized to the unknown value. An attempt is made to return an
* object of the correct type (i.e., the getter method's return type).
* If this fails for some reason, a new instance of <code>unknown</code>
* is returned.
*
* @param getter
* Getter method whose return type is used to determine the type
* of object to return.
*
* @return An object whose type matches the return type of the getter
* method, or an instance of <code>unknown</code>.
*/
static BaseDataType unknownValue(Method getter)
{
BaseDataType value;
if (getter.getName().equals("primaryKey"))
{
return new rowid();
}
try
{
value = (BaseDataType) BaseDataTypeFactory.instantiate(getter.getReturnType());
value.setUnknown();
}
catch (InstantiationException exc)
{
value = new unknown();
}
catch (ReflectiveOperationException exc)
{
throw new RuntimeException(exc);
}
return value;
}
/**
* Determine the data type that will be returned when this field reference
* is resolved.
*
* @return Class of the appropriate data type.
*/
public Class<?> getType()
{
return getGetter().getReturnType();
}
/**
* Resolve the field reference's value, bracketed by calls to the error
* manager to first enable, then disable, warning mode. This is meant to
* support the need to continue processing after resolution of a field
* reference fails, due to an empty record buffer.
*
* @return Current value of the target property in the referent, or the
* unknown value if this resolution fails.
*/
public BaseDataType getValue()
{
boolean warning = ErrorManager.isWarningMode();
if (!warning)
{
ErrorManager.warningModeEnable();
}
try
{
return get();
}
finally
{
if (!warning)
{
ErrorManager.warningModeDisable();
}
}
}
/**
* Resolve the current value of the referent field by invoking the getter
* method for the target property. Note that this may return the unknown
* value.
*
* @return Current value of the target property in the referent.
*/
public BaseDataType resolve()
{
return get();
}
/**
* Resolve the current value of the referent field by invoking the getter
* method for the target property. Note that this may return the unknown
* value.
*
* @return Current value of the target property in the referent.
*
* @throws ErrorConditionException
* if any persistence level error occurs getting the value from
* the DMO.
* @throws IllegalStateException
* if the backing property is not read accessible (i.e., no getter
* method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there
* is some other error at the invocation target.
*/
public BaseDataType get()
{
return (BaseDataType) getObject();
}
/**
* Resolve the current value of the referent field by invoking the getter method for the
* target property. This must be used only in case of extent fields.
*
* @return Current value of the target property in the referent.
*
* @throws ErrorConditionException
* if any persistence level error occurs getting the value from the DMO.
* @throws IllegalStateException
* if the backing property is not read accessible (i.e., no getter method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there is some other error at
* the invocation target.
*/
public BaseDataType[] getExtentValues()
{
if (getExtent() == null)
{
throw new RuntimeException("Trying to retrieve extent data for a non-extent field.");
}
return (BaseDataType[]) getObject();
}
/**
* Returns index used with the DMO indexed getter method.
*
* @return index used with the DMO indexed getter method or <code>-1</code> if the getter is
* not indexed.
*/
public int getIndex()
{
if (index != null)
{
return index;
}
else if (indexExpr != null)
{
NumberType num = indexExpr.get();
return num.isUnknown() ? -1 : num.intValue();
}
return -1;
}
/**
* Get the DMO or proxy instance associated with this field reference.
*
* @return DMO.
*/
public Object getDMO()
{
return dmo;
}
/**
* Get property name (converted field name).
*
* @return property name.
*/
public String getProperty()
{
return property;
}
/**
* Get legacy field name as it is defined in 4GL.
*
* @return field name.
*/
public String getLegacyFieldName()
{
if (legacyName == null)
{
// lazy field init here
RecordBuffer buffer = RecordBuffer.get((DataModelObject)dmo);
if (buffer != null)
{
legacyName = buffer.getDmoInfo().getFieldInfo(property).legacy;
}
}
return legacyName != null && legacyName.isEmpty() ? legacyName : property;
}
/**
* Set the current value of the referent field by invoking the setter
* method for the target property.
*
* @param value
* New value to set into the field represented by this object.
*
* @throws ErrorConditionException
* if this field reference represents an element in an array, but
* the index provided at construction is the unknown value;
* if any persistence-level error occurs setting the value into
* the DMO, including validation errors.
* @throws IllegalStateException
* if the backing property is not write accessible (i.e., no
* setter method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there
* is some other error at the invocation target.
*/
public void set(BaseDataType value)
{
set(new BaseDataType[] { value });
}
/**
* Set the current value of the referent field by invoking the setter method for the target
* property.
* <p>
* For non-extent fields, the value is placed on the first position in the array. For extent
* fields, the value is the actual value which needs to be set for the array.
*
* @param value
* New value to set into the field represented by this object.
*
* @throws ErrorConditionException
* if this field reference represents an element in an array, but the index provided
* at construction is the unknown value; if any persistence-level error occurs setting
* the value into the DMO, including validation errors.
* @throws IllegalStateException
* if the backing property is not write accessible (i.e., no setter method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there is some other error at
* the invocation target.
*/
public void set(BaseDataType[] value)
{
Method setter = getSetter();
if (setter == null)
{
throw new IllegalStateException(
property + " property is not write-accessible [" +
dmo.getClass().getName() + "]");
}
Object[] args;
// if we are not extent or if we have a subscript, then the value is not an array
if (getExtent() == null || hasIndex())
{
args = new Object[] { value[0] };
}
else
{
args = new Object[] { value };
}
// Handle extent fields.
if (hasIndex())
{
int idx = getIndex();
// An unknown value index constitutes an error.
if (idx == -1)
{
ErrorManager.recordOrThrowError(
26,
"Array subscript 0 is out of range");
return;
}
// Set up arguments, including index.
args = new Object[] { idx, args[0] };
}
// Invoke the setter method on the proxy.
try
{
Utils.invoke(setter, dmo, args);
}
catch (InvocationTargetException exc)
{
handleException(exc);
Throwable cause = exc.getCause();
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Error setting value into field", cause);
}
if (cause instanceof NumberedException)
{
ErrorManager.recordOrThrowError((NumberedException) cause);
}
else if (cause instanceof ConditionException)
{
throw (ConditionException) cause;
}
else
{
// represents programming error
throw new RuntimeException(cause);
}
}
catch (IllegalAccessException | IllegalArgumentException exc)
{
// represents programming or JVM configuration error
throw new RuntimeException(exc);
}
}
/**
* Return a string representation of this field reference, in the form:
* <pre>
* alias.property
* </pre>
* or, if indexed:
* <pre>
* alias.property[index]
* </pre>
* If the field reference is to a case-insensitive character field, the result will be
* enclosed in the <code>upper()</code> function.
*
* @return String representation of this object. This may be the empty string if the field
* reference has not yet been realized (i.e., no DMO has been assigned).
*/
public String toString()
{
if (dmo == null)
{
// field reference is unrealized
return ((BufferImpl) proxy).doGetName() + "." + property;
}
String alias = getParentBuffer().getDMOAlias();
StringBuilder buf = new StringBuilder();
if (uppercase)
{
buf.append("upper(");
}
buf.append(alias);
buf.append('.');
buf.append(property);
int i = getIndex();
if (i >= 0)
{
buf.append('[');
buf.append(i);
buf.append(']');
}
if (uppercase)
{
buf.append(")");
}
return buf.toString();
}
/**
* Get full converted name of the associated field in the format {@code alias_name.field_name}.
* If this is a component of an extent field, the index is also added:
* {@code alias_name.field_name[index]}.
*
* @return see above.
*/
public String getFullFieldName()
{
StringBuilder sb = new StringBuilder();
String legacyName = getParentBuffer().getLegacyName();
sb.append(legacyName);
sb.append('.');
sb.append(property);
if (getExtent() != null && hasIndex())
{
if (index != null)
{
int idx = getIndex();
if (idx != -1)
{
sb.append('[').append(idx).append(']');
}
}
else
{
// indexExpr != null
sb.append("[expr]");
}
}
return sb.toString();
}
/**
* Get the interface of the DMO associated with this field reference.
*
* @return DMO interface.
*/
public Class<? extends DataModelObject> getDMOInterface()
{
return (Class<? extends DataModelObject>) getGetter().getDeclaringClass();
}
/**
* Indicate whether resolved character value will be uppercased/right-trimmed.
*
* @return Value of flag.
*/
public boolean isUpperCase()
{
return uppercase;
}
/**
* Returns the record buffer which contains the referent DMO.
* <p>
* <strong>Note:</strong> this method only should be invoked on instances of this class which
* were constructed with a DMO proxy (as opposed to a DMO implementation class instance).
*
* @return See above
*
* @throws NullPointerException
* if this field reference was not constructed with a DMO proxy instance.
*/
public RecordBuffer getParentBuffer()
{
if (proxy == null)
{
throw new NullPointerException("DMO proxy not available");
}
if (buffer == null)
{
buffer = RecordBuffer.get(proxy);
}
return buffer;
}
/**
* Returns <code>true</code> if index is specified (either as a scalar or as an expression).
*
* @return <code>true</code> if index is specified (either as a scalar or as an expression).
*/
public boolean hasIndex()
{
return index != null || indexExpr != null;
}
/**
* Return field's extent, if field reference is to an extent field, else <code>null</code>.
*
* @return Field's extent or <code>null</code>.
*/
public Integer getExtent()
{
if (!isExtentComputed)
{
isExtentComputed = true;
extent = getExtent(dmoIface, property);
}
return extent;
}
/**
* Test the equality with another object.
*
* @param o
* The object to test against.
*
* @return {@code true} if both objects refer the same field.
*/
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null || getClass() != o.getClass())
{
return false;
}
FieldReference that = (FieldReference) o;
return uppercase == that.uppercase &&
Objects.equals(property, that.property) &&
Objects.equals(getExtent(), that.getExtent()) &&
Objects.equals(index, that.index) &&
Objects.equals(indexExpr, that.indexExpr) &&
Objects.equals(dmo, that.dmo);
}
/**
* Compute the hash code for this object.
*
* @return the hash code for this object.
*/
@Override
public int hashCode()
{
return Objects.hash(property, getExtent(), index, indexExpr, dmo, uppercase);
}
/**
* Resolve the current value of the referent field by invoking the getter
* method for the target property, using the given DMO as the target.
* Note that this may return <code>null</code> (or the unknown value for
* <code>BaseDataTypes</code> return types).
*
* @param dmo
* Target object which is used as the method's referent.
*
* @return Current value of the target property in the referent or
* <code>null</code> if no value exists.
*
* @throws ErrorConditionException
* if any persistence level error occurs getting the value from
* the DMO.
* @throws IllegalStateException
* if the backing property is not read accessible (i.e., no getter
* method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there
* is some other error at the invocation target.
*/
Object get(DataModelObject dmo)
{
this.dmo = dmo;
return getObject();
}
/**
* Resolve the current value of the referent field by invoking the getter
* method for the target property, using the given DMO as the target.
* Note that this may return <code>null</code> (or the unknown value for
* <code>BaseDataTypes</code> return types).
*
* @param dmo
* Target object which is used as the method's referent.
*
* @return Current value of the target property in the referent or
* <code>null</code> if no value exists.
*
* @throws ErrorConditionException
* if any persistence level error occurs getting the value from
* the DMO.
* @throws IllegalStateException
* if the backing property is not read accessible (i.e., no getter
* method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there
* is some other error at the invocation target.
*/
Object get(Record dmo)
{
this.dmo = dmo;
return getObject();
}
/**
* Resolve the current value of the referent field by invoking the getter
* method for the target property, using the given DMO as the target.
* Note that this may return <code>null</code> (or the unknown value for
* <code>BaseDataTypes</code> return types).
* <p>
* Unlike the {@link #get(com.goldencode.p2j.persist.Record)} method, it
* doesn't make this field reference use the passed DMO for future calls.
*
* @param dmo
* Target object which is used as the method's referent.
*
* @return Current value of the target property in the referent or
* <code>null</code> if no value exists.
*
* @throws ErrorConditionException
* if any persistence level error occurs getting the value from
* the DMO.
* @throws IllegalStateException
* if the backing property is not read accessible (i.e., no getter
* method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there
* is some other error at the invocation target.
*/
Object getTemp(Record dmo)
{
Object oldDmo = this.dmo;
try
{
return get(dmo);
}
finally
{
this.dmo = oldDmo;
}
}
/**
* Resolve the current value of the referent field by invoking the getter
* method for the target property, using the given DMO as the target.
* Note that this may return <code>null</code> (or the unknown value for
* <code>BaseDataTypes</code> return types).<p>
* Unlike the {@link #get(com.goldencode.p2j.persist.DataModelObject)} method, it
* doesn't make this field reference use the passed DMO for future calls.
*
* @param dmo
* Target object which is used as the method's referent.
*
* @return Current value of the target property in the referent or
* <code>null</code> if no value exists.
*
* @throws ErrorConditionException
* if any persistence level error occurs getting the value from
* the DMO.
* @throws IllegalStateException
* if the backing property is not read accessible (i.e., no getter
* method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there
* is some other error at the invocation target.
*/
Object getTemp(DataModelObject dmo)
{
Object oldDmo = this.dmo;
try
{
return get(dmo);
}
finally
{
this.dmo = oldDmo;
}
}
/**
* Create an instance of unknown value of the correct type for this field reference.
*
* @return Unknown value.
*/
BaseDataType unknownValue()
{
return FieldReference.unknownValue(getGetter());
}
/**
* Look up the extent for the specified property and interface. Introspection is used
* the first time this method is invoked for a new DMO type; thereafter, the extent is
* retrieved from a cache.
*
* @param dmoIface
* Referent DMO interface.
* @param property
* Name of target property (i.e., field).
*
* @return Extent, or <code>null</code> if <code>property</code> does not match any
* property name in the DMO API or is not an extent property.
*/
private Integer getExtent(Class<? extends DataModelObject> dmoIface, String property)
{
Map<String, Integer> propMap = PropertyHelper.extentsByProperty(dmoIface);
return propMap.get(property);
}
/**
* Look up the getter method for the specified property and interface. Introspection is used
* the first time this method is invoked for a new DMO type; thereafter, the getter is
* retrieved from a cache.
* <p>
* For the reserved {@code id} property, {@link Record} interface is substituted for
* {@code dmoIface}.
* <p>
* For extent properties the method is capable to return the indexed or the non-indexed getter.
*
* @return Getter method, or {@code null} if {@code property} does not match any property name
* in the DMO API.
*/
private Method getGetter()
{
if (isGetterComputed)
{
return getter;
}
isGetterComputed = true;
if (!isBulkComputed)
{
isBulkComputed = true;
bulk = bulk && (getExtent() != null) && (index == null);
}
Map<String, Method> propMap;
if (bulk)
{
propMap = PropertyHelper.bulkLegacyGettersByProperty(dmoIface);
}
else
{
Class<?> iface = (ID.equals(property) ? Persistable.class : dmoIface);
propMap = PropertyHelper.allGettersByProperty(iface);
}
Method method = propMap.get(property);
if (method != null)
{
getter = method;
return getter;
}
if (property.startsWith("_"))
{
for (Method m : TempRecord.class.getMethods())
{
if (m.getReturnType() != void.class &&
m.getReturnType() != Void.class &&
m.getName().equals(property))
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Property '" + property + "' accessed through 'TempRecord' " +
"interface instead of converted legacy method.");
}
getter = m;
return getter;
}
}
}
// sanity to make sure we have a getter (only getter for primary key, which is read-only)
if (getter == null)
{
throw new IllegalArgumentException(
"Invalid field reference [" + getParentBuffer().getDMOName() + "." +
property + (index == null ? "" : ":" + index.toString()) +
"]");
}
if (uppercase && !getter.getReturnType().equals(character.class))
{
throw new IllegalArgumentException(
"Only a character-based FieldReference can be set to uppercase");
}
return null;
}
/**
* Look up the setter method for the specified property and interface. Introspection is used
* the first time this method is invoked for a new DMO type; thereafter, the setter is
* retrieved from a cache.
* <p>
* For extent properties the method is capable to return the indexed or the non-indexed setter.
*
* @return Setter method, or {@code null} if {@code property} does not match any property name
* in the DMO API.
*/
private Method getSetter()
{
if (isSetterComputed)
{
return setter;
}
isSetterComputed = true;
if (!isBulkComputed)
{
isBulkComputed = true;
bulk = bulk && (getExtent() != null) && (index == null);
}
Map<String, Method> propMap = bulk
? PropertyHelper.bulkLegacySettersByProperty(dmoIface)
: PropertyHelper.allSettersByProperty(dmoIface);
Method method = propMap.get(property);
if (method != null)
{
setter = method;
return setter;
}
if (property.startsWith("_"))
{
for (Method m : TempRecord.class.getMethods())
{
if ((m.getReturnType() == void.class ||
m.getReturnType() == Void.class) &&
m.getName().equals(property))
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Property '" + property + "' accessed through 'TempRecord' " +
"interface instead of converted legacy method.");
}
setter = m;
return setter;
}
}
}
// sanity to make sure we have a setter
if (setter == null && !isIdAccessor)
{
throw new IllegalArgumentException(
"Invalid field reference [" + getParentBuffer().getDMOName() + "." +
property + (index == null ? "" : ":" + index.toString()) +
"]");
}
return null;
}
/**
* Resolve the current value of the referent field by invoking the getter method for the target property.
* <p>
* Note that this may return {@code null} (or the unknown value for {@code BaseDataTypes} return types).
*
* @return Current value of the target property in the referent or {@code null} if no value exists.
*
* @throws ErrorConditionException
* if any persistence level error occurs getting the value from the DMO.
* @throws IllegalStateException
* if the backing property is not read accessible (i.e., no getter method was found).
* @throws RuntimeException
* if the backing, getter method cannot be accessed or if there is some other error at the
* invocation target.
*/
private Object getObject()
{
Method getter = getGetter();
if (getter == null)
{
throw new IllegalStateException(property + " property is not read-accessible [" +
dmo.getClass().getName() + "]");
}
Object[] args;
// Handle extent fields.
if (hasIndex())
{
int idx = getIndex();
// An unknown value index propagates to an unknown value result.
if (idx == -1)
{
return unknownValue(getter);
}
// Set index argument.
args = new Object[] { idx };
}
else
{
args = null;
}
// invoke the getter method on the proxy and return the result
try
{
// special case when the surrogate field 'id' is evaluated (and dmo represents the
// proxy, not the underlying Record object)
if (isIdAccessor && dmo == proxy)
{
return ((Buffer) dmo).rowID(); // use the wider rowid datatype
}
Object result = Utils.invoke(getter, dmo, args);
if (!isIdAccessor && !(result instanceof BaseDataType))
{
Class<?> cls = result.getClass();
if (cls == Long.class)
{
result = new int64((Long) result);
}
else if (cls == Integer.class)
{
result = new integer((Integer) result);
}
else if (cls == String.class)
{
result = new character((String) result);
}
}
// if marked for uppercase, then we must uppercase and right trim the result. This is a
// special case where the result will be used in Progress-compatible comparisons.
if (uppercase)
{
character c = (character) result;
result = TextOps.toUpperCase(TextOps.rightTrim(c, " "));
}
if (result instanceof LargeObject)
{
((LargeObject) result).setFieldReference(this);
}
return result;
}
catch (InvocationTargetException exc)
{
handleException(exc);
Throwable cause = exc.getCause();
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Error getting value from field", cause);
}
if (cause instanceof NumberedException)
{
ErrorManager.recordOrThrowError((NumberedException) cause);
}
else if (cause instanceof ConditionException)
{
throw (ConditionException) cause;
}
else
{
// Represents programming error.
throw new RuntimeException(cause);
}
return unknownValue(getter);
}
catch (IllegalAccessException exc)
{
// Represents programming or JVM configuration error.
throw new RuntimeException(exc);
}
}
/**
* Handle a thread interruption or database connection error specially,
* otherwise return normally.
*
* @param exc
* Exception to be inspected.
*
* @see DBUtils#handleException(Database, Exception)
*/
private void handleException(Exception exc)
{
if (database == null && dmo instanceof DataModelObject)
{
RecordBuffer buffer = RecordBuffer.get((DataModelObject) dmo);
database = buffer.getDatabase();
}
DBUtils.handleException(database, exc);
}
}