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