jobject.java

/*
** Module   : jobject.java
** Abstract : wrapper class for all Java objects managed by 4GL code.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20190929 First version.
** 002 IAS 20201007 Added Type enum
** 003 CA  20210609 Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly 
**                  initialized at the method's execution, and not at the caller's arguments.
**     AL2 20220319 Added value proxy check in assign.
**     TJD 20220504 Java 11 compatibility minor changes
** 004 CA  20230215 'duplicate()' method returns the real type instead of BDT.
** 005 CA  20230712 Allow any kind of type to be referenced by 'jobject'.
** 006 CA  20240331 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so logical
**                  type checks must include sub-classes, too.
** 007 CA  20241002 Fixed a possible NPE in assign(BDT), if the arg is null.
** 008 CA  20241009 Passing an argument to a direct Java call must use the actual reference (which may be 
**                  null), and not raise an ERROR condition.
** 009 CA  20241107 Added character constant support.
** 010 ICP 20250123 Added support fot integer and int64 constants.
*/

/*
** 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 java.io.*;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.Date;

import com.goldencode.p2j.util.BaseDataType.*;
import com.goldencode.p2j.util.character.characterConstant;

/**
 * Wrapper class for all Java instances accessed from legacy 4GL code.
 */
public class jobject<T extends Object>
extends BaseDataType
{
   /** The contained Java instance. <code>null</code> is unknown value. */
   private T ref = null;
   
   /** The object's type. */
   private Class<T> type = null;
   
   /**
    * Default constructor.
    */
   public jobject()
   {
   }
   
   /**
    * Create a new {@link jobject} with the specified type and assign it the given value.
    * 
    * @param    type
    *           The expected type.
    * @param    value
    *           The reference.  If needed, convert from legacy 4GL type to Java.
    */
   public jobject(Class<? extends Object> type, BaseDataType value)
   {
      this.type = (Class<T>) type;
      assign(value);
   }
   
   /**
    * Create a new {@link jobject} and assign it the given value.
    * 
    * @param    value
    *           The reference.  If needed, convert from legacy 4GL type to Java.
    */
   public jobject(BaseDataType value)
   {
      this(toJava(value));
   }
   
   /**
    * Initialize this object reference, allowing only assignment to objects compatible with the 
    * specified type.
    * 
    * @param    type
    *           The type of references this object can hold.
    */
   public jobject(Class<? extends Object> type)
   {
      this.type = (Class<T>) type;

      this.ref = null;
   }

   /**
    * Initialize this object with the given reference.
    * 
    * @param    ref
    *           The reference.
    */
   public jobject(T ref)
   {
      if (ref instanceof jobject)
      {
         assign((jobject) ref);
      }
      else if (ref instanceof BaseDataType)
      {
         assign(toJava((BaseDataType) ref));
      }
      else
      {
         this.type = (Class<T>) ref.getClass();
         set(ref);
      }
   }
   
   /**
    * Copy constructor.
    *
    * @param    other
    *           The object from which to copy.
    */
   public jobject(jobject<T> other)
   {
      // no need to avoid assignment if null, a straight copy is just as correct
      this.type = other.type;
      set(other.ref);
   }
   
   /**
    * Convert the given BDT value to a {@link jobject} instance.
    * 
    * @param    value
    *           The BDT instance to be converted to Java.
    *           
    * @return   A {@link jobject} instance wrapping the converted value.  Note that if the 
    *           conversion is not possible, then an ERROR condition will be thrown.
    */
   public static <T> jobject<T> toJava(BaseDataType value)
   {
      if (value instanceof jobject)
      {
         return (jobject<T>) value;
      }
      
      Class<?> type = null;
      if (value instanceof integer)
      {
         type = Integer.class;
      }
      else if (value instanceof int64)
      {
         type = Long.class;
      }
      else if (value instanceof decimal)
      {
         type = Double.class;
      }
      else if (value instanceof Text)
      {
         type = String.class;
      }
      else if (value instanceof logical)
      {
         type = Boolean.class;
      }
      else if (value instanceof raw)
      {
         type = byte[].class;
      }
      else if (type == date.class)
      {
         type = Date.class;
      }
      else if (type == datetime.class)
      {
         type = Timestamp.class;
      }
      else
      {
         ErrorManager.recordOrThrowError(-1, "Can not convert value " + value.toStringMessage() + 
                                             " of type " + value.getClass() + " to Java!");
         return new jobject<T>(type);
      }
      
      jobject<T> var = new jobject<T>(type);
      var.assign(value);
      
      return var;
   }
   
   /**
    * Convert the given BDT value to a {@link jobject} instance.
    * 
    * @param    type
    *           The expected type.
    * @param    value
    *           The BDT instance to be converted to Java.
    *           
    * @return   A {@link jobject} instance wrapping the converted value.  Note that if the 
    *           conversion is not possible, then a ERROR condition will be thrown.
    */
   public static <T> jobject<T> toJava(Class<T> type, BaseDataType value)
   {
      jobject<T> var = new jobject<T>(type);
      var.assign(value);
      
      return var;
   }
   
   /**
    * Convert the given Java-style value to a BDT instance.
    * 
    * @param    type
    *           The expected BDT instance for the value.
    * @param    value
    *           The {@link jobject} wrapping a Java-style value.
    *           
    * @return   The converted BDT value.
    */
   public static <T extends BaseDataType> T fromJava(Class<T> type, jobject<?> value)
   {
      T var = (T) BaseDataType.generateUnknown(type);
      var.assign(value);
      
      return var;
   }
   
   /**
    * Convert the given Java-style value to a BDT instance.
    * 
    * @param    value
    *           The {@link jobject} wrapping a Java-style value.
    *           
    * @return   The converted BDT value.
    */
   static <T extends BaseDataType> T fromJava(jobject<?> value)
   {
      if (value.isUnknown())
      {
         return (T) new unknown();
      }
      
      Class<?> jtype = value.type;
      if (jtype == Integer.class || jtype == Short.class || jtype == Byte.class ||
          jtype == int.class || jtype == short.class || jtype == byte.class)
      {
         return (T) new integer(((Number) value.ref).intValue());
      }
      else if (jtype == Long.class || jtype == long.class)
      {
         return (T) new int64(((Number) value.ref).longValue());
      }
      else if (jtype == Double.class || jtype == Float.class ||
               jtype == double.class || jtype == float.class)
      {
         return (T) new decimal(((Number) value.ref).doubleValue());
      }
      else if (jtype == BigDecimal.class)
      {
         return (T) new decimal((BigDecimal) value.ref);
      }
      else if (jtype == Boolean.class || jtype == boolean.class)
      {
         return (T) new logical(((Boolean) value.ref).booleanValue());
      }
      else if (jtype == String.class)
      {
         return (T) new character((String) value.ref);
      }
      else if (jtype == byte[].class)
      {
         return (T) new raw((byte[]) value.ref);
      }
      else if (jtype == Date.class)
      {
         return (T) new date((Date) value.ref);
      }
      else if (jtype == Timestamp.class)
      {
         return (T) new datetime((Timestamp) value.ref);
      }

      ErrorManager.recordOrThrowError(-1, "Can not convert value " + value.toStringMessage() + 
                                      " of type " +jtype + " from Java!");

      return (T) new unknown();
   }
   
   /**
    * Return the wrapped reference if this instance is not set to unknown value.
    *
    * @return   The contained reference or raise an error if this is unknown value.
    */
   public T ref()
   {
      if (isUnknown())
      {
         String msg = "Invalid handle. Not initialized or points to a deleted object";
         ErrorManager.recordOrThrowError(3135, msg);
         
         // TODO: is there any scenario where we can actually return null in silent error mode?
         //       (If so, that is very bad.)
         return null;
      }

      return ref;
   }
   
   /**
    * Returns the exact {@link #ref reference}.  Null values are allowed.
    * 
    * @return   See above.
    */
   public T arg()
   {
      return ref;
   }

   /**
    * Compares this instance with the specified instance and returns -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</code>
    * 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   &le;-1, 0, or &ge;1 depending on if this instance is less
    *           than, equal to or greater (respectively) than the specified
    *           instance <code>obj</code>.
    */
   @Override
   public int compareTo(Object obj)
   {
      // TODO: use the BaseObject.equals ??
      return -1;
   }
   
   /**
    * A minimal implementation which satisfies the need for a concrete method.
    *
    * @return  The hash code for this object instance.
    */
   public int hashCode()
   {
      int result = 17;
      
      if (isUnknown())
      {
         return result;
      }
      
      result = 37 * result + ref.hashCode();
      
      return result;
   }
   
   /**
    * Set this instance {@link #ref} to the specified value, directly.
    * <p>
    * If <code>ref</code> is <code>null</code>, it will be set to unknown.
    * <p>
    * If the {@link #type} is not compatible with <code>ref</code>, an error condition is raised.
    * 
    * @param    ref
    *           The instance to assign.
    */
   public void assign(Object ref)
   {
      if (ref == null)
      {
         setUnknown();
      }
      else if (ref instanceof BaseDataType)
      {
         assign(((BaseDataType) ref).val());
      }
      else
      {
         assign(new jobject(ref));
      }
   }
   
   /**
    * Assign the current instance reference to the referenced wrapped by the specified value.
    * 
    * @param    value
    *           The Java-style wrapped reference.
    */
   public void assign(jobject<?> value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      if (BaseDataType.isProxy(value))
      {
         assign(value.val());
         return;
      }
      
      if (this.type == null)
      {
         this.type = (Class<T>) value.type;
      }
      
      assign((BaseDataType) value);
   }
   
   /**
    * Get the type
    * @return type
    */
   public Type getType()
   {
      return Type.JOBJECT;
   }

   /**
    * Sets the state (data and unknown value) of this instance based on the state of the passed
    * instance.
    * <p>
    * If the value is not of the same type, some form of automatic type conversion may occur, or
    * an error will be raised.
    * <p>
    * This variant is meant to handle the cases of built-in functions and methods in the 4GL
    * which have polymorphic return types (e.g. DYNAMIC-FUNCTION()). This should NOT be used for
    * non-polymorphic assignments.
    *
    * @param    value
    *           The instance from which to copy state.
    */
   public void assign(BaseDataType value)
   {      
      // the value may be a proxy; make sure to unwrap and work with the real value
      value = value == null ? null : value.val();
      if (value == null || value.isUnknown())
      {
         // don't call setUnknown here
         ref = null;
         return;
      }
      
      Object vref = null;
      if (value instanceof jobject)
      {
         vref = ((jobject) value).ref;
      }
      else
      {
         vref = toJavaInstance(type, value);
      }
      
      if (type != null && !type.isInstance(vref))
      {
         Class<?> primType = type;
         
         // check primitive types
         if (type.isPrimitive())
         {
            if (type == short.class)
            {
               primType = Short.class;
            }
            else if (type == byte.class)
            {
               primType = Byte.class;
            }
            else if (type == int.class)
            {
               primType = Integer.class;
            }
            else if (type == long.class)
            {
               primType = Long.class;
            }
            else if (type == float.class)
            {
               primType = Float.class;
            }
            else if (type == double.class)
            {
               primType = Double.class;
            }
            else if (type == boolean.class)
            {
               primType = Boolean.class;
            }
         }

         if (!primType.isInstance(vref))
         {
            // must be a sub-class or the same as this one
            String lt1 = vref.getClass().getName();
            String lt2 = type().getName();
            ErrorManager.recordOrThrowError(13448, 
                                            "A variable of class '" + lt1 + "' cannot be assigned " +
                                            "to a variable of class '" + lt2 + "'");
            
            // reference remains unchanged
            return;
         }
      }

      set(vref);
   }
   
   /**
    * 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</code> method is
    * called, although this is not a strict requirement.
    *
    * @param    old
    *           The backup instance from which to copy data.
    */
   public void assign(Undoable old)
   {
      if (old instanceof BaseDataType)
      {
         assign((BaseDataType) old);
      }
      else
      {
         // TODO: error out?
      }
   }
   
   /**
    * Reports if this instance represents the <code>unknown value</code>.
    *
    * @return   <code>true</code> if this instance is set to the
    *           <code>unknown value</code>.
    */
   public boolean isUnknown()
   {
      return ref == null;
   }
   
   /**
    * Determines if this instance is valid (to be used).
    *
    * @return   <code>true</code> if this instance can be used.
    */
   public logical isValid()
   {
      return new logical(!isUnknown());
   }
   
   /**
    * Sets the state of this instance's <code>unknown value</code> flag or state to
    * <code>true</code>.
    * <p>
    * <b>Warning: the data stored in this instance will be invalid after calling this method.</b>
    */
   public void setUnknown()
   {
      ref = null;
   }
   
   /**
    * Does the same as standard <code>clone()</code> method but returns an
    * instance of <code>BaseDataType</code> and doesn't throw the
    * <code>CloneNotSupportedException</code>.
    *
    * @return   A clone of this instance.
    */
   public jobject<T> duplicate()
   {
      return new jobject<>(this);
   }

   /**
    * Creates a new instance of the same type that represents the <code>unknown value</code>.
    *
    * @return   An instance that represents the <code>unknown value</code>.
    */
   public BaseDataType instantiateUnknown()   
   {
      return new jobject<>();
   }
   
   /**
    * Creates a string representation of the instance data using the given
    * format string.  If the instance represents the
    *  <code>unknown value</code>, a '?' will be returned.
    *
    * @param    fmt
    *           The format string to use.
    *
    * @return   The formatted string.
    */
   public String toString(String fmt)
   {
      return toStringMessage();
   }
   
   /**
    * Creates a string representation of the instance data in a form that is
    * compatible with the <code>MESSAGE</code> language statement.  If the
    * instance represents the <code>unknown value</code>, a '?' will be
    * returned.
    *
    * @return   The 'message' formatted string.
    */
   public String toStringMessage()
   {
      return isUnknown() ? "?" : ref.toString();
   }
   
   /**
    * Creates a string representation of the instance data using the 'export'
    * format.  If the instance represents the <code>unknown value</code>, a
    * '?' will be returned.
    *
    * @return   The 'export' formatted string.
    */
   public String toStringExport()
   {
      return toStringMessage();
   }
   
   /**
    * Return the default display format string for this type.
    *
    * @return   The default format string.
    */
   public String defaultFormatString()
   {
      // objects can't be formatted
      return null;
   }
   
   /**
    * 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   NotSerializableException
    *           Always, as legacy objects can't be serialized.
    */
   public void readExternal(ObjectInput in)
   throws IOException, ClassNotFoundException
   {
      // there is no need to serialize this object
      throw new NotSerializableException("Legacy objects can't be serialized!");
   }

   /**
    * 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   NotSerializableException
    *           Always, as legacy objects can't be serialized.
    */
   public void writeExternal(ObjectOutput out)
   throws IOException
   {
      // there is no need to serialize this object
      throw new NotSerializableException("Legacy objects can't be serialized!");
   }

   /**
    * Set this object's reference and type from the given instance.
    * 
    * @param    ref
    *           The object instance.
    */
   void set(Object ref)
   {
      // allow any kind of element type for a jobject
      this.ref = (T) ref;
      if (this.type == null && ref != null)
      {
         this.type = (Class<T>) ref.getClass();
      }
   }
   
   /**
    * Get the object's declared class.
    * 
    * @return   The {@link #type}.
    */
   Class<T> type()
   {
      return type;
   }

   /**
    * 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()
   {
      jobject res = new jobject();
      res.type = type;
      return res;
   }
   
   /**
    * Convert the specified legacy BDT value to a Java-compatible instance.
    * 
    * @param    type
    *           The expected type of the Java instance.
    * @param    value
    *           The legacy BDT value.
    *           
    * @return   The converted Java instance; ERROR condition is raised if conversion is not 
    *           possible.
    */
   private static Object toJavaInstance(Class<?> type, BaseDataType value)
   {
      // the BDT value must be compatible with the expected type
      Class<?> cls = value.getClass();
      if (cls == character.class || cls == character.characterConstant.class)
      {
         if (type == String.class)
         {
            return ((character) value).toStringMessage();
         }
      }
      else if (cls == longchar.class)
      {
         if (type == String.class)
         {
            return ((longchar) value).toStringMessage();
         }
      }
      else if (cls == integer.class || cls == integer.integerConstant.class)
      {
         if (type == Short.class || type == short.class)
         {
            return Short.valueOf((short) (int) ((integer) value).toJavaIntegerType());
         }
         else if (type == Byte.class || type == byte.class)
         {
            return Byte.valueOf((byte) (int) ((integer) value).toJavaIntegerType());
         }
         else if (type == Integer.class || type == int.class)
         {
            return ((integer) value).toJavaIntegerType();
         }
         else if (type == Long.class || type == long.class)
         {
            return ((integer) value).toJavaLongType();
         }
         else if (type == Float.class || type == float.class)
         {
            return Float.valueOf(((integer) value).toJavaLongType());
         }
         else if (type == Double.class || type == double.class)
         {
            return Double.valueOf(((integer) value).toJavaLongType());
         }
         else if (type == BigDecimal.class)
         {
            return ((integer) value).toBigDecimal();
         }
      }
      else if (cls == int64.class || cls == int64.int64Constant.class)
      {
         if (type == Short.class || type == short.class)
         {
            return Short.valueOf((short) (int) ((int64) value).toJavaIntegerType());
         }
         else if (type == Byte.class || type == byte.class)
         {
            return Byte.valueOf((byte) (int) ((int64) value).toJavaIntegerType());
         }
         else if (type == Integer.class || type == int.class)
         {
            return ((int64) value).toJavaIntegerType();
         }
         else if (type == Long.class || type == long.class)
         {
            return ((int64) value).toJavaLongType();
         }
         else if (type == Float.class || type == float.class)
         {
            return  Float.valueOf((float)((int64) value).toJavaLongType());
         }
         else if (type == Double.class || type == double.class)
         {
            return Double.valueOf(((int64) value).toJavaLongType());
         }
         else if (type == BigDecimal.class)
         {
            return ((int64) value).toBigDecimal();
         }
      }
      else if (cls == decimal.class)
      {
         if (type == Short.class || type == short.class)
         {
            return Short.valueOf((short) (int) ((decimal) value).toJavaType().doubleValue());
         }
         else if (type == Byte.class || type == byte.class)
         {
            return Byte.valueOf((byte) (int) ((decimal) value).toJavaType().doubleValue());
         }
         else if (type == Integer.class || type == int.class)
         {
            return ((decimal) value).toJavaType().doubleValue();
         }
         else if (type == Long.class || type == long.class)
         {
            return ((decimal) value).toJavaType().doubleValue();
         }
         else if (type == Float.class || type == float.class)
         {
            // check for range
            return  Float.valueOf((float)((decimal) value).toJavaType().doubleValue());
         }
         else if (type == Double.class || type == double.class)
         {
            return Double.valueOf(((decimal) value).toJavaType().doubleValue());
         }
         else if (type == BigDecimal.class)
         {
            return ((decimal) value).toJavaType();
         }
      }
      else if (cls == logical.class || cls == logical.logicalConstant.class)
      {
         if (type == Boolean.class || type == boolean.class)
         {
            return ((logical) value).booleanValue();
         }
      }
      else if (cls == raw.class)
      {
         if (type == byte[].class)
         {
            return ((raw) value).asByteArray();
         }
      }
      else if (cls == date.class)
      {
         if (type == Date.class)
         {
            return ((date) value).dateValue();
         }
      }
      else if (cls == datetime.class)
      {
         if (type == Timestamp.class)
         {
            return new Timestamp(((datetime) value).dateValue().getTime());
         }
      }
      
      if (type == Object.class)
      {
         return value;
      }
      
      throw new ErrorConditionException("Can not convert value " + value.toStringMessage() + 
                                        " of type " + cls + " to Java!");
   }
}