Legacy4GLMethod.java

/*
** Module   : Legacy4GLMethod.java
** Abstract : Implementation of the builtin class.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------Description--------------------------------
** 001 CA  20190526 First version, stubs taken by converting the skeleton using FWD.
**     RFB 20191022 Added LegacyResource annotation
** 002 CA  20191024 Added method support levels and updated the class support level.
** 003 CA  20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy
**                  signature.
**     ME  20211028 Added 'POLY' return data type for invoke. 
** 004 ME  20230905 Implement remaining stubs methods.
**     ME  20230918 Use function blocks for method that needs error handling, cache string representation.
** 005 CA  20231130 Do not use 'TypeFactory.object' in cases where the skeleton class is not instantiated via
**                  ObjectOps (like the reflection classes), as this will force pending ObjectOps scopeable 
**                  registration, which never gets executed, as the legacy constructors are not used.
** 006 CA  20231106 Properly report DATASET/TABLE/-HANDLE parameters. 
** 007 CA  20240314 'ablSignature' must be accessed only via its getter.
** 008 ES  20250523 Removed function call from the Legacy 4GL class. 
*/

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

import com.goldencode.p2j.util.*;

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

import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.InternalEntry.Type;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;


/**
 * Implementation of the Progress.Reflect.Method builtin class. 
 */
