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