Java – Dynamic Enums (Database supported Enums)

Since I needed a good implementation of dynamic enum feature in Java, is tried to find a way to do it. I found this post that explained how to do it using reflection, but since it was too complicated, I wanted to find a better solution. After few days, I have something that can be used in projects… It is implemented using javassist library, and this is something that you have to run after compilation is done. So, after compilation of your Java project, you have to run “DynamicEnumGenerator.java” with correct parameters in its main method (enum full name and path of generated classes) in order for javassist o modify default compiled code.

If you want to automatize this part, you can use one of many Maven or Gradle plugins avaliable. For testing purposes, you can just start main method on the DynamicEnumGenerator class, but be sure to modify this line

generator.makeDynamic("me.vukas.enumeration.DynamicEnum", "./target/classes/");

– it tells what enum to modify and where are the original classes that needs to be modified before final usage.

Here is the description of the classes. First, lets talk about enum that needs to be write-enabled:

public enum DynamicEnum {
    ONE(1, "one"),
    TWO(2, "two"),
    THREE(3, "three");

    private int id;
    private String alternativeName;

    DynamicEnum(int id){
        this(id, null);
    }

    DynamicEnum(int id, String alternativeName){
        this.id = id;
        this.alternativeName = alternativeName;
    }

    public int getId() {
        return id;
    }

    public String getAlternativeName() {
        return alternativeName;
    }

    //must have methods per constructor that will be used in runtime.
    //first parameter is name of the enum that wants to be added
    //body will be replaced in the compile time
    public static boolean add(String value, int id, String alternativeName){ throw new IllegalStateException(); }
    public static boolean add(String value, int id){ throw new IllegalStateException(); }

    //must have this single method per enum. allows removal of dynamic enum in runtime
    //body will be replaced in the compile time
    public static boolean remove(String value){ throw new IllegalStateException(); }
}

In runtime, you cannot delete defined enums ONE, TWO and THREE. You can remove only dynamically added values.

In order to add values, you have to define methods using signature as stated above – first param must be String, and other parameters must be equal with type to Enum constructors.

In order to remove values, you need only single remove method (you can copy it from above). This method receives name of dynamically created enum that should be deleted in runtime.

Now, let’s see code that is used by javassist in order to modify original enum and enable run-time write operations:

import javassist.*;

import java.io.IOException;

