Skip to content

Commit

Permalink
Generate enums as interface + enum + class for unknown values
Browse files Browse the repository at this point in the history
The new generated enum is represented as an interface with two nested classes:
- enum class Known with the list of known enumeration values
- class Unknown which represents any not-known value

The enum `enum` would be generated as:

```java
// interface to abstract known and unknown values
interface IEnum extends IKaitaiEnum {
    // Storage for unknown values
    public static class Unknown extends IKaitaiEnum.Unknown implements IEnum {
        Unknown(long id) { super(id); }
        @OverRide
        public String toString() { return "Enum(" + this.id + ")"; }
        @OverRide
        public int hashCode() {
            // Analogues to this code, but without boxing
            // return Objects.hash("Enum", this.id);
            final int result = 31 + "Enum".hashCode();
            return 31 * result + Long.hashCode(this.id);
        }
        @OverRide
        public boolean equals(Object other) { return other instanceof Enum && this.id == ((Enum)other).id; }
    }
}
// Storage for known values
class Enum implements IEnum {
    VARIANT_1(1),
    VARIANT_2(2),
    VARIANT_3(3);

    private final long id;

    private static HashMap<Long, IEnum> variants = new HashMap<>(3);
    static {
        for (final Enum e : values()) {
            variants.put(e.id, e);
        }
    }

    public static IEnum byId(final long id) {
        return variants.computeIfAbsent(id, _id -> new IEnum.Unknown(id));
    }

    private Enum(long id) { this.id = id; }
    @OverRide
    public long id() { return this.id; }
}
```
  • Loading branch information
Mingun committed Aug 20, 2024
1 parent 3e44e7f commit 68f2dfb
Showing 1 changed file with 99 additions and 40 deletions.
139 changes: 99 additions & 40 deletions shared/src/main/scala/io/kaitai/struct/languages/JavaCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -535,37 +535,48 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
// doing this workaround for enums.

val onType = typeProvider._currentSwitchType.get
val isNullable = onType match {
case _: EnumType => true
case _ => false
}

if (isNullable) {
val nameSwitchStr = expression(NAME_SWITCH_ON)
out.puts("{")
out.inc
out.puts(s"${kaitaiType2JavaType(onType)} $nameSwitchStr = ${expression(on)};")
out.puts(s"if ($nameSwitchStr != null) {")
out.inc
onType match {
case EnumType(name, owner, _) => {
val enumName = types2class(owner :+ name)
// Open scope for "on" isolation
out.puts("{")
out.inc
out.puts(s"final ${enum2iface(name, owner)} on = ${expression(on)};")
out.puts(s"if (on instanceof $enumName) {")
out.inc
out.puts(s"switch (($enumName)on) {")
out.inc

cases.foreach { case (condition, result) =>
condition match {
case SwitchType.ELSE_CONST =>
// skip for now
case _ =>
switchCaseStart(condition)
normalCaseProc(result)
switchCaseEnd()
}
}

super.switchCasesRender(id, on, cases, normalCaseProc, elseCaseProc)
out.dec
out.puts("} // switch")
out.dec
cases.get(SwitchType.ELSE_CONST) match {
case Some(result) =>
out.puts("} else {")
out.inc
elseCaseProc(result)
out.dec
out.puts("}")
case None =>
out.puts("}")
}

out.dec
cases.get(SwitchType.ELSE_CONST) match {
case Some(result) =>
out.puts("} else {")
out.inc
elseCaseProc(result)
out.dec
out.puts("}")
case None =>
out.puts("}")
// Close "on" isolation scope
out.dec
out.puts("}")
}

out.dec
out.puts("}")
} else {
super.switchCasesRender(id, on, cases, normalCaseProc, elseCaseProc)
case _ => super.switchCasesRender(id, on, cases, normalCaseProc, elseCaseProc)
}
}

Expand Down Expand Up @@ -699,9 +710,44 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)

