Property.java

/*
** Module   : Property.java
** Abstract : Enhance DMO manipulation performance by caching data from Property annotation.
**
** Copyright (c) 2020-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 OM  20200110 First revision.
** 002 SVL 20201111 Convert "reserved null" to true null for LABEL and COLUMN-LABEL.
**     OM  20201218 Fixed implementation for error and rejected attributes/hidden fields.
**     CA  20210831 Track the getter method for TempRecord reserved properties.
**     OM  20220722 Added property identifier.
**     CA  20221114 Added flag to mark a denormalized extent property.
**     OM  20220706 The static constant is prefered to String literal.
** 003 HC  20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
**                  for performance.
**     CA  20221114 Added flag to mark a denormalized extent 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 com.goldencode.p2j.persist.*;
import com.goldencode.p2j.util.*;
import java.lang.reflect.*;

/**
 * The purpose of this class is to enhance DMO manipulation performance. It does this in several ways:
 * 
 * <ul>
 *    <li>caches the values from DMO {@link com.goldencode.p2j.persist.annotation.Property} annotation,
 *          avoiding this way the super cost of slow access via the slow map implementation and
 *          {@code sun.reflect.annotation.AnnotationInvocationHandler#hashCodeImpl};</li>
 *    <li>carries extra information not available in annotation: the annotated method and the type
 *          of the property;</li>
 *    <li>computes and caches some boolean properties ({@code _isCharacter}, {@code _isDatetimeTz})
 *          so the callers do not need to compare the type each time;</li>
 *    <li>it makes sure the {@code Property} annotation is processed only once. This is especially important
 *          as some costing operations like {@code intern()} -ing the primary string attributes happens
 *          a single time, when the target DMO is loaded (usually at server startup);</li>
 *    <li>the class is immutable. This allows all members to be public and be accessed directly, without
 *          the extra CPU clocks for invoking a (virtual) method.</li>
 * </ul>
 * 
 * Notes:
 * <br>* A single object of this type is created for a specified field, when the DMO interface is parsed
 *       for the first time. Having two or more of these objects associated with same property of a DMO
 *       is a programmatic error.
 * <br>* These objects do not hold a reference to their parent {@link DmoMeta}. If ever needed in future it
 *       can be easily added.
 * <br>* Objects of this class isolate the access to DMO interfaces and
 *       {@code com.goldencode.p2j.persist.annotation} package.
 */
public class Property
{
   /**
    * The id of this field in the sequence of the fields of the buffer. It is needed by the
    * dynamic {@code bufferField(int)} attribute of the {@code BufferImpl} for
    * emulating the {@code BUFFER-FIELD(integer)} buffer handle attribute.
    */
   public final int id;
   
   /**
    * The identifier of the property. In case there are denormalized fields in the parent table,
    * {@code propId != id}, otherwise they are equals.
    */
   public final int propId;
   
   /** The name of the DMO property. */
   public final String name;
   
   /** The name of the legacy field associated with the DMO property. */
   public final String legacy;

   /** The name of the legacy field associated with the DMO property in lower case. */
   public final String legacyLower;

   /** The name of the column in the SQL database, associated with the DMO property. */
   public final String column;
   
   /**
    * Extent for array (extent) fields (defaults to 0 for scalar fields). Will be set to the
    * original extent for both normalized and denormalized fields.
    */
   public final int extent;
   
   /** Flag indicating if this extent property has been denormalized. */
   public final boolean denormalized;
   
   /**
    * One-based index in the legacy extent field associated with a DMO property. Will be set only
    * for field with custom extents.
    */
   public final int index;
   
   /** Mandatory FORMAT attribute of this field. */
   public final String format;
   
   /** FORMAT-SA attribute of this field. */
   public final String format_sa;
   
   /** HELP attribute of this field. */
   public final String help;
   
   /** HELP-SA attribute of this field. */
   public final String help_sa;
   
   /** INITIAL attribute of this field. */
   public final String initial;
   
   /** INITIAL-SA attribute of this field. */
   public final String initial_sa;
   
   /** INITIAL-NULL pseudo-attribute of this field. */
   public final boolean initialNull;
   
   /** LABEL attribute of this field. */
   public final String label;
   
   /** LABEL-SA option of this table. */
   public final String label_sa;
   
   /** COLUMN-LABEL attribute of this field. */
   public final String columnLabel;
   
