comhandle.java

/*
** Module   : comhandle.java
** Abstract : Progress 4GL compatible COM handle object
**
** Copyright (c) 2010-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description---------------------------------
** 001 GES 20101005 Placeholder version with some stubs for attributes and methods.
** 002 SIY 20101014 Changed signatures.
** 003 OM  20170523 Added new methods and completed implementation of existing ones.
** 004 CA  20171026 Changes for native COM automation support.
** 005 EVL 20171101 Fix for COM object handle reading with client-server exchange.
** 006 GES 20171207 Removed explicit bypass on pending error (new silent error mode).
** 007 CA  20180517 Fixed comhandle FWD-specific resource management.
** 008 OM  20120223 Added support for indexed properties. Added ref-counter.
** 009 VVT 20190915 method and property access re-designed, erroneous NAME property handling fixed.
** 010 MAG 20191211 Fixed NPE raised while unboxing Long null value to primitive long.
** 011 VVT 20200107 The comhandle(Object value) now accepts the null argument with no complaint
** 012 HC  20200313 Javadoc fixes.
** 013 VVT 20200916 showInvalidAccessError() now calls recordOrShowError() instead of displayError().
**                  See #4887.
** 013 IAS 20201007 Added Type enum
** 014 HC  20211201 Added support for runtime conversion of converted OCX object references to
**                  comhandle.
**     AL2 20220319 Added value proxy check in assign.
**     HC  20220414 comhandle may be constructed from a handle of a converted OCX object.
** 015 CA  20230215 'instantiateDefault' now is implemented by each sub-class.  Added unknown.UNKNOWN 
**                  singleton (used by FWD runtime at this time).
**     CA  20230215 'duplicate()' method returns the real type instead of BDT.
** 016 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 017 ES  20230905 Added modifiyResourceIfNotPresent and createPOJOComObjectResource methods
**     ES  20230905 Change mapping of POJOComObject after the id of its wrapped object
** 018 CA  20231007 Fixed a NPE when assigning to an invalid handle. 
** 019 AL2 20250530 Added getIndependentFromContext.
*/

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

import com.goldencode.p2j.comauto.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.logging.*;

import java.io.*;
import java.util.*;
import java.util.logging.*;

/**
 * A class that represents a Progress 4GL compatible COM handle data type. It provides a mechanism
 * to group the functionality that is specific to a COM handle in Progress.
 */