@LegacyResource(resource = "Progress.Reflect.Method")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
public class Legacy4GLMethod
extends BaseObject
{
   private final LegacyClass originatingClass;
   private final InternalEntry ie;
   private LegacySignature ablSignature;
   private String txtSig;
   
   private object<? extends LegacyClass> declaringClass = new object<>(LegacyClass.class);

   private logical isOverride = TypeFactory.logical(null);

   private object<? extends DataType> returnType = new object<>(DataType.class);
   
   private List<Parameter> _parameters = null;

   /**
    * Create a new legacy class object.
    * 
    * @param    cls
    *           The originating legacy class.
    * @param    javaMethod
    *           The method (reflection).
    * 
    */
   public Legacy4GLMethod(LegacyClass cls, Method javaMethod)
   {
      this.originatingClass = cls;
      this.ie = SourceNameMapper.buildInternalEntry(_getSignature(javaMethod), javaMethod);
   }

   /**
    * Create a new legacy class object.
    * 
    * @param    cls
    *           The originating legacy class.
    * @param    internalEntry
    *           The InternalEntry.
    * 
    */
   public Legacy4GLMethod(LegacyClass cls, InternalEntry internalEntry)
   {
      this.originatingClass = cls;
      this.ie = internalEntry;
   }
   
   /**
    * Check if the method has a valid LegacySignature (type is method).
    * @param javaMethod the java method
    * @return <code>true</code> if the method has a valid signature
    */
   public static boolean is4GLMethod (Method javaMethod)
   {
      return _getSignature(javaMethod) != null;
   }
   
   /**
    * Check if the method has a valid LegacySignature (type is method) 
    * and conforms to the specific flag selection criteria.
    * @param javaMethod the java method
    * @param flags the Flags enum to use as selection criteria
    * @return <code>true</code> if the method has a valid signature for the input flags
    */
   public static boolean is4GLMethod (Method javaMethod, Flags flags)
   {
      if (!is4GLMethod(javaMethod)
               || (Modifier.isStatic(javaMethod.getModifiers())
                        && !flags.isFlagSet(Flags.static_).booleanValue())
               || (!Modifier.isStatic(javaMethod.getModifiers())
                        && !flags.isFlagSet(Flags.instance).booleanValue()))
      {
         return false;
      }

      if (Modifier.isPublic(javaMethod.getModifiers()))
      {
         return flags.isFlagSet(Flags.public_).booleanValue();
      }
      else if (Modifier.isProtected(javaMethod.getModifiers()))
      {
         return flags.isFlagSet(Flags.protected_).booleanValue();
      }
      else if (Modifier.isPrivate(javaMethod.getModifiers()))
      {
         return flags.isFlagSet(Flags.private_).booleanValue();
      }

      // we might need to add a new attribute in LegacySignature if we need to differentiate between the two
      return flags.isFlagSet(Flags.packageProtected).booleanValue()
               || flags.isFlagSet(Flags.packagePrivate).booleanValue();
   }
   
   @LegacySignature(returns = "OBJECT", qualified = "Progress.Reflect.AccessMode", type = Type.GETTER, name = "AccessMode")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public object<? extends AccessMode> getAccessMode()
   {
      return AccessMode.getEnum(ie.getMethod().getModifiers());
   }

   @LegacySignature(returns = "OBJECT", qualified = "Progress.Lang.Class", type = Type.GETTER, name = "DeclaringClass")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public object<? extends LegacyClass> getDeclaringClass()
   {
      if (declaringClass.isUnknown())
      {
         declaringClass.assign(ObjectOps.getLegacyClass((Class<? extends _BaseObject_>) ie.getMethod().getDeclaringClass()));
      }
            
      return declaringClass.duplicate();
   }

   @LegacySignature(returns = "LOGICAL", type = Type.GETTER, name = "IsAbstract")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public logical getIsAbstract()
   {
      return new logical(Modifier.isAbstract(ie.getMethod().getModifiers()));
   }

   @LegacySignature(returns = "LOGICAL", type = Type.GETTER, name = "IsFinal")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public logical getIsFinal()
   {
      return new logical(Modifier.isFinal(ie.getMethod().getModifiers()));
   }

   @LegacySignature(returns = "LOGICAL", type = Type.GETTER, name = "IsOverride")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public logical getIsOverride()
   {
      if (isOverride.isUnknown())
      {
         if (Modifier.isPrivate(ie.getMethod().getModifiers()) 
                  || !originatingClass.getJavaClass().equals(ie.getMethod().getDeclaringClass())) {
            isOverride.assign(false);
         }
         else 
         {
            isOverride.assign(isOverride(ie.getMethod(), originatingClass.getJavaClass()));
         }
      }

      return isOverride.duplicate();
   }

   @LegacySignature(returns = "LOGICAL", type = Type.GETTER, name = "IsStatic")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public logical getIsStatic()
   {
      return new logical(Modifier.isStatic(ie.getMethod().getModifiers()));
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "Name")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public character getName()
   {
      return new character(_getSignature().name());
   }

   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "NumParameters")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public integer getNumParameters()
   {
      return new integer(_getSignature().parameters().length);
   }

   @LegacySignature(returns = "OBJECT", type = Type.METHOD, name = "GetParameters", extent = -1, qualified = "progress.reflect.parameter")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public object<? extends Parameter>[] getParameters()
   {
      List<Parameter> params = _getParameters();
      
      object<Parameter>[] parameters = TypeFactory.objectExtent(params.size(), Parameter.class, false);

      for (int i = 0; i < params.size(); i++)
      {
         ArrayAssigner.assignSingle(parameters, i + 1, new object<> (params.get(i)));
      }
      
      return parameters;
   }
   
   @LegacySignature(returns = "OBJECT", qualified = "Progress.Lang.Class", type = Type.GETTER, name = "OriginatingClass")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public object<? extends LegacyClass> getOriginatingClass()
   {
      return new object<>(originatingClass);
   }

   @LegacySignature(returns = "INTEGER", type = Type.GETTER, name = "ReturnExtent")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public integer getReturnExtent()
   {
      int ext = _getSignature().extent();
      
      switch (ext)
      {
         case SourceNameMapper.NO_EXTENT:
            return new integer(0);
         case SourceNameMapper.DYNAMIC_EXTENT:
            return new integer();
         default:
            return new integer(ext);
      }
   }

   @LegacySignature(returns = "OBJECT", qualified = "Progress.Reflect.DataType", type = Type.GETTER, name = "ReturnType")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public object<? extends DataType> getReturnType()
   {
      if (returnType.isUnknown())
      {
         returnType.assign(DataType.getEnum(new character(_getSignature().returns())));
      }
         
      return returnType.duplicate();
   }

   @LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "ReturnTypeName")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public character getReturnTypeName()
   {
      String qualified = _getSignature().qualified();
      
      return new character(qualified != null && qualified.isEmpty() ? null : qualified);
   }

   @LegacySignature(type = Type.METHOD, name = "Invoke", returns = "BDT", parameters = 
   {
      @LegacyParameter(name = "params", type = "OBJECT", qualified = "progress.lang.parameterlist", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public BaseDataType invoke(final object<? extends ParameterList> _params)
   {
      if (!getIsStatic().booleanValue())
      {
         String msg   = String.format("Non-static method '%s' of class '%s' may not be invoked as if it were static", 
                                      getName().getValue(),
                                      getDeclaringClass().ref().getTypeName().getValue());
            
         ErrorManager.recordOrThrowError(15320, msg, false);
         return BaseDataType.generateUnknown(returnType.getTypeName().getClass());
      }
         
      if (_params.isUnknown())
      {
         ErrorManager.recordOrThrowError(18353, "You did not supply a valid ParameterList to Invoke", false);
         return BaseDataType.generateUnknown(returnType.getTypeName().getClass());
      }
         
      if (!_params.ref().isComplete())
      {
         ErrorManager.recordOrThrowError(15313, "All parameters in a ParameterList object must be intialized before using it in a NEW or Invoke method", false);
         
      }
         
      return _invoke(_params, originatingClass.getJavaClass());
   }

   @LegacySignature(type = Type.METHOD, name = "Invoke", returns = "BDT", parameters = 
   {
      @LegacyParameter(name = "instance", type = "OBJECT", qualified = "progress.lang.object", mode = "INPUT"),
      @LegacyParameter(name = "params", type = "OBJECT", qualified = "progress.lang.parameterlist", mode = "INPUT")
   })
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public BaseDataType invoke(final object<? extends _BaseObject_> _instance, final object<? extends ParameterList> _params)
   {
      if (getIsStatic().booleanValue())
      {
         String msg   = String.format("To invoke static method '%s' you should use the Invoke() overload that does not take and object instance parameter", getName().getValue());
            
         ErrorManager.recordOrThrowError(15291, msg, false);
         return BaseDataType.generateUnknown(returnType.getTypeName().getClass());
      }
         
      if (_params.isUnknown())
      {
         ErrorManager.recordOrThrowError(18353, "You did not supply a valid ParameterList to Invoke", false);
         return BaseDataType.generateUnknown(returnType.getTypeName().getClass());
      }
         
      if (!_params.ref().isComplete())
      {
         ErrorManager.recordOrThrowError(15313, "All parameters in a ParameterList object must be intialized before using it in a NEW or Invoke method", false);
         return BaseDataType.generateUnknown(returnType.getTypeName().getClass());
      }
         
      return _invoke(_params, _instance.ref());
   }

   @LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "ToString")
   @LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
   public character toLegacyString()
   {
      return new character(toString());
   }
   
   /**
    * Reports if the given instance is managed as part of the legacy 4GL object life cycle. This
    * includes reference counting, linking in the SESSION object chain, managed construction and
    * destruction.
    *
    * @return   Always {@code false} for 4GL internals (class/reflection).
    */
   @Override
   public boolean isTracked()
   {
      return false;
   }
   
   @Override
   public String toString()
   {
      if (txtSig != null)
      {
         return txtSig;
      }
      
      LegacySignature sig = _getSignature();
      StringBuffer sb = new StringBuffer("METHOD ");
      String ret = !sig.qualified().isEmpty() ? sig.qualified() : getReturnType().ref().toLegacyString().getValue().toUpperCase();
      
      sb.append(getAccessMode().ref().toLegacyString().getValue().toUpperCase()).append(" ");
      
      if (getIsOverride().booleanValue())
      {
         sb.append("OVERRIDE ");
      }
      
      if (!"VOID".equals(ret))
      {
         sb.append(ret).append(" ");
         
         if (sig.extent() != SourceNameMapper.NO_EXTENT)
         {
            sb.append("EXTENT ");
            if (sig.extent() != SourceNameMapper.DYNAMIC_EXTENT)
            {
               sb.append(sig.extent()).append(" ");
            }
         }
      }
      
      sb.append(sig.name()).append("(").append(
               _getParameters().stream().map(Object::toString).collect(Collectors.joining(", ")))
               .append(")");
      
      txtSig = sb.toString();
      
      return txtSig;
   }
   
   /**
    * Build the list of method parameters (lazy load).
    * @return  The parameter list.
    */
   private List<Parameter> _getParameters()
   {
      if (_parameters == null)
      {
         LegacyParameter[] params = _getSignature().parameters();
         _parameters = new ArrayList<Parameter>(params.length);

         for (int i = 0; i < params.length; i++)
         {
            _parameters.add(new Parameter(i, params[i], this.ie.getMethod()));
         }
      }
      
      return _parameters;
   }
   
   /**
    * Get the LegacySignature of current method.
    * 
    * @return  The LegacySignature of current method.
    */
   private LegacySignature _getSignature() {
      if (ablSignature == null) 
      {
         ablSignature = _getSignature(ie.getMethod());
      }
      
      return ablSignature;
   }

   /**
    * Get the LegacySignature annotation for the Java method.
    * 
    * @param   javaMethod 
    *          The method.
    * @return  The annotation if present or null.      
    */
   private static LegacySignature _getSignature(Method javaMethod) {
      if (javaMethod != null) 
      {
         LegacySignature lsig = javaMethod.getAnnotation(LegacySignature.class);
         if (lsig != null && lsig.type() == InternalEntry.Type.METHOD)
         {
            return lsig;
         }
      }
      
      return null;
   }
   
   /**
    * Check if the given method is an override for a method defined in it's super class(es).
    * 
    * @param   javaMethod 
    *          The method.
    * @param   javaClass  
    *          The super class to start searching from.
    * @return  True if the method is an override.
    */
   private boolean isOverride (Method javaMethod, Class<?> javaClass) {
      if (javaMethod != null && javaClass != null)
      {
         for (Method m : javaClass.getDeclaredMethods()) 
         {
            if (javaMethod.getName().equals(m.getName()) 
                     && Modifier.isStatic(javaMethod.getModifiers()) ==  Modifier.isStatic(m.getModifiers())
                     && !Modifier.isPrivate(m.getModifiers())
                     && Arrays.equals(javaMethod.getParameters(), m.getParameters())) 
            {
               return true;
            }
         }
         
         return isOverride(javaMethod, javaClass.getSuperclass());
      }
      
      return false;
   }
   
   /**
    * Invoke the current method.
    *  
    * @param   _params 
    *          The parameter list.
    * @param   _instance
    *          The object instance or the originating class for static method.
    * @return  Any returned value, or unknown if the method is void.
    */
   private BaseDataType _invoke(final object<? extends ParameterList> _params, final Object _instance)
   {
      ParameterList params = _params.ref();
      Class<?> retType = ie.getMethod().getReturnType();
      
      BaseDataType[] retVal = new BaseDataType[1];
      Runnable task = () -> retVal[0] = (BaseDataType) ControlFlowOps.invokeLegacyMethod(ie, 
               retType == void.class || retType == Void.class, 
               _instance, originatingClass.getTypeName().getValue(), getName().getValue(), params.getModes(), params.getArgs());
      
      // TODO: refactor so that the invoke is done in the method instead of the class itself
      LegacyClass.invokeImpl(params, task);
      
      return retVal[0];
   }
}