   /** COLUMN-LABEL-SA attribute of this field. */
   public final String columnLabel_sa;
   
   /** MANDATORY attribute of this field. */
   public final boolean mandatory;
   
   /** Mandatory ORDER attribute from the original schema. */
   public final int order;
   
   /** POSITION attribute from the original schema. */
   public final int position;
   
   /** VALIDATE-MESSAGE attribute of this field. */
   public final String validateMessage;
   
   /** VALMSG-SA option of this table. */
   public final String valmsg_sa;
   
   /** VALIDATE-EXPRESSION attribute of this field. */
   public final String validateExpression;
   
   /** DESCRIPTION attribute of this field. */
   public final String desc;
   
   /** MAX-WIDTH attribute of this field. -1 if not defined  */
   public final int width;
   
   /** Original property name (not exposed in DMO) for denormalized field with extent. */
   public final String original;
   
   /** Flag the case sensitive character fields. Other datatypes are not affected. */
   public final boolean caseSensitive;
   
   /** Scale for decimal fields (decimals, precision, in 4GL terms). Other datatypes are not affected. */
   public final int scale;
   
   /** Flag indicating the field should not be included in serialized output. */
   public final boolean serializeHidden;
   
   /** Name of field in serialized output. */
   public final String serializeName;
   
   /** XML schema data type of field. */
   public final String xmlDataType;
   
   /** Name of element or attribute representing field in XML output. */
   public final String xmlNodeName;
   
   /** Node type of field in XML output. */
   public final String xmlNodeType;
   
   /** Code page of the CLOB field. */
   public final String codePage;
   
   /** Name of the source field specified by LIKE option (either at the table- or field-level). */
   public final String like;
   
   /** VIEW-AS field property */
   public final String viewAs;
   
   /** CAN-READ option of this field. */
   public final String canRead;
   
   /** CAN-WRITE option of this field. */
   public final String canWrite;
   
   /** Lowercase type of this property. */
   public final String _ablType;
   
   /** The FWD class that of the property. */
   public final Class<? /*extends BaseDataType*/> _fwdType;
   
   /** Quick test if this property is a character (maybe add other similar). */
   public final boolean _isCharacter;
   
   /** Quick test if this property is a character (maybe add other similar). */
   public final boolean _isDatetimeTz;
   
   /** The method which was annotated. This is only visible in orm package. */
   public final Method annMethod; 
   
   /** The back-reference to the meta structure. */
   PropertyMeta meta;
   
   /**
    * The constructor that initialize this immutable object.
    * 
    * @param   source
    *          The {@code Property} annotation taken from DMO interface.
    * @param   annotatedMethod
    *          The method which was annotated.
    */
   Property(com.goldencode.p2j.persist.annotation.Property source, Method annotatedMethod)
   {
      this.id = source.id();
      this.denormalized = source.propertyId() != Integer.MIN_VALUE;
      this.propId = !this.denormalized ? this.id : source.propertyId();
      this.name = source.name().intern();
      this.legacy = source.legacy().intern();
      this.legacyLower = this.legacy.toLowerCase().intern();
      this.column = source.column().intern();
      this.extent = source.extent();
      this.index = source.index();
      this.format = source.format();
      this.format_sa = source.format_sa();
      this.help = source.help();
      this.help_sa = source.help_sa();
      this.initial = source.initial().intern();
      this.initial_sa = source.initial_sa();
      this.initialNull = source.initialNull();
      this.label = convertReservedNull(source.label());
      this.label_sa = source.label_sa();
      this.columnLabel = convertReservedNull(source.columnLabel());
      this.columnLabel_sa = source.columnLabel_sa();
      this.mandatory = source.mandatory();
      this.order = source.order();
      this.position = source.position();
      this.validateMessage = source.validateMessage();
      this.valmsg_sa = source.valmsg_sa();
      this.validateExpression = source.validateExpression();
      this.desc = source.desc();
      this.width = source.width();
      this.original = source.original().intern();
      this.caseSensitive = source.caseSensitive();
      this.scale = source.scale();
      this.serializeHidden = source.serializeHidden();
      this.serializeName = source.serializeName();
      this.xmlDataType = source.xmlDataType();
      this.xmlNodeName = source.xmlNodeName();
      this.xmlNodeType = source.xmlNodeType();
      this.codePage = source.codePage();
      this.like = source.like();
      this.viewAs = source.viewAs();
      this.canRead = source.canRead();
      this.canWrite = source.canWrite();
      
      annMethod = annotatedMethod;
      this._fwdType = annotatedMethod.getReturnType();
      _isCharacter = _fwdType == character.class;
      _isDatetimeTz = _fwdType == datetimetz.class;
      _ablType = getAblType();
   }
   
