day0ops
day0ops

Reputation: 7482

Java Generics to return Enum based on value

I have number of Enums that share the method across them. I would like to move it to an interface if possible so I don't have to duplicate it and the code would look a lot cleaner. But after much effort I am still unable to move the methods to an interface.

public enum TypeA {
    ValueAA ("Value AA"),
    ValueAB ("Value AB");

    private final String type;

    TypeA (final String type) {
        this.type = type;
    }

    @JsonValue
    public String getType() {
        return this.type;
    }

    @JsonCreator
    public static TypeA fromValue(final String value) {
        for (TypeA t : TypeA.values()) {
            if (t.getType().equalsIgnoreCase(value)) {
                return t;
            }
        }

        StringBuilder allTypes = new StringBuilder();
        boolean bFirstTime = true;
        for (TypeA val : TypeA.values()) {
            allTypes.append(bFirstTime ? "" : ", ").append(val);
            bFirstTime = false;
        }

        throw new IllegalArgumentException(value + " is an invalid value. Supported values are " + allTypes);
    }
}

public enum TypeB {
    ValueBA ("Value BA"),
    ValueBB ("Value BB");

    private final String type;

    TypeB (final String type) {
        this.type = type;
    }

    @JsonValue
    public String getType() {
        return this.type;
    }

    @JsonCreator
    public static TypeB fromValue(final String value) {
        for (TypeB t : TypeB.values()) {
            if (t.getType().equalsIgnoreCase(value)) {
                return t;
            }
        }

        StringBuilder allTypes = new StringBuilder();
        boolean bFirstTime = true;
        for (TypeB val : TypeB.values()) {
            allTypes.append(bFirstTime ? "" : ", ").append(val);
            bFirstTime = false;
        }

        throw new IllegalArgumentException(value + " is an invalid value. Supported values are " + allTypes);
    }
}

Using Generics and Java 8 is there anyway to move getType and fromValue methods to an interface so that I can share across all the Enums ? Also note the Jackson annotations JsonValue & JsonCreator.

Upvotes: 1

Views: 1159

Answers (1)

Holger
Holger

Reputation: 298203

You can move the fromValue implementation to an interface, however, I suppose, you have to keep stubs in the concrete types for supporting the JSON factory annotation:

interface TypeX {
    String getType();
    static <T extends Enum<T>&TypeX> T fromValue(String value, Class<T> type) {
        EnumSet<T> all=EnumSet.allOf(type);
        for (T t: all) {
            if (t.getType().equalsIgnoreCase(value)) {
                return t;
            }
        }
        throw new IllegalArgumentException(all.stream().map(t -> t.getType())
            .collect(Collectors.joining(", ",
                value+" is an invalid value. Supported values are ", "")));
    }
}

public enum TypeA implements TypeX {
    ValueAA ("Value AA"),
    ValueAB ("Value AB");

    private final String type;

    TypeA (final String type) {
        this.type = type;
    }

    @JsonValue
    public String getType() {
        return this.type;
    }

    @JsonCreator
    public static TypeA fromValue(final String value) {
        return TypeX.fromValue(value, TypeA.class);
    }
}

enum TypeB implements TypeX {
    ValueBA ("Value BA"),
    ValueBB ("Value BB");

    private final String type;

    TypeB (final String type) {
        this.type = type;
    }

    @JsonValue
    public String getType() {
        return this.type;
    }

    @JsonCreator
    public static TypeB fromValue(final String value) {
        return TypeX.fromValue(value, TypeB.class);
    }
}

For completeness, since the type property is invariant, it is possible to move the getType method to the interface, if we use a different implementation:

interface TypeX {
    @Retention(RetentionPolicy.RUNTIME) @interface Type { String value(); }

    @JsonValue default String getType() {
        for(Field f: getDeclaringClass().getDeclaredFields()) try {
            if(f.isEnumConstant() && f.get(null)==this) {
                return f.getAnnotation(Type.class).value();
            }
        } catch(IllegalAccessException ex) {
            throw new AssertionError(ex);
        }
        throw new IllegalStateException();
    }

    Class<? extends TypeX> getDeclaringClass();

    static <T extends Enum<T>&TypeX> T fromValue(String value, Class<T> type) {
        EnumSet<T> all=EnumSet.allOf(type);
        for (T t: all) {
            if (t.getType().equalsIgnoreCase(value)) {
                return t;
            }
        }
        throw new IllegalArgumentException(all.stream().map(t -> t.getType())
            .collect(Collectors.joining(", ",
                value+" is an invalid value. Supported values are ", "")));
    }
}

public enum TypeA implements TypeX {
    @Type("Value AA") ValueAA,
    @Type("Value AB") ValueAB;

    @JsonCreator
    public static TypeA fromValue(final String value) {
        return TypeX.fromValue(value, TypeA.class);
    }
}

enum TypeB implements TypeX {
    @Type("Value BA") ValueBA,
    @Type("Value BB") ValueBB;

    @JsonCreator
    public static TypeB fromValue(final String value) {
        return TypeX.fromValue(value, TypeB.class);
    }
}

But this has several drawbacks, e.g. the Reflection based access is not checked at compile-time and may have performance disadvantages at runtime. And I don’t know whether the @JsonValue will be respected the intended way when appearing at a default method inherited from an interface.

Upvotes: 3

Related Questions