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];
}
}
}