JavaClassDefinition.java

/*
** Module   : JavaClassDefinition.java
** Abstract : defines the 4GL API for a Java class or interface definition
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description---------------------------------------
** 001 GES 20190903 First version which contains a Java class instance and uses reflection
**                  to answer any queries.
** 002 CA  20191211 Convert unknowns to Java null, when invoking Java code from 4GL code.
** 003 CA  20200412 Added incremental conversion support.
** 004 GES 20210708 Removed dead code.
**     TJD 20220504 Java 11 compatibility minor changes
**         20220428 Set class and package name methods as overrides.
** 005 CA  20230712 Allow Java array and 'object' parameters at direct Java calls.
** 006 CA  20230718 Fixed a regression in 'typeToJavaClass', arrays need to be handled separately.
** 007 CA  20230929 Method chain calls starting with SUPER keyword can not be morphed to dynamic invoke, as 
**                  the super calls require JVM to handle them.
** 008 CA  20241002 Candidate public methods must be explicitly searched in the type and all super-classes and
**                  super-interfaces: synthetic methods can't be excluded, as for some reason in some cases
**                  the bytecode contains a synthetic method for the super-class override, and in some other
**                  cases an inherited method is copied to the sub-class (see StringBuilder.append and length).
**                  Optimization in 'fuzzyMethodLookup' - a single-arg match to a java.lang.Object parameter
**                  will not consider java.lang.Object if there are other matches.
**                  Fixed non-primitive array type calculation in 'nonArrayType'.
** 009 CA  20250319 A provisional class var found during pre-scan, which is referenced before the definition
**                  statement and is referenced via a chain, must be resolved even if is provisional - this is
**                  the only case allowed in 4GL, when a var can be defined after it is referenced.
*/

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

import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.Utils;

import antlr.*;

/**
 * Contains a Java class instance and uses reflection to answer any queries. 
 */