public class comhandle
extends BaseDataType
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(handle.class.getName());

   /** Stores context-local state variables. */
   private static final ContextLocal<WorkArea> work =
   new ContextLocal<WorkArea>()
   {
      @Override
      public WeightFactor getWeight()
      {
         return WeightFactor.LAST;
      };
      
      @Override
      protected synchronized WorkArea initialValue()
      {
         return new WorkArea();
      }
   };

   /** The empty parameter marker for property access and method calls. */
   public static final Object EMPTY = new Object();
   
   /** The object stored by this special handle. */
   protected ComObject value = null;
   
   /** 
    * Special handler that shows an invalid access error when a attribute or method member is
    * accessed for an unknown/invalid handle.
    */
   private final ControlFrameComObject invalidAccessHandler = new ControlFrameComObject()
   {
      /**
       * Shows an invalid access error.
       *
       * @param   ocx
       *          Ignored.
       * @param   ctrl
       *          Ignored.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public logical loadControls(String ocx, String ctrl)
      {
         showInvalidAccessError("LoadControls");
         return new logical();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public comhandle getControlName()
      {
         showInvalidAccessError("CONTROL-NAME");
         return new comhandle();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public comhandle getControls()
      {
         showInvalidAccessError("CONTROLS");
         return new comhandle();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public handle getWidgetHandle()
      {
         showInvalidAccessError("WIDGET-HANDLE");
         return new handle();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public integer getWidth()
      {
         showInvalidAccessError("WIDTH");
         return new integer();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   width
       *          Ignored.
       */
      @Override
      public void setWidth(long width)
      {
         showInvalidAccessError("WIDTH");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   width
       *          Ignored.
       */
      @Override
      public void setWidth(NumberType width)
      {
         showInvalidAccessError("WIDTH");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public integer getHeight()
      {
         showInvalidAccessError("HEIGHT");
         return new integer();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   height
       *          Ignored.
       */
      @Override
      public void setHeight(long height)
      {
         showInvalidAccessError("HEIGHT");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   height
       *          Ignored.
       */
      @Override
      public void setHeight(NumberType height)
      {
         showInvalidAccessError("HEIGHT");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public integer getLeft()
      {
         showInvalidAccessError("LEFT");
         return new integer();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   left
       *          Ignored.
       */
      @Override
      public void setLeft(long left)
      {
         showInvalidAccessError("LEFT");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   left
       *          Ignored.
       */
      @Override
      public void setLeft(NumberType left)
      {
         showInvalidAccessError("LEFT");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public integer getTop()
      {
         showInvalidAccessError("TOP");
         return new integer();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   top
       *          Ignored.
       */
      @Override
      public void setTop(long top)
      {
         showInvalidAccessError("LEFT");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   top
       *          Ignored.
       */
      @Override
      public void setTop(NumberType top)
      {
         showInvalidAccessError("LEFT");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public integer getWidgetId()
      {
         showInvalidAccessError("WIDGET-ID");
         return new integer();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public handle getFrameHandle()
      {
         showInvalidAccessError("FRAME");
         return new handle();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public comhandle getActiveX(String axName)
      {
         showInvalidAccessError(axName);
         return new comhandle();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @return  always {@code unknown}.
       */
      @Override
      public character name()
      {
         showInvalidAccessError("NAME");
         return new character();
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   name
       *          Ignored.
       */
      @Override
      public void name(String name)
      {
         showInvalidAccessError("NAME");
      }
      
      /**
       * Shows an invalid access error.
       *
       * @param   name
       *          Ignored.
       */
      @Override
      public void name(character name)
      {
         showInvalidAccessError("NAME");
      }
   };
   
   /**
    * Default constructor, creates an instance that represents the unknown value.
    */
   public comhandle()
   {
   }
   
   /**
    * Get the type
    * @return type
    */
   public Type getType()
   {
      return Type.COM_HANDLE;
   }
   
   /**
    * Constructs an instance that has the contained object reference and {@code unknown} that 
    * exactly matches that of the passed-in instance.
    *
    * @param    that
    *           The instance to pattern from.
    */
   public comhandle(comhandle that)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      if (BaseDataType.isProxy(that))
      {
         if (that.val() instanceof comhandle)
         {
            that = (comhandle) that.val();
         }
         else
         {
            initialize(that.val());
            return;
         }
      }
      if (that.isUnknown())
      {
         this.setUnknown();
      }
      else
      {
         _setValue(that.value);
      }
   }
   
   /**
    * Constructs an instance that wraps the given object reference as a {@link WrappedResource}.
    *
    * @param    value
    *           The instance to contain.
    */
   public comhandle(Object value)
   {
      if (value instanceof BaseDataType)
      {
         // the value may be a proxy; make sure to unwrap and work with the real value
         value = value == null ? null : ((BaseDataType) value).val();
      }
      if (value instanceof comhandle)
      {
         comhandle that = (comhandle) value;
         if (that.isUnknown())
         {
            this.setUnknown();
         }
         else
         {
            _setValue(that.value);
         }
      }
      else if (value instanceof ComObject)
      {
         _setValue((ComObject) value);
      }
      else if (value instanceof handle)
      {
         if (((handle) value)._isValid())
         {
            // this may be a converted OCX object
            _setValue(createPOJOComObjectResource(value));
         }
         else
         {
            setUnknown();
         }
      }
      else if (value == null)
      {
         setUnknown();
      }
      else
      {
         setUnknown();
         LOG.severe("Invalid construct!");
      }
   }
   
   /**
    * Convert the string representation of a comhandle into the original comhandle instance. 
    * This facility can only work for comhandles that have been converted at least once into a 
    * string via {@link #toString}. Such comhandles are stored in a map for conversion purposes.
    *
    * @param    txt
    *           A valid text representation of a comhandle ID.
    *
    * @return   The converted comhandle instance.
    */
   public static comhandle fromString(String txt)
   {
      Long id = ResourceIdHelper.idFromString(txt);
      if (ResourceIdHelper.isUnknown(id))
      {
         return new comhandle();
      }
      else if (ResourceIdHelper.isZero(id))
      {
         return new comhandle(ZERO_RESOURCE);
      }
      
      return fromResourceId(id);
   }
   
   /**
    * Converts the number representation of a handle into the original handle instance.
    * <p>
    * For handles <em>whose backing resources still exist</em> and which have been cached by
    * their IDs via {@link #resourceId(ComObject)} (which is called at various points in
    * the handle's life cycle), the method returns a valid handle instance. For handles whose
    * backing resources no longer exist, or which have never had their backing resources cached,
    * the method returns a zero handle, i.e. handle with "0" string representation. 
    * 
    * @param   resourceId
    *          Resource identifier.
    *
    * @return  The converted comhandle instance.
    */
   public static comhandle fromResourceId(Long resourceId)
   {
      WorkArea wa = work.get();
      WrappedResource resource = wa.idResources.get(resourceId);
      
      if (resource != null)
      {
         return new comhandle(resource);
      }
      
      return new comhandle(ZERO_RESOURCE);
   }
   
   /**
    * Get the ID of the given resource, as used by the {@link #toString} and
    * {@link #fromString} APIs.
    * <p>
    * The method allocates additional resources together with the provided wrapped resource, 
    * thus it is necessary that {@link #removeResource(ComObject)} is called when 
    * the returned resource id is not needed anymore (i.e. the related resource is deleted),
    * see {@link #removeResource(ComObject)} for more details.
    * 
    * @param    resource
    *           A resource instance.
    *
    * @return   The ID of this resource.
    */
   public static Long resourceId(ComObject resource)
   {
      if (resource == ZERO_RESOURCE)
      {
         return 0L;
      }
      
      Long existingId = resource.id();
      
      // resource id already set at the resource?
      if (existingId != null)
      {
         return existingId;
      }
      
      WorkArea wa = work.get();
      
      // resource id already allocated?
      existingId = wa.resourceIds.get(resource);
      if (existingId != null)
      {
         // yes, save it at the resource, too
         resource.id(existingId);
         
         return existingId;
      }
      
      if (!resource.valid())
      {
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, 
                    "You are creating a new ID for an invalid resource " + 
                    resource.getClass().getName() + "!",
                    new Exception());
         }
         else if (LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING, 
                    "You are creating a new ID for an invalid resource " + 
                    resource.getClass().getName() + "! (enable FINE logging for a stacktrace)");
         }
      }
      
      // allocate new resource id - collision with existing ids must be prevented
      Long id = null;
      do 
      {
         id = ResourceIdHelper.getNext();
      } 
      while (wa.idResources.containsKey(id));
      
      wa.idResources.put(id, resource);
      wa.resourceIds.put(resource, id);
      
      // save it at the resource, too
      resource.id(id);
      
      return id;
   }

   /**
    * Remove the given resource from the context-local map of all resource.
    * <p>
    * This method should be called by all com objects resources when released,
    * that is all resources which are sub-classes of {@link ComObject}.
    * 
    * @param    resource
    *           A resource instance to be removed.
    */
   public static void removeResource(ComObject resource)
   {
      if (resource == null)
      {
         return;
      }
      
      WorkArea wa = work.get();
      Long id = wa.resourceIds.remove(resource);
      if (id != null)
      {
         wa.idResources.remove(id);
      }
   }

   /**
    * Modify the contained data of the instance to copy the instance data from the given instance.
    * Generally, the class of the given instance must match the class upon which the 
    * {@code assign} method is called, although this is not a strict requirement.
    *
    * @param   newVal
    *          The backup instance from which to copy data.
    */
   @Override
   public void assign(Undoable newVal)
   {
      if (newVal instanceof comhandle)
      {
         _setValue(((comhandle) newVal).value);
      }
      else if (newVal instanceof ComObject)
      {
         _setValue((ComObject) newVal);
      }
      else if (newVal instanceof handle)
      {
         _setValue(modifiyResourceIfNotPresent(((handle) newVal).getResource()));
      }
      else
      {
         LOG.severe("Invalid construct!");
      }
   }
   
   /**
    * Dedicated assignment method for loading a {@code ComObject} into this {@code comhandle}.
    * This method is only called by {@link ComServer#create(String, comhandle)}. Stores a newly
    * created {@code ComObject} into this handle.
    * 
    * @param   newVal
    *          The new value to be stored by this handle. 
    */
   public void assign(ComObject newVal)
   {
      // TODO: check undoable ?
      _setValue(newVal);
   }
   
   /**
    * Sets the state (data and unknown value) of this instance based on the state of the passed
    * instance.
    *
    * @param    from
    *           The instance from which to copy state.
    */
   @Override
   public void assign(BaseDataType from)
   {
      if (from == null)
      {
         setUnknown();
         return;
      }

      // the value may be a proxy; make sure to unwrap and work with the real value
      from = from.val();
      if (from.isUnknown())
      {
         setUnknown();
      }
      else if (from instanceof comhandle)
      {
         comhandle that = (comhandle) from;
         _setValue(that.value);
      }
      else if (from instanceof handle)
      {
         // special case when ZERO_RESOURCE is passed in, convert it to local ZERO_RESOURCE
         WrappedResource res = ((handle) from).getResource();
         if (res != null && Long.valueOf(0).equals(res.id()))
         {
            _setValue(ZERO_RESOURCE); 
         }
         else
         {
            _setValue(new POJOComObject(((handle) from).get()));
         }
      }
      else
      {
         String err = "Incompatible data types in expression or assignment.";
         ErrorManager.recordOrThrowError(223, err);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void assign(Object value)
   {
      if (value instanceof WrappedResource)
      {   
         assign(modifiyResourceIfNotPresent((WrappedResource) value));
      }
      else
      {
         super.assign(value);
      }
   }

   /**
    * Determines if this instance is valid (FWD internal access).
    *
    * @return  boolean {@code true} if this instance can be used.
    */
   public boolean _isValid()
   {
      return (value != null) && value.valid();
   }
   
   /**
    * Determines if this instance is valid.
    *
    * @return  {@code true} if this instance can be used.
    */
   public logical isValid()
   {
      return new logical(_isValid());
   }
   
   /**
    * Creates a new instance of the same type that represents the {@code unknown} value.
    *
    * @return   An instance that represents the {@code unknown} value.
    */
   @Override
   public BaseDataType instantiateUnknown()
   {
      return new comhandle();
   }
   
   /**
    * Creates a new instance of the same type that represents the default initialized value.
    *
    * @return   An instance that represents the default value.
    */
   @Override
   public BaseDataType instantiateDefault()
   {
      return new comhandle();
   }

   /**
    * Does the same as standard {@code clone()} method but returns an instance of
    * {@code BaseDataType} and doesn't throw the {@code CloneNotSupportedException}.
    *
    * @return   A clone of this instance.  
    */
   @Override
   public comhandle duplicate()
   {
      return new comhandle(this);
   }
   
   /**
    * Reports if this instance represents the {@code unknown}.
    *
    * @return   {@code true} if this instance is set to the {@code unknown} value.
    */
   @Override
   public boolean isUnknown()
   {
      return value == null;
   }
   
   /**
    * Custom implementation of {@link Object#hashCode}.
    *  
    * @return  a hashcode for current object.
    */
   @Override
   public int hashCode()
   {
      if (isUnknown()) // Ie. value == null
      {
         return ResourceIdHelper.UNKNOWN_STRING.hashCode();
      }
      
      return value.hashCode();
   }
   
   /**
    * Sets the state of this instance's {@code unknown value} flag to {@code true}.
    * <p>
    * <b>Warning: a separate call is needed to ensure that the data of this instance is set to the
    * correct value.</b>
    */
   @Override
   public void setUnknown()
   {
      if (!isUnknown())
      {
         checkUndoable(true);
      }
      
      _setValue(null);
   }
   
   /**
    * Compares this instance with the specified instance and returns a -1 if this instance is less
    * than the specified, 0 if the two instances are equal and 1 if this instance is greater than
    * the specified instance.  This is the implementation of the {@code Comparable} interface.
    * <p>
    * The algorithm will fail to give meaningful results in the case where one tries to sort
    * against other objects that do not represent compatible values.
    *
    * @param    obj
    *           The instance to compare against.
    *
    * @return   0 1 or -1 depending on if the passed instance is or is not {@code unknown}.
    */
   @Override
   public int compareTo(Object obj)
   {
      // handle is also a BDT type
      if (obj instanceof comhandle)
      {
         comhandle that = (comhandle) obj;
         
         // two unknown values are equals
         if (isUnknown() || that.isUnknown())
         {
            // an unknown handle is greater than a known handle
            return Boolean.compare(isUnknown(), that.isUnknown());
         }
         
         return (comhandle.resourceId(this.value) == comhandle.resourceId(that.value)) ? 0 : 1;
      }
      
      return -1;
   }
   
   /**
    * Creates a string representation of the instance data using the format 
    * {@link ResourceIdHelper#DEFAULT_FORMAT}. If the instance represents the {@code unknown}
    * value, a '?' will be returned.
    *
    * @return  The formatted string.
    */
   @Override
   public String toString()
   {
      return toString(null);
   }
   
   /**
    * Creates a string representation of the instance data using the user specified format string
    * (Progress 4GL compatible) or if no such string is provided, the default format of 
    * {@link ResourceIdHelper#DEFAULT_FORMAT}. If the instance represents the {@code unknown}
    * value, a '?' will be returned.
    *
    * @param   fmt
    *          The Progress 4GL format string or {@code null} if the default format is to be used.
    *
    * @return  The formatted string.
    */
   @Override
   public String toString(String fmt)
   {
      if (isUnknown())
      {
         return ResourceIdHelper.UNKNOWN_STRING;
      }
      
      Long id = comhandle.resourceId(value);
      
      return ResourceIdHelper.idToString(fmt, id);
   }
   
   /**
    * Obtain the legacy name of this COM object.
    *
    * @return  the name of this COM object.
    */
   public character getName()
   {
      return value == null ? new character() : new character(value.getName());
   }
   
   /**
    * Sets the legacy name of this COM object.
    *
    * @param   newName
    *          The new legacy name of this COM object. 
    */
   public void setName(character newName)
   {
      if (value != null)
      {
         value.setName(newName.toJavaType());
      }
   }
   
   /**
    * Creates a string representation of the instance data in a form that is compatible with the
    * {@code MESSAGE} language statement.  If the instance represents the {@code unknown value},
    * a '?' will be returned.
    *
    * @return  The 'message' formatted string.
    */
   @Override
   public String toStringMessage()
   {
      return toString();
   }
   
   /**
    * Creates a string representation of the instance data using the 'export' format.  If the
    * instance represents the {@code unknown value}, a '?' will be returned.
    *
    * @return   The 'export' formatted string.
    */
   @Override
   public String toStringExport()
   {
      return toString();
   }
   
   /**
    * Return the default display format string for this type.
    * <p>
    * Warning: this is larger than the Progress version because the range of handle values can be
    * a 64-bit integer in this implementation.
    *
    * @return   The default format string.
    */
   @Override
   public String defaultFormatString()
   {
      return ResourceIdHelper.DEFAULT_FORMAT;
   }   
   
   /**
    * Replacement for the default object reading method. The latest state is read from the input
    * source.
    * 
    * @param    in
    *           The input source from which fields will be restored.
    *
    * @throws   IOException
    *           In case of I/O errors.
    * @throws   ClassNotFoundException
    *           If payload can't be instantiated.
    */
   @Override
   public void readExternal(ObjectInput in)
   throws IOException,
          ClassNotFoundException
   {
      // if value is not null we use generic reader
      if (value != null)
      {
         if (!(value instanceof Externalizable))
         {
            throw new NotSerializableException("comhandle");
         }
         
         ((Externalizable) value).readExternal(in);
      }
      else
      {
         // there are the cases when we need to read the object before we can
         // decide if it is Externalizable or not, assuming to get native COM
         _setValue((ComObject) in.readObject());
      }
   }
   
   /**
    * Replacement for the default object writing method. The latest state is written to the output
    * destination.
    * 
    * @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
   {
      if (value != null && !(value instanceof Externalizable))
      {
         throw new NotSerializableException("comhandle");
      }

      // the object is Externalizable or null
      out.writeObject(value);
   }
   
   /**
    * Unwrap this com-handle to the {@link ControlFrameComObject} instance of the parent 
    * {@link ControlFrame}. If the comhandle is not obtained from a Control Frame using 
    * {@code COM-HANDLE} attribute handle or is invalid, a special {@link ControlFrameComObject}
    * is returned that handles invalid accesses.
    *
    * @return  See above. 
    */
   public ControlFrameComObject unwrapComObject()
   {
      // to the best effort to identify the object 
      if (value instanceof ControlFrameComObject)
      {
         return (ControlFrameComObject) value;
      }
      
      return invalidAccessHandler;
   }
   
   /**
    * 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.  
    */
   public void setProperty(String prop, Object newVal)
   {
      setIndexedProperty(prop, newVal);
   }
   
   /**
    * Sets a COM property for the COM object stored by the handle, with arbitrary indices.
    * 
    * @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.
    */
   public /*final*/ void setIndexedProperty(String prop, Object newVal, Object... indices)
   {
      if (value == null)
      {
         showInvalidAccessError(prop.toUpperCase());
         return;
      }
      
      try
      {
         if (!value.setProperty(prop, newVal, indices))
         {
            showAccessError(prop.toUpperCase(), "0x80020006", "Unknown name.");
         }
      }
      catch (PropertyAccessException pae)
      {
         showAccessError(prop.toUpperCase(), pae.getErrorCode(), pae.getMessage());
      }
   }
   
   /**
    * Obtain the COM object held by this comhandle.
    * 
    * @return  the COM object held by this comhandle.
    */
   public ComObject getResource()
   {
      return value;
   }
   
   /**
    * 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.
    *
    * @return  The value of the requested property or {@code unknown} on exceptions.
    */
   public /*final*/ BaseDataType getProperty(String prop)
   {
      if (value != null
          && (value instanceof ControlFrameComObject)
          && value.getActivexName().equalsIgnoreCase(prop)
          )
      {
         return new comhandle(value); 
      }
      
      return getIndexedProperty(prop);
   }
   
   /**
    * 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.
    */
   public /*final*/ BaseDataType getIndexedProperty(String prop, Object... indices)
   {
      if (value == null)
      {
         showInvalidAccessError(prop.toUpperCase());
         return unknown.UNKNOWN;
      }
      
      return value.getProperty(prop, indices);
   }
   
   /**
    * Chains access to COM property/methods. This call is generated when a chained construct is
    * encountered in legacy code. For example, {@code ch:prop1:prop2:prop-or-method} will be
    * converted to something like: {@code ch.chain("prop1").chain("prop2").get("prop-or-method")} 
    * 
    * @param   property
    *          The property to access. It should normally be a com-object property of the object
    *          stored in this {@code comhandle}. 
    * 
    * @return  A {@code comhandle} to the COM object property of currently referred resource. It
    *          normally should be chained with another access for a property of method. 
    */
   public comhandle chain(String property)
   {
      return new comhandle(getProperty(property));
   }
   
   /**
    * Calls a COM-method 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.
    *          TODO: marshal to BDT (?) When code is generated [param] is already BDT.
    *
    * @return  The result of the called method, if any.
    */
   public BaseDataType call(String methodName, Object... params)
   {
      if (value == null)
      {
         showInvalidAccessError(methodName.toUpperCase());
         //what is the error
         return unknown.UNKNOWN;
      }
      
      return value.call(methodName, params);
   }
   
   /**
    * 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.
    *          TODO: marshal to BDT (?) When code is generated [param] is already BDT.
    *
    * @return  The result of the called method, in a {@code comhandle}, so it can be used in a
    *          chained call or property access. 
    */
   public comhandle chainCall(String methodName, Object... params)
   {
      if (value == null)
      {
         showInvalidAccessError(methodName.toUpperCase());
         //what is the error
         return new comhandle();
      }
      
      return value.chainCall(methodName, params);
   }
   
   /**
    * Shows a standard error for accessing an property of method of a com object stored in a
    * {@code com-handle}.
    *
    * @param   prop
    *          The legacy name of the property/method being accessed.
    * @param   errCode
    *          Error code.
    * @param   errMsg
    *          Error message.
    */
   public static void showAccessError(String prop, String errCode, String errMsg)
   {
      String msg = "Error occurred while accessing component property/method: " +
                   prop + ".\n" + errMsg + "\nError code: " + errCode + " " +
                   ProcedureManager.thisProcedure().unwrap().name().toStringMessage();
      ErrorManager.displayError(5890, msg, false);
   }
   
   /**
    * Identify if this data is dependent upon the context local. This identifies
    * if the data is safe to be cached in cross-session places or the data should
    * not leak from the context. If is is not safe to be shared cross-session, then
    * this should return {@code null}. If other representation is suitable for caching,
    * then this can return another object that wraps internal values that are not
    * dependent upon the context (e.g. date, date-time, date-time-tz).
    * 
    * @return   Always. {@code null}.
    */
   @Override
   public Object getIndependentFromContext()
   {
      return null;
   }
   
   
   /**
    * Shows a standard error for accessing an invalid {@code com-handle}.
    *
    * @param   prop
    *          The legacy name of the property or method being accessed. 
    */
   private static void showInvalidAccessError(String prop)
   {
      String msg = "Invalid component-handle referenced while processing method/statement: " +
                   prop + ".\n\n" + ProcedureManager.thisProcedure().unwrap().name().toStringMessage();
      ErrorManager.recordOrShowError(5884, msg, false);
   }
   
   /**
    * Updates the current value. Takes care to update the refcount for both of them. 
    * 
    * @param   val
    *          The new value to be assigned.
    */
   private void _setValue(ComObject val)
   {
      if (value == val)
      {
         return; // no-op
      }
      
      if (value != null && value.valid())
      {
         value.deref();
      }
      
      value = val;
      
      if (val != null && value.valid())
      {
         value.ref();
      }
   }
   
   /**
    * Create a new POJOComObject, if not already present in the idResource to ComObject mapping  .
    * 
    * @param   value
    *          Handle to be wrapped inside a POJOComObject.
    *          
    * @return  A new POJOComObject if handle not present in a mapping, or existing POJOComObject
    *          for given {@value}.
    */
   private POJOComObject createPOJOComObjectResource(Object value)
   {
      WorkArea wa = work.get();
      
      Long id = handle.resourceId(((handle) value).getResource());
      ComObject pojo = wa.idResources.get(id);
      if (pojo == null)
      {
         pojo = new POJOComObject(((handle) value).getResource());
         wa.idResources.put(id, pojo);
         wa.resourceIds.put(pojo, id);
      }
      
      return (POJOComObject) pojo;
   }
   
   /**
    * Change the wrapped value of a comhandle, if @newVlaue not already present in the mapping,
    * also deletes the old value from the mapping.
    * 
    * @param   newValue
    *          A wrapped resource with an id.
    *          
    * @return  Newly created POJOComObject if @newValue not present in the mapping,
    *          or old of @this otherwise.
    */
   private POJOComObject modifiyResourceIfNotPresent(WrappedResource newValue)
   {
      WorkArea wa = work.get();
      
      Long oldId = null;
      if (value != null)
      {         
         oldId = comhandle.resourceId(value);
      }
      Long newId = handle.resourceId(newValue);
      
      ComObject pojo = wa.idResources.get(newId);
      if (pojo == null)
      {
         pojo = new POJOComObject((newValue));
         wa.idResources.put(newId, pojo);
         wa.resourceIds.put(pojo, newId);
         
         if (value != null)
         {            
            wa.resourceIds.remove(value);
            wa.idResources.remove(oldId);
         }
         
         return (POJOComObject) pojo;
      }
      
      return (POJOComObject) pojo;
   }
   
   /**
    * A "zero" pseudo-resource. A "zero" resource is used for handles created by the
    * {@link handle#fromString(String)} method when a formally valid ID is provided, but the ID
    * doesn't denote a valid resource.
    * <p>
    * A zero resource is not valid but is not unknown.
    */
   public static final ComObject ZERO_RESOURCE = new ComObject()
   {
      @Override
      protected void init()
      {
         // disable any initialization
      }
      
      @Override
      public boolean valid()
      {
         return false;
      }
      
      @Override
      public boolean unknown()
      {
         return false;
      }
      
      @Override
      public Long id()
      {
         return 0L;
      }
      
      @Override
      public void id(long id)
      {
         // no-op
      }
      
      @Override
      public String getActivexName()
      {
         return null;
      }
      
      @Override
      public void delete()
      {
      }
   };
   
   /** 
    * Stores global data relating to the state of the current context.
    */
   private static class WorkArea
   {  
      /** 
       * Stores comhandles that have been converted to strings and may need to be converted back 
       * into a comhandle.
       */
      private Map<Long, ComObject> idResources = new HashMap<>();
      
      /**
       * Map of resources and their resource ids. Resource ids are tracked here because they are 
       * naturally related to comhandles.
       */
      private Map<ComObject, Long> resourceIds = new /*Weak*/HashMap<>();
   }
}