public class DynamicEnumGenerator {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        DynamicEnumGenerator generator = new DynamicEnumGenerator();
        generator.makeDynamic("me.vukas.enumeration.DynamicEnum", "./target/classes/");
    }

    private void makeDynamic(String className, String targetDirectory) throws NotFoundException, CannotCompileException, IOException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.get(className);

        int hardcodedEnumsCount = 0;
        for(CtField ctField : ctClass.getFields()){
            if(ctField.getType().equals(ctClass)){
                hardcodedEnumsCount++;
            }
        }

        CtField ctSizeField = CtField.make(String.format("private final static int $SIZE = %d;", hardcodedEnumsCount), ctClass);
        ctClass.addField(ctSizeField);
        CtField ctConstructorsField = CtField.make("private static java.util.Map $CONSTRUCTORS = new java.util.concurrent.ConcurrentHashMap();", ctClass);
        ctClass.addField(ctConstructorsField);

        int constructorsCount = 0;
        CtClass ctEmbeddedClass = ctClass.makeNestedClass("Embedded", true);
        CtField ctCallableField = CtField.make("public java.util.concurrent.Callable kon;", ctEmbeddedClass);
        ctEmbeddedClass.addField(ctCallableField);
        CtConstructor ctc12 = new CtConstructor(new CtClass[]{ classPool.get("java.util.concurrent.Callable") }, ctEmbeddedClass);
        ctc12.setBody("{ this.kon = $1; }");
        ctEmbeddedClass.addConstructor(ctc12);
        ctEmbeddedClass.toClass();
        ctEmbeddedClass.writeFile(targetDirectory);

        CtClass ctBasicConstructorClass = ctClass.makeNestedClass("BasicConstructor", true);
        CtConstructor ctConstructor = new CtConstructor(null, ctBasicConstructorClass);
        ctConstructor.setBody("{}");
        ctBasicConstructorClass.addConstructor(ctConstructor);
        CtField ctOrdinalField = new CtField(CtPrimitiveType.intType, "ordinal", ctBasicConstructorClass);
        ctOrdinalField.setModifiers(Modifier.PUBLIC);
        ctBasicConstructorClass.addField(ctOrdinalField);
        ctBasicConstructorClass.toClass();
        ctBasicConstructorClass.writeFile(targetDirectory);

        for(CtMethod method : ctClass.getDeclaredMethods()){
            if(method.getName().equals("add")
                    && method.getReturnType().getName().equals("boolean")
                    && Modifier.isStatic(method.getModifiers())
                    && Modifier.isPublic(method.getModifiers())
                    && method.getParameterTypes().length > 0
                    && method.getParameterTypes()[0].getName().equals("java.lang.String")
                    ){
                CtClass constructorClass = ctClass.makeNestedClass("Constructor" + constructorsCount, true);
                constructorClass.setSuperclass(ctBasicConstructorClass);
                CtClass ctCallableClass = classPool.get("java.util.concurrent.Callable");
                constructorClass.addInterface(ctCallableClass);
                constructorClass.addField(new CtField(classPool.get("java.lang.String"), "name", constructorClass));
                CtClass[] constructorTypes = new CtClass[method.getParameterTypes().length+1];

                constructorTypes[0] = classPool.get("java.lang.String");
                constructorTypes[1] = CtPrimitiveType.intType;

                StringBuilder constructorBody = new StringBuilder();
                constructorBody.append("{ this.name=$1; this.ordinal=$2; ");

                for(int i=2; i<=method.getParameterTypes().length; i++){
                    constructorTypes[i] = method.getParameterTypes()[i-1];
                    constructorClass.addField(new CtField(constructorTypes[i], "param" + i, constructorClass));
                    constructorBody.append("this.param");
                    constructorBody.append(i);
                    constructorBody.append("=$");
                    constructorBody.append(i+1);
                    constructorBody.append("; ");
                }
                constructorBody.append("} ");

                CtConstructor constructor = new CtConstructor(constructorTypes, constructorClass);
                constructor.setBody(constructorBody.toString());
                constructorClass.addConstructor(constructor);

                CtClass objectClass = classPool.getCtClass("java.lang.Object");

                CtMethod callMethod = new CtMethod(objectClass, "call", null, constructorClass);
                callMethod.setExceptionTypes(new CtClass[]{classPool.getCtClass("java.lang.Exception")});

                StringBuilder callableBody = new StringBuilder();
                callableBody.append("{return new ");
                callableBody.append(className);
                callableBody.append("(name,ordinal");

                for(int i =2; i<=method.getParameterTypes().length; i++){
                    callableBody.append(",param");
                    callableBody.append(i);
                }
                callableBody.append(");}");

                callMethod.setBody(callableBody.toString());
                constructorClass.addMethod(callMethod);

                constructorClass.toClass();
                constructorClass.writeFile("./target/classes/");

                StringBuilder methodBody = new StringBuilder("{ for(int i=0; i<$VALUES.length; i++){ if($VALUES[i].name().equals($1)){ return false; }} int currentNumOfEnums = $VALUES.length; %1$s[] temp = new %1$s[currentNumOfEnums + 1]; System.arraycopy($VALUES, 0, temp, 0, currentNumOfEnums); $CONSTRUCTORS.put($1, new ");
                methodBody.append(className);
                methodBody.append(".Embedded(new ");
                methodBody.append(className);
                methodBody.append(".Constructor");
                methodBody.append(constructorsCount);
                methodBody.append("($1, currentNumOfEnums");

                for(int i =2; i<=method.getParameterTypes().length; i++){
                    methodBody.append(", $");
                    methodBody.append(i);
                }

                methodBody.append("))); try { temp[currentNumOfEnums] = ((");
                methodBody.append(className);
                methodBody.append(".Constructor");
                methodBody.append(constructorsCount);
                methodBody.append(")((");
                methodBody.append(className);
                methodBody.append(".Embedded)$CONSTRUCTORS.get($1)).kon).call(); } catch (Exception e) { e.printStackTrace(); } $VALUES = temp; return true;}");

                method.setBody(String.format(methodBody.toString(), className));

                constructorsCount++;
            }

            if(method.getName().equals("remove")
                    && method.getReturnType().getName().equals("boolean")
                    && Modifier.isStatic(method.getModifiers())
                    && Modifier.isPublic(method.getModifiers())
                    && method.getParameterTypes().length > 0
                    && method.getParameterTypes()[0].getName().equals("java.lang.String")
                    ){
                method.setBody(String.format("{ for(int i=0; i<$VALUES.length; i++){ if($VALUES[i].name().equals($1)){ if(i < %1$s.$SIZE){ return false; } int currentNumOfEnums = $VALUES.length; %1$s[] temp = new %1$s[currentNumOfEnums - 1]; System.arraycopy($VALUES, 0, temp, 0, i); System.arraycopy($VALUES, i+1, temp, i, currentNumOfEnums-i-1); $CONSTRUCTORS.remove($VALUES[i].name()); for(int j=i; j<currentNumOfEnums-1; j++){ try { ((" + className + ".BasicConstructor)((" + className + ".Embedded)$CONSTRUCTORS.get(temp[j].name())).kon).ordinal = j; temp[j] = (((" + className + ".Embedded)$CONSTRUCTORS.get(temp[j].name())).kon).call(); } catch (Exception e) { e.printStackTrace(); } } $VALUES = temp; return true; } } return false; }", className));
            }
        }
        ctClass.writeFile(targetDirectory);
    }
}

