PropertyReference.java

/*
** Module   : PropertyReference.java
** Abstract : OO property reference which allows deferred read/write access
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20190524 Created initial version.
** 002 CA  20190604 Added missing javadocs.
** 003 CA  20190620 Fixed initialization order.
** 004 CA  20190724 When calling the setter, the value must be converted to the property's type, 
**                 if is not the same type.
** 005 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.
** 006 CA  20240215 Fixed issues using CALL property/field references as arguments, associated with a 
**                  DATASET/TABLE-HANDLE parameter. 
** 007 CA  20240520 Fixed validation of the DYNAMIC-PROPERTY arguments for statement and function.
** 008 PBB 20250509 Modified implementation of DYNAMIC-PROPERTY function to introduce support for
**                  DYNAMIC-INVOKE builtin function that returns or might return an extent.
** 009 PBB 20250522 Added support for DYNAMIC-PROPERTY statement (setter) mostly related to extents.
*/

/*
** 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.util;

import java.lang.reflect.*;
import java.util.*;

import com.goldencode.p2j.oo.lang.*;

/**
 * Dynamic access to a legacy property.
 */
public final class PropertyReference
implements Resolvable,
           Accessor
{
   /** The fully qualified legacy class name for this property, in case of static properties. */
   private String className = null;
   
   /** The object instance, in case of instance properties. */
   private object<? extends _BaseObject_> instance = null;
   
   /** The legacy property name to access. */
   private String property = null;
   
   /** The index, in case of extent properties. */
   private int64 index = null;
   
   /** The property's extent. */
   private Integer extent = null;

   /** The legacy type to which the property belongs. */
   private Class<? extends _BaseObject_> type;

   /** The property's data type. */
   private Class<? extends BaseDataType> propType;

   /** The associated getter for the property. */
   private InternalEntry defaultGetter = null;

   /** The associated setter for the property. */
   private InternalEntry defaultSetter = null;

   /** The list of all associated setters for the property */
   private List<InternalEntry> settersList = null;

   /** In case of an instance property, the legacy reference. */
   private _BaseObject_ reference;
   
   /**
    * Constructor.
    * 
    * @param   className
    *          The class name in case of static property access.
    * @param   property
    *          Name of class property member.
    */
   public PropertyReference(String className, String property)
   {
      this.className = className;
      this.property = property;
      
      init();
   }
   
   /**
    * Constructor.
    * 
    * @param   instance
    *          The class instance, for instance property access.
    * @param   property
    *          Name of class property member.
    */
   public PropertyReference(object<? extends _BaseObject_> instance, String property)
   {
      this.instance = new object<>(instance);
      this.property = property;
      
      init();
   }
   
   /**
    * Constructor.
    * 
    * @param   className
    *          The class name in case of static property access.
    * @param   property
    *          Name of class property member.
    * @param   index
    *          Index to pass to property's getter method at resolve time.
    */
   public PropertyReference(String className, String property, NumberType index)
   {
      this.className = className;
      this.property = property;
      this.index = new int64(index);
      
      init();
   }
   
   /**
    * Constructor.
    * 
    * @param   className
    *          The class name in case of static property access.
    * @param   property
    *          Name of class property member.
    * @param   index
    *          Index to pass to property's getter method at resolve time.
    */
   public PropertyReference(String className, String property, int index)
   {
      this.className = className;
      this.property = property;
      this.index = new int64(index);
      
      init();
   }
   
   /**
    * Constructor.
    * 
    * @param   instance
    *          The class instance, for instance property access.
    * @param   property
    *          Name of class property member.
    * @param   index
    *          Index to pass to property's getter method at resolve time.
    */
   public PropertyReference(object<? extends _BaseObject_> instance, String property, int index)
   {
      this.instance = new object<>(instance);
      this.property = property;
      this.index = new int64(index);
      
      init();
   }
   
   /**
    * Constructor.
    * 
    * @param   instance
    *          The class instance, for instance property access.
    * @param   property
    *          Name of class property member.
    * @param   index
    *          Index to pass to property's getter method at resolve time.
    */
   public PropertyReference(object<? extends _BaseObject_> instance, String property, NumberType index)
   {
      this.instance = new object<>(instance);
      this.property = property;
      this.index = new int64(index);
      
      init();
   }

   /**
    * Get the property accessor index.
    * 
    * @return   The {@link #index}.
    */
   public Integer getIndex()
   {
      return index.isUnknown() ? null : index.toJavaIntegerType();
   }
   
   /**
    * Get the property's extent.
    * 
    * @return   The {@link #extent}.
    */
   public Integer getPropertyExtent()
   {
      return extent;
   }
   
   /**
    * Resolve the property's type, the legacy (default) getter and setter,
    * and the list of all the setters.
    */
   private void init()
   {
      this.type = className == null ? instance.ref.getClass() : ObjectOps.resolveClass(className);
      this.reference = instance == null ? null : instance.ref();
      
      this.extent = SourceNameMapper.findLegacyPropertyExtent(type, property);
      this.defaultGetter = SourceNameMapper.findLegacyAccessor(type, property, true, extent != null, index != null);
      this.defaultSetter = SourceNameMapper.findLegacyAccessor(type, property, false, extent != null, index != null);

      if (this.extent != null)
      {
         // Save all the setters for an extent property for choosing the right one at runtime
         this.settersList = SourceNameMapper.findLegacySetters(type, property);
      }

      if (this.defaultSetter == null && index != null && extent == null)
      {
         // defaultSetter calls with a specified index for a non-extent property will ignore the index
         this.defaultSetter = SourceNameMapper.findLegacyAccessor(type, property, false, false, false);
         this.index = null;
      }
      
      if (this.defaultSetter != null)
      {
         this.defaultSetter.getMethod().setAccessible(true);
      }
      
      if (this.defaultGetter != null)
      {
         this.defaultGetter.getMethod().setAccessible(true);
         this.propType = (Class<? extends BaseDataType>) this.defaultGetter.getMethod().getReturnType();
      }
      else if (this.defaultSetter != null)
      {
         Parameter p = this.defaultSetter.getParameter(0);
         this.propType = BaseDataType.fromTypeName(p.getType());
      }
      
   }

   /**
    * Get a copy of the referenced object's current runtime value (this does not return the 
    * original instance itself).
    *
    * @return   The copy of the current value.
    */
   @Override
   public BaseDataType get()
   {
      OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();

      try
      {
         Object result = (index == null ? defaultGetter.getMethod().invoke(reference)
                                        : defaultGetter.getMethod().invoke(reference, index));

         if (result.getClass().isArray())
         {
            String errMsg = "An array was specified in an expression, " +
                           "on the right-hand side of an assignment, " +
                           "or as a parameter when no array is appropriate or expected";
            ErrorManager.recordOrThrowError(361, errMsg, true);
            return unknown.UNKNOWN;
         }

         return (BaseDataType) result;
      }
      catch (InvocationTargetException exc)
      {
         // TODO: ignore ? Check for errors?

         return BaseDataType.generateUnknown(propType);
      }
      catch (IllegalAccessException exc)
      {
         // Represents programming or JVM configuration error.
         throw new RuntimeException(exc);
      }
      finally
      {
         TransactionManager.registerOutputParameterAssigner(opa);
      }
   }

   /**
    * Get a copy of the referenced object's current runtime extent value (this does not return the
    * original instance itself).
    *
    * @return   The copy of the current extent value.
    */
   public BaseDataType[] getExtent()
   {
      OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();

      try
      {
         return (BaseDataType[]) (index == null ? defaultGetter.getMethod().invoke(reference)
                  : defaultGetter.getMethod().invoke(reference, index));
      }
      catch (Exception e)
      {
         // Represents programming or JVM configuration error.
         throw new RuntimeException(e);
      }
      finally
      {
         TransactionManager.registerOutputParameterAssigner(opa);
      }
   }

   /**
    * Set the referenced object's current runtime value to the given value.
    *
    * @param    val
    *           The current value to be assigned into the referenced object.
    */
   @Override
   public void set(BaseDataType val)
   {
      try
      {
         if (extent != null)
         {
            if (index == null)
            {
               Object currentState = defaultGetter.getMethod().invoke(reference);
               if (extent == SourceNameMapper.DYNAMIC_EXTENT && Array.getLength(currentState) == 0)
               {
                  String errMsg = "Invalid assignment to an unfixed indeterminate extent";
                  ErrorManager.recordOrThrowError(11389, errMsg, false);
               }
               for (InternalEntry ie : this.settersList)
               {
                  if (ie.getParameterList().size() == 1 && !ie.getParameter(0).isExtent())
                  {
                     ie.getMethod().invoke(reference, val);
                     return;
                  }
               }
               String errMsg = "No proper setter found for '%s' extent property of %s class.";
               throw new RuntimeException(String.format(errMsg,
                                                        this.property,
                                                        this.className != null ?
                                                              this.className :
                                                              this.instance.type().getName()));
            }
            else
            {
               for (InternalEntry ie : this.settersList)
               {
                  if (ie.getParameterList().size() == 2)
                  {
                     ie.getMethod().invoke(reference, val, index);
                  }
               }
            }
         }
         else if (val.getClass() != propType)
         {
            // TODO: Assigning a non-extent value to an extent property
            BaseDataType v2 = BaseDataType.generateDefault(propType);
            v2.assign(val);
            val = v2;
         }
         
         if (index == null)
         {
            this.defaultSetter.getMethod().invoke(reference, val);
         }
         else
         {
            this.defaultSetter.getMethod().invoke(reference, val, index);
         }
      }
      catch (InvocationTargetException exc)
      {
         // TODO: ignore ? Check for errors?
      }
      catch (IllegalAccessException exc)
      {
         // Represents programming or JVM configuration error.
         throw new RuntimeException(exc);
      }
   }

   /**
    *  Set the referenced object's current runtime extent value to the given extent value.
    *
    * @param    source
    *           The extent values to be assigned to the referenced extent object.
    */
   public void setExtent(BaseDataType[] source)
   {
      if (extent == null || (extent != source.length && extent != SourceNameMapper.DYNAMIC_EXTENT))
      {
         String errMsg = "Whole-array assignment target and source must have the same extent " +
                  "unless the target is indeterminate";
         ErrorManager.recordOrThrowError(14905, errMsg, false);
         return;
      }
      if (source.length > 0 && source.getClass().getComponentType() == propType.getComponentType())
      {
         try
         {
            Object currentState = defaultGetter.getMethod().invoke(reference);
            if (extent == SourceNameMapper.DYNAMIC_EXTENT &&
                Array.getLength(currentState) > 0         &&
                Array.getLength(currentState) != source.length)
            {
               String errMsg = String.format("Indeterminate extent is already fixed to a dimension" +
                        " of %d", Array.getLength(currentState));
               ErrorManager.recordOrThrowError(13738, errMsg, false);
               return;
            }
            defaultSetter.getMethod().invoke(reference, (Object) source);
         }
         catch (InvocationTargetException e)
         {
            // TODO: ignore ? Check for errors?
         }
         catch (IllegalAccessException e)
         {
            throw new RuntimeException(e);
         }
      }
      else
      {
         String[] errMsgs = new String[]
         {
            "Incompatible datatypes found during runtime conversion",
            "Could not convert data for array assignment"
         };
         ErrorManager.recordOrThrowError(new int[] { 5729, 14907 }, errMsgs, false);
      }
   }

   /**
    * Resolve this object to its current runtime value.
    *
    * @return  Resolved value.
    */
   @Override
   public BaseDataType resolve()
   {
      return get();
   }

   /**
    * Report the return type expected from resolving the object.
    *
    * @return  Class indicating the type which will be returned by the
    *          object.  Must be a variant of {@link BaseDataType}.
    */
   @Override
   public Class getType()
   {
      return propType;
   }

   /**
    * Wrap a class property (used as an OUTPUT or INPUT-OUTPUT argument), so that it can be 
    * updated when the call returns.
    * 
    * @param    io
    *           Flag indicating the argument is INPUT-OUTPUT or just OUTPUT.
    *           
    * @return   A new BDT value, initialized depending on the argument mode.
    */
   public <T extends BaseDataType> T wrap(boolean io)
   {
      BaseDataType init = io ? resolve() : null;
      T wrapper = (T) OutputParameter.createVariable(propType, 
                                                     init, 
                                                     get(),
                                                     (bdt) -> new PropertyAssigner(bdt));

      return wrapper;
   }
   
   /**
    * Get the {@link #instance} for resolving this property.
    * 
    * @return   The {@link #instance} field.
    */
   object<? extends _BaseObject_> getInstance()
   {
      return instance;
   }
   
   /**
    * A simple assigner to update the backing class property, when the call returns.
    */
   private class PropertyAssigner
   extends AbstractSimpleParameter
   {
      /**
       * Create a new instance using the variable reference as argument.
       */
      protected PropertyAssigner(BaseDataType variable)
      {
         super(variable);
      }

      /**
       * Assign the current value of the parameter variable back to the associated class property.
       */
      @Override
      protected void assign()
      {
         set(outputValue);
      }
   }
}