ComObject.java

/*
** Module   : ComObject.java
** Abstract : The base class for all COM objects, with minimal management.
**
** Copyright (c) 2017-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description-----------------------------------
** 001 OM  20170530 First commit.
** 002 CA  20171026 Changes for native COM automation support.
** 003 CA  20171102 The comhandle must use the comId as its ID reported by the id() API.
**     EVL 20171110 Fix for array out of index exception issue when merging OUTPUT or INPUT-OUTPUT
**                  parameters back to Java side.
** 004 CA  20180517 Fixed comhandle FWD-specific resource management.
** 005 OM  20190223 Added support for indexed properties. Fixed properties signature lookup.
**                  Added ref-counting support.
** 006 HC  20190816 Various fixes to COM extension objects - mostly loading and invocation.
** 007 CA  20191212 Fixed constructor lookup during marshal (look for super-class or interface 
**                  match).
** 008 MAG 20191024 Fix NPE bug in call() method when params argument is null.
** 009 VVT 20200203 The default implementations for the valid() and delete() methods were added;
**                  An internal cache is used for method lookup.
**                  The internal xcall() method introduced to handle all COM method- and property-
**                  related calls.
** 010 VVT 20200501 convertInputParameter: javadoc added; input parameter renamed to better match
**                  its purpose; arguments of the BaseDataType are now treated specially if the
**                  destination type is Object.
**     VVT 20200722 The decimal 4gl type can now be converted to Java native integer types.
**     VVT 20200723 Direct output to stdout replaced by log calls in the xcall() method.
** 011 CA  20200924 Replaced Method.invoke with ReflectASM.
**     VVT 20201015 Logging of remote calls has been optimized. Also the call times are also logged now.
**     CA  20210218 Arithmetic operations with integer operands are evaluated as int64: changed to allow 
**                  conversion of integer arguments. 
**     VVT 20211125 convertInputParameter() now is complete and can be accessed from Unit tests.
**     VVT 20211126 convertInputParameter(): conversion decimal -> Object support added.
**     HC  20211201 Added support for runtime conversion of converted OCX object references to comhandle.
**     HC  20220421 Added optional method and property resolution without the need to annotate methods with 
**                  ComMethod or ComProperty annotations.
**     VVT 20220427 Javadocs minor fixes.
**     VVT 20220720 Access to getComObjectInstance() is now public to allow COM parameter conversion.
**                  See #6244.
**     TJD 20220504 Java 11 compatibility minor changes
**     VVT 20230203 marshal() method: added support for LocalDate. See #6953.
** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 013 GBB 20230602 Lowered logging level of debug messages.
** 014 CA  20240331 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so logical
**                  type checks must include sub-classes, too.
** 015 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
**                  default methods cause the Java metaspace to spike one order of magnitude.
*/

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

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.time.*;
import java.time.format.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.logging.*;

import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.google.common.collect.*;

/**
 * Base class for COM objects that will be stored using {@link comhandle}s. Does minimal
 * management of the COM Object.
 */