override def enumDeclaration(curClass: String, enumName: String, enumColl: Seq[(Long, String)]): Unit = {
val enumClass = type2class(enumName)
val enumIface = enum2iface(enumName)

// Enums are always contained in some other class so we can generate two public items

// public interface I<enum> extends IKaitaiEnum { ... }
out.puts
out.puts(s"public interface $enumIface extends IKaitaiEnum {")
out.inc
out.puts(s"public static final class Unknown extends IKaitaiEnum.Unknown implements $enumIface {")
out.inc
out.puts("Unknown(long id) { super(id); }")
out.puts
out.puts("@Override");
out.puts(s"public String toString() { return \"${enumClass}(\" + this.id + \")\"; }");
out.puts
out.puts(s"public enum $enumClass {")
out.puts("@Override");
out.puts(s"public int hashCode() {")
out.inc
// Use the hashCode implementation that is generated by Eclipse and VSCode Java plugin.
// It uses prime 31 and this is how Arrays.hashCode() is implemented
out.puts(s"final int result = 31 + \"${enumClass}\".hashCode();")
out.puts("return 31 * result + Long.hashCode(this.id);")
out.dec
out.puts("}");
out.puts
out.puts("@Override");
out.puts(s"public boolean equals(Object other) {");
out.inc
out.puts("return other instanceof Unknown && this.id == ((Unknown)other).id;")
out.dec
out.puts("}");
out.dec
out.puts("}") // close class
out.dec
out.puts("}") // close interface

// public enum <enum> implements I<enum> { ... }
out.puts(s"public enum $enumClass implements $enumIface {")
out.inc

if (enumColl.size > 1) {
Expand All @@ -716,23 +762,32 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)

out.puts
out.puts("private final long id;")
out.puts(s"$enumClass(long id) { this.id = id; }")
out.puts("public long id() { return id; }")
out.puts(s"private static final Map<Long, $enumClass> byId = new HashMap<Long, $enumClass>(${enumColl.size});")
out.puts(s"private static final HashMap<Long, $enumIface> variants = new HashMap<>(${enumColl.size});")
out.puts("static {")
out.inc
out.puts(s"for ($enumClass e : $enumClass.values())")
out.puts(s"for ($enumClass e : values()) {")
out.inc
out.puts(s"byId.put(e.id(), e);")
out.puts(s"variants.put(e.id, e);")
out.dec
out.puts("}")// for
out.dec
out.puts("}")
out.puts(s"public static $enumClass byId(long id) { return byId.get(id); }")
out.puts("}")// static initializer
out.puts
out.puts(s"public static $enumIface byId(final long id) {")
out.inc
out.puts(s"return variants.computeIfAbsent(id, _id -> new $enumIface.Unknown(id));")
out.dec
out.puts("}")
out.puts("}")// byId(...)
out.puts
out.puts(s"private $enumClass(long id) { this.id = id; }")
out.puts
out.puts("@Override")
out.puts("public long id() { return id; }")
out.dec
out.puts("}")// enum

importList.add("java.util.Map")
importList.add("java.util.HashMap")
importList.add("io.kaitai.struct.IKaitaiEnum")
}

override def debugClassSequence(seq: List[AttrSpec]) = {
Expand Down Expand Up @@ -859,7 +914,7 @@ object JavaCompiler extends LanguageCompilerStatic
case KaitaiStructType | CalcKaitaiStructType(_) => kstructName

case t: UserType => types2class(t.name)
case EnumType(name, owner, _) => types2class(owner :+ name)
case EnumType(name, owner, _) => enum2iface(name, owner)

case _: ArrayType => kaitaiType2JavaTypeBoxed(attrType, importList)

Expand Down Expand Up @@ -903,7 +958,7 @@ object JavaCompiler extends LanguageCompilerStatic
case KaitaiStructType | CalcKaitaiStructType(_) => kstructName

case t: UserType => types2class(t.name)
case EnumType(name, owner, _) => types2class(owner :+ name)
case EnumType(name, owner, _) => enum2iface(name, owner)

case at: ArrayType => {
importList.add("java.util.ArrayList")
Expand All @@ -916,6 +971,10 @@ object JavaCompiler extends LanguageCompilerStatic

def types2class(names: Iterable[String]) = names.map(x => type2class(x)).mkString(".")

def enum2iface(name: String) = s"I${type2class(name)}"
def enum2iface(name: String, owner: List[String]): String =
(owner.map(type2class) :+ enum2iface(name)).mkString(".")

override def kstreamName: String = "KaitaiStream"
override def kstructName: String = "KaitaiStruct"
}

0 comments on commit 68f2dfb

Please sign in to comment.