LegacyEnum.java
/*
** Module : LegacyEnum.java
** Abstract : Implementation of the Progress.Lang.Enum builtin class.
**
** Copyright (c) 2020-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------Description--------------------------------
** 001 ME 20200312 First version.
** 002 ME 20200414 Add base implementation for getEnum methods for default error handling.
** 003 ME 20200421 Use annotations to build the list of enums.
** CA 20200429 Renamed from Enum to LegacyEnum. Added APIs required by the generated,
** converted code.
** GES 20200429 Early rework to new enums approach.
** GES 20200507 Completed first working version.
** GES 20200603 Added legacy class registration.
** GES 20200605 Fixed instance initialization and some lookup methods.
** 004 CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class.
** 005 CA 20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy
** signature.
** TJD 20220504 Java 11 compatibility minor changes
** VVT 20221221 Serialization support added (see #4658).
** 006 CA 20230321 Synchronized the 'registry' access.
** 007 CA 20230731 Legacy ENUM is stored in a EnumObject instance - this ensures that when they are created,
** no context is saved for them, as they are JVM-wide constants.
** 008 ME 20230905 Remove prev/next sibling overrides, internal objects handled in BaseObject.
*/
/*
** 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.oo.lang;
import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.InternalEntry.Type;
import java.io.*;
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;
/**
* Implementation of the Progress.Lang.Enum builtin class.
*/
@LegacyResource(resource = "Progress.Lang.Enum")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public class LegacyEnum
extends BaseObject
implements Comparable<LegacyEnum>,
Externalizable
{
/**
* Default constructor. Required for de-serialization.
*/
public LegacyEnum()
{
}
/** Maps enum classes to their control record. */
private static final Map<Class<?>, EnumControlRecord> registry = new IdentityHashMap<>();
/** ToObject error message specification. */
private static final String TO_SPEC = "Invalid value specified for parameter '%s' " +
"of method \"ToObject\"";
/** The case-preserved legacy name for the enum. */
protected String name;
/** The 64-bit enum value. */
protected long value;
/** Override for the symbol name in cases where it is different from the enum name. */
private String symname = null;
static
{
ObjectOps.registerClass("Progress.Lang.Enum", LegacyEnum.class);
}
/**
* Construct an immutable enum instance with the given state.
*
* @param name
* The case-preserved legacy name for the enum.
* @param value
* The 64-bit enum value.
*/
protected LegacyEnum(String name, long value)
{
this.name = name;
this.value = value;
}
/**
* Return the enum instance of the given type which is associated with the provided name.
*
* @param <T>
* The enum type.
* @param cname
* The name of the enum class to process. Must not be {@code null} or unknown.
* @param name
* The name of the enum to return. If the enum type is a flags enum, the value may
* be a comma-separated list of enum names to return as a multi-bit enum. Comma
* separated lists cannot be used with regular enums. Must not be {@code null} or
* unknown.
*
* @return The enum instance with the provided name(s).
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
@LegacySignature(returns = "OBJECT", qualified = "Progress.Lang.Enum", type = Type.METHOD, name = "ToObject", parameters =
{
@LegacyParameter(name = "cname", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "name", type = "CHARACTER", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public static <T extends LegacyEnum> object<T> toObject(character cname, character name)
{
Class<T> cls = findClass(cname,
LegacyEnum::generateInvalidTypeTO,
LegacyEnum::generateFailedToFindEnumType);
if (cls == null)
{
return new object<T>();
}
return lookupWorker(cls, name, LegacyEnum::generateInvalidValueTO);
}
/**
* Return the enum instance of the given type which is associated with the provided value.
*
* @param <T>
* The enum type.
* @param cname
* The name of the enum class to process. Must not be {@code null} or unknown.
* @param value
* The value of the enum to return. Must not be {@code null} or unknown.
*
* @return The enum instance with the provided name(s).
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
@LegacySignature(returns = "OBJECT", qualified = "Progress.Lang.Enum", type = Type.METHOD, name = "ToObject", parameters =
{
@LegacyParameter(name = "cname", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "value", type = "INT64", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public static <T extends LegacyEnum> object<T> toObject(character cname, int64 value)
{
Class<T> cls = findClass(cname,
LegacyEnum::generateInvalidTypeTO,
LegacyEnum::generateFailedToFindEnumType);
if (cls == null)
{
return new object<T>();
}
return lookupWorker(cls, value, LegacyEnum::generateInvalidValueTO);
}
/**
* Return the enum class associated with the provided 4GL OO class name.
*
* @param <T>
* The enum type.
* @param cname
* The 4GL name of the enum class to find. Must not be {@code null} or unknown.
* @param unknown
* Error generator method for an unknown name.
* @param noFind
* Error generator method for an enum that was not found.
*
* @return The enum class.
*/
public static <T extends LegacyEnum> Class<T> findClass(character cname,
Runnable unknown,
Consumer<String> noFind)
{
if (cname == null || cname.isUnknown())
{
unknown.run();
return null;
}
return findClass(cname.toStringMessage(), unknown, noFind);
}
/**
* Return the enum class associated with the provided 4GL OO class name.
*
* @param <T>
* The enum type.
* @param cname
* The 4GL name of the enum class to find. Must not be {@code null} or unknown.
* @param unknown
* Error generator method for an unknown name.
* @param noFind
* Error generator method for an enum that was not found.
*
* @return The enum class.
*/
public static <T extends LegacyEnum> Class<T> findClass(String cname,
Runnable unknown,
Consumer<String> noFind)
{
if (cname == null)
{
unknown.run();
return null;
}
Class<?> cls = ObjectOps.resolveClass(cname);
if (cls == null || !(LegacyEnum.class.isAssignableFrom(cls)))
{
noFind.accept(cname);
return null;
}
return (Class<T>) cls;
}
/**
* Return the enum instance of the given type which is associated with the provided name.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
* @param name
* The name of the enum to return. If the enum type is a flags enum, the value may
* be a comma-separated list of enum names to return as a multi-bit enum. Comma
* separated lists cannot be used with regular enums. Must not be {@code null} or
* unknown.
* @param unk
* Error generator method for an unknown name.
*
* @return The enum instance with the provided name(s).
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
public static <T extends LegacyEnum> object<T> lookupWorker(Class<T> cls,
character name,
Consumer<Class<?>> unk)
{
if (name == null || name.isUnknown())
{
unk.accept(cls);
return null;
}
EnumControlRecord<T> ecr = getECR(cls);
return new object<T>(ecr.lookupName.apply(ecr, name.toStringMessage()));
}
/**
* Return the enum instance of the given type which is associated with the provided value.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
* @param value
* The value of the enum to return. Must not be {@code null} or unknown.
* @param unk
* Error generator method for an unknown name.
*
* @return The enum instance with the provided value.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
public static <T extends LegacyEnum> object<T> lookupWorker(Class<T> cls,
int64 value,
Consumer<Class<?>> unk)
{
if (value == null || value.isUnknown())
{
unk.accept(cls);
return null;
}
EnumControlRecord<T> ecr = getECR(cls);
return new object<T>(ecr.lookupValue.apply(ecr, value.longValue()));
}
/**
* Generate the not found error.
*
* @param type
* Enum type name which was not found.
*/
public static void generateFailedToFindEnumType(String type)
{
String msg = String.format("Failed to find enum type '%s'", type);
ErrorManager.recordOrThrowError(18109, msg);
}
/**
* Generate an error 17971 if this is an attempt to instantiate an enum type.
*
* @param cls
* The class which needs to be checked.
*
* @return {@code true} if this was an attempt to instantiate an enum.
*/
public static boolean failEnumInstantiation(Class<?> cls)
{
// enums cannot be instantiated
if (LegacyEnum.class.isAssignableFrom(cls))
{
String cname = ObjectOps.getLegacyName((Class<? extends _BaseObject_>)cls);
String msg = String.format("Cannot NEW class %s because it is an interface or enum",
cname);
ErrorManager.recordOrThrowError(17971, msg, false);
return true;
}
return false;
}
/**
* Returns a comma-separated text list of the values of all predefined enums in this legacy class, in
* the order of their values. For regular enums, this is a simple ascending sort. For flag enums, it
* is ascending order starting from zero through the positive values and then followed by any negative
* values (also in ascending order).
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
*
* @return The comma-separated text list of the predefined enum values.
*
* @throws ErrorConditionException
* When called for a class that is not a subclass of {@code LegacyEnum} or {@code FlagsEnum}.
*/
public static <T extends LegacyEnum> character getEnumValues(Class<T> cls)
{
EnumControlRecord<T> ecr = getECR(cls);
if (ecr.values == null)
{
ecr.values = getEnumsValueSort(ecr).stream()
.map(e -> Long.toString(e.value))
.collect(Collectors.joining(","));
}
return new character(ecr.values);
}
/**
* Returns a comma-separated text list of the names of all predefined enums in this legacy class, in
* the order of their values. For regular enums, this is a simple ascending sort. For flag enums, it
* is ascending order starting from zero through the positive values and then followed by any negative
* values (also in ascending order).
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
*
* @return The comma-separated text list of the predefined enum names.
*
* @throws ErrorConditionException
* When called for a class that is not a subclass of {@code LegacyEnum} or {@code FlagsEnum}.
*/
public static <T extends LegacyEnum> character getEnumNames(Class<T> cls)
{
EnumControlRecord<T> ecr = getECR(cls);
Function<T, String> render = (T e) ->
{
LegacyEnum en = e;
return en.symname == null ? en.name : en.symname;
};
if (ecr.names == null)
{
ecr.names = getEnumsValueSort(ecr).stream()
.map(render)
.collect(Collectors.joining(","));
}
return new character(ecr.names);
}
/**
* Obtain the value of the named enum. For regular enums, the given name must be a single name that
* matches exactly to a predefined enum name. For flags enums, it can one name or a comma-separated
* list of valid names, whose corresponding values are bitwise OR'd together. Matching is
* case-insensitive and leading/trailing whitespace is ignored for flags enums. Strangely, for regular
* enums the names cannot have leading/trailing whitespace. This 4GL quirk is maintained here. If any
* name is invalid, then unknown value is returned. For regular enums, a comma-separated list is an
* invalid name.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
* @param name
* The enum name to lookup.
*
* @return The value found or unknown value if there is an invalid name.
*
* @throws ErrorConditionException
* If the given name is unknown value.
*/
public static <T extends LegacyEnum> int64 getEnumValue(Class<T> cls, character name)
{
if (name == null || name.isUnknown())
{
generateInvalidValue(cls, "GetEnumValue");
return new int64();
}
EnumControlRecord<T> ecr = getECR(cls);
LegacyEnum enm = ecr.lookupNameSafe.lookup(ecr, name.toStringMessage(), false);
return (enm == null) ? new int64() : new int64(enm.value);
}
/**
* Obtain the name of the enum(s) specified by this value. For regular enums, the given value must be
* an exact match to a single predefined enum. For flags enums, it can be a single enum's value or it
* can be a bitset that matches more than one enum. In the multiple flag enum case, it will only match
* enum values which are full subsets of the given value (see {@code isFlagSet}). If there is no exact
* match or for flag enums there is no single enum that is a subset, then unknown value is returned.
* <p>
* If multiple enums match (flag enums case), then each of the names will be returned in a comma-separated
* list.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
* @param value
* The enum value to lookup.
*
* @return The name found or unknown value if there is an invalid value.
*
* @throws ErrorConditionException
* If the given input value is the unknown value.
*/
public static <T extends LegacyEnum> character getEnumName(Class<T> cls, int64 value)
{
if (value == null || value.isUnknown())
{
generateInvalidValue(cls, "GetEnumName");
return new character();
}
EnumControlRecord<T> ecr = getECR(cls);
LegacyEnum enm = ecr.lookupValueSafe.apply(ecr, value.longValue());
return (enm == null) ? new character() : enm.toLegacyString();
}
/**
* Returns the 64-bit value of the enum.
*
* @return The 64-bit value.
*/
@LegacySignature(returns = "INT64", type = Type.METHOD, name = "GetValue")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public int64 getValue()
{
return new int64(value);
}
/**
* Returns the 64-bit value of the enum.
*
* @return The 64-bit value.
*/
public long _getValue()
{
return value;
}
/**
* Compares this instance with the specified instance and returns -1 if this instance is less than the
* instance 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. In the 4GL, only enum instances of the same exact
* type are allowed. This method enforces that rule.
* <p>
* This implementation is consistent with {@code equals} and {@code hashCode}.
*
* @param enm
* 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>enm</code>.
*/
@Override
public int compareTo(LegacyEnum enm)
{
if (!checkTypes(this, enm))
{
return -1;
}
return compareWorker(enm);
}
/**
* Determines if this instance and the given instance are equivalent. This implementation is consistent
* with {@code compareTo} and {@code hashCode}, which means these instances are safe for usage as a key
* in an associative collection.
*
* @param obj
* The instance to compare against.
*
* @return {@code true} if the objects compare equivalently; {@code false} if they do not, or if the
* parameter is not a {@code LegacyEnum} instance of the same subclass type.
*/
@Override
public boolean equals(Object obj)
{
if (!checkTypes(this, obj))
{
return false;
}
return compareWorker((LegacyEnum) obj) == 0;
}
/**
* Calculates a hash code using the instance's value. This implementation is consistent with
* {@code equals}, so it is usable for Java's associative collections. It ignores the name
* of the enum in this calculation (which is how {@code equals} is implemented as well).
*
* @return The hash code for this object instance.
*/
@Override
public int hashCode()
{
// this is the same as (value * 31)
return (int) ((value << 5) - value);
}
/**
* Compares this instance with the specified instance and returns -1 if this instance is less than the
* given instance, 0 if the two instances are equal and 1 if this instance is greater than the given
* instance.
* <p>
* This is the replacement for the 4GL {@code Progress.Lang.Enum:CompareTo} method. It is NOT an
* implementation of {@code Comparable}.
*
* @param that
* The enum instance to compare against.
*
* @return -1, 0, or 1 depending on if this instance is less than, equal to or
* greater than the given instance.
*/
@LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "CompareTo", parameters =
{
@LegacyParameter(name = "that", type = "OBJECT", qualified = "progress.lang.enum", mode = "INPUT")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public integer compareTo(final object<? extends LegacyEnum> that)
{
if (that == null || that.isUnknown())
{
return new integer();
}
return new integer(compareWorker(that.ref()));
}
/**
* Check if this reference matches the given one.
*
* @param other
* The other reference.
*
* @return {@code true} if the other reference is not unknown value and if the reference
* is the same as this object's reference.
*/
@LegacySignature(returns = "LOGICAL", type = Type.METHOD, name = "Equals", parameters =
{
@LegacyParameter(name = "other", type = "OBJECT", mode = "INPUT", qualified="Progress.Lang.Object"),
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL|RT_LVL_FULL)
@Override
public logical legacyEquals(object<? extends _BaseObject_> other)
{
if (other == null || other.isUnknown() || !checkTypes(this, other.ref()))
{
return new logical(false);
}
return new logical(compareWorker((LegacyEnum) other.ref()) == 0);
}
/**
* Get the string representation for this instance.
* <p>
* Although the original method name is {@code toString()}, this version returns a
* {@code character} type which would conflict with the {@code java.lang.Object.toString()}
* version. For this reason, the name is mapped differently during conversion.
*
* @return The enum's case-preserved name as originally defined in the code (for
* predefined enums or as a comma separated list of names for dynamically
* instantiated multi-bit flag enums.
*/
@LegacySignature(returns = "CHARACTER", type = Type.METHOD, name = "ToString")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
@Override
public character toLegacyString()
{
return new character(name);
}
/**
* Reports if the given instance is managed as part of the legacy 4GL object life cycle. This
* includes reference counting, linking in the SESSION object chain, managed construction and
* destruction.
*
* @return Always {@code false} because enums exist outside of the OO 4GL life cycle
* mechanisms.
*/
@Override
public boolean isTracked()
{
return false;
}
/**
* Compares this instance with the specified instance and returns -1 if this instance is less
* than the given instance, 0 if the two instances are equal and 1 if this instance is greater
* than the given instance.
*
* @param that
* The enum instance to compare against.
*
* @return -1, 0, or 1 depending on if this instance is less than, equal to or
* greater than the given instance.
*/
public int compareWorker(LegacyEnum that)
{
long diff = this.value - that.value;
return (diff < 0) ? -1 : ( (diff > 0) ? 1 : 0 );
}
/**
* The object implements the writeExternal method to save its contents
* by calling the methods of DataOutput for its primitive values or
* calling the writeObject method of ObjectOutput for objects, strings,
* and arrays.
*
* @serialData Overriding methods should use this tag to describe
* the data layout of this Externalizable object.
* List the sequence of element types and, if possible,
* relate the element to a public/protected field and/or
* method of this Externalizable class.
*
* @param out the stream to write the object to
* @exception IOException Includes any I/O exceptions that may occur
*/
@Override
public void writeExternal(final ObjectOutput out)
throws IOException
{
out.writeObject(name);
out.writeLong(value);
out.writeObject(symname);
}
/**
* The object implements the readExternal method to restore its
* contents by calling the methods of DataInput for primitive
* types and readObject for objects, strings and arrays. The
* readExternal method must read the values in the same sequence
* and with the same types as were written by writeExternal.
*
* @param in the stream to read data from in order to restore the object
* @exception IOException if I/O errors occur
* @exception ClassNotFoundException If the class for an object being
* restored cannot be found.
*/
@Override
public void readExternal(final ObjectInput in)
throws IOException,
ClassNotFoundException
{
name = (String) in.readObject();
value = in.readLong();
symname = (String) in.readObject();
}
/**
* Return the enum instance of the given type which is associated with the provided value.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
* @param name
* The name of the enum to return. If the enum type is a flags enum, the value may
* be a comma-separated list of enum names to return as a multi-bit enum. Comma
* separated lists cannot be used with regular enums. Must not be {@code null} or
* unknown.
*
* @return The enum instance with the provided name(s).
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> object<T> getEnum(Class<T> cls, character name)
{
return lookupWorker(cls, name, LegacyEnum::generateInvalidValueGE);
}
/**
* Return the enum instance of the given type which is associated with the provided value.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
* @param value
* The value of the enum to return. Must not be {@code null} or unknown.
*
* @return The enum instance with the provided value.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> object<T> getEnum(Class<T> cls, int64 value)
{
return lookupWorker(cls, value, LegacyEnum::generateInvalidValueGE);
}
/**
* Return the enum instance of the given type which is associated with the provided name.
*
* @param <E>
* The enum type.
* @param ecr
* The enum control record for the enum to be processed. Must not be {@code null}.
* @param name
* The name of a single enum to return. This will process an exact match only. This
* MUST NOT be a comma-separated list of enum names even if the enum to process is a
* flag enum. Must not be {@code null}.
*
* @return The enum instance with the provided name(s).
*/
protected static <E extends LegacyEnum> E lookupNameWorker(EnumControlRecord<E> ecr, String name)
{
E enm = lookupNameWorkerSafe(ecr, name, true);
if (enm == null)
{
if (name.indexOf(',') != -1)
{
generateInvalidCommaSeparatedName(ecr.cls, name);
}
else
{
generateNotFoundName(ecr.cls, name);
}
return null;
}
return enm;
}
/**
* Return the enum instance of the given type which is associated with the provided name. This will not
* raise an error but it may return {@code null}.
*
* @param <E>
* The enum type.
* @param ecr
* The enum control record for the enum to be processed. Must not be {@code null}.
* @param name
* The name of a single enum to return. This will process an exact match only. This
* MUST NOT be a comma-separated list of enum names even if the enum to process is a
* flag enum. Must not be {@code null}.
* @param trim
* {@code true} to trim leading/trailing whitespace in the name before searching.
*
* @return The enum instance with the provided name(s) or {@code null} if there is no match.
*/
protected static <E extends LegacyEnum> E lookupNameWorkerSafe(EnumControlRecord<E> ecr,
String name,
boolean trim)
{
name = trim ? StringHelper.safeTrim(name) : name;
return ecr.byName.get(name.toLowerCase());
}
/**
* Return the enum instance of the given type which is associated with an exact match to the provided
* value.
*
* @param <E>
* The enum type.
* @param ecr
* The enum control record for the enum to process. Must not be {@code null}.
* @param value
* The value of the enum to return. Only exact matches are considered.
*
* @return The enum instance with the provided value.
*/
protected static <E extends LegacyEnum> E lookupValueWorker(EnumControlRecord<E> ecr, long value)
{
E enm = lookupValueWorkerSafe(ecr, value);
if (enm == null)
{
generateNotFoundValue(ecr.cls, value);
return null;
}
return enm;
}
/**
* Return the enum instance of the given type which is associated with an exact match to the provided
* value.
*
* @param <E>
* The enum type.
* @param ecr
* The enum control record for the enum to process. Must not be {@code null}.
* @param value
* The value of the enum to return. Only exact matches are considered.
*
* @return The enum instance with the provided value or {@code null} if there is no match.
*/
protected static <E extends LegacyEnum> E lookupValueWorkerSafe(EnumControlRecord<E> ecr, long value)
{
return ecr.byValue.get(value);
}
/**
* Return an existing instance if it exists with the exact value provided, otherwise create a new
* instance and return it.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
* @param value
* The value of the enum to return.
*
* @return The enum instance with the provided value.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> T getExisting(Class<T> cls, long value)
{
EnumControlRecord<T> ecr = getECR(cls);
return ecr.byValue.get(value);
}
/**
* Create a new instance and return it.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process. Must not be {@code null}.
* @param name
* The name of the enum.
* @param value
* The value of the enum.
*
* @return A new enum instance with the provided name and value.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> T createWorker(Class<T> cls, String name, long value)
{
EnumControlRecord<T> ecr = getECR(cls);
return ecr.create.apply(name, value);
}
/**
* Obtains a list of all predefined enums for the given enum type, in the order of their values. For
* regular enums, this is a simple ascending sort. For flag enums, it is ascending order starting from
* zero through the positive values and then followed by any negative values (also in ascending order).
*
* @param <T>
* The enum type.
* @param ecr
* The enum control record.
*
* @return The list of predefined enums.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> List<T> getEnumsValueSort(EnumControlRecord<T> ecr)
{
ArrayList list = new ArrayList(ecr.enums);
list.sort(ecr.valueSort);
return list;
}
/**
* Obtains a list of all predefined enums for the given enum type, in the order of their definition.
*
* @param <T>
* The enum type.
* @param cls
* The enum class being processed.
*
* @return The list of predefined enums.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> List<T> getEnumsDefineOrder(Class<T> cls)
{
EnumControlRecord<T> ecr = getECR(cls);
return getEnumsDefineOrder(ecr);
}
/**
* Obtains a list of all predefined enums for the given enum type, in the order of their definition.
*
* @param <T>
* The enum type.
* @param ecr
* The enum control record.
*
* @return The list of predefined enums.
*/
protected static <T extends LegacyEnum> List<T> getEnumsDefineOrder(EnumControlRecord<T> ecr)
{
return new ArrayList(ecr.enums);
}
/**
* Obtains a maximal bitfield for the given enum type. The maximal bitfield is the bitwise OR of all
* predefined enum values for this type.
*
* @param <T>
* The enum type.
* @param cls
* The enum class to process.
*
* @return The maximal bitfield.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> long maximalBitfield(Class<T> cls)
{
return getECR(cls).maximal;
}
/**
* Implicitly create the enum instance by incrementing the value from the most recently created enum.
* If this is the first instance, set the value to 1.
*
* @param <T>
* The enum type.
* @param cls
* The enum class.
* @param name
* The enum legacy name.
*
* @return The created object instance for this enum.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> object<T> createEnum(Class<T> cls, String name)
{
EnumControlRecord<T> ecr = getECR(cls);
long value = 1;
if (!ecr.enums.isEmpty())
{
T ori = ecr.enums.getLast();
value = ecr.flags ? nextPowerOf2(name, ori.value) : ori.value + 1;
}
// we don't need to pass the original enum as a pattern in this case
return createEnumWorker(ecr, name, value, null);
}
/**
* Explicitly create the enum instance with the given name but with the value from the specified alias.
*
* @param <T>
* The enum type.
* @param cls
* The enum class.
* @param name
* The enum legacy name.
* @param alias
* The source of the enum's value. Must be previously defined.
*
* @return The created object instance for this enum.
*
* @throws IllegalStateException
* If the enum is not yet registered, the class is not a valid enum or the alias is
* unknown.
*/
protected static <T extends LegacyEnum> object<T> createEnum(Class<T> cls, String name, String alias)
{
EnumControlRecord<T> ecr = getECR(cls);
// by using the enum-type-specific helper here, we get the right behavior for either regular
// or flags enums; if the alias is a comma-separated list, then the resulting enum is a new enum
// which represents multuple predefined enums and has the multibit flag set
T ori = ecr.lookupName.apply(ecr, alias);
if (ori == null)
{
throw new IllegalStateException("Unknown alias cannot be used to initialize an enum.");
}
// the pattern enum muust be passed here
return createEnumWorker(ecr, name, ori.value, ori);
}
/**
* Explicitly create an enum instance with the specified value and legacy name.
*
* @param <T>
* The enum type.
* @param cls
* The enum class.
* @param name
* The enum legacy name.
* @param value
* The enum value.
*
* @return The created object instance for this enum.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
protected static <T extends LegacyEnum> object<T> createEnum(Class<T> cls, String name, long value)
{
EnumControlRecord<T> ecr = getECR(cls);
return createEnumWorker(ecr, name, value, null);
}
/**
* Register this enum.
*
* @param <T>
* The enum type.
* @param cls
* The enum class.
* @param create
* Lambda for creation of new instances.
* @param legacyName
* The legacy, fully qualified, name.
*/
protected static <T extends LegacyEnum> void registerEnum(Class<T> cls,
BiFunction<String, Long, T> create,
String legacyName)
{
registerEnum(cls,
create,
LegacyEnum::lookupNameWorker,
LegacyEnum::lookupNameWorkerSafe,
LegacyEnum::lookupValueWorker,
LegacyEnum::lookupValueWorkerSafe,
null,
LegacyEnum::compareValues,
legacyName);
}
/**
* Register this enum.
*
* @param <T>
* The enum type.
* @param cls
* The enum class.
* @param create
* Lambda for creation of new instances.
* @param lookupName
* Lambda for lookup of enums by name.
* @param lookupNameSafe
* Lambda for lookup of enums by name, without raising an error.
* @param lookupValue
* Lambda for lookup of enums by value.
* @param lookupValueSafe
* Lambda for lookup of enums by value, without raising an error.
* @param initHelper
* Initializer helper.
* @param valueSort
* Sorts enums by value.
* @param legacyName
* The legacy, fully qualified, name.
*/
protected static
<T extends LegacyEnum> void registerEnum(Class<T> cls,
BiFunction<String, Long, T> create,
BiFunction<EnumControlRecord<T>, String, T> lookupName,
NameLookupHelper<T> lookupNameSafe,
BiFunction<EnumControlRecord<T>, Long, T> lookupValue,
BiFunction<EnumControlRecord<T>, Long, T> lookupValueSafe,
EnumInitializer<T> initHelper,
Comparator<LegacyEnum> valueSort,
String legacyName)
{
synchronized (registry)
{
registry.put(cls, new EnumControlRecord<T>(cls,
create,
lookupName,
lookupNameSafe,
lookupValue,
lookupValueSafe,
initHelper,
valueSort,
legacyName));
}
ObjectOps.registerClass(legacyName, cls);
}
/**
* Generate the not found error for a value lookup.
*
* @param cls
* The enum for which the value is invalid.
* @param value
* Enum instance value which cannot be found.
*/
protected static void generateNotFoundValue(Class<?> cls, long value)
{
String cname = ObjectOps.getLegacyName((Class<? extends _BaseObject_>)cls);
String msg = String.format("Could not get enum of type '%s' for value %d", cname, value);
ErrorManager.recordOrThrowError(18207, msg);
}
/**
* Generate the invalid value error (15246) for an enum method call.
*
* @param cls
* The enum for which the value is invalid.
* @param meth
* Method name which had the failure.
*/
protected static void generateInvalidValue(Class<?> cls, String meth)
{
generateInvalidValue(ObjectOps.getLegacyName((Class<? extends _BaseObject_>)cls), meth);
}
/**
* Generate the invalid value error (15246) for an enum method call.
*
* @param cname
* The legacy class name for which the value is invalid.
* @param meth
* Method name which had the failure.
*/
protected static void generateInvalidValue(String cname, String meth)
{
String msg = String.format("Invalid value specified for %s:%s", cname, meth);
ErrorManager.recordOrThrowError(15246, msg);
}
/**
* Render a list of enums (name and value) into a string.
*
* @param list
* The list of enums to dump. Must NOT be {@code null}.
*
* @return The rendered text.
*/
protected static String dumpEnums(Collection<LegacyEnum> list)
{
return list.stream().map(LegacyEnum::dumpEnum).collect(Collectors.joining(", "));
}
/**
* Render an enum (name and value) into a simple string.
*
* @param en
* The enum to dump. Must NOT be {@code null}.
*
* @return The rendered text.
*/
protected static String dumpEnum(LegacyEnum en)
{
return "(" + en.name + ":" + Long.toString(en.value) + ")";
}
/**
* Obtain the enum control record for the given enum class.
*
* @param <T>
* The enum type.
* @param cls
* Enum class.
*
* @return The control record.
*
* @throws IllegalStateException
* If the enum is not yet registered or the class is not a valid enum.
*/
private static <T extends LegacyEnum> EnumControlRecord getECR(Class<T> cls)
{
synchronized (registry)
{
EnumControlRecord<T> ecr = registry.get(cls);
if (ecr == null)
{
throw new IllegalStateException("Attempt to create enum instance before registration.");
}
return ecr;
}
}
/**
* Initialize the enum instance with the specified value and legacy name.
*
* @param <T>
* The enum type.
* @param ecr
* The enum control record.
* @param name
* The enum legacy name.
* @param value
* The enum value.
* @param pattern
* An enum from which some state is patterned.
*
* @return The created object instance for this enum.
*/
private static <T extends LegacyEnum> object<T> createEnumWorker(EnumControlRecord<T> ecr,
String name,
long value,
T pattern)
{
String text = name;
if (pattern != null)
{
// multibit flags enum patterns created from comma-separated alias lists are not predefined and
// have blank names; these must be avoided
if (pattern.name != null && !pattern.name.isEmpty())
{
text = pattern.name;
}
}
else
{
// we cannot use the helper here (because for flags enums it will create a new instance on the fly)
// so we use the simple lookup only
T ori = LegacyEnum.lookupValueWorkerSafe(ecr, value);
if (ori != null)
{
text = ori.name;
}
}
T instance = ecr.create.apply(text, value);
if (text != name)
{
LegacyEnum en = instance;
en.symname = name;
}
ecr.byName.put(StringHelper.safeTrim(name).toLowerCase(), instance);
ecr.byValue.putIfAbsent(Long.valueOf(value), instance); // only the first enum of a given value is stored
ecr.enums.add(instance);
if (ecr.initHelper != null)
{
ecr.initHelper.init(ecr, instance, pattern);
}
return new EnumObject(instance);
}
/**
* Calculate the next higher power of 2 over the given value.
*
* @param name
* The name of the enum member being created.
* @param value
* The number to process.
*
* @return The next higher power of 2.
*/
private static long nextPowerOf2(String name, long value)
{
if (value == 0)
{
return 0x01;
}
int bit = highestSetBit(value);
if (bit > 62)
{
// in the 4GL, this is a compiler error so this really should NOT happen unless the
// enum is incorrectly defined
String msg = String.format("Invalid value specified for enum member '%s'", name);
ErrorManager.recordOrThrowError(18215, msg);
}
return 1L << (bit + 1);
}
/**
* Detect which is the highest bit set in the given value (the most significant bit is
* on the left and is bit number 63, while the least significant bit is on the right and
* is bit 0). Do NOT call this with a 0 value, since that would yield nonsense.
*
* @param value
* The bitfield to process.
*
* @return The bit number of the highest bit that is set.
*
* @throws IllegalArgumentException
* If the given value is zero.
*/
private static int highestSetBit(long value)
{
if (value == 0)
throw new IllegalArgumentException("Value 0 is not allowed.");
// quick out, negative numbers have the sign bit set
if (value < 0)
return 63;
int i;
// check the rest of the bits
for (i = 0; i <= 62; i++)
{
value >>>= 1;
if (value == 0)
break;
}
return i;
}
/**
* Regular enum comparator. Sorts using the natural sorting of the numeric values of the enums.
*
* @param e1
* First enum.
* @param e2
* Second enum.
*
* @return -1, 0, or 1 depending on if e1 is less than, equal to or greater than e2.
*/
private static int compareValues(LegacyEnum e1, LegacyEnum e2)
{
return e1.compareWorker(e2);
}
/**
* Check if the data types represent the same exact enum class, raise error 223 if they are different.
*
* @param o1
* Object instance 1.
* @param o2
* Object instance 2.
*
* @return {@code true} if the classes of each object are identical.
*
* @throws ErrorConditionException
* If the types are different.
*/
private static boolean checkTypes(Object o1, Object o2)
{
if (o1.getClass() != o2.getClass())
{
ErrorManager.recordOrThrowError(223, "Incompatible data types in expression or assignment");
return false;
}
return true;
}
/**
* Generate the invalid type error for ToObject.
*
* @throws ErrorConditionException
* Always.
*/
private static void generateInvalidTypeTO()
{
ErrorManager.recordOrThrowError(18193, String.format(TO_SPEC, "enumTypeName"));
}
/**
* Generate the invalid value error for ToObject.
*
* @param cls
* The enum for which the value is invalid.
*
* @throws ErrorConditionException
* Always.
*/
private static void generateInvalidValueTO(Class<?> cls)
{
ErrorManager.recordOrThrowError(18193, String.format(TO_SPEC, "value"));
}
/**
* Generate the invalid value error for getEnum.
*
* @param cls
* The enum for which the value is invalid.
*
* @throws ErrorConditionException
* Always.
*/
private static void generateInvalidValueGE(Class<?> cls)
{
generateInvalidValue(cls, "GetEnum");
}
/**
* Generate the not found error for a name lookup.
*
* @param cls
* The enum for which the value is invalid.
* @param ename
* Enum instance name which cannot be found.
*
* @throws ErrorConditionException
* Always.
*/
private static void generateNotFoundName(Class<?> cls, String ename)
{
String cname = ObjectOps.getLegacyName((Class<? extends _BaseObject_>)cls);
String msg = String.format("Could not find '%s' in enum '%s'", ename, cname);
ErrorManager.recordOrThrowError(18008, msg);
}
/**
* Generate the comma-separated name list error for non-flags enums.
*
* @param cls
* The enum for which the value is invalid.
* @param ename
* Comma-separated name list which was invalid.
*
* @throws ErrorConditionException
* Always.
*/
private static void generateInvalidCommaSeparatedName(Class<?> cls, String ename)
{
String cname = ObjectOps.getLegacyName((Class<? extends _BaseObject_>)cls);
String msg = String.format("Comma separated list of names '%s' not valid for non-flag " +
"enum '%s'",
ename,
cname);
ErrorManager.recordOrThrowError(18017, msg);
}
/**
* Post-process a new enum to set state.
*
* @param <E>
* The enum type associated with the control record.
*/
@FunctionalInterface
protected interface EnumInitializer<E extends LegacyEnum>
{
/**
* Post-process a new enum to set state.
*
* @param ecr
* The control record.
* @param created
* Newly created enum needing initialization.
* @param pattern
* Enum from which some state can be patterned.
*/
void init(EnumControlRecord<E> ecr, E created, E pattern);
}
/**
* Helper to lookup a predefined enum by its name.
*
* @param <E>
* The enum type associated with the control record.
*/
@FunctionalInterface
protected interface NameLookupHelper<E extends LegacyEnum>
{
/**
* Lookup a predefined enum by its name.
*
* @param ecr
* The control record.
* @param name
* Enum name.
* @param trim
* {@code true} to force trimming the name before searching.
*
* @return The enum or {@code null} if no match was found.
*/
E lookup(EnumControlRecord<E> ecr, String name, boolean trim);
}
/**
* Stores the static data associated with a given enum subclass.
*
* @param <E>
* The enum type associated with the control record.
*/
protected static class EnumControlRecord<E extends LegacyEnum>
{
/** Fully qualified legacy enum name. */
public final String legacyName;
/** num class for which this record is defined. */
public final Class<E> cls;
/** Map of values to enum instances. */
public final HashMap<Long, E> byValue = new HashMap<>();
/** Map of names to enum instances. */
public final HashMap<String, E> byName = new HashMap<>();
/** All enum instances in definition order. */
public final LinkedList<E> enums = new LinkedList<>();
/** Lambda for creation of new instances. */
public final BiFunction<String, Long, E> create;
/** Lambda for lookup of enums by name. */
public final BiFunction<EnumControlRecord<E>, String, E> lookupName;
/** Lambda for lookup of enums by name, without raising an error. */
public final NameLookupHelper<E> lookupNameSafe;
/** Lambda for lookup of enums by value. */
public final BiFunction<EnumControlRecord<E>, Long, E> lookupValue;
/** Lambda for lookup of enums by value, without raising an error. */
public final BiFunction<EnumControlRecord<E>, Long, E> lookupValueSafe;
/** Initialization helper. */
public final EnumInitializer<E> initHelper;
/** Sorts enums by value. */
public final Comparator<LegacyEnum> valueSort;
/** Marks the enum if it is a flags enum. */
public boolean flags = false;
/** For flags enums, this is the maximal bitfield (all set bits in defined enums). */
public long maximal = 0;
/** {@code true} if any enum value is a negative number. */
public boolean negatives = false;
/** {@code true} if any enum value is between -1 and -32768 inclusive. */
public boolean highNegs = false;
/** Cached instance of the sorted values string. */
public String values = null;
/** Cached instance of the sorted names string. */
public String names = null;
/**
* Construct an instance.
*
* @param cls
* Enum class for which this record is defined.
* @param create
* Lambda for creation of new instances.
* @param lookupName
* Lambda for lookup of enums by name.
* @param lookupNameSafe
* Lambda for lookup of enums by name, without raising an error.
* @param lookupValue
* Lambda for lookup of enums by value.
* @param lookupValueSafe
* Lambda for lookup of enums by value, without raising an error.
* @param initHelper
* Initialization helper.
* @param valueSort
* Sorts enums by value.
* @param legacyName
* The legacy, fully qualified, name.
*/
protected EnumControlRecord(Class<E> cls,
BiFunction<String, Long, E> create,
BiFunction<EnumControlRecord<E>, String, E> lookupName,
NameLookupHelper<E> lookupNameSafe,
BiFunction<EnumControlRecord<E>, Long, E> lookupValue,
BiFunction<EnumControlRecord<E>, Long, E> lookupValueSafe,
EnumInitializer<E> initHelper,
Comparator<LegacyEnum> valueSort,
String legacyName)
{
this.cls = cls;
this.create = create;
this.lookupName = lookupName;
this.lookupNameSafe = lookupNameSafe;
this.lookupValue = lookupValue;
this.lookupValueSafe = lookupValueSafe;
this.initHelper = initHelper;
this.legacyName = legacyName;
this.valueSort = valueSort;
}
}
}