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