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 ≤-1, 0, or ≥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!");
}
}