# 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 + "}";
       }
    }