public abstract class ComObject
implements WrappedResource,
           Deletable
{
   /** Cache of Java methods implementing COM properties. */
   protected static final Map<String, Multimap<String, Method>> propertyTable = new HashMap<>();

   /** Cache of Java methods implementing COM methods. */
   protected static final Map<String, Multimap<String, Method>> methodTable = new HashMap<>();

   /**
    * When set to {@code true} the method and property resolution mechanism will include also methods not
    * annotated with {@linkplain ComMethod} or {@linkplain ComProperty}.
    */
   protected boolean includeUnannotatedMethods;

   /**
    * The resource id. Used by {@code WrappedResource} management. 
    */
   private Long resourceId = null;

   /**
    * The parent {@code ControlFrameWidget} widget. This container is used during the dispatch of 
    * the events generated by this COM Object.
    */
   private ControlFrameWidget parentControlFrame = null;

   /** The legacy name of this com object. */
   private String name = "unknown";

   /**
    * The creator of this object. Cannot be {@code null}. Used for locating the event callbacks 
    * as its internal procedure. When becomes invalid, this object also gets invalid and should be
    * deleted. 
    */
   private handle parentProcedure = null;

   /** Counter for {@link comhandle}s that refer this object. */
   private AtomicInteger refcount = new AtomicInteger(1);

   /** The logger for COM methods. Should be public. Used by a customer code. */
   public final static CentralLogger LOGGER = CentralLogger.get(ComObject.class.getName());

   /** The class for void Java type */
   private static final Class<?> VOID = Void.TYPE;

   /** Build a new COM Object. Calls {@code init()} to initialize internal data structure. */
   public ComObject()
   {
      init();
   }

   /**
    * Initialize internal data structure of a fresh COM: the parent procedure is detected.
    */
   protected void init()
   {
      parentProcedure = ProcedureManager.thisProcedure();
   }

   /**
    * Converts a value to a BDT object. The method does not check whether the value is already a
    * {@code BaseDataType}, in fact is assume it is not. Note that, because this method uses the
    * best matching constructor, this method may do some implicit conversions if the passed in
    * value is compatible with the class required (like from a {@code String}).
    * 
    * @param   aVal
    *          The value to be converted.
    *
    * @return  A {@code BaseDataType} instance that represent the same value as {@code aVal}.
    */
   protected static BaseDataType marshal(Object aVal)
   {
      if (aVal != null)
      {
         if (aVal instanceof BaseDataType)
         {
            return (BaseDataType) aVal;
         }
         else if (aVal instanceof String)
         {
            return new character((String) aVal);
         }
         else if (aVal instanceof Boolean)
         {
            return new logical((Boolean) aVal);
         }
         else if (aVal instanceof Integer)
         {
            return new integer((Integer) aVal);
         }
         else if (aVal instanceof Long)
         {
            return new int64((Long) aVal);
         }
         else if (aVal instanceof Double)
         {
            return new decimal((Double) aVal);
         }
         else if (aVal instanceof Float)
         {
            return new decimal((Float) aVal);
         }
         else if (aVal instanceof Date)
         {
            return new date((Date) aVal);
         }
         else if (aVal instanceof Enum)
         {
            return new integer(((Enum<?>) aVal).ordinal());
         }
         else if (aVal instanceof LocalDate)
         {
            final LocalDate localDate = (LocalDate) aVal;
            return new date(localDate.getMonthValue(),
                     localDate.getDayOfMonth(),
                     localDate.getYear(),
                     false);
         }
      }

      return new comhandle();
   }

   /**
    * Return true by default.
    */
   @Override
   public boolean valid()
   {
      return true;
   }

   /**
    * Do nothing by default
    */
   @Override
   public void delete()
   {
      // no-op
   }
   
   /**
    * Check if the resource can veto the delete or not, before attempting the {@link #delete()} call.
    * 
    * @param    ref
    *           The exact handle holding the reference, on which the delete is attempted.
    * @param    explicit
    *           Flag indicating this is called via <code>DELETE OBJECT</code> from the application.
    *           
    * @return   <code>false</code> if the delete is vetoed.
    */
   @Override
   public boolean allowDelete(handle ref, boolean explicit)
   {
      return true;
   }

   /**
    * Converts a value from a {@link ComParameter}, {@link BaseDataType} or other Java type to
    * a {@link ComParameter} instance which can be passed as an argument to a COM method call.
    * 
    * @param   aVal
    *          The value to be converted.
    *
    * @return  A {@code ComParameter} instance.
    * 
    * @throws  IllegalArgumentException
    *          If the value can't be passed by FWD as an argument for a COM method call.
    */
   protected static ComParameter unmarshal(Object aVal)
   {
      if (aVal == comhandle.EMPTY)
      {
         return null;
      }

      boolean extent = false;

      if (aVal instanceof ComParameter)
      {
         ComParameter aparam = (ComParameter) aVal;
         extent = aparam.isExtent();

         if (aparam.isOutput())
         {
            // replace the BDT instance for OUTPUT parameters and set it to default value
            ComParameter oparam = new ComParameter(aparam);

            Object value;

            if (extent)
            {
               Class<?> cls = oparam.getValue().getClass().getComponentType();

               value = Array.newInstance(cls, oparam.getLength());
               for (int i = 0; i < oparam.getLength(); i++)
               {
                  Array.set(value, i, BaseDataType.generateDefault(cls));
               }
            }
            else
            {
               value = ((BaseDataType) aparam.getValue()).instantiateDefault();
            }
            oparam.setValue(value);

            aVal = oparam;
         }
      }

      String[] fwdDataType = new String[1];

      Function<Object, Object> process = (value) -> {
         if (value instanceof BaseDataType)
         {
            BaseDataType bdt = (BaseDataType) value;
            fwdDataType[0] = bdt.getTypeName();

            if (bdt.isUnknown() || bdt instanceof unknown)
            {
               value = null;
            }
            else if (bdt instanceof memptr)
            {
               value = ((memptr) bdt).getPointerValue().longValue();
            }
            else if (bdt instanceof raw)
            {
               value = ((raw) bdt).getByteArray();
            }
            else if (bdt instanceof comhandle)
            {
               ComObject com = ((comhandle) bdt).getResource();

               if (com instanceof NativeComObject)
               {
                  value = ((NativeComObject) com).getComId();
               }
               else
               {
                  throw new IllegalArgumentException(
                           "FWD COM implementations can't be invoked on native side!");
               }
            }
            else if (bdt instanceof date || bdt instanceof datetime || bdt instanceof datetimetz)
            {
               value = ((date) bdt).dateValue();
            }
            else if (bdt instanceof handle)
            {
               throw new IllegalArgumentException(
                        "HANDLE data-type is not supported by native COM arguments!");
            }
            else if (bdt instanceof logical)
            {
               value = ((logical) bdt).booleanValue();
            }
            else if (bdt instanceof decimal)
            {
               value = ((decimal) bdt).doubleValue();
            }
            else if (bdt instanceof recid)
            {
               throw new IllegalArgumentException(
                        "RECID data-type is not supported by native COM arguments!");
            }
            else if (bdt instanceof integer)
            {
               value = ((integer) bdt).intValue();
            }
            else if (bdt instanceof int64)
            {
               value = ((int64) bdt).longValue();
            }
            else if (bdt instanceof rowid)
            {
               throw new IllegalArgumentException(
                        "ROWID data-type is not supported by native COM arguments!");
            }
            else if (bdt instanceof Text)
            {
               value = ((Text) bdt).getValue();
            }
            else
            {
               throw new IllegalArgumentException(
                        fwdDataType[0] + " data-type is not supported by native COM arguments!");
            }
         }
         else
         {
            if (value == null)
            {
               fwdDataType[0] = "unknown";
            }
            if (value instanceof String)
            {
               fwdDataType[0] = "character";
            }
            else if (value instanceof Boolean)
            {
               fwdDataType[0] = "logical";
            }
            else if (value instanceof Integer)
            {
               fwdDataType[0] = "integer";
            }
            else if (value instanceof Long)
            {
               fwdDataType[0] = "int64";
            }
            else if (value instanceof Double)
            {
               fwdDataType[0] = "decimal";
            }
            else if (value instanceof Float)
            {
               fwdDataType[0] = "decimal";
            }
            else if (value instanceof Date)
            {
               fwdDataType[0] = "date";
            }
            else
            {
               throw new IllegalArgumentException(value.getClass()
                        + " data-type is not supported by native COM arguments!");
            }
         }

         return value;
      };

      ComParameter ret;
      Object value;

      if (aVal instanceof ComParameter)
      {
         ComParameter cparam = (ComParameter) aVal;

         ret = new ComParameter(cparam);
         ret.setByPointer(cparam.isByPointer());
         ret.setByVariantPointer(cparam.isByVariantPointer());
         ret.setDataType(cparam.getDataType());
         ret.setMode(cparam.getMode());

         value = cparam.getValue();
      }
      else
      {
         ret = new ComParameter();
         ret.setByPointer(false);
         ret.setByVariantPointer(false);
         ret.setDataType(null);
         ret.setMode('I');

         value = aVal;
      }

      if (value != null && value.getClass().isArray())
      {
         ret.setExtent(true);
         ret.setLength(Array.getLength(value));
      }

      if (ret.isExtent())
      {
         Object avalue = Array.newInstance(Object.class, ret.getLength());

         for (int i = 0; i < ret.getLength(); i++)
         {
            Array.set(avalue, i, process.apply(Array.get(value, i)));
         }

         value = avalue;
      }
      else
      {
         value = process.apply(value);
      }

      ret.setFwdDataType(fwdDataType[0].toUpperCase());
      ret.setValue(value);

      return ret;
   }

   /**
    * Converts an array of values from a {@link ComParameter}, {@link BaseDataType} or other Java 
    * type to a {@link ComParameter} instance which can be passed as an argument to a COM method 
    * call.
    * 
    * @param   aVals
    *          The values to be converted.
    *
    * @return  A {@code ComParameter} instance.
    * 
    * @throws  IllegalArgumentException
    *          If the value can't be passed by FWD as an argument for a COM method call.
    */
   protected static ComParameter[] unmarshal(Object[] aVals)
   {
      // the parameter list can be empty
      if (aVals == null)
      {
         return null;
      }

      ComParameter[] result = new ComParameter[aVals.length];

      for (int i = 0; i < aVals.length; i++)
      {
         result[i] = unmarshal(aVals[i]);
      }

      return result;
   }

   /**
    * Check the parameters received from a COM method call and copy the values into OUTPUT or
    * INPUT-OUTPUT arguments.
    * 
    * @param    vals
    *           The received values.  The arguments are 1-indexed (as on index 0 is the return 
    *           value), and each argument has an entry in this array; {@code null} values 
    *           represent INPUT arguments or {@link unknown} values.
    * @param    params
    *           The actual references received by FWD for this method call.  All OUTPUT or INPUT-
    *           OUTPUT arguments must be {@link ComParameter} instances.
    */
   protected static void checkParameters(BaseDataType[][] vals, Object[] params)
   {
      if (params == null)
      {
         return;
      }

      for (int i = 0, k = 0; i < params.length; i++)
      {
         if (!(params[i] instanceof ComParameter))
         {
            continue;
         }

         ComParameter comParam = (ComParameter) params[i];
         if (comParam.isOutput() || comParam.isInputOutput())
         {
            // we increase k counter before indexing the vals to account vals[0] is
            // not a parameter but result
            BaseDataType[] val = vals[++k];

            Object aval = comParam.getValue();

            if (comParam.isExtent())
            {
               if (comParam.getLength() == 0)
               {
                  aval = ArrayAssigner.resize((BaseDataType[]) aval, val.length);
                  // and also replace the reference...
                  comParam.getRefReplacement().accept((BaseDataType[]) aval);
               }

               if (comParam.getLength() != val.length)
               {
                  throw new IllegalStateException("Incorrect source and dest array dimension!");
               }

               // now we can just copy the values - a dynamic extent was resized and its reference
               // was properly switched
               for (int j = 0; j < comParam.getLength(); j++)
               {
                  ((BaseDataType) Array.get(aval, j)).assign(val[j]);
               }
            }
            else
            {
               if (!(aval instanceof BaseDataType))
               {
                  throw new IllegalStateException(
                           "COM call sent an OUTPUT/INPUT-OUTPUT argument which was not passed"
                                    + " as a BDT from FWD side!");
               }

               BaseDataType bdt = (BaseDataType) aval;
               if (val[0] == null)
               {
                  bdt.setUnknown();
               }
               else
               {
                  bdt.assign(val[0]);
               }
            }
         }
      }
   }

   /**
    * Reports if this object is unknown.  
    *
    * @return   <code>true</code> if object is unknown.
    */
   @Override
   public boolean unknown()
   {
      return false;
   }

   /**
    * Get this resource's ID, if is already set.
    *
    * @return   The resource's ID or <code>null</code> if not set.
    */
   @Override
   public Long id()
   {
      return resourceId;
   }

   /**
    * Set this resource's ID.
    *
    * @param    id
    *           The resource's ID.
    */
   @Override
   public void id(long id)
   {
      resourceId = id;
   }

   /**
    * Obtain the legacy name of this COM object.
    * 
    * @return  the name of this COM object.
    */
   public String getName()
   {
      return name;
   }

   /**
    * Sets the legacy name of this COM object.
    * 
    * @param   newName
    *          The new legacy name of this COM object.
    */
   public void setName(String newName)
   {
      this.name = newName;
   }

   /**
    * Obtain the name of this ActiveX object.
    * 
    * @return  the name of this ActiveX object.
    */
   public abstract String getActivexName();

   /**
    * Destroy all resources kept by FWD related to this COM object.
    */
   public final void destroy()
   {
      comhandle.removeResource(this);
   }

   /**
    * Obtain the {@code ControlFrameWidget} container for this COM, if any.
    * 
    * @return  container for this COM.
    */
   public ControlFrameWidget getParentControlFrame()
   {
      return parentControlFrame;
   }

   /**
    * Sets the {@code ControlFrameWidget} container for this COM.
    *
    * @param   pcf
    *          The container for this COM.
    */
   public void setParentControlFrame(ControlFrameWidget pcf)
   {
      this.parentControlFrame = pcf;
   }

   /**
    * Get the parent procedure where this COM was created.
    *  
    * @return  the parent procedure of this object.
    */
   public handle getParentProcedure()
   {
      return parentProcedure;
   }

   /**
    * Sets a COM property for the COM object stored by the handle.
    * 
    * @param   prop
    *          The legacy property name. Case insensitive. If no property is found then a warning
    *          is displayed and method returns without altering the stored object.
    * @param   newVal
    *          The new value for the property. It must be of a compatible type with the property.
    * @param   indices
    *          A variable number of indices used to access this property's element.
    * 
    * @return  {@code true} if the property was set.
    * 
    */
   public boolean setProperty(String prop, Object newVal, Object... indices)
   {
      int indexCount = indices == null ? 0 : indices.length;
      Object[] params = new Object[1 + indexCount];
      params[0] = newVal;
      if (indices != null && indexCount > 0)
      {
         System.arraycopy(indices, 0, params, 1, indexCount);
      }

      Collection<Method> matchingMethods = methodsFor(propertyTable,
                                                      ComProperty.class,
                                                      getComObjectClass(),
                                                      prop);

      if (includeUnannotatedMethods)
      {
         matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), "set" + prop));
      }

      xcall(prop, matchingMethods, params);
      return true;
   }

   /**
    * Obtain a property value from the COM object stored in this handle. 
    * 
    * @param   prop
    *          The legacy name of the property. Case insensitive. If no property is found then a
    *          warning is displayed and method returns {@code unknown} value.
    * @param   indices
    *          A variable number of indices used to access this property's element.
    *
    * @return  The value of the requested property or {@code unknown} on exceptions.
    */
   public BaseDataType getProperty(String prop, Object... indices)
   {
      int indexCount = indices.length;
      Collection<Method> matchingMethods = methodsFor(propertyTable,
                                                      ComProperty.class,
                                                      getComObjectClass(),
                                                      prop);

      if (includeUnannotatedMethods)
      {
         matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), "get" + prop));
         matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), "is" + prop));
      }

      /**
       * In 4gl, it is valid to refer a no-argument method call without parenthesis,
       * in this case the method call is taken for a get property call by the FWD conversion.
       * 
       * So, if we found no property with the name given, try to search for a matching method too.
       */
      if (indexCount == 0)
      {
         matchingMethods.addAll(methodsFor(methodTable, ComMethod.class, getComObjectClass(), prop));

         if (includeUnannotatedMethods)
         {
            matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), prop));
         }
      }

      return xcall(prop, matchingMethods, indices);
   }

   /**
    * Returns the com object class.
    * This implementation simply returns {@code this.getClass}. To be overriden by classes
    * providing support for "dynamic" com object access.
    *
    * @return  See above.
    */
   protected Class getComObjectClass()
   {
      return getClass();
   }

   /**
    * Returns the com object instance.
    * This implementation simply returns {@code this}. To be overriden by classes
    * providing support for "dynamic" com object access.
    *
    * @return  See above.
    */
   public Object getComObjectInstance()
   {
      return this;
   }

   /**
    * Select all methods whose name, class and COM annotation match the argument.
    * 
    * @param <T>
    *        the annotation type: either {@link ComMethod} or {@link ComProperty}.
    * @param table
    *        the method class+name - method cache table, lazily updated by this method
    * @param annClass
    *        If not {@code null}, then only methods annotated with this annotation class are tested,
    *        otherwise all methods are tested.
    *        The argument must match the {@code T} template argument.
    * @param clazz
    *        the class of the objects we are looking for the methods in
    * @param comName
    *        the method name in any case.
    * 
    * @return the collection of the matching methods
    */
   private static <T extends Annotation> Collection<Method> methodsFor(
            Map<String, Multimap<String, Method>> table, Class<T> annClass,
            Class<? extends ComObject> clazz, String comName)
   {
      String className = clazz.getName();
      Multimap<String, Method> classTable = table.get(className);

      if (classTable == null)
      {
         classTable = MultimapBuilder.treeKeys().hashSetValues().build();
         table.put(className, classTable);

         if (annClass != null)
         {
            for (Class<?> cls = clazz; cls != Object.class; cls = cls.getSuperclass())
            {
               Class<?>[] faces = cls.getInterfaces();
               for (Class<?> face : faces)
               {
                  for (Method m : face.getMethods())
                  {
                     testAndRegisterMethod(annClass, classTable, m);
                  }
               }

               for (Method m : cls.getMethods())
               {
                  testAndRegisterMethod(annClass, classTable, m);
               }
            }
         }
         else
         {
            for (Method m : clazz.getMethods())
            {
               classTable.put(m.getName().toUpperCase(), m);
            }
         }
      }

      return classTable.get(comName.toUpperCase());
   }

   /**
    * Test if the method passed as the {@code method} argument is
    * annotated with the annotation of the type passed in the {@code annClass}
    * argument. If yes, then register the method in the
    * {@code classTable} under the name extracted from the annotation name()
    * field and converted to the upper case.
    *  
    * @param annClass the annotation class, must be either {@link ComParameter} or {@link ComMethod}
    * @param classTable the map to register the matching method in
    * @param method the input method to test
    */
   private static void testAndRegisterMethod(Class<? extends Annotation> annClass,
                                             Multimap<String, Method> classTable,
                                             Method method)
   {
      Annotation ann = method.getAnnotation(annClass);
      if (ann != null)
      {
         String name;
         if (ann instanceof ComProperty)
            name = ((ComProperty) ann).name();
         else if (ann instanceof ComMethod)
            name = ((ComMethod) ann).name();
         else
            throw new IllegalArgumentException();
         classTable.put(name.toUpperCase(), method);
      }
   }

   /**
    * Try to convert a value to the given type.
    * 
    * @param arg
    *        the value to convert
    * @param targetType
    *        the destination type to convert the value to
    * @param dest
    *        the output array to store the converted value
    * @param idx
    *        the index in the output array to store the successful conversion result
    * 
    * @return {@code true} if conversion succeeded.
    */
   static boolean convertInputParameter(final Object arg, final Class<?> targetType, final Object[] dest,
            final int idx)
   {
      /**
       * If the value is null, and the target type is not a primitive type, convert to null.
       */
      if (arg == null)
      {
         if (targetType.isPrimitive())
         {
            return false;
         }
         dest[idx] = null;
         return true;
      }

      Class<? extends Object> argClass = arg.getClass();

      /**
       * If the value is compatible with the target type, just use it,
       * with one exception: if the target class is Object, and the value
       * is 4gl data type, then convert the value to some base Java type.
       */
      if (targetType.isAssignableFrom(argClass) && !(Object.class.equals(targetType)
               && BaseDataType.class.isAssignableFrom(argClass)))
      {
         dest[idx] = arg;
         return true;
      }

      if ((arg instanceof BaseDataType) && ((BaseDataType) arg).isUnknown())
      {
         /**
          * Try to pass null as the value.
          */
         return convertInputParameter(null, targetType, dest, idx);
      }

      if (comhandle.class.equals(argClass))
      {
         return convertInputParameter(((comhandle) arg).getResource(), targetType, dest, idx);
      }

      if (ComObject.class.isAssignableFrom(targetType))
      {
         ComObject co = (ComObject) arg;
         if (!co.valid())
         {
            throw new IllegalArgumentException();
         }
         dest[idx] = arg;
         return true;
      }

      ///
      // Java -> Java
      ///

      if (arg instanceof String)
      {
         // String -> long
         // String -> Long
         // String -> Long -> int
         // String -> Long -> Integer
         // String -> Long -> int64
         // String -> Long -> integer
         if (Long.class.equals(targetType) || Long.TYPE.equals(targetType)
                  || Integer.class.equals(targetType) || Integer.TYPE.equals(targetType)
                  || int64.class.equals(targetType) || integer.class.equals(targetType))
         {
            try
            {
               final long longValue = Long.parseLong(((String) arg).trim());
               return convertInputParameter(longValue, targetType, dest, idx);
            }
            catch (@SuppressWarnings("unused") NumberFormatException e)
            {
               return false;
            }
         }

         // String -> double
         // String -> Double
         // String -> Double -> decimal
         if (Double.class.equals(targetType) || decimal.class.equals(targetType)
                  || Double.TYPE.equals(targetType))
         {
            try
            {
               final Double doubleValue = Double.parseDouble(((String) arg).trim());
               return convertInputParameter(doubleValue, targetType, dest, idx);
            }
            catch (@SuppressWarnings("unused") NumberFormatException e)
            {
               return false;
            }
         }

         // String -> Boolean
         // String -> boolean
         // String -> Boolean -> logical
         if (Boolean.class.equals(targetType) || Boolean.TYPE.equals(targetType)
                  || logical.class.isAssignableFrom(targetType))
         {
            return convertInputParameter(Boolean.parseBoolean((String) arg), targetType, dest,
                     idx);
         }

         // String -> LocalDate
         // String -> LocalDate -> date
         if (LocalDate.class.equals(targetType) || date.class.equals(targetType))
         {
            try
            {
               final LocalDate value = LocalDate.parse(((String) arg).trim());
               return convertInputParameter(value, targetType, dest, idx);
            }
            catch (@SuppressWarnings("unused") DateTimeParseException e)
            {
               return false;
            }
         }

         // String -> LocalDateTime
         // String -> LocalDateTime -> datetime
         if (LocalDateTime.class.equals(targetType) || datetime.class.equals(targetType))
         {
            try
            {
               final LocalDateTime value = LocalDateTime.parse(((String) arg).trim());
               return convertInputParameter(value, targetType, dest, idx);
            }
            catch (@SuppressWarnings("unused") DateTimeParseException e)
            {
               return false;
            }
         }
      }

      if (arg instanceof LocalDate)
      {
         // LocalDate -> date
         if (date.class.equals(targetType))
         {
            final LocalDate localDate = (LocalDate) arg;
            return convertInputParameter(new date(localDate.getMonthValue(),
                     localDate.getDayOfMonth(), localDate.getYear()), targetType, dest, idx);
         }

         // LocalDate -> LocalDateTime
         // LocalDate -> LocalDateTime -> datetime
         if (LocalDateTime.class.equals(targetType) || datetime.class.equals(targetType))
         {
            final LocalDate localDate = (LocalDate) arg;
            return convertInputParameter(LocalDateTime.of(localDate.getYear(),
                     localDate.getMonth(), localDate.getDayOfMonth(), 0, 0, 0), targetType, dest,
                     idx);
         }
      }

      if (arg instanceof LocalDateTime)
      {
         // LocalDateTime -> datetime
         if (datetime.class.equals(targetType))
         {
            final LocalDateTime localDateTime = (LocalDateTime) arg;
            return convertInputParameter(new datetime(localDateTime.getMonthValue(),
                     localDateTime.getDayOfMonth(), localDateTime.getYear(),
                     localDateTime.getHour(), localDateTime.getMinute(),
                     localDateTime.getSecond(), localDateTime.getNano() / 1000000), targetType,
                     dest, idx);
         }

         // LocalDateTime -> LocalDate
         // LocalDateTime -> LocalDate -> date
         if (LocalDate.class.equals(targetType) || date.class.equals(targetType))
         {
            final LocalDateTime localDateTime = (LocalDateTime) arg;
            return convertInputParameter(LocalDate.of(localDateTime.getYear(),
                     localDateTime.getMonth(), localDateTime.getDayOfMonth()), targetType, dest,
                     idx);
         }
      }

      if (arg instanceof Long)
      {
         // Long -> Double
         // Long -> Double -> decimal
         if (Double.class.equals(targetType) || Double.TYPE.equals(targetType)
                  || decimal.class.equals(targetType))
         {
            return convertInputParameter(Double.valueOf(((Long) arg)), targetType, dest, idx);
         }

         // Long -> Integer
         // Long -> int
         if (Integer.class.equals(targetType) || Integer.TYPE.equals(targetType))
         {
            dest[idx] = Integer.valueOf(((Long) arg).intValue());
            return true;
         }
      }

      // Integer -> long
      // Integer -> Long
      // Integer -> Long -> Double
      // Integer -> Long -> double
      // Integer -> Long -> decimal
      // Integer -> Long -> integer
      // Integer -> Long -> int64
      // FIXME: always convert Integer -> Long?
      if (arg instanceof Integer && (Double.class.equals(targetType)
               || Double.TYPE.equals(targetType) || Long.class.equals(targetType)
               || Long.TYPE.equals(targetType) || decimal.class.equals(targetType)
               || int64.class.equals(targetType) || integer.class.equals(targetType)))
      {
         return convertInputParameter(Long.valueOf(((Integer) arg)), targetType, dest, idx);
      }

      // Integer -> String
      // Long -> String
      // Double -> String
      // Boolean -> String
      if ((Number.class.isAssignableFrom(argClass) || arg instanceof Boolean)
               && String.class.equals(targetType))
      {
         dest[idx] = arg.toString();
         return true;
      }

      // Double -> long
      // Double -> Long
      // Double -> Long -> Integer
      // Double -> Long -> int
      // Double -> Long -> int64
      // Double -> Long -> integer
      if (arg instanceof Double && (Long.class.equals(targetType) || Long.TYPE.equals(targetType)
               || Integer.class.equals(targetType) || Integer.TYPE.equals(targetType)
               || int64.class.equals(targetType) || integer.class.equals(targetType)))
      {
         return convertInputParameter(((Double) arg).longValue(), targetType, dest, idx);
      }

      // Integer -> int
      // Long -> long
      // Double -> double
      // Boolean -> boolean
      if ((arg instanceof Integer && Integer.TYPE.equals(targetType))
               || (arg instanceof Long && Long.TYPE.equals(targetType))
               || (arg instanceof Double && Double.TYPE.equals(targetType))
               || (arg instanceof Boolean && Boolean.TYPE.equals(targetType)))
      {
         dest[idx] = arg;
         return true;
      }

      // LocalDate -> String
      // LocalDate -> String -> character
      // LocalDateTime -> String
      // LocalDateTime -> String -> character
      if ((arg instanceof LocalDate || arg instanceof LocalDateTime)
               && (String.class.equals(targetType) || character.class.equals(targetType)))
      {
         return convertInputParameter(arg.toString(), targetType, dest, idx);
      }

      ///
      // BaseDataType -> Java
      ///

      if (arg instanceof character)
      {
         // character -> String
         // character -> Object
         if (String.class.equals(targetType) || Object.class.equals(targetType))
         {
            // Note: we do not strip zero characters here: the OCX might depend on its presence
            dest[idx] = ((character) arg).getValue();
            return true;
         }

         // character -> String -> *
         return convertInputParameter(((character) arg).toStringMessage(), targetType, dest, idx);
      }

      // logical -> Boolean
      // logical -> Boolean -> *
      if (arg instanceof logical)
      {
         final Boolean booleanValue = Boolean.valueOf(((logical) arg).booleanValue());
         if (Boolean.class.equals(targetType) || Boolean.TYPE.equals(targetType))
         {
            dest[idx] = booleanValue;
            return true;
         }

         return convertInputParameter(booleanValue, targetType, dest, idx);
      }

      // int64 -> Long
      // int64 -> long
      // int64 -> Long -> *
      if (arg instanceof int64)
      {
         return convertInputParameter(((int64) arg).longValue(), targetType, dest, idx);
      }

      if (arg instanceof decimal)
      {
         // decimal -> Double
         // decimal -> double
         // decimal -> Double -> Integer
         // decimal -> Double -> int
         // decimal -> Double -> integer
         // decimal -> Double -> int64
         // decimal -> Double -> long
         // decimal -> Double -> Long
         if (Double.class.equals(targetType) || Double.TYPE.equals(targetType)
                  || Integer.class.equals(targetType) || Integer.TYPE.equals(targetType)
                  || Long.class.equals(targetType) || Long.TYPE.equals(targetType)
                  || int64.class.equals(targetType) || integer.class.equals(targetType)
                  || Object.class.equals(targetType))
         {
            return convertInputParameter(((decimal) arg).doubleValue(), targetType, dest, idx);
         }

         // decimal -> String
         // decimal -> String -> character
         if (String.class.equals(targetType) || character.class.equals(targetType))
         {
            return convertInputParameter(((decimal) arg).toString().trim(), targetType, dest,
                     idx);
         }
      }

      // datetime -> LocalDateTime -> *
      // Note: datetime class is more specific than date, so the datetime conversion branch
      //       must precede the date conversion branch
      if (arg instanceof datetime)
      {
         final datetime datetimeArg = (datetime) arg;
         return convertInputParameter(
                  LocalDateTime.of(datetimeArg.getYear(), datetimeArg.getMonth(),
                           datetimeArg.getDay(), datetimeArg.getHours(), datetimeArg.getMinutes(),
                           datetimeArg.getSeconds(), datetimeArg.getMilliseconds() * 1000000),
                  targetType, dest, idx);
      }

      // date -> LocalDate -> *
      if (arg instanceof date)
      {
         final date dateArg = (date) arg;
         return convertInputParameter(
                  LocalDate.of(dateArg.getYear(), dateArg.getMonth(), dateArg.getDay()),
                  targetType, dest, idx);
      }

      ///
      // Java -> BaseDataType 
      ///

      // String -> character
      // Integer -> character
      // Long -> character
      // Double -> character
      if (character.class.equals(targetType))
      {
         if (arg instanceof String || arg instanceof Integer || arg instanceof Long
                  || arg instanceof Double || arg instanceof Boolean || arg instanceof date
                  || arg instanceof datetime || arg instanceof logical)
         {
            dest[idx] = character.valueOf(arg);
            return true;
         }
      }

      // Boolean -> logical
      if (logical.class.isAssignableFrom(targetType) && (arg instanceof Boolean))
      {
         dest[idx] = new logical((Boolean) arg);
         return true;
      }

      // Integer -> integer
      // Long -> integer
      // int64 -> integer
      if (integer.class.equals(targetType))
      {
         if (arg instanceof Integer)
         {
            dest[idx] = new integer((Integer) arg);
            return true;
         }

         if (arg instanceof Long)
         {
            dest[idx] = new integer(((Long) arg).intValue());
            return true;
         }

         if (arg instanceof int64)
         {
            final int64 i = (int64) arg;
            dest[idx] = new integer(i.intValue());
            return true;
         }

         return false;
      }

      // Integer -> int64
      // Long -> int64
      if (int64.class.equals(targetType) && arg instanceof Long)
      {
         dest[idx] = new int64(((Long) arg).intValue());
         return true;
      }

      // Number -> decimal
      if (decimal.class.equals(targetType) && arg instanceof Number)
      {
         dest[idx] = new decimal((Number) arg);
         return true;
      }

      if (NumberType.class.equals(targetType))
      {
         if (arg instanceof Integer)
         {
            dest[idx] = new integer((int) arg);
            return true;
         }

         if (arg instanceof Long)
         {
            dest[idx] = new integer((int) arg);
            return true;
         }

         if (arg instanceof Double)
         {
            dest[idx] = new decimal((double) arg);
            return true;
         }

         if (arg instanceof Float)
         {
            dest[idx] = new decimal((float) arg);
            return true;
         }
      }

      return false;
   }

   /**
    * Calls a COM-method without parameters on the COM-object stored in this com-handle.
    *
    * @param   _name
    *          The method name, case insensitive. If the COM-object does not declare such method
    *          an error message is displayed and this method returns an empty {@link comhandle}.
    * @param   params
    *          The list of actual parameters. Their types must be compatible with the parameters
    *          of called method.
    *
    * @return  The result of the called method, if any, or undefined {@link comhandle} if no compatible method was found.
    */
   public BaseDataType call(String _name, Object... params)
   {
      final Collection<Method> matchingMethods = methodsFor(methodTable,
                                                            ComMethod.class,
                                                            getComObjectClass(),
                                                            _name);

      if (includeUnannotatedMethods)
      {
         matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), _name));
      }

      /**
       * Note: we cannot really tell the method call from the indexed property,
       * so we will look the property dictionary either up.
       */
      matchingMethods.addAll(methodsFor(propertyTable, ComProperty.class, getComObjectClass(), _name));

      if (includeUnannotatedMethods)
      {
         matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), "get" + _name));
      }

      return xcall(_name, matchingMethods, params);
   }

   /**
    * Select a method from the given list, and call the method with the arguments specified.
    * 
    * For the method to match, the provided argument list must coerce into the method parameter types.
    * 
    * @param _name the method name to search for, used for "method not found" reporting only.
    * @param methods the list of all methods of the class matching the name
    * @param args the arguments to pass to the method call
    * 
    * @return if the method was selected and called successfully, then
    *         return the value returned from the call coerced to the {@link BaseDataType}.
    *         Otherwise return {@code null}
    */
   private BaseDataType xcall(String _name, Collection<Method> methods, Object[] args)
   {
      // TODO: OUTPUT/INPUT-OUTPUT parameters
      if (!methods.isEmpty())
      {
         boolean isFinerLevelEnabled = LOGGER.isLoggable(Level.FINER);

         Object[] argValues = new Object[args.length];

         for (int i = 0; i < args.length; i++)
         {
            Object param = args[i];
            argValues[i] = (param instanceof ComParameter) ? ((ComParameter) param).getValue()
                     : param;
         }

         outer: for (Method m : methods)
         {
            int methodParamCount = m.getParameterCount();
            if (args.length != methodParamCount)
            {
               continue outer;
            }
            Class<?>[] methodParamTypes = m.getParameterTypes();
            int j = 0;
            for (Object param : argValues)
            {
               if (!convertInputParameter(param, methodParamTypes[j], argValues, j))
               {
                  continue outer;
               }
               j++;
            }

            try
            {
               final Class<?> returnType = m.getReturnType();

               long beginNanos = 0;
               if (isFinerLevelEnabled)
               {
                  LOGGER.log(Level.FINER, () -> {
                     StringBuilder sb = new StringBuilder(100);
                     sb.append(this);
                     sb.append(' ');
                     sb.append(_name);
                     sb.append(' ');
                     for (Object arg : argValues)
                     {
                        sb.append(arg);
                        sb.append(' ');
                     }
                     
                     return sb.toString();
                  });
                  beginNanos = System.nanoTime();
               }
               if (VOID.equals(returnType))
               {
                  Utils.invoke(m, getComObjectInstance(), argValues);
                  if (isFinerLevelEnabled)
                  {
                     LOGGER.log(Level.FINER, "Completed in "
                              + ((System.nanoTime() - beginNanos) / 1000.0) + " microseconds");
                  }

                  return new comhandle();
               }

               Object ret = Utils.invoke(m, getComObjectInstance(), argValues);

               if (isFinerLevelEnabled)
               {
                  LOGGER.log(Level.FINER,
                          "Completed in " + ((System.nanoTime() - beginNanos) / 1000.0)
                                    + " microseconds, return value: " + ret);
               }

               if (ret instanceof BaseDataType)
               {
                  return (BaseDataType) ret;
               }

               if (ret instanceof ComObject)
               {
                  return new comhandle(ret);
               }

               return marshal(ret);
            }
            catch (IllegalAccessException e)
            {
               LOGGER.warning("", e);
            }
            catch (InvocationTargetException e)
            {
               final Throwable cause = e.getCause();
               if (cause instanceof ConditionException)
               {
                  throw (ConditionException) cause;
               }
               LOGGER.warning("", e);
            }
         }
      }
      // No method resolved
      comhandle.showAccessError(_name.toUpperCase(), "0x80020006", "Unknown name.");
      return new comhandle();
   }

   /**
    * Calls a COM-method on the COM-object stored in this com-handle.
    *
    * @param   methodName
    *          The method name. Case insensitive. If the COM-object does not declare such method
    *          an error message is displayed and this method returns {@code unknown} value.
    * @param   params
    *          The list of actual parameters. Their types must be compatible with the parameters
    *          of called method.
    *          TODO: marshal to BDT (?) When code is generated [param] is already BDT.
    *
    * @return  The result of the called method, in a {@code comhandle}, so it can be used in a
    *          chained call or property access. 
    */
   public comhandle chainCall(String methodName, Object... params)
   {
      // TODO: marshal parameters to BDT
      // TODO: OUTPUT/INPUT-OUTPUT parameters
      // TODO: if no compatible method found, display error message
      BaseDataType result = call(methodName, params);

      return result instanceof comhandle ? (comhandle) result : new comhandle(result);
   }

   /**
    * Increments the reference counter for this COM object.
    * 
    * TODO: what if this is invalid?
    */
   public void ref()
   {
      if (refcount.get() <= 0)
      {
         System.out.println("Refcounting imbalanced (ref) for " + this);
         return;
      }
      refcount.incrementAndGet();
   }

   /**
    * Increments the reference counter for this COM object. Automatically deletes the object when
    * reference counter reaches 0 as there are no more reference to this object.
    */
   public void deref()
   {
      if (refcount.get() <= 0)
      {
         System.out.println("Refcounting imbalanced (deref) for " + this);
         return;
      }

      int count = refcount.decrementAndGet();
      if (count == 0)
      {
         delete();
         destroy();
      }
   }
}