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