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();
}
}
}