And finally, the test code that shows up how this can be used (you should run this only after full compilation, and after execution of DynamicEnumGenerator – otherwise you’ll get IllegalStateException.

import org.junit.Test;

public class DynamicEnumTest {

    @Test
    public void DemoEnumUsage(){
        System.out.println("==DEMO ENUM TEST ==");
        for(DynamicEnum dynamicEnum : DynamicEnum.values()){
            System.out.println("Name: " + dynamicEnum.name() + "; Id:" + dynamicEnum.getId() + "; AlternativeName: " + dynamicEnum.getAlternativeName() + "; Ordinal: " + dynamicEnum.ordinal());
        }

        System.out.println("Adding 4,5,6 in runtime...");

        DynamicEnum.add("FOUR", 4);
        DynamicEnum.add("FIVE", 5, "five");
        DynamicEnum.add("SIX", 6, "six");

        System.out.println("Values after update:");
        for(DynamicEnum dynamicEnum : DynamicEnum.values()){
            System.out.println("Name: " + dynamicEnum.name() + "; Id:" + dynamicEnum.getId() + "; AlternativeName: " + dynamicEnum.getAlternativeName() + "; Ordinal: " + dynamicEnum.ordinal());
        }

        System.out.println("Deleting 5...");

        DynamicEnum.remove("FIVE");

        System.out.println("Values after removal of enum 5 (Six got new ordinal):");
        for(DynamicEnum dynamicEnum : DynamicEnum.values()){
            System.out.println("Name: " + dynamicEnum.name() + "; Id:" + dynamicEnum.getId() + "; AlternativeName: " + dynamicEnum.getAlternativeName() + "; Ordinal: " + dynamicEnum.ordinal());
        }

        System.out.println("Readding 5 and adding 10...");

        DynamicEnum.add("FIVE", 5);
        DynamicEnum.add("TEN", 10, "ten");

        System.out.println("Values after update:");
        for(DynamicEnum dynamicEnum : DynamicEnum.values()){
            System.out.println("Name: " + dynamicEnum.name() + "; Id:" + dynamicEnum.getId() + "; AlternativeName: " + dynamicEnum.getAlternativeName() + "; Ordinal: " + dynamicEnum.ordinal());
        }

        System.out.println("Trying to remove 1 (cannot because it is hardcoded) and remove 6...");

        DynamicEnum.remove("ONE");
        DynamicEnum.remove("SIX");

        System.out.println("Values after update:");
        for(DynamicEnum dynamicEnum : DynamicEnum.values()){
            System.out.println("Name: " + dynamicEnum.name() + "; Id:" + dynamicEnum.getId() + "; AlternativeName: " + dynamicEnum.getAlternativeName() + "; Ordinal: " + dynamicEnum.ordinal());
        }
    }

}

This is the output of this program:

 

==DEMO ENUM TEST ==
Name: ONE; Id:1; AlternativeName: one; Ordinal: 0
Name: TWO; Id:2; AlternativeName: two; Ordinal: 1
Name: THREE; Id:3; AlternativeName: three; Ordinal: 2
Adding 4,5,6 in runtime…
Values after update:
Name: ONE; Id:1; AlternativeName: one; Ordinal: 0
Name: TWO; Id:2; AlternativeName: two; Ordinal: 1
Name: THREE; Id:3; AlternativeName: three; Ordinal: 2
Name: FOUR; Id:4; AlternativeName: null; Ordinal: 3
Name: FIVE; Id:5; AlternativeName: five; Ordinal: 4
Name: SIX; Id:6; AlternativeName: six; Ordinal: 5
Deleting 5…
Values after removal of enum 5 (Six got new ordinal):
Name: ONE; Id:1; AlternativeName: one; Ordinal: 0
Name: TWO; Id:2; AlternativeName: two; Ordinal: 1
Name: THREE; Id:3; AlternativeName: three; Ordinal: 2
Name: FOUR; Id:4; AlternativeName: null; Ordinal: 3
Name: SIX; Id:6; AlternativeName: six; Ordinal: 4
Readding 5 and adding 10…
Values after update:
Name: ONE; Id:1; AlternativeName: one; Ordinal: 0
Name: TWO; Id:2; AlternativeName: two; Ordinal: 1
Name: THREE; Id:3; AlternativeName: three; Ordinal: 2
Name: FOUR; Id:4; AlternativeName: null; Ordinal: 3
Name: SIX; Id:6; AlternativeName: six; Ordinal: 4
Name: FIVE; Id:5; AlternativeName: null; Ordinal: 5
Name: TEN; Id:10; AlternativeName: ten; Ordinal: 6
Trying to remove 1 (cannot because it is hardcoded) and remove 6…
Values after update:
Name: ONE; Id:1; AlternativeName: one; Ordinal: 0
Name: TWO; Id:2; AlternativeName: two; Ordinal: 1
Name: THREE; Id:3; AlternativeName: three; Ordinal: 2
Name: FOUR; Id:4; AlternativeName: null; Ordinal: 3
Name: FIVE; Id:5; AlternativeName: null; Ordinal: 4
Name: TEN; Id:10; AlternativeName: ten; Ordinal: 5

Process finished with exit code 0

Please be aware that remove() method updates ordinal values on each update/delete operation. Hardcoded enum values cannot be removed from the enum. add returns true if it successfully added the value, false otherwise. Similar situation is with remove – if removal is successful true is returned.

In case that somebody needs full IntelliJ IDEA Project, here is the link: DynamicEnum.zip