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