public class JavaClassDefinition
extends ClassDefinition
{
   /** Contained Java class instance for which we are the wrapper. */
   private Class cls = null; 
   
   /**
    * Construct an instance.
    *
    * @param    cls
    *           Java class instance to wrap. Must NOT be {@code null}.
    */
   public JavaClassDefinition(Class<?> cls)
   {
      super(cls.getName(), null, null, false, decodeOOType(cls), false, cls.getName());
      this.cls = cls;
   }
   
   /**
    * Report if the given modifiers have the static bit set.
    *
    * @param    mods
    *           Modifiers bitset to check.
    *
    * @return   {@code true} if the static bit is set.
    */
   public static boolean isStaticModifier(int mods)
   {
      return Modifier.isStatic(mods); 
   }
   
   /**
    * Convert the Java Modifier access mode into the Progress token type for an access mode.
    *
    * @param    access
    *           Access mode ({@code Modifier.PRIVATE}, {@code Modifier.PROTECTED},
    *           {@code Modifier.PUBLIC}).
    *
    * @return   {@code KW_PUBLIC}, {@code KW_PROTECTD} or {@code KW_PRIVATE} or -1 if there
    *           is an error.
    */
   public static int javaModToTokenType(int access)
   {
      int mod = -1;
      
      switch (access)
      {
         case Modifier.PUBLIC:
            mod = KW_PUBLIC;
            break;
         case Modifier.PROTECTED:
            mod = KW_PROTECTD;
            break;
         case Modifier.PRIVATE:
            mod = KW_PRIVATE;
            break;
      }
      
      return mod;
   }
   
   /**
    * Check the given modifiers to see which of the Java private, protected or public access
    * level bits is set.
    *
    * @param    modifiers
    *           Modifiers bitset to check.
    *
    * @return   {@code Modifier.PRIVATE}, {@code Modifier.PROTECTED}, {@code Modifier.PUBLIC}
    *           or 0 if there is an error.
    */
   public static int javaAccessLevel(int modifiers)
   {
      if ((modifiers & Modifier.PRIVATE) == Modifier.PRIVATE)
      {
         return Modifier.PRIVATE;
      }
      else if ((modifiers & Modifier.PROTECTED) == Modifier.PROTECTED)
      {
         return Modifier.PROTECTED;
      }
      else if ((modifiers & Modifier.PUBLIC) == Modifier.PUBLIC)
      {
         return Modifier.PUBLIC;
      }
      
      return 0;
   }
   
   /**
    * Get the simple Java class name for this definition.
    * 
    * @return   See above.
    */
   @Override
   public String getSimpleJavaName()
   {
      return cls.getSimpleName();
   }
   
   /**
    * Get the Java package for this definition.
    * 
    * @return   See above.
    */
   @Override
   public String getJavaPackage()
   {
      return cls.getPackage().getName();
   }

   /**
    * Get the Java method name for a legacy signature.  This signature may be from a super-class.
    * 
    * @param    legacySig
    *           The legacy signature.
    *
    * @return   The converted method name or {@code null} if no match is found.
    */
   @Override
   public String getConvertedMethodName(String legacySig)
   {
      throw new UnsupportedOperationException("This should not be called!");
   }

   /**
    * Check if this class definition was already fully parsed.
    * 
    * @return   The {@link #processed} flag.
    */
   @Override
   public boolean isProcessed()
   {
      return true;
   }
   
   /**
    * Check the parent hierarchy to determine if the 4GL class has a .NET ancestor. Store
    * the result for future use.  This just reports the indirect .NET dependency and does
    * not report if this is itself a .NET class.
    */
   @Override
   public void derivedFromDotNet()
   {
   }
   
   /**
    * Report if the parent hierarchy includes a .NET ancestor which cannot ever be {@code true}.
    *
    * @return   Always {@code false}. 
    */
   @Override
   public boolean isDerivedFromDotNet()
   {
      return false;
   }
   
   /**
    * Obtains the empty set of derived (parent hierarchy) .NET references.
    *
    * @return   An empty set.
    */
   @Override
   public Set<String> getDerivedDotNet()
   {
      return new HashSet<String>();
   }
   
   /**
    * Calculate if the given reference is a direct or indirect .NET reference and update out
    * counters.
    *
    * @param    ref
    *           The class being referenced.
    */
   @Override
   public void markDotNetUsage(ClassDefinition ref)
   {
   }
   
   /**
    * Reports if the class has any direct .NET references, which cannot ever be {@code true}.
    *
    * @return   Always {@code false}.
    */
   @Override
   public boolean hasDirectDotNet()
   {
      return false;
   }
   
   /**
    * Obtains the direct .NET references.
    *
    * @return   Always an empty set.
    */
   @Override
   public Set<String> getDirectDotNet()
   {
      return new HashSet<String>();
   }
   
   /**
    * Reports if the class has any indirect .NET referenceswhich cannot ever be {@code true}.
    *
    * @return   Always {@code false}.
    */
   @Override
   public boolean hasIndirectDotNet()
   {
      return false;
   }
   
   /**
    * Obtains the indirect .NET references, an empty set.
    *
    * @return   Always an empty set.
    */
   @Override
   public Set<String> getIndirectDotNet()
   {
      return new HashSet<String>();
   }
   
   /**
    * Reports if the a reference to this class should be considered an indirect .NET reference
    * in the calling class, which cannot ever be {@code true}.
    *
    * @return   Always {@code false}.
    */
   @Override
   public boolean isIndirectDotNetReference()
   {
      return false;
   }
   
   /**
    * Obtains the parent class definition for this class.  This is only used for processing
    * 4GL class definitions and/or their possible usage of 4GL resources in the inheritance
    * hierarchy.  Java classes don't participate in this because they (by definition) are not
    * 4GL class definitions AND they don't support 4GL resources (datasets, buffers...). 
    *
    * @return   Always <code>null</code>.
    */
   @Override
   public ClassDefinition[] getParents()
   {
      return null;
   }
   
   /**
    * Obtains the list of interfaces implemented by this class.  This is only used for 
    * recursively parsing all implemented interfaces.  Java classes don't require parsing
    * so they don't participate in that process.
    *
    * @return   Implemented interfaces.
    */
   @Override
   public Set<String> getInterfaces()
   {
      return null;
   }
   
   /**
    * Access the flag that denotes if this is a built-in class definition.
    *
    * @return   Always {@code false}.
    */
   @Override
   public boolean isBuiltIn()
   {
      return false;
   }
   
   /**
    * Access the flag that denotes if this is a .NET class definition.
    *
    * @return   Always {@code false}.
    */
   @Override
   public boolean isDotNet()
   {
      return false;
   }

   /**
    * Access the flag that denotes if this is a Java class definition.
    *
    * @return   Always {@code true}.
    */
   @Override
   public boolean isJava()
   {
      return true;
   }

   /**
    * The method indicates whether this inctance represents a "mock" class definition
    * used to resolve symbols during class pre-scan mode in case the class file has not been
    * parsed.
    *
    * @return   Always {@code false}.
    */
   @Override
   public boolean isMock()
   {
      return false;
   }
   
   /**
    * Find the named method without any knowledge of the signature. This method will search up
    * the parent hierarchy (recursively) if no match is found in the current class.
    * <p>
    * <b>If a positive value is returned, the caller can be assured that there is an accessible
    * method with that name BUT the actual type of the return value cannot be known until the
    * signature is known. This is due to the fact that the same method name can be present with
    * differing return values and only the parameter signature matching can differentiate the
    * exact method that will be called.</b>
    * <p>
    * <b>Do not call this from any location where the type must be known authoritatively.</b>
    *
    * @param    name
    *           Resource name.
    * @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
    *           Ignored.
    *
    * @return   The type found or <code>-1</code> if no match exists.
    */
   @Override
   public int guessMethodType(String name, int access, boolean isStatic, boolean internal)
   {
      Method m = guessMethod(name, access, isStatic);
      
      return (m == null) ? -1 : decodeReturnType(m.getReturnType());
   }
   
   /**
    * Annotate the given method invocation reference with details for the specified method
    * in this class.  This is a no-operation if the method doesn't exist or if the node is
    * <code>null</code>.
    *
    * @param    mname
    *           Method name, {@code null} for a c'tor invocation.
    * @param    signature
    *           Array of type values for the parameter list, in left to right order.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           <code>true</code> if the method is static.
    * @param    isSuper
    *           <code>true</code> if the rvalue refnode is the SUPER keyword.
    * @param    internal
    *           <code>true</code> if the lookup is internal to the current class definition.
    * @param    node
    *           The AST to be annotated.
    */
   @Override
   public void annotateMethodCall(String         mname,
                                  ParameterKey[] signature,
                                  int            access,
                                  boolean        isStatic,
                                  boolean        isSuper,
                                  boolean        internal,
                                  Aast           node)
   {
      if (node != null)
      {
         MethodMatch match = lookupMethodWorker(mname, signature, access, isStatic, node);
         
         if (match != null)
         {
            Executable m = match.method;
            
            if (m instanceof Method)
            {
               Class<?> ret  = ((Method) m).getReturnType();
               int      type = decodeReturnType(ret);
               
               // the type may have been set incorrectly based on guessing, since the parameter
               // signature was not yet available; force it to the correct value here
               if (node.getType() != FUNC_CLASS)
               {
                  node.setType(type);
               }
               
               String rname = ret.getName();
               
               if (rname.charAt(0) == '[')
               {
                  // all Java arrays are indeterminate (no fixed size at compile time)
                  node.putAnnotation("extent", Long.valueOf(-1));
               }
               
               if (type == OO_METH_CLASS)
               {
                  // we deliberately do not lowercase here; that means that any usage of a
                  // user-defined OO 4GL class won't work downstream
                  node.putAnnotation("qualified", nonArrayType(rname));
               }
            }
            
            int mods    = m.getModifiers();
            int jaccess = javaModToTokenType(javaAccessLevel(mods));
            
            // the access mode won't necessarily be the same as that passed in
            node.putAnnotation("access-mode", Long.valueOf(jaccess));
            node.putAnnotation("static", Boolean.valueOf(isStaticModifier(mods)));
            
            node.putAnnotation("found-in-cls", cls.getName());
            node.putAnnotation("found-in-source-file", cls.getName());
            
            node.putAnnotation("javaname", m.getName());
            node.putAnnotation("is-java", true);
            
            annotateCallSignature(node, signature, match);
         }
      }
   }
      
   /**
    * Obtain the token type of the data member (variable) or property given
    * the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Variable or property name.
    * @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.
    *
    * @return   Return token type or -1 if no such variable exists. 
    */
   @Override
   public int lookupVariable(String name, int access, boolean isStatic)
   {
      Field[] flds = getCandidateFields(name, access, isStatic);
      return (flds.length == 0) ? -1 : decodeVarType(flds[0].getType());
   }
   
   /**
    * Obtain the fully qualified class name of the object instance 
    * represented by the data member (variable) or property given
    * the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent
    * (if one exists) will be checked. This means a recursive call can
    * occur here.
    *
    * @param    name
    *           Variable or property name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic    
    *           <code>true</code> if this is a static reference.
    *
    * @return   Fully qualified class name of the the object instance
    *           represented by this variable or data member, or 
    *           <code>null</code> if no such variable exists. 
    */
   @Override
   public String lookupVariableClassName(String name, int access, boolean isStatic)
   {
      Field[] flds = getCandidateFields(name, access, isStatic);
      return (flds.length == 0) ? null : flds[0].getType().getName();
   }
   
   /**
    * Obtain the <code>Variable</code> wrapper for the data member (variable) or property given
    * the name and access mode.
    * <p>
    * If the resource does not exist in this class definition the parent (if one exists) will
    * be checked. This means a recursive call can occur here.
    *
    * @param    name
    *           Variable or property name.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    * @param    isStatic    
    *           <code>true</code> if this is a static reference.
    * @param    chainedReference
    *           Flag indicating that the reference being looked is from a ":" chain.
    *
    * @return   The associated wrapper or <code>null</code> if no such variable exists. 
    */
   @Override
   public Variable lookupVariableWrapper(String  name, int access, boolean isStatic, boolean chainedReference)
   {
      Field[] flds = getCandidateFields(name, access, isStatic);
      
      JavaFieldDefinition jfd = null;
      
      if (flds.length != 0)
      {
         int type = decodeVarType(flds[0].getType());
         Class<?> fieldCls = flds[0].getType();
         String ftype = null;
         if (!fieldCls.isPrimitive())
         {
            ftype = fieldCls.getName();
         }
         
         jfd = new JavaFieldDefinition(flds[0], name, type, ftype);
      }
      
      return jfd;
   }
   
   /**
    * Reports if the given data member is a static member of the given class.
    *
    * @param    name
    *           Member name to lookup.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
    *           or <code>KW_PRIVATE</code>).
    *
    * @return   <code>true</code> if the member is static.  <code>false</code> otherwise
    *           including the case where the member does not exist at all.
    */
   @Override
   public boolean isStaticDataMember(String name, int access)
   {
      Field[] flds = getCandidateFields(name, access, true);
      return flds.length > 0;
   }

   /**
    * Get all defined methods in this class.
    * 
    * @return   Always {@code null}.
    */
   @Override
   public Set<String> getDefinedMethods()
   {
      // not needed for Java classes, but only for built-in OO 4GL
      return null;
   }

   /**
    * Process a method call to mark any "fuzzy" parameters with the wrapper type that must
    * be used for the actual call.
    * 
    * @param    call
    *           The OO_METH_* node whose parameters will be annotated.
    * @param    callSig
    *           The array of parameter types that is the method definition signature.
    * @param    match
    *           The details of the match found.
    */
   private void annotateCallSignature(Aast call, ParameterKey[] callSig, MethodMatch match)
   {
      int idx = 0;
      
      Class<?>[] cvt = match.cvt;
      
      // iterate all parameters and extract their types
      Aast parm = (Aast) call.getFirstChild();
      
      if (call.isAnnotation("oldtype") && (long) call.getAnnotation("oldtype") == KW_NEW)
      {
         parm = (Aast) call.getImmediateChild(LPARENS, null);
         parm = (Aast) parm.getFirstChild();
      }

      while (parm != null)
      {
         int type = parm.getType();
         
         // avoid the optional mode or table parm modifiers
         if (type != KW_INPUT   &&
             type != KW_IN_OUT  &&
             type != KW_OUTPUT  &&
             type != KW_APPEND  && 
             type != KW_BIND    &&
             type != KW_BY_REF  &&
             type != KW_BY_VALUE)
         {
            if (cvt != null && cvt[idx] != null)
            {
               boolean primitive = cvt[idx].isPrimitive() || cvt[idx].equals(String.class);
               if (primitive)
               {
                  parm.putAnnotation("primitive", primitive);
               }

               parm.putAnnotation("classname", cvt[idx].getName());
            }
            else
            {
               parm.putAnnotation("classname", callSig[idx].type);
            }
            
            idx++;
         }
         
         parm = (Aast) parm.getNextSibling();
      }
   }
      
   /**
    * Find the named method without any knowledge of the signature. This method will search up
    * the parent hierarchy (recursively) if no match is found in the current class.
    * <p>
    * <b>If a non-null value is returned, the caller can be assured that there is an accessible
    * method with that name BUT the actual type/details of the method cannot be known until the
    * signature is known. This is due to the fact that the same method name can be present with
    * differing return values/parameter signature and only the parameter signature matching can
    * differentiate the exact method that will be called.</b>
    * <p>
    * <b>Do not call this from any location where the method details must be known
    * authoritatively.</b>
    *
    * @param    name
    *           Method name.
    * @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.
    *
    * @return   The method found or <code>null</code> if no match exists.
    */
   private Method guessMethod(String name, int access, boolean isStatic)
   {
      // get those methods which match on name, access mode and instance/static
      Executable[] meths = getCandidateMethods(name, -1, access, isStatic);
      
      return (meths.length == 0) ? null : (Method) meths[0];
   }
   
   /**
    * Obtain the method instance given the name, signature and access mode.
    *
    * @param    name
    *           Method name, {@code null} for a c'tor invocation.
    * @param    sig
    *           Array describing the signature of the method call. 
    * @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    node
    *           The AST referencing this method.
    *
    * @return   Method found or <code>null</code> if no such method exists. 
    */
   private MethodMatch lookupMethodWorker(String         name,
                                          ParameterKey[] sig,
                                          int            access,
                                          boolean        isStatic,
                                          Aast           node)
   {
      for (int i = 0; i < sig.length; i++)
      {
         if (sig[i].mode != null && sig[i].mode.intValue() != KW_INPUT)
         {
            String spec  = "Java method %s parameter %d mode must be unspecified or INPUT.";
            String mname = (name == null) ? "<init>" : name;
            
            throw new IllegalArgumentException(String.format(spec, mname, i));  
         }
      }
      
      MethodMatch m = exactMethodLookup(name, sig, access, isStatic);
         
      if (m == null)
      {
         m = fuzzyMethodLookup(name, sig, access, isStatic, node);
      }
      
      if (m == null)
      {
         noMethodMatchError(name, sig, access, isStatic, node);
      }
      
      return m;
   }
   
   /**
    * Find the named method based on an exact signature match. This method will search up the
    * parent hierarchy (recursively) if no exact match is found in the current class.
    *
    * @param    name
    *           Method name, {@code null} for a c'tor invocation.
    * @param    sig
    *           Method call signature.
    * @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.
    *
    * @return   Method found or <code>null</code> if no match exists.
    */
   private MethodMatch exactMethodLookup(String         name,
                                         ParameterKey[] sig,
                                         int            access,
                                         boolean        isStatic)
   {
      Executable m = null;
      
      try
      {
         Class<?>[] parms = createJavaParameters(sig);
         
         if (name == null)
         {
            m = cls.getConstructor(parms);
         }
         else
         {
            m = cls.getMethod(name, parms);
         }
      }
      
      catch (NoSuchMethodException nsme)
      {
         // doesn't exist
      }
      
      return (m != null && checkModifiers(m, access, isStatic)) ? new MethodMatch(m) : null;
   }
   
   /**
    * Check if the given member matches the access level and instance/static requirement.
    *
    * @param    m
    *           Member to check.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or
    *           <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, the method must be static.
    *
    * @return   {@code true} if the method matches the access and instance/static requirements.
    */
   private boolean checkModifiers(Member m, int access, boolean isStatic)
   {
      int mods = m.getModifiers();
      
      // make sure the access mode requirements is met
      if (checkAccessRights(mods, access))
      {
         boolean staticMethod = isStaticModifier(mods);
         
         // make sure the static/instance requirements are met
         if ((!isStatic && !staticMethod) || (isStatic && staticMethod))
         {
            return true;
         }
      }
      
      return false;
   }
   
   /**
    * Find the named method based on a fuzzy signature match. This method will search up the
    * parent hierarchy (recursively) if no fuzzy match is found in the current class.
    *
    * @param    name
    *           Method name, {@code null} for a c'tor invocation.
    * @param    sig
    *           Method call signature.
    * @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    node
    *           The AST referencing this method.
    *
    * @return   Resource found or <code>null</code> if no match exists.
    */
   private FuzzyMatch fuzzyMethodLookup(String         name,
                                        ParameterKey[] sig,
                                        int            access,
                                        boolean        isStatic,
                                        Aast           node)
   {
      FuzzyMatch match = null;
      
      TreeSet<FuzzyMatch> set = new TreeSet<>();
      
      // get those methods which match on name, number of parms, access mode and instance/static
      Executable[]    meths = getCandidateMethods(name, sig.length, access, isStatic);
      Class<?>[][]    exact = getExactMatchCriteria(sig);
      Set<Class<?>>[] near  = getNearMatchCriteria(exact);
      
      // iterate through the array and find all possible matches based on the parameters
      for (Executable cand : meths)
      {
         FuzzyMatch f = testFuzzyMatch(cand, exact, near);
         
         // add each possible match to the sorted set
         if (f != null)
         {
            set.add(f);
         }
      }
      
      // if the set is empty there is no match
      if (!set.isEmpty())
      {
         // the first element in the sorted set is the best match
         match = set.first();
         if (set.size() > 1 && sig.length == 1 && match.method.getParameterTypes()[0] == Object.class)
         {
            // this is an optimization for single-arg overloads with 'java.lang.Object' - if we match on this,
            // and there are other matches, remove it.
            set.remove(match);
            match = set.first();
         }
      }
      
      return match;
   }
   
   /**
    * Display a detailed error message to report a method that was not found.
    *
    * @param    name
    *           Method name, {@code null} for a c'tor invocation.
    * @param    sig
    *           Method call signature.
    * @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    node
    *           The AST referencing this method.
    */
   private void noMethodMatchError(String         name,
                                   ParameterKey[] sig,
                                   int            access,
                                   boolean        isStatic,
                                   Aast           node)
   {
      System.out.printf("WARNING: no matching Java method definition for %s.%s()\n", 
                        cls.getName(),
                        (name == null) ? "<init>" : name);
      
      System.out.printf("\n- access %s, static %b\n   PARAMETERS: %d\n", 
                        ProgressParser.lookupTokenName(access),
                        isStatic,
                        sig.length);
      for (int i = 0; i < sig.length; i++)
      {
         System.out.printf("      %s\n", sig[i]);
      }
      
      System.out.printf("\ncalling location: %s", node.dumpTree());
   }
   
   /**
    * Determine if the given parameter match criteria can match the given method.
    *
    * @param    m
    *           The method candidate to test.
    * @param    exact
    *           The types of exact matches.
    * @param    near
    *           The types of the near matches.
    *
    * @return   A match instance if a match s possible or {@code null} if a match does not exist.
    */
   private FuzzyMatch testFuzzyMatch(Executable m, Class<?>[][] exact, Set<Class<?>>[] near)
   {
      Class<?>[] parms = m.getParameterTypes();
      Class<?>[] cvt   = new Class<?>[parms.length];
      
      int ex     = 0;
      int unwrap = 0;
      int nr     = 0;
      int parent = 0;
      
      for (int i = 0; i < parms.length; i++)
      {
         if (parms[i] == exact[i][0] || exact[i][0] == unknown.class)
         {
            ex++;
         }
         else if (parms[i] == exact[i][1])
         {
            // instead of marking this for conversion, should we mark it for unwrapping?
            unwrap++;
            cvt[i] = parms[i];
         }
         else if (near[i].contains(parms[i]))
         {
            nr++;
            cvt[i] = parms[i];
         }
         else if (parms[i].isAssignableFrom(exact[i][0]))
         {
            parent++;
         }
         else if (exact[i][1] != null && parms[i].isAssignableFrom(exact[i][1]))
         {
            // instead of marking this for conversion, should we mark it for unwrapping?
            parent++;
            cvt[i] = parms[i];
         }
         else
         {
            // no match
            return null;
         }
      }
      
      // all parms matched
      return new FuzzyMatch(m, ex, unwrap, nr, parent, cvt);
   }
   
   /**
    * Return the array of method instances that match by name, number of parameters, access
    * mode and static/instance.
    *
    * @param    name
    *           Method name.
    * @param    parms
    *           Number of parameters or -1 if the number of parameters is not known.
    * @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.
    *
    * @return   Array of methods or c'tors found, possibly an empty array.
    */
   private Executable[] getCandidateMethods(String name, int parms, int access, boolean isStatic)
   {
      Executable[]          meths = resolvePublicExecutables(name).toArray(new Executable[0]);
      ArrayList<Executable> list  = new ArrayList<>();
      
      for (Executable m : meths)
      {
         // subset by name, number of parameters, access mode, isStatic
         if (matchesParmNum(parms, m) && checkModifiers(m, access, isStatic))
         {
            list.add(m);
         }
      }
      
      meths = list.toArray(new Executable[0]);
      list.clear();
      
      // eliminate overridden methods
      for (int i = 0; i < meths.length; i++)
      {
         Executable m1 = meths[i];
         if (m1 == null)
         {
            continue;
         }
         
         for (int j = 0; j < meths.length; j++)
         {
            Executable m2 = meths[j];

            if (m2 == null || i == j)
            {
               continue;
            }
            
            if (m2.getDeclaringClass() != m1.getDeclaringClass()                && 
                m2.getDeclaringClass().isAssignableFrom(m1.getDeclaringClass()) && 
                Arrays.equals(m1.getParameterTypes(), m2.getParameterTypes()))
            {
               // if m1 is an override of m2, then eliminate m2
               meths[j] = null;
            }
         }
      }
      
      for (int i = 0; i < meths.length; i++)
      {
         if (meths[i] != null)
         {
            list.add(meths[i]);
         }
      }
      
      return list.toArray(new Executable[0]);
   }
   
   /**
    * Resolve the public executables from this class and all super-classes or super-interfaces.  Interfaces
    * will be searched only if the name is specified, as when null, a constructor is assumed.
    * 
    * @param    name
    *           Method name, null for constructor.
    *           
    * @return   The list of public methods/constructors.
    */
   private List<Executable> resolvePublicExecutables(String name)
   {
      Set<Class<?>> ifaces = new HashSet<>();
      Utils.collectInterfaces(cls, ifaces);
      
      List<Executable> res = new ArrayList<>();
      if (name != null)
      {
         for (Class<?> iface : ifaces)
         {
            Executable[] execs = iface.getDeclaredMethods();
            for (int i = 0; i < execs.length; i++)
            {
               Executable e = execs[i];
               if (e.isSynthetic())
               {
                  continue;
               }
               
               if (Modifier.isPublic(e.getModifiers()) && name.equals(e.getName())) 
               {
                  res.add(e);
               }
            }
         }
      }
      
      Class<?> type = this.cls;
      while (type != null)
      {
         Executable[] execs = name == null ? type.getDeclaredConstructors() : type.getDeclaredMethods();
         for (int i = 0; i < execs.length; i++)
         {
            Executable e = execs[i];
            if (e.isSynthetic())
            {
               continue;
            }
            
            if (Modifier.isPublic(e.getModifiers()) && (name == null || name.equals(e.getName()))) 
            {
               res.add(e);
            }
         }
         type = type.getSuperclass();
      }
      
      return res;
   }
   
   /**
    * Check if the number of given parameters matches the number of parms in the
    * candidate member.
    *
    * @param    parms
    *           Number of parameters or -1 if the number of parameters is not known.
    * @param    e
    *           The method or c'tor to check.
    *
    * @return   {@code true} if the number of parameters matches or if it is not known.
    */
   private boolean matchesParmNum(int parms, Executable e)
   {
      return (parms == -1 || e.getParameterCount() == parms);
   }
   
   /**
    * Return the array of field instances that match by name, access mode and static/instance.
    *
    * @param    name
    *           Field name.
    * @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.
    *
    * @return   Array of fields found, possibly an empty array.
    */
   private Field[] getCandidateFields(String name, int access, boolean isStatic)
   {
      Field[]          flds = cls.getFields();
      ArrayList<Field> list = new ArrayList<>();
      
      for (Field f : flds)
      {
         // subset by name, access mode, isStatic
         if (name.equals(f.getName())                           &&
             checkModifiers(f, access, isStatic))
         {
            list.add(f);
         }
      }
      
      return list.toArray(new Field[0]);
   }
   
   /**
    * Convert a 4GL parameter signature into a Java class parameter list.
    *
    * @param    sig
    *           List of 4GL parameters.
    *
    * @return   The Java parameter list.
    */
   private static Class<?>[] createJavaParameters(ParameterKey[] sig)
   {
      Class<?>[] parms = new Class<?>[sig.length];
      
      for (int i = 0; i < sig.length; i++)
      {
         parms[i] = parameterTypeToJavaClass(sig[i].type);
      }
      
      return parms;
   }
   
   /**
    * Convert a 4GL parameter type into a Java class, handling arrays as needed.
    *
    * @param    type
    *           4GL parameter.
    *
    * @return   The Java class.
    */
   private static Class<?> parameterTypeToJavaClass(String type)
   {
      // this is a 4GL type name as returned from ECW.expressionType() and possibly appended
      // with [] (for indeterminate extent) or [%d] (for fixed extent); object references will
      // have a classname in object<? extends __>  format but non-object references will have
      // the simple BDT basename (e.g. character or comhandle)
      
      int    idx   = type.indexOf('[');
      String jname = (idx != -1) ? type.substring(0, idx) : type;
      
      Class<?> cls = typeToJavaClass(jname, "com.goldencode.p2j.util.", (idx != -1));
      if (cls == null)
      {
         cls = typeToJavaClass(jname, null, (idx != -1));
      }
      
      return cls;
   }
   
   /**
    * Unwrap the 4GL object parameter type into a referenced Java class, handling arrays as
    * needed.
    *
    * @param    type
    *           4GL parameter.
    *
    * @return   The Java class or {@code null} if this is not an object reference.
    */
   private static Class<?> unwrapObjectTypeToJavaClass(String type)
   {
      // this is a 4GL type name as returned from ECW.expressionType() and possibly appended
      // with [] (for indeterminate extent) or [%d] (for fixed extent); object references will
      // have a classname in object<? extends __>  format but non-object references will have
      // the simple BDT basename (e.g. character or comhandle)
      
      // this should find the last char before the object reference type, if it is object
      int ex = type.lastIndexOf(' ');
      
      if (ex == -1)
      {
         return null;
      }
      
      int    idx   = type.indexOf('[');
      String jname = (idx != -1) ? type.substring(0, idx) : type;
      String oname = jname.substring(ex + 1, jname.length() - 1);
      
      return typeToJavaClass(oname, null, (idx != -1));
   }
   
   /**
    * Convert a type into a Java class.
    *
    * @param    type
    *           Type name, possibly with a package.
    * @param    pkg
    *           Optional package prefix to prepend.
    * @param    array
    *           {@code true} if this is an array type.
    *
    * @return   The Java class.
    */
   public static Class<?> typeToJavaClass(String type, String pkg, boolean array)
   {
      if (type.startsWith("jobject"))
      {
         type = type.substring(type.lastIndexOf(' ') + 1, type.length() - 1);
         try
         {
            if (array)
            {
               String prim = javaPrimitiveTypeCode(type);
               if (prim != null)
               {
                  throw new UnsupportedOperationException("Java primitive arrays can't be used from 4GL code.");
               }
               boolean isObject = prim == null;
               return Class.forName("["  + (isObject ? "L" : "" ) + type + (isObject ? ";" : ""));
            }
            else
            {
               return Class.forName(type);
            }
         }
         catch (ClassNotFoundException ex)
         {
            return null;
         }
      }
      else if (type.startsWith("object<"))
      {
         type = "object";
      }
      
      if (!array && type.indexOf('.') < 0 && (pkg == null || pkg.isEmpty()))
      {
         // this must be a native type
         switch (type)
         {
            case "int":
               return int.class;
            case "byte":
               return byte.class;
            case "short":
               return short.class;
            case "long":
               return long.class;
            case "float":
               return float.class;
            case "double":
               return double.class;
            case "boolean":
               return boolean.class;
            case "char":
               return char.class;
         }
      }
      Class<?> cls = null;
      
      pkg = (pkg == null) ? "" : pkg;
      
      // this is a 4GL type name as returned from ECW.expressionType() and possibly appended
      // with [] (for indeterminate extent) or [%d] (for fixed extent); object references will
      // have a classname in object<? extends __>  format but non-object references will have
      // the simple BDT basename (e.g. character or comhandle)
      String jname = null;
      
      if (array)
      {
         String prim = javaPrimitiveTypeCode(type);
         
         prim = (prim == null) ? "L" : prim;
         
         jname = "[" + prim + pkg + type + ";";
      }
      else
      {
         jname = pkg + type;
      }
      
      try
      {
         cls = Class.forName(jname);
      }
      
      catch (ClassNotFoundException cnfe)
      {
         // ignore
      }
      
      return cls;
   }
   
   /**
    * Convert a primitive type name into its Java array type code.
    *
    * @param    type
    *           Java primitive type name.
    *
    * @return   Type code used in Java array names.
    */
   private static String javaPrimitiveTypeCode(String type)
   {
      String code = null;
      
      switch (type)
      {
         case "boolean":
            code = "Z";
            break;
            
         case "byte":
            code = "B";
            break;
            
         case "char":
            code = "C";
            break;
            
         case "double":
            code = "D";
            break;
            
         case "float":
            code = "F";
            break;
            
         case "int":
            code = "I";
            break;
            
         case "long":
            code = "J";
            break;
            
         case "short":
            code = "S";
            break;
      }
      
      return code;
   }
   
   /**
    * Confirm if the needed access level is allowed.
    *
    * @param    access
    *           Access level of the resource, as a Java modifiers bitfield.
    * @param    needed
    *           Required access level as a Progress token type ({@code KW_PUBLIC},
    *           {@code KW_PROTECTD} or {@code KW_PRIVATE}).
    *
    * @return   {@code true} if the given resource is accessable.
    */
   private boolean checkAccessRights(int access, int needed)
   {
      access = javaAccessLevel(access);
      needed = tokenTypeToJavaMod(needed);
      
      if (access != 0 && needed != 0)
      {
         // check for a match to the access mode:
         // 1. searching for private methods match everything
         // 2. a direct match is always OK
         // 3. a public method matches a protected search 
         return (needed == Modifier.PRIVATE)  ||
                (needed == access) ||
                (needed == Modifier.PROTECTED && access == Modifier.PUBLIC);
      }
      
      return false;
   }
   
   /**
    * Convert the Progress token type for an access mode into the Java Modifier access mode.
    *
    * @param    access
    *           Access mode ({@code KW_PUBLIC}, {@code KW_PROTECTD} or {@code KW_PRIVATE}).
    *
    * @return   {@code Modifier.PRIVATE}, {@code Modifier.PROTECTED}, {@code Modifier.PUBLIC}
    *           or 0 if there is an error.
    */
   private static int tokenTypeToJavaMod(int access)
   {
      int mod = 0;
      
      switch (access)
      {
         case KW_PUBLIC:
            mod = Modifier.PUBLIC;
            break;
         case KW_PROTECTD:
            mod = Modifier.PROTECTED;
            break;
         case KW_PRIVATE:
            mod = Modifier.PRIVATE;
            break;
      }
      
      return mod;
   }
   
   /**
    * Determine the type of the Java class.
    *
    * @param    cls
    *           The class to inspect.
    *
    * @return   The decoded type.
    */
   private static OOType decodeOOType(Class<?> cls)
   {
      OOType type = OOType.CLASS; 
      
      if (cls.isInterface())
      {
         type = OOType.INTERFACE;
      }
      else if (cls.isEnum())
      {
         type = OOType.ENUM;
      }
      
     return type;
   }
   
   /**
    * Obtain the array of Java classes that represent an exact match.  This will include
    * an array type if it is an extent parameter.
    *
    * @param    parms
    *           The parameter list as passed by the 4GL code.
    *
    * @return   The 2 dimensional array of class instances matching each type. The 2nd
    *           dimension is only non-null if this is an object type in which case the
    *           2nd dimension will be the unwrapped type of the reference.
    */
   private static Class<?>[][] getExactMatchCriteria(ParameterKey[] parms)
   {
      Class<?>[][] cls = new Class<?>[parms.length][2];
      
      for (int i = 0; i < parms.length; i++)
      {
         cls[i][0] = parameterTypeToJavaClass(parms[i].type);
         if (cls[i][0] == null && parms[i].type.startsWith("object"))
         {
            cls[i][0] = jobject.class;
         }
         cls[i][1] = unwrapObjectTypeToJavaClass(parms[i].type);
         if (cls[i][1] == null && cls[i][0] == jobject.class)
         {
            cls[i][1] = Object.class;
         }
      }
      
      return cls;
   }
   
   /**
    * Obtain the array of sets of Java classes that represent near matches.  This will include
    * an array type if it is an extent parameter.
    *
    * @param    exact
    *           The two dimensional array of exact matches.
    *
    * @return   The array of sets of class instances matching each type.
    */
   private static Set<Class<?>>[] getNearMatchCriteria(Class<?>[][] exact)
   {
      Set<Class<?>>[] near = new HashSet[exact.length];
      
      for (int i = 0; i < exact.length; i++)
      {
         near[i] = getNearMatches(exact[i][0]);
      }
      
      return near;
   }
   
   /**
    * Obtain the set of data types that are near matches.  A near match is one which the
    * original type can be converted to without loss of data.
    *
    * @param    cls
    *           The class for which to find the near matches.
    *
    * @return   The set of near matches.  The set may be empty if there are no near matches.
    */
   private static Set<Class<?>> getNearMatches(Class<?> cls)
   {
      Set<Class<?>> set = new HashSet();
      
      String  name   = cls.getName();
      String  simple = nonArrayType(name);
      boolean extent = (name.charAt(0) == '[');
      
      switch (simple)
      {
         case "com.goldencode.p2j.util.character":
            set.add(typeToJavaClass("java.lang.String", null, extent));
            set.add(typeToJavaClass("com.goldencode.p2j.util.longchar", null, extent));
            break;
            
         case "com.goldencode.p2j.util.date":
            set.add(typeToJavaClass("java.util.Date", null, extent));
            set.add(typeToJavaClass("com.goldencode.p2j.util.datetime", null, extent));
            set.add(typeToJavaClass("com.goldencode.p2j.util.datetimetz", null, extent));
            break;
            
         case "com.goldencode.p2j.util.datetime":
            set.add(typeToJavaClass("java.sql.Timestamp", null, extent));
            set.add(typeToJavaClass("com.goldencode.p2j.util.datetimetz", null, extent));
            break;
         
         case "com.goldencode.p2j.util.decimal":
            set.add(typeToJavaClass("double", null, extent));
            set.add(typeToJavaClass("java.lang.Double", null, extent));
            set.add(typeToJavaClass("java.math.BigDecimal", null, extent));
            break;
         
         case "com.goldencode.p2j.util.integer":
            set.add(typeToJavaClass("int", null, extent));
            set.add(typeToJavaClass("java.lang.Integer", null, extent));
            set.add(typeToJavaClass("long", null, extent));
            set.add(typeToJavaClass("java.lang.Long", null, extent));
            set.add(typeToJavaClass("double", null, extent));
            set.add(typeToJavaClass("java.lang.Double", null, extent));
            set.add(typeToJavaClass("java.math.BigDecimal", null, extent));
            break;
            
         case "com.goldencode.p2j.util.int64":
            set.add(typeToJavaClass("long", null, extent));
            set.add(typeToJavaClass("java.lang.Long", null, extent));
            set.add(typeToJavaClass("java.math.BigDecimal", null, extent));
            break;
            
         case "com.goldencode.p2j.util.logical":
            set.add(typeToJavaClass("boolean", null, extent));
            set.add(typeToJavaClass("java.lang.Boolean", null, extent));
            break;
          
         case "com.goldencode.p2j.util.longchar":
            set.add(typeToJavaClass("java.lang.String", null, extent));
            break;
         
         case "com.goldencode.p2j.util.raw":
            set.add(typeToJavaClass("byte", null, true));
            break;
         
         case "com.goldencode.p2j.util.unknown":
            set.add(unknown.class); // java has no Class associated with null, so use "unknown"
            break;
      }
      
      return set;
   }
   
   /**
    * Map the given class to a valid 4GL token type for a variable. The return
    * value will be between {@code BEGIN_VAR_TYPE} and {@code END_VAR_TYPE}.
    *
    * @param    cls
    *           The class to inspect.
    *
    * @return   The variable token type or -1 if something went wrong with the decode.
    */
   public static int decodeVarType(Class<?> cls)
   {
      int type = -1;
      
      String name   = cls.getName();
      String simple = nonArrayType(name);
      
      // special case where we want to detect byte arrays
      if (name.charAt(0) == '[' && "byte".equals(simple))
      {
         simple = "byte[]";
      }
      
      switch (simple)
      {
         case "char":
         case "java.lang.String":
         case "com.goldencode.p2j.util.character":
            type = VAR_CHAR;
            break;
            
         case "com.goldencode.p2j.util.comhandle":
            type = VAR_COM_HANDLE;
            break;
         
         case "java.util.Date":
         case "com.goldencode.p2j.util.date":
            type = VAR_DATE;
            break;
            
         case "java.sql.Timestamp":
         case "com.goldencode.p2j.util.datetime":
            type = VAR_DATETIME;
            break;
         
         case "com.goldencode.p2j.util.datetimetz":
            type = VAR_DATETIME_TZ;
            break;
         
         case "float":
         case "double":
         case "java.lang.Float":
         case "java.lang.Double":
         case "java.math.BigDecimal":
         case "com.goldencode.p2j.util.decimal":
            type = VAR_DEC;
            break;
         
         case "com.goldencode.p2j.util.handle":
            type = VAR_HANDLE;
            break;
         
         case "byte":
         case "short":
         case "int":
         case "java.lang.Byte":
         case "java.lang.Short":
         case "java.lang.Integer":
         case "com.goldencode.p2j.util.integer":
            type = VAR_INT;
            break;
            
         case "long":
         case "java.lang.Long":
         case "com.goldencode.p2j.util.int64":
            type = VAR_INT64;
            break;
            
         case "boolean":
         case "java.lang.Boolean":
         case "com.goldencode.p2j.util.logical":
            type = VAR_LOGICAL;
            break;
          
         case "com.goldencode.p2j.util.longchar":
            type = VAR_LONGCHAR;
            break;
         
         case "com.goldencode.p2j.util.memptr":
            type = VAR_MEMPTR;
            break;
         
         case "byte[]":
         case "com.goldencode.p2j.util.raw":
            type = VAR_RAW;
            break;
            
         case "com.goldencode.p2j.util.recid":
            type = VAR_RECID;
            break;
         
         case "com.goldencode.p2j.util.rowid":
            type = VAR_ROWID;
            break;
            
         // must be an object type
         default:
            type = VAR_CLASS;
      }
      
      return type;
   }   
   
   /**
    * Map the given class to a valid 4GL token type for a method call. The return
    * value will be between {@code BEGIN_OO_METH} and {@code END_OO_METH}.
    *
    * @param    cls
    *           The class to inspect.
    *
    * @return   The method call token type or -1 if something went wrong with the decode.
    */
   private static int decodeReturnType(Class<?> cls)
   {
      int type = -1;
      
      String name   = cls.getName();
      String simple = nonArrayType(name);
      
      // special case where we want to detect byte arrays
      if (name.charAt(0) == '[' && "byte".equals(simple))
      {
         simple = "byte[]";
      }
      
      switch (simple)
      {
         case "char":
         case "java.lang.String":
         case "com.goldencode.p2j.util.character":
            type = OO_METH_CHAR;
            break;
            
         case "com.goldencode.p2j.util.comhandle":
            type = OO_METH_COM_HANDLE;
            break;
         
         case "java.util.Date":
         case "com.goldencode.p2j.util.date":
            type = OO_METH_DATE;
            break;
            
         case "java.sql.Timestamp":
         case "com.goldencode.p2j.util.datetime":
            type = OO_METH_DATETIME;
            break;
         
         case "com.goldencode.p2j.util.datetimetz":
            type = OO_METH_DATETIME_TZ;
            break;
         
         case "float":
         case "double":
         case "java.lang.Float":
         case "java.lang.Double":
         case "java.math.BigDecimal":
         case "com.goldencode.p2j.util.decimal":
            type = OO_METH_DEC;
            break;
         
         case "com.goldencode.p2j.util.handle":
            type = OO_METH_HANDLE;
            break;
         
         case "byte":
         case "short":
         case "int":
         case "java.lang.Byte":
         case "java.lang.Short":
         case "java.lang.Integer":
         case "com.goldencode.p2j.util.integer":
            type = OO_METH_INT;
            break;
            
         case "long":
         case "java.lang.Long":
         case "com.goldencode.p2j.util.int64":
            type = OO_METH_INT64;
            break;
            
         case "boolean":
         case "java.lang.Boolean":
         case "com.goldencode.p2j.util.logical":
            type = OO_METH_LOGICAL;
            break;
          
         case "com.goldencode.p2j.util.longchar":
            type = OO_METH_LONGCHAR;
            break;
         
         case "com.goldencode.p2j.util.memptr":
            type = OO_METH_MEMPTR;
            break;
         
         case "byte[]":
         case "com.goldencode.p2j.util.raw":
            type = OO_METH_RAW;
            break;
            
         case "com.goldencode.p2j.util.recid":
            type = OO_METH_RECID;
            break;
         
         case "com.goldencode.p2j.util.rowid":
            type = OO_METH_ROWID;
            break;
            
         case "void":
            type = OO_METH_VOID;
            break;
         
         // must be an object type
         default:
            type = OO_METH_CLASS;
      }
      
      return type;
   }
   
   /**
    * Convert the Java type specification into a simple type name that does not include any
    * array prefix.  The array notation, if present, is normalized into the regular type
    * name syntax.
    *
    * @param    name
    *           The name to decode.
    *
    * @return   The simple, normalized form of any array name or the input text if it is not
    *           an array form.
    */
   private static String nonArrayType(String name)
   {
      String simple = name;
      
      // strip off any array definitions (which only appear at the beginning), there can be
      // multiple (e.g. [[Z is a 2 dimensional boolean array)
      if (name.charAt(0) == '[')
      {
         simple = name.substring(name.lastIndexOf('[') + 1);
      
         // at this point there should be at least 1 char (for L also a class name)
         switch (simple.charAt(0))
         {
            case 'Z':
               simple = "boolean";
               break;
               
            case 'B':
               simple = "byte";
               break;
               
            case 'C':
               simple = "char";
               break;
               
            case 'L':
               simple = simple.substring(1);
               if (simple.endsWith(";"))
               {
                  // array of objects have the class name ending with a ';'
                  simple = simple.substring(0, simple.length() - 1);
               }
               break;
               
            case 'D':
               simple = "double";
               break;
               
            case 'F':
               simple = "float";
               break;
               
            case 'I':
               simple = "int";
               break;
               
            case 'J':
               simple = "long";
               break;
               
            case 'S':
               simple = "short";
               break;
         }
      }
      
      return simple;
   }
   
   /**
    * Store a method.
    */
   private static class MethodMatch
   {
      /** The method or c'tor being matched. */
      protected Executable method = null;
      
      /** The parameters that need to be converted/wrapped. */
      protected Class<?>[] cvt = null;
      
      /**
       * Construct an instance.
       *
       * @param    method
       *           The method being matched.
       */
      public MethodMatch(Executable method)
      {
         this.method  = method;
      }
      
      /**
       * Report if this is or is not an exact match.
       *
       * @return   Always {@code true}.
       */
      public boolean isExact()
      {
         return true;
      }
   }
   
   /**
    * Store a method and its match criteria in a sortable form.
    */
   private static class FuzzyMatch
   extends MethodMatch
   implements Comparable<FuzzyMatch>
   {
      /** Source for each instance's unique ordinal, constantly growing over time. */
      private static int next = 0;
      
      /** Number of exact matches. */
      private int exact = 0;
      
      /** Number of object unwrapping matches. */
      private int unwrap = 0;
      
      /** Number of "near" type equivalent matches. */
      private int near = 0;
      
      /** Number of parent matches. */
      private int parent = 0;
      
      /** Unique fallback value to force ordering based on construction order. */
      private int ordinal;
      
      /**
       * Construct an instance.
       *
       * @param    method
       *           The method or c'tor being matched.
       * @param    exact
       *           The number of exact matches.
       * @param    unwrap
       *           The number of object unwrap matches.
       * @param    near
       *           The number of near type equivalents.
       * @param    parent
       *           The number of parent matches.
       * @param    cvt
       *           The parameters that need to be converted/wrapped.
       */
      public FuzzyMatch(Executable method, 
                        int        exact,
                        int        unwrap,
                        int        near,
                        int        parent,
                        Class<?>[] cvt)
      {
         super(method);
         this.exact   = exact;
         this.unwrap  = unwrap;
         this.near    = near;
         this.parent  = parent;
         this.cvt     = cvt;
         this.ordinal = next++;
      }
      
      
      /**
       * Report if this is or is not an exact match.
       *
       * @return   Always {@code false}.
       */
      public boolean isExact()
      {
         return false;
      }
      
      /**
       * Compares this instance with the given instance for purposes of sorting.
       * <p>
       * The precedence order (higher precedence sorts first):
       * <p>
       * <ol>
       *   <li> The combined number of exact matches and unwrap matches.
       *   <li> Near matches.
       *   <li> Parent matches.
       *   <li> The order in which the method was returned by Java reflection.
       * </ol>
       *
       * @param    other
       *           The instance being compared to the "this" instance.
       *
       * @return   Negative if this is less than, zero if the objects are equivalent, or
       *           positive if the given instance is greater than this instance.
       */
      public int compareTo(FuzzyMatch other)
      {
         if (other == null)
         {
            throw new NullPointerException();
         }
         
         int result = 0;
         
         if (this.method != other.method)
         {
            if (this.exact == other.exact && this.unwrap == other.unwrap)
            {
               if (this.near == other.near)
               {
                  if (this.parent == other.parent)
                  {
                     // if all other ordering information is the same, the ordinal controls
                     result = this.ordinal - other.ordinal;
                  }
                  else
                  {
                     result = this.parent - other.parent;
                  }
               }
               else
               {
                  result = this.near - other.near;
               }
            }
            else
            {
               result = (this.exact + this.unwrap) - (other.exact + other.unwrap);
            }
         }
         
         return result;
      }
   }
}