ComServer.java
/*
** Module : ComServer.java
** Abstract : Management class for COM objects.
**
** Copyright (c) 2017-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description-----------------------------------
** 001 OM 20170530 First commit.
** 002 OM 20170829 Added overloaded methods for [connect] options.
** 003 CA 20171026 Changes for native COM automation support.
** 004 CA 20180517 Fixed comhandle FWD-specific resource management.
** 005 OM 20190225 Fixed event emitting method. Added support for creating FWD Com object using
** their class.
** CA 20190322 Wait for the fired async event to finish before continuing.
** 006 HC 20190810 Added MsgBlaster to the map of known objects.
** 007 HC 20200119 Added extension points to allow to externalize widget implementation.
** 008 VVT 20200203 The convenience createAutomationServer() method added to eliminate code duplication.
** @SuppressWarning annotations added as appropriate.
** 009 TJD 20220504 Upgrade do Java 11
** 010 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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 com.goldencode.p2j.extension.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.client.event.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import java.util.*;
import java.util.concurrent.*;
/**
* This is a starting point for java-emulated COM objects.
*
* A list of known rewritten in Java entities is used to lookup the "registered" COM objects.
*/
public class ComServer
{
/** The map of all registered COM objects. */
private static Map<String, Class<? extends ComObject>> vault = new HashMap<>();
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(ComServer.class);
static
{
// this will be read form some xml or other kind of file
vault.put("PSTIMER", PSTimer.class);
vault.put("MSGBLST32", MsgBlaster.class);
}
/**
* Registers a com object in the internal map of com name to com class.
*
* @param name
* COM object name.
* @param clazz
* COM class.
*/
public static void registerComObject(String name, Class<? extends ComObject> clazz)
{
vault.put(name.toUpperCase(), clazz);
}
/**
* Releases resources occupied by a valid {@code comhandle}.
*
* @param handle
* The {@code comhandle} that holds the COM object to be released.
*/
public static void release(comhandle handle)
{
handle.assign((ComObject) null);
}
/**
* Deletes a valid {@code ComObject} store in a {@code comhandle} and releases any resources
* occupied by it.
*
* @param handle
* The {@code comhandle} that holds the COM object to be released.
*/
public static void delete(comhandle handle)
{
WrappedResource res = handle.getResource();
if (res != null)
{
ComObject com = (ComObject) res;
if (com.valid())
{
com.delete();
com.destroy();
}
}
}
/**
* Emit an event by asynchronously calling the associated procedure.
*
* @param event
* The event to be delivered.
* @param sync
* If {@code true} the event is emitted synchronously, by invoking the callback
* directly. Otherwise it is posted as a server event.
*/
public static void emit(ComEvent event, boolean sync)
{
ComObject com = event.getEmitter();
if (!com.valid())
{
// the emitter got invalid while the event travelled the queues. 0xbaadf00d, must drop!
return;
}
String ctrlName = null;
ControlFrameWidget cf = com.getParentControlFrame();
if (cf != null)
{
ctrlName = cf.name().getValue();
}
if (ctrlName == null)
{
ctrlName = com.getName();
}
String callback = ctrlName + "." + com.getActivexName() + "." + event.getEventName();
// "<ControlName> Dot <AxName> Dot <AxEventName>"
if (!checkValid(com))
{
return;
}
if (sync)
{
ControlFlowOps.invokeIn(callback,
com.getParentProcedure(),
(Object[]) event.getParameters());
return;
}
try
{
CountDownLatch latch = new CountDownLatch(1);
// invoke asynchronously:
LogicalTerminal.postServerEvent(
new ServerEvent(
event.getEmitter().id(),
"COM-EVENT",
() ->
{
try
{
if (checkValid(com))
{
ControlFlowOps.invokeIn(callback,
com.getParentProcedure(),
(Object[]) event.getParameters());
}
}
finally
{
latch.countDown();
}
}));
try
{
latch.await();
}
catch (@SuppressWarnings("unused") InterruptedException ie)
{
// ignore
}
}
catch (Exception e)
{
LOG.severe("Error posting event:", e);
}
}
/**
* Checks whether the {@code com} object is valid. If the object is found not to be valid it is
* deleted and destroyed.
*
* @param com
* The {@code ComObject} to be tested.
*
* @return {@code true} if the object is valid, {@code false} otherwise.
*/
public static boolean checkValid(ComObject com)
{
if (!com.valid())
{
return false;
}
if (com.getParentProcedure()._isValid())
{
return true;
}
com.delete();
com.destroy();
return false;
}
/**
* Creates a new instance of the {@code automation} object and assigns it to provided
* {@link comhandle} if operation is successful.
* <p>
* A new instance of the Server for object is launched.
*
* @param comObjectType
* The name of the {@code automation}.
* @param handle
* A {@code comhandle} to store the handle to new object.
*/
public static void create(String comObjectType, comhandle handle)
{
create(comObjectType, handle, false, "");
}
/**
* Creates a new instance of the {@code automation} object and assigns it to provided
* {@link comhandle} if operation is successful.
* <p>
* A new instance of the Server for object is launched.
*
* @param comType
* The name of the {@code automation}.
* @param handle
* A {@code comhandle} to store the handle to new object.
*/
public static void create(Text comType, comhandle handle)
{
String strComType = (comType == null || comType.isUnknown()) ? null : comType.toJavaType();
create(strComType, handle, false, "");
}
/**
* Creates a new instance of the {@code automation} object and assigns it to provided
* {@link comhandle} if operation is successful.
* <p>
* A new instance of the Server for object is launched.
*
* @param comObjectType
* The name of the {@code automation}.
* @param handle
* A {@code comhandle} to store the handle to new object.
* @param connect
* If {@code true} attempt to connect to an active (instantiated) Automation object
* identified by {@code comObjectType}.
*/
public static void create(String comObjectType, comhandle handle, boolean connect)
{
create(comObjectType, handle, connect, "");
}
/**
* Creates a new instance of the {@code automation} object and assigns it to provided
* {@link comhandle} if operation is successful.
* <p>
* A new instance of the Server for object is launched.
*
* @param comType
* The name of the {@code automation}.
* @param handle
* A {@code comhandle} to store the handle to new object.
* @param connect
* If {@code true} attempt to connect to an active (instantiated) Automation object
* identified by {@code comObjectType}.
*/
public static void create(Text comType, comhandle handle, boolean connect)
{
String strComType = (comType == null || comType.isUnknown()) ? null : comType.toJavaType();
create(strComType, handle, connect, "");
}
/**
* Creates a new instance of the {@code automation} object and assigns it to provided
* {@link comhandle} if operation is successful.
*
* @param comObjectType
* The name of the {@code automation}.
* @param handle
* A {@code comhandle} to store the handle to new object.
* @param connect
* If {@code true} attempt to connect to an active (instantiated) Automation object
* identified by {@code comObjectType}.
* @param documentPath
* The document that will be open or connect to.
*/
public static void create(String comObjectType,
comhandle handle,
boolean connect,
String documentPath)
{
if (comObjectType == null)
{
ErrorManager.displayError(3179, "Unable to evaluate widget type", false);
return;
}
// syntax #1: no-connect: create a top-level automation and start the new server, ignoring
// whether other are already running
if (!connect)
{
if (comObjectType.isEmpty())
{
String msg = "Connecting to an automation object requires an automation object or file name.\n\n" +
ProcedureManager.thisProcedure().unwrap().name().toStringMessage();
ErrorManager.displayError(5886, msg, false);
return;
}
ComObject aServer = createAutomationServer(comObjectType);
if (aServer != null)
{
handle.assign(aServer);
}
return;
}
// syntax #2: connect to an active top-level Automation object only
if (documentPath == null || documentPath.isEmpty())
{
if (comObjectType.isEmpty())
{
String msg = "Connecting to an automation object requires an automation object or file name.\n\n" +
ProcedureManager.thisProcedure().unwrap().name().toStringMessage();
ErrorManager.displayError(5886, msg, false);
return;
}
ComObject aServer = getAutomation(comObjectType, true, true /* NO CREATE */);
if (aServer == null)
{
// TODO: oops, the server must exist in order to CONNECT to it, also it must be a server
return;
}
handle.assign(aServer);
return;
}
// syntax #3: open a new document in an (eventually) existing automation Server
if (!comObjectType.equals(""))
{
// create it now (will launch automatically) if not exist, if there are multiple
// instances, pick one randomly
ComObject aServer = createAutomationServer(comObjectType);
if (aServer == null)
{
return;
}
ServerComObject comServer = (ServerComObject) aServer;
ComObject automation = comServer.open(documentPath);
if (automation == null)
{
return; // TODO: oops, check error messages
}
handle.assign(automation);
return;
}
// syntax 4: [comObjectType] == "", [documentPath] must exist!
// automatically determine the Automation server based on the type of the document
// TODO: we should interrogate the underlying OS or pick the server name from a map
}
/**
* Create and return a new automation server. Return a new server or {@code null}
* in case of error.
*
* @param comObjectType The name of the automation.
*
* @return See above.
*/
private static ComObject createAutomationServer(String comObjectType)
{
ComObject aServer = getAutomation(comObjectType, true, false /* allow new */);
if (aServer == null)
{
String msg = "The automation server for " + comObjectType + " is not registered properly.\n" +
"Please reinstall this server or try registering it again";
ErrorManager.recordOrThrowError(5893, msg, false);
return null;
}
return aServer;
}
/**
* Creates a new instance of the {@code automation} object and assigns it to provided
* {@link comhandle} if operation is successful.
* <p>
* A new instance of the Server for object is launched.
*
* @param comType
* The name of the {@code automation}.
* @param handle
* A {@code comhandle} to store the handle to new object.
* @param connect
* If {@code true} attempt to connect to an active (instantiated) Automation object
* identified by {@code comObjectType}.
* @param documentPath
* The document that will be open or connect to.
*/
public static void create(Text comType, comhandle handle, boolean connect, String documentPath)
{
String strComType = (comType == null || comType.isUnknown()) ? null : comType.toJavaType();
create(strComType, handle, connect, documentPath);
}
/**
* Creates a new instance of the {@code automation} object and assigns it to provided
* {@link comhandle} if operation is successful.
* <p>
* A new instance of the Server for object is launched.
*
* @param comObjectType
* The name of the {@code automation}.
* @param handle
* A {@code comhandle} to store the handle to new object.
* @param connect
* If {@code true} attempt to connect to an active (instantiated) Automation object
* identified by {@code comObjectType}.
* @param docPath
* The document that will be open or connect to.
*/
public static void create(String comObjectType,
comhandle handle,
boolean connect,
Text docPath)
{
String strDocPath = (docPath == null || docPath.isUnknown()) ? null : docPath.toJavaType();
create(comObjectType, handle, connect, strDocPath);
}
/**
* Creates a new instance of the {@code automation} object and assigns it to provided
* {@link comhandle} if operation is successful.
* <p>
* A new instance of the Server for object is launched.
*
* @param comType
* The name of the {@code automation}.
* @param handle
* A {@code comhandle} to store the handle to new object.
* @param connect
* If {@code true} attempt to connect to an active (instantiated) Automation object
* identified by {@code comObjectType}.
* @param docPath
* The document that will be open or connect to.
*/
public static void create(Text comType, comhandle handle, boolean connect, Text docPath)
{
String strComType = (comType == null || comType.isUnknown()) ? null : comType.toJavaType();
String strDocPath = (docPath == null || docPath.isUnknown()) ? null : docPath.toJavaType();
create(strComType, handle, connect, strDocPath);
}
/**
* Obtain a reference to an automation. It can be a new instance or a reference to an
* already existing top-level automation.
*
* @param comObjectType
* The name of the automation.
* @param topLevel
* Only return top-level. If the requested {@code comObjectType} is not a top-level,
* return {@code null}.
* @param noCreate
* Must exist. Do not allow creation of new instances. Only used with {@code topLevel}
* requests.
*
* @return The {@code ComObject} requested, or {@code null} if such automation does not exist
* or it does not satisfy the parameter constraints.
*/
private static ComObject getAutomation(String comObjectType,
boolean topLevel,
boolean noCreate)
{
Class<?> aClass = vault.get(comObjectType.toUpperCase());
if (aClass == null)
{
// check whether the comObjectType is the actual Java class, configured from a hints file
try
{
aClass = Class.forName(comObjectType);
}
catch (@SuppressWarnings("unused") ClassNotFoundException e)
{
// do nothing, [aClass] remains null
}
}
if (aClass != null)
{
try
{
return (ComObject) aClass.getDeclaredConstructor().newInstance();
}
catch (@SuppressWarnings("unused") ReflectiveOperationException e)
{
// nothing: go on, with eventual error messages
}
}
return NativeComObject.getAutomation(comObjectType, topLevel, noCreate);
}
}