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