FuzzyMethodRt.java

/*
** Module   : FuzzyMethodRt.java
** Abstract : Implements runtime method resolution when there are multiple overloads which may match.
**
** Copyright (c) 2023-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20230928 Initial version.
** 002 CA  20250319 A method from a super-class can have the same name as current class, so calls to this kind
**                  of methods must not be resolved as a constructor for the current 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.util;

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

import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.util.logging.*;

/**
 * Implements the rules to perform fuzzy method lookup, for runtime. 
 */
public class FuzzyMethodRt
extends FuzzyMethodLookup
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(FuzzyMethodRt.class);

   /** The type where the search will start. */
   private final Class<? extends _BaseObject_> type;
   
   /** The overloads which will be used to calculate the {@link #candidates}. */
   private final List<InternalEntry> overloads;
   
   /** The candidates used to perform the lookup. */
   private List<MatchMetrics> candidates;
   
   /**
    * Initialize this instance.
    * 
    * @param    type
    *           The type where the search will start.
    * @param    overloads
    *           The overloads which will be used to calculate the {@link #candidates}.
    */
   public FuzzyMethodRt(Class<? extends _BaseObject_> type, List<InternalEntry> overloads)
   {
      this.type = type;
      this.overloads = overloads;
   }
   
   /**
    * Perform a runtime fuzzy lookup of the given method, in the {@link #overloads} list.
    * 
    * @param    thisType
    *           The type where the call is being performed (external program or legacy class).
    * @param    methodName
    *           The target method name.
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    * @param    modes
    *           The argument modes.
    * @param    args
    *           The arguments.
    *           
    * @return   The resolved method (as its {@link InternalEntry}) or <code>null</code> if none found.
    */
   public InternalEntry lookup(Class<?> thisType, 
                               String   methodName, 
                               boolean  isStatic, 
                               String   modes,
                               Object[] args)
   {
      boolean internal = thisType == type;
      int accessMode = calcAccessMode(thisType);
      ParameterKey[] sig = resolveSignature(args, modes);
      candidates = new ArrayList<>();
      for (InternalEntry entry : overloads)
      {
         candidates.add(new MatchMetrics(new InternalMethodData(entry)));
      }
      
      MethodSearchResult result = fuzzyMethodLookup(methodName, sig, accessMode, isStatic, internal, null,
                                                    false);
      
      InternalEntry entry = result == null || result.getData() == null 
                               ? null 
                               : ((InternalMethodData) result.getData()).getEntry();
      return entry;
   }

   /**
    * Get the class name where this method lookup starts from.
    * 
    * @return   See above.
    */
   @Override
   protected String getClassName()
   {
      return ObjectOps.getLegacyName(type);
   }

   /**
    * Detect if the callee class is an instance of an implemented interface of the caller class.
    * 
    * @param    caller
    *           The caller type.
    * @param    callee
    *           The callee type.
    *           
    * @return   {@code true} if the given class is an implemented interface in this class. 
    */
   @Override
   protected boolean isImplemented(String caller, String callee)
   {
      Class<?> src = parseObjectSpec(caller);
      Class<?> tgt = parseObjectSpec(callee);
      
      return isImplemented(src, tgt);
   }

   /**
    * Detect if the callee class is an instance of a parent class or an implemented interface of a parent
    * class, of the caller.
    * 
    * @param    caller
    *           The caller type.
    * @param    callee
    *           The callee type to check.
    * @param    incrementer
    *           Code to be executed to increment the inheritance level.
    *
    * @return   {@code true} if the given class is a parent class or an implemented interface of a parent
    *           class. 
    */
   @Override
   protected boolean isInheritedFrom(String caller, String callee, Runnable incrementer)
   {
      Class<?> src = parseObjectSpec(caller);
      Class<?> tgt = parseObjectSpec(callee);
      
      return isInheritedFrom(src, tgt, true, incrementer);
   }

   /**
    * Reports if the class name is Progress.Lang.ParameterList and the method name is {@code setParameter()}.
    *
    * @param    method
    *           The method name.
    *
    * @return   {@code true} if this is a setParameter call.
    */
   @Override
   protected boolean isSetParam(String method)
   {
      return ParameterList.class == type && "setParameter".equalsIgnoreCase(method);
   }
   
   /**
    * Get the list of all possible methods that could match in this class AND its parent classes. The
    * actual types are not checked but the method name, access mode, static/instance and number of
    * parameters must all match.
    *
    * @param    name
    *           Method name.
    * @param    num
    *           Number of parameters.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up. Otherwise any method can be
    *           returned.
    * @param    internal
    *           {@code true} if the lookup is internal to the current class definition.
    * @param    first
    *           {@code true} if the first match should be immediately returned as the result.
    * @param    exist
    *           The set of existing methods already represented in the list.
    * @param    constructor
    *           Flag indicating this must resolve a constructor.
    *           
    * @return   List of possible methods that match.  This may be an empty list if no match exists.
    */
   @Override
   protected List<MatchMetrics> candidates(String            name,
                                           int               num,
                                           int               access,
                                           boolean           isStatic,
                                           boolean           internal,
                                           boolean           first,
                                           Set<SignatureKey> exist,
                                           boolean           constructor)
   {
      return candidates;
   }
   
   /**
    * Give the class for the context where this method is being invoked (i.e. THIS-PROCEDURE or THIS-OBJECT),
    * determine what access mode is allowed for the candidate methods.
    * 
    * @param    thisType
    *           The type where this method call is being performed.
    *           
    * @return   See above.
    */
   private int calcAccessMode(Class<?> thisType)
   {
      int access = ProgressParserTokenTypes.KW_PUBLIC;
      
      if (thisType != null)
      {
         if (thisType == type)
         {
            // inside a class def and explicitly referencing the current class, this is a private search
            access = ProgressParserTokenTypes.KW_PRIVATE;
         }
         else
         {
            if (thisType.isAssignableFrom(type))
            {
               // this is explicit access to a parent class from within a subclass definition, this is a 
               // protected search
               access = ProgressParserTokenTypes.KW_PROTECTD;
            }
         }
      }
      
      return access;

   }
   
   /**
    * Check if the source class implements the specified interface.
    * 
    * @param    src
    *           The source type.
    * @param    tgt
    *           The lookup type.
    *           
    * @return   See above.
    */
   private boolean isImplemented(Class<?> src, Class<?> tgt)
   {
      Set<Class<?>> all = new LinkedHashSet<>();
      Utils.collectInterfaces(src, all);
      
      return all.contains(tgt);
   }
   
   /**
    * Check if the source type implements or extends the target type, and calculate the distance from this
    * parent.
    * 
    * @param    src
    *           The source type.
    * @param    tgt
    *           The target type.
    * @param    ifaces
    *           {@code true} if implemented interfaces should be checked. 
    * @param    incrementer
    *           Code to be executed to increment the inheritance level.
    *           
    * @return   See above.
    */
   private boolean isInheritedFrom(Class<?> src, Class<?> tgt, boolean ifaces, Runnable incrementer)
   {
      Class<?>[] parents = getParents(src);
      
      if (parents != null)
      {
         for (Class<?> parent : parents)
         {
            // stay within legacy code
            if (!_BaseObject_.class.isAssignableFrom(parent))
            {
               continue;
            }
            
            if (tgt == parent || (ifaces && isImplemented(parent, tgt)))
            {
               // track the current parent level
               incrementer.run();

               return true;
            }
            
            if (isInheritedFrom(parent, tgt, ifaces, incrementer))
            {
               // track our level's contribution to the parent level
               incrementer.run();
               
               return true;
            }
         }
      }
      
      if (ifaces)
      {
         Class<?>[] interfaces = getInterfaces(src);
         
         if (interfaces != null)
         {
            for (Class<?> parent : interfaces)
            {
               // stay within legacy code
               if (!_BaseObject_.class.isAssignableFrom(parent))
               {
                  continue;
               }

               if (tgt == parent || isImplemented(parent, tgt))
               {
                  // track the current parent level
                  incrementer.run();
   
                  return true;
               }
            }
         }
      }
      
      return false;
   }
   
   /**
    * Get the interfaces implemented by this type.  If the type is an interface, in 4GL terms the interfaces
    * are inherited, and not implemented, so an empty array is returned.
    * 
    * @param    src
    *           The type.
    *           
    * @return   The interfaces.
    */
   private Class<?>[] getInterfaces(Class<?> src)
   {
      if (src.isInterface())
      {
         return new Class<?>[0];
      }
      else
      {
         return src.getInterfaces();
      }
   }
   
   /**
    * Get the parents of the given type.  In OpenEdge terms, the parents of an interface are all inherited
    * interfaces, and for class, its direct super-class.
    * 
    * @param    src
    *           The type.
    *           
    * @return   The parent list.
    */
   private Class<?>[] getParents(Class<?> src)
   {
      if (src.isInterface())
      {
         return src.getInterfaces();
      }
      else
      {
         return new Class[] { src.getSuperclass() };
      }
   }
   
   /**
    * Parse the given object spec (in the form "object<? extends _CLS_>" or "jobject<? extends _CLS_>") and
    * return the type associated with _CLS_.
    * 
    * @param    spec
    *           The specification to parse.
    *           
    * @return   The type or {@code null} if the spec was malformed or if there is no such definition.
    */
   private Class<?> parseObjectSpec(String spec)
   {
      String sig = "object<? extends ";
      int    idx = spec.indexOf(sig);
      Class<?> cls = null;
      
      if (idx != -1)
      {
         String cname = spec.substring(idx + sig.length(), spec.length() - 1);
         
         cls = ObjectOps.resolveClass(cname);
         
         if (cls == null)
         {
            LOG.log(Level.SEVERE, String.format("No class found for %s (object spec %s)!", cname, spec));
         }
      }
      else
      {
         LOG.log(Level.SEVERE, String.format("Malformed object spec %s!", spec));
      }
      
      return cls;
   }

   /**
    * Given a list of arguments and their modes, resolve the signature in conversion-compatible mode.
    * 
    * @param    args
    *           The arguments.
    * @param    modes
    *           The modes.
    *           
    * @return   See above.
    */
   private ParameterKey[] resolveSignature(Object[] args, String modes)
   {
      ParameterKey[] sig = new ParameterKey[args.length];
      
      for (int i = 0; i < args.length; i++)
      {
         Object arg = args[i];
         Integer mode = null;
         String type = null;
         
         if (modes != null)
         {
            switch (modes.charAt(i))
            {
               case 'I':
                  mode = ProgressParserTokenTypes.KW_INPUT;
                  break;
               case 'O':
                  mode = ProgressParserTokenTypes.KW_OUTPUT;
                  break;
               case 'U':
                  mode = ProgressParserTokenTypes.KW_IN_OUT;
                  break;
            }
         }
         
         Class<?> argType = ControlFlowOps.calculateType(arg);
         
         if (argType == null)
         {
            // TODO: what?
         }
         // TODO: OO types like InputOutputTableParameter, etc
         else if (arg instanceof TableParameter)
         {
            type = "TEMP-TABLE"; // TODO: signature
         }
         else if (arg instanceof DataSetParameter)
         {
            type = "DATASET"; // TODO: signature
         }
         else if (arg instanceof DataModelObject)
         {
            type = "BUFFER"; // TODO: signature
         }
         else if (BaseDataType.class.isAssignableFrom(argType)) 
         {
            if (mode == null)
            {
               mode = ProgressParserTokenTypes.KW_INPUT;
            }
            
            type = arg.getClass().getSimpleName();
            boolean isArray = arg.getClass().isArray();
            int sz = isArray ? Array.getLength(arg) : 0;
            
            if (argType == object.class || argType == jobject.class)
            {
               String qname = "progress.lang.object";
               if (isArray)
               {
                  if (sz != 0)
                  {
                     Object v = Array.get(arg, 0);
                     if (v instanceof object)
                     {
                        Class<? extends _BaseObject_> vtype = ((object) v).type();
                        if (vtype != null)
                        {
                           qname = ObjectOps.getLegacyName(vtype);
                        }
                     }
                     else if (v instanceof jobject)
                     {
                        Class<?> vtype = ((jobject) v).type();
                        if (vtype != null)
                        {
                           qname = vtype.toString();
                        }
                     }
                  }
               }
               else
               {
                  if (arg instanceof object)
                  {
                     Class<? extends _BaseObject_> vtype = ((object) arg).type();
                     if (vtype != null)
                     {
                        qname = ObjectOps.getLegacyName(vtype);
                     }
                  }
                  else if (arg instanceof jobject)
                  {
                     Class<?> vtype = ((jobject) arg).type();
                     if (vtype != null)
                     {
                        qname = vtype.toString();
                     }
                  }
               }

               type = type + "<? extends " + qname + ">";
            }
            
            if (isArray)
            {
               type = type + "[" + (sz == 0 ? "" : sz) + "]";
            }
         }
         
         sig[i] = new ParameterKey(mode, type);
      }
      
      return sig;
   }

   /**
    * The state used by {@link MatchMetrics} to calculate the match.
    */
   private static class InternalMethodData
   implements MethodData
   {
      /** The entry candidate. */
      private final InternalEntry entry;

      /** The conversion-compatible calculated signature. */
      private final ParameterKey[] signature;

      /**
       * Create a new method data for the given entry.
       * 
       * @param    entry
       *           The method entry.
       */
      public InternalMethodData(InternalEntry entry)
      {
         this.entry = entry;
         
         Method method = entry.getMethod();
         this.signature = new ParameterKey[method.getParameterCount()];
         
         LegacySignature ls = method.getAnnotation(LegacySignature.class);
         for (int i = 0; i < signature.length; i++)
         {
            LegacyParameter lp = ls.parameters()[i];
            this.signature[i] = new ParameterKey(lp);
         }
      }
      
      /**
       * Get the {@link #entry method}.
       * 
       * @return   See above.
       */
      public InternalEntry getEntry()
      {
         return entry;
      }
      
      /**
       * Get the signature of this method.
       * 
       * @return   See above.
       */
      @Override
      public ParameterKey[] getSignature()
      {
         return signature;
      }

      /**
       * Get the signature for the parameter at the specified 0-based index.
       * 
       * @param    idx
       *           The 0-based index for the parameter.
       *           
       * @return   See above.
       */
      @Override
      public ParameterKey getSignature(int idx)
      {
         return signature[idx];
      }
   }
}