PropertyMeta.java
/*
** Module : PropertyMeta.java
** Abstract : Keeps information about a property.
**
** Copyright (c) 2019-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20191001 First revision.
** OM 20191110 Added property type support and toString().
** 002 OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access.
** AIL 20201210 Added checker for reserved properties.
** 20201216 Added initial value getter.
** OM 20201120 Fixed value returned by getDecimals() method.
** OM 20220303 Exposed the property's format.
** OM 20220727 FieldId and PropertyId are different for denormalized extent fields.
** ECF 20221005 Added support for dynamic initial values.
** OM 20220914 Use MAX-WIDTH to generate custom sized varchar columns.
** 003 CA 20230523 Added a getter for the DMO property.
** 004 CA 20240320 Save the the property meta instance as back-reference in the orm.Property instance.
*/
/*
** 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.orm;
import java.util.function.*;
import com.goldencode.p2j.persist.orm.types.*;
import com.goldencode.p2j.util.*;
/**
* Metadata for the field within which a DMO data element is stored. Each datum is stored within
* a single element of a one-dimensional array. A scalar field takes up one slot in the array;
* an extent field takes up one slot per extent field element.
* <p>
* To keep the class hierarchy flat and fast, some information which only applies to certain
* data types (e.g., case-sensitivity) is maintained by this class. Fields are directly
* accessible to avoid method call overhead.
*/
public final class PropertyMeta
implements Comparable<PropertyMeta>
{
/** The dmo property which contains the data read from annotation. */
final Property dmoProperty;
/** Default initial (low-level) data value; will be a ({@code Supplier}) for a dynamic initializer. */
final Object initialValue;
/** Property ID */
private final int id;
/** Field extent (0 for scalar, including denormalized extent fields) */
private final int extent;
/** The handler for setting this data type as parameter to a {@code PreparedStatement}.*/
private final DataHandler dataHandler;
/** Number of properties of like extent; non-0 only in first instance in a series */
int seriesSize = 0;
/**
* One-based index of column in select clause or insert statement during load or insert.
* <p>
* In case of multi-column properties, this is the index of the first column. The other(s) come
* in sequence.
*/
int columnIndex = 0;
/**
* The number of columns in the table. When the object is read from database all
* {@code columnCount} columns are used to rehydrate the restore the object's internal data.
*/
int columnCount = 0;
/**
* The offset of the property in {@code data} array. For extent fields, this is the offset of
* the first element.
*/
int offset = -1;
/**
* The setter's name for this property. Its name be used by {@link DmoClass} to assemble the
* setter implementations on-the-fly.
*/
final String setterName;
/**
* The getter's name for this property. Its name be used by {@link DmoClass} to assemble the
* getter implementations on-the-fly.
*/
final String getterName;
/**
* Constructor which initializes the instance with the {@link ReservedProperty} details.
*
* @param prop
* The property details.
*/
PropertyMeta(ReservedProperty prop)
{
this(prop, "get" + prop.name, "set" + prop.name);
}
/**
* Constructor which accepts a {@code Property} from DMO interface and the name of the accessor methods
* associated with the property.
*
* @param prop
* The {@code Property} annotation to be analyzed for object attributes.
* @param getterName
* The name of the getter for this property.
* @param setterName
* The name of the setter for this property.
*/
PropertyMeta(Property prop, String getterName, String setterName)
{
prop.meta = this;
this.dmoProperty = prop; // prop comes with interned String members
this.id = prop.propId;
// set extent; denormalized fields (and only they) will have index > 0 and are treated
// as scalar for this purpose
boolean denorm = prop.index > 0;
this.extent = denorm ? 0 : prop.extent;
// set state that is only used by certain data types (defaults for others)
this.dataHandler = TypeManager.getDataHandler((Class<? extends BaseDataType>) prop._fwdType);
// save the name of the getters and setters for when we will create the DMO implementations
this.getterName = getterName.intern();
this.setterName = setterName.intern();
// assign this last, as the handler might need other information in this object assigned above
// N.B.: the data handler might return a Supplier instead of a static value, in the event of a
// dynamic initializer (e.g., "now", "today")
this.initialValue = prop.initialNull
? null
: dataHandler.initialValue(prop.initial, this);
}
/**
* Compare this {@code PropertyMeta} field to another. This method defines the natural sort of
* this object, which is first in ascending order by extent (0 for scalar fields, thus, they
* sort first). Fields of the same extent are sorted in ascending order by ID.
* <p>
* This sort does not match the legacy order of the fields, but is instead organized for the
* case where the fields are used within SQL statements, in which case they need to be in a
* well-known order.
*
* @param that
* {@code PropertyMeta} object with which to compare.
*
* @return Negative value if this instance should sort before {@code that} instance;
* 0 if they should sort equally;
* positive value if this instance should sort after {@code that} instance.
*/
@Override
public int compareTo(PropertyMeta that)
{
// first by extent, scalar fields first
int extDiff = this.extent - that.extent;
if (extDiff != 0)
{
return extDiff;
}
// next by id
return this.id - that.id;
}
/**
* Obtain the original id of this property from the DMO interface.
*
* @return the original id of this property from the DMO interface.
*/
public int getId()
{
return id;
}
/**
* Obtain the index 0-base of the property in the {@code data} array of the {@code BaseRecord}.
* In case on extent properties, this is the offset of the first element.
*
* @return the offset of the property in {@code data} array. For extent fields, this is the
* offset of the first element.
*/
public int getOffset()
{
return offset;
}
/**
* Obtain the FWD type of the field.
*
* @return the FWD type of the field.
*/
public Class<? extends BaseDataType> getType()
{
return (Class<? extends BaseDataType>) dmoProperty._fwdType;
}
/**
* Obtain numeric scale (precision in 4GL terms) (applies to {@code decimal} fields only) ad defined in
* the field declaration. The method returns 0 when no such declaration was encountered in ABL source, but
* the default value to be used in this case is 10.
*
* @return the scale/precision type of the {@code decimal} field.
*/
public int getDecimals()
{
// dmoProperty.scale stores the legacy scale, if defined. If not specified in ABL source, it will
// be 0. But the default value is 10 decimals.
return dmoProperty.scale != 0 ? dmoProperty.scale : 10;
}
/**
* Test whether this property is case sensitive. Only for {@code character} data this method is expected to
* return {@code true}.
*
* @return {@code true} if this is a case-sensitive character property.
*/
public boolean isCaseSensitive()
{
return dmoProperty.caseSensitive;
}
/**
* Get the maximum acceptable length for this {@code character} property.
*
* @return the maximum acceptable length for this {@code character} property. 0 otherwise, or not defined.
*/
public int getMaxWidth()
{
return dmoProperty.width;
}
/**
* Check whether this property has {@code character} type.
*
* @return {@code true} if the type of this property is {@code character} and {@code false}
* otherwise.
*/
boolean isCharacter()
{
return dmoProperty._isCharacter;
}
/**
* Check whether this property has {@code datetime-tz} type.
*
* @return {@code true} if the type of this property is {@code datetime-tz} and {@code false}
* otherwise.
*/
boolean isDateTimeTz()
{
return dmoProperty._isDatetimeTz;
}
/**
* Obtain the {@code DataHandler} used for setting a value of this property as a parameter
* in a prepared query. It will automatically handle {@code null} values.
*
* @return the {@code DataHandler} for values of this property.
*/
public DataHandler getDataHandler()
{
return dataHandler;
}
/**
* Retrieve the column name in the SQL table of this property.
*
* @return the name of the column of this property.
*/
public String getColumn()
{
return dmoProperty.column;
}
/**
* Retrieve the name of this property.
*
* @return the name of this property.
*/
public String getName()
{
return dmoProperty.name;
}
/**
* Retrieve the legacy name of this property.
*
* @return the legacy name of this property.
*/
public String getLegacyName()
{
return dmoProperty.legacy;
}
/**
* Retrieve the extent of this property.
*
* @return The number of elements in this extent property. If this is not an extent property, or
* it represents a denormalized extent property, 0 is returned.
*/
public int getExtent()
{
return extent;
}
/**
* Test whether this is a mandatory property.
*
* @return {@code true} for mandatory fields.
*/
public boolean isMandatory()
{
return dmoProperty.mandatory;
}
/**
* Check if this property meta is for a reserved property.
*
* @return {@code true} if the referenced property is reserved.
*/
public boolean isReserved()
{
return dmoProperty.propId < 0;
}
/**
* Retrieve the original extent of this property, which differs from @extent@ only for denormalized
* extent fields. The actual extent is returned (not 0) for denormalized extent fields.
*
* @return The number of elements in this extent property. If this is not an extent property, 0 is
* returned.
*/
public int getOriginalExtent()
{
return dmoProperty.extent;
}
/**
* Retrieve the original name of this property, for extent fields which were denormalized.
* <p>
* Only denormalized properties will have a non-empty string for this value. All other properties,
* scalar or otherwise, will report empty string.
*
* @return Original property name for a denormalized extent field.
*/
public String getOriginalName()
{
return dmoProperty.original;
}
/**
* Retrieve the original initial value of this property. Note that if the initial value is a dynamic
* function (e.g., date "today", datetime/datetimetz "now"), the value returned will be the result of
* invoking that function at the moment this method is called.
*
* @return The initial value for this property.
*/
public Object getInitialValue()
{
return initialValue instanceof Supplier ? ((Supplier) initialValue).get() : initialValue;
}
/**
* Retrieve the format used by this property.
*
* @return the format used by this property.
*/
public String getFormat()
{
return dmoProperty.format;
}
/**
* Get the {@link #dmoProperty} instance.
*
* @return See above.
*/
public Property getDmoProperty()
{
return dmoProperty;
}
/**
* Returns a short text describing this object. Used for debugging.
*
* @return a short description of this object to be used when debugging.
*/
@Override
public String toString()
{
return "Meta{" + dmoProperty.name + "(SQL=" + dmoProperty.column + " ABL=" + dmoProperty.legacy + "):" +
dmoProperty._fwdType.getSimpleName() + (extent == 0 ? "" : ":" + extent) +
" series-idx=" + columnIndex + "}";
}
}