   /**
    * Creates a new {@code Property} object. This is a constructor dedicated to building reserved properties
    * which are not declared in DMO interface.
    *
    * @param   id
    *          The id of this field in the sequence of the fields of the buffer.
    * @param   name
    *          The name of the DMO property.
    * @param   legacy
    *          The name of the legacy field associated with the DMO property.
    * @param   column
    *          The name of the column in the SQL database, associated with the DMO property.
    * @param   mandatory
    *          Flag for not-null fields.
    * @param   type
    *          property type
    * @param   initialNull
    *          The property has {@code null} as default value.
    * @param   mthd
    *          The getter method for this property.
    */
   Property(int id,
            String name,
            String legacy,
            String column,
            boolean mandatory,
            Class<?> type,
            boolean initialNull,
            Method mthd)
   {
      this.id = this.propId = id; // this constructor is only invoked for reserved properties which have
                                  // negative and equal values for both [id] and [propId]
      this.denormalized = false;
      this.name = name.intern();
      this.legacy = (legacy == null) ? null : legacy.intern();
      this.legacyLower = (legacy == null) ? null : this.legacy.toLowerCase().intern();
      this.column = column.intern();
      this.extent = 0;
      this.index = 0;
      this.format = "";
      this.initial = "";
      this.initialNull = initialNull;
      this.label = "";
      this.columnLabel = "";
      this.mandatory = mandatory;
      this.order = 0;
      this.position = 0;
      this.validateMessage = "";
      this.validateExpression = "";
      this.original = "";
      this.caseSensitive = false;
      this.scale = 0;
      this.serializeHidden = false;
      this.serializeName = "";
      this.xmlDataType = "";
      this.xmlNodeName = "";
      this.xmlNodeType = XmlNode.ELEMENT;
      this.codePage = "";
      this.like = "";
      this.help = "";
      this.format_sa = "";
      this.initial_sa = "";
      this.label_sa = "";
      this.columnLabel_sa = "";
      this.valmsg_sa = "";
      this.help_sa = "";
      this.viewAs = "";
      this.canRead = "*";
      this.canWrite = "*";
      this.desc = null;
      this.width = 0;
      
      annMethod = mthd;
      this._fwdType = type;
      _isCharacter = (type == character.class);
      _isDatetimeTz = false;                    // no dtzs here
      _ablType = getAblType();
   }
   
   /**
    * Get the {@link #meta property meta} instance.
    * 
    * @return   See above.
    */
   public PropertyMeta getMeta()
   {
      return meta;
   }
   
   /**
    * Converts "reserved null" to true <code>null</code>. Returns <code>null</code> if the <code>value</code>
    * equals "reserved null" for {@link com.goldencode.p2j.persist.annotation.Property} annotation. Otherwise
    * returns the <code>value</code> itself.
    *
    * @param  value
    *         Value to check.
    *
    * @return see above.
    */
   private String convertReservedNull(String value)
   {
      return com.goldencode.p2j.persist.annotation.Property.NULL_STRING.equals(value) ? null : value;
   }
   
   /**
    * Computes the ABL type name from the Java type. Usually this is the same as java class (because FWD makes
    * an exception here from standard class naming), but there is also an exception (@code datetime-tz)
    * because the character {@code -} is not permitted in a Java identifier.
    * 
    * @return  The ABL type name mapped by the specified {@code type}.
    */
   private String getAblType()
   {
      // a bit of a hack to get the proper ABL type name 
      String strType = _isDatetimeTz ? "datetime-tz" : _fwdType.getSimpleName().toLowerCase();
      return strType.intern();
   }
   
   /**
    * Obtain a short description of the object used in debugging.
    *
    * @return  a short description of the object used in debugging.
    */
   @Override
   public String toString()
   {
      StringBuilder sb = new StringBuilder();
      sb.append("ORM-Property{").append(name);
      if (extent > 0)
      {
         sb.append("[").append(extent).append("]");
      }
      sb.append(":").append(_ablType).append(" OE=").append(legacy)
        .append(" SQL=").append(column).append(" fid=").append(id).append(" pid=").append(propId).append("}");
      return sb.toString();
   }
}