# NativeComObject.java

    /*
    ** Module   : NativeComObject.java
    ** Abstract : A representation of a native COM object.  Delegates access to native APIs to access
    **            it, via a network server to access the client-side.
    **
    ** Copyright (c) 2017-2019, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 CA  20171025 Created initial version.
    ** 002 EVL 20171101 Changed reading external value for long value based method.  Adding override
    **                  for id() method to return proper COM object ID as resource ID.  Adding COM
    **                  object to registry once creation but only for the server side.  The method
    **                  call can also return comhandle to be registered and released later. Adding
    **                  support for arrays returned from native COM property and method.
    ** 003 CA  20180517 Fixed comhandle FWD-specific resource management.
    ** 004 OM  20190223 Added support for generic OCXes and indexed properties.
    */

    /*
    ** 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.io.*;
    import java.util.*;

    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;

    /**
     * This is representation of a native COM object.  It delegates access to native APIs in
     * {@link ComOleHelper} to access and/or manage it.
     */
    public class NativeComObject
    extends ComObject
    implements Externalizable
    {
       /** Stores context-local state variables. */
       private static final ContextLocal<WorkArea> work =
       new ContextLocal<WorkArea>()
       {
          @Override
          public WeightFactor getWeight()
          {
             return WeightFactor.LAST;
          };

          protected synchronized WorkArea initialValue()
          {
             return new WorkArea();
          }
       };

       /** Flag which marks this COM object as deleted/released. */
       private boolean deleted = false;

       /** The ID used to identify this COM object on the native side. */
       private long comId;

       /** The COM automation name. */
       private String name;

       /**
        * Save the reference to the {@link ComOleOps} proxy which delegates the calls to the
        * client-side.
        */
       private ComOleOps comOps;

       /**
        * Default c'tor, for deserialization purposes only.
        */
       public NativeComObject()
       {
          // no-op, for deserialization only
       }

       /**
        * Constructor which can be used by the native (or client-side) to create an instance marked
        * with the {@link #comId}, which will be mapped via the {@link WorkArea#registry} on
        * server-side.
        *
        * @param    comId
        *           The ID used to identify this COM object on the native side.
        */
       public NativeComObject(long comId)
       {
          // from native-side only
          this.comId = comId;
       }

       /**
        * Create a new instance with the specified details.
        *
        * @param    name
        *           The COM automation name.
        * @param    comId
        *           The ID used to identify this COM object on the native side.
        */
       private NativeComObject(String name, long comId)
       {
          this.comId = comId;
          this.name = name;

          WorkArea wa = work.get();
          comOps = wa.comOps;
          wa.registry.put(comId, this);
       }

       /**
        * 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.
        */
       static ComObject getAutomation(String comObjectType,
                                      boolean topLevel,
                                      boolean noCreate)
       {
          WorkArea wa = work.get();

          if (!wa.supported)
          {
             // TODO: show error?
             return null;
          }

          long nativeId = wa.comOps.create(comObjectType, topLevel, noCreate);
          if (nativeId != 0)
          {
             return new NativeComObject(comObjectType, nativeId);
          }

          // failed to create the object
          return null;
       }

       /**
        * Create a new {@link comhandle} by re-using the existing native COM object or create a new
        * one.
        *
        * @param    nco
        *           The {@link NativeComObject} with the COM details.
        *
        * @return   See above.
        */
       private static comhandle createComHandle(NativeComObject nco)
       {
          WorkArea wa = work.get();
          NativeComObject com = null;

          if (wa.registry.containsKey(nco.comId))
          {
             com = wa.registry.get(nco.comId);
          }
          else
          {
             String name = wa.comOps.getName(nco.comId);
             com = new NativeComObject(name, nco.comId);
          }

          return new comhandle(com);
       }

       /**
        * Get the {@link #comId}.
        *
        * @return   See above.
        */
       public long getComId()
       {
          return comId;
       }

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

       /**
        * This is a no-op, as the native COM objects get their IDs from the native side.
        *
        * @param    id
        *           Ignored.
        */
       @Override
       public void id(long id)
       {
          // no-op
       }

       /**
        * Get the COM automation {@link #name}.
        *
        * @return   See above.
        */
       @Override
       public String getActivexName()
       {
          return name;
       }

       /**
        * Reports if this object is valid for use.
        *
        * @return   <code>true</code> if we are valid (can be used).
        */
       @Override
       public boolean valid()
       {
          return !deleted && work.get().registry.containsKey(comId);
       }

       /**
        * Perform actual delete of an resource. At the time of this call, it is assumed the resource
        * is valid for deletion (the handle and the resource are both valid).
        */
       @Override
       public void delete()
       {
          if (valid())
          {
             try
             {
                comOps.release(comId);
             }
             finally
             {
                destroy();
                deleted = true;

                work.get().registry.remove(comId);
             }
          }
       }

       /**
        * 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.
        */
       @Override
       public BaseDataType getProperty(String prop, Object... indices)
       {
          // TODO: extent property?

          // TODO: how to handle errors?
          ComParameter[] aIndices = unmarshal(indices);
          BaseDataType ret = comOps.getProperty(comId, prop, aIndices);

          if (ret instanceof comhandle)
          {
             return createComHandle((NativeComObject) ((comhandle) ret).getResource());
          }

          return ret;
       }

       /**
        * 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</code> if the property was set.
        */
       @Override
       public boolean setProperty(String prop, Object newVal, Object... indices)
       {
          ComParameter val = unmarshal(newVal);
          ComParameter[] aIndices = unmarshal(indices);

          return comOps.setProperty(comId, prop, val, aIndices);
       }

       /**
        * Calls a COM-method without parameters 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.
        *
        * @return  The result of the called method, if any.
        */
       @Override
       public BaseDataType call(String methodName, Object... params)
       {
          // TODO: extent return type?

          ComParameter[] aParams = unmarshal(params);

          BaseDataType[][] vals = comOps.call(comId, methodName, aParams);

          checkParameters(vals, params);

          // method also can return COM-HANDLE and we need to put it in registry
          // to be able to release later
          if (vals[0] != null && vals[0][0] instanceof comhandle)
          {
             vals[0][0] = createComHandle((NativeComObject) ((comhandle) vals[0][0]).getResource());
          }

          return vals[0][0];
       }

       /**
        * Calls a COM-method without parameters 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.
        *
        * @return  The result of the called method, in a {@code comhandle}, so it can be used in a
        *          chained call or property access.
        */
       @Override
       public comhandle chainCall(String methodName, Object... params)
       {
          ComParameter[] aParams = unmarshal(params);

          BaseDataType[][] vals = comOps.call(comId, methodName, aParams);

          checkParameters(vals, params);

          return createComHandle((NativeComObject) ((comhandle) vals[0][0]).getResource());
       }

       /**
        * Replacement for the default object writing method.  Only the {@link #comId} is used on
        * network transfer.  The receiving side must know to create a new instance based on this ID.
        *
        * @param    out
        *           The output destination to which fields will be saved.
        *
        * @throws   IOException
        *           In case of I/O errors.
        */
       @Override
       public void writeExternal(ObjectOutput out)
       throws IOException
       {
          out.writeLong(comId);
       }

       /**
        * Replacement for the default object reading method.  Only the {@link #comId} is used on
        * network transfer.  The receiving side must know to create a new instance based on this ID.
        *
        * @param    in
        *           The input source from which fields will be restored.
        *
        * @throws   IOException
        *           In case of I/O errors.
        */
       public void readExternal(ObjectInput in)
       throws IOException
       {
          comId = in.readLong();
       };

       /**
        * Holds context-local data specific to native COM objects, on server-side.
        */
       private static class WorkArea
       {
          /** The registry of native COM objects available. */
          private final Map<Long, NativeComObject> registry = new HashMap<>();

          /** The {@link ComOleOps} network proxy which delegates the calls to FWD client-side. */
          private final ComOleOps comOps;

          /** Flag indicating if the current system has proper support for COM Automation. */
          private final boolean supported;

          /**
           * Default c'tor, initializes the context-local fields.
           */
          public WorkArea()
          {
             comOps = (ComOleOps) RemoteObject.obtainNetworkInstance(ComOleOps.class);

             supported = comOps.isSupported();
          }
       }
    }