At my company we have many Enums which implement an Interface like this:
public interface DomainEnum {
String getDomainId();
}
@Getter
@AllArgsConstructor
public enum ExampleEnum implements DomainEnum {
A("1"), B("2"), C("3");
private final String domainId;
}
I would like to use MapStruct to map Strings to any Enum implementing DomainEnum
by using the domainId. From the docs I have seen that you can implement a custom enum naming strategy which describes exactly what I want. My current problem is that I'm not sure how to get the domainId
of the enum. This is my code so far:
public class CustomEnumMappingStrategy extends DefaultEnumMappingStrategy {
// other methods like in example from docs
protected String getCustomEnumConstant(TypeElement enumTypeElement, String enumConstant) {
var enumValue = getEnumValue(enumTypeElement, enumConstant);
// How can I now get the domainId from the ElementKind?
}
protected ElementKind getEnumValue(TypeElement enumTypeElement, String enumConstant) {
return enumTypeElement.getEnclosedElements().stream()
.filter(it -> it.getKind().equals(ElementKind.ENUM_CONSTANT))
.filter(it -> it.toString().equals(enumConstant))
.findFirst()
.orElse(null);
}
}
So how would I get the domainId
from the ElementKind
now to finish my mapping strategy?
Annotation Processing is limited to signatures only. For enum constant definitions, it ends with the name.
("1")
part in A("1"),
in an annotation processor.ExampleEnum.values()
to then call .getDomainId()
on each value in turn from within your annotation processor: You run during compilation, the ExampleEnum
is just a source file at this point, it doesn't exist yet as a class file. Even if it did, loading in arbitrary classes during compilation is a chicken and egg problem and thus a rather bad idea that will make your code very fragile. For example, if one of your enums uses a non java-core type anywhere in it, e.g. a method public SomeTypeYouDefined getFoo()
? Compilation asplodes; your code can no longer be compiled at all.Thus we get to the answer to your question: Unfortunately, what you want is impossible.
Some further problems is that your 'definition' of what you want is fundamentally unworkable.
For example, here is a correct implementation of your system:
public enum MyHackyEnum implements DomainEnum {
A, B, C;
@Override public String getDomainId() {
return "foo-" + LocalDateTime.now();
}
}
You won't get a compiler error or a runtime error, that will 'just work', but obviously any at compile time attempt to create a mapping of all domain IDs will fail completely. That's a bad place to be.
I strongly suggest you therefore stop this plan. It just doesn't "fit". Doing compile time stuff where one of the inputs depends on implementations of a method decreed by an interface is never a good idea for this exact reason.
Annotate the enum constants themselves!
> cat Test.java
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface DomainId {
String value();
}
interface DomainEnum {
public default String getDomainId() {
try {
Enum<?> self = (Enum<?>) this;
return self.getDeclaringClass().getField(self.name()).getAnnotation(DomainId.class).value();
} catch (ClassCastException e) {
throw new IllegalStateException("Only enums must implement DomainEnum. Class " + this.getClass() + " violates this rule");
} catch (NoSuchFieldException e) {
throw new RuntimeException("Unexpected - an enum value isnt a field in its own def?", e);
}
}
}
enum MyDomainEnum implements DomainEnum {
@DomainId("1") A,
@DomainId("2") B,
@DomainId("3") C,
}
public class Test {
public static void main(String[] args) {
System.out.println(MyDomainEnum.A.getDomainId());
}
}
> javac Test.java
> java Test
1
This snippet is stand-alone, just toss it in Test.java
and follow along.
The snippet shows how you can access the domain IDs at runtime. Accessing them at annotation time would be a significantly more complex stand alone example, but it's basic annotation processing logic. Trigger on the annotation and inspect the 'element' that the annotation is annotating. Enum values are treated as fields for the purpose, and you can get their name in your annotation processor just fine.
NB: The above example needs some fleshing out; detect that the DomainId
annotation is missing and produce an appropriate exception, for example. Your annotation processor should detect any enums that implement DomainEnum
but don't have an @DomainId
annotation on every constant they define and emit an error. This is basic AP stuff and requires no special tricks: You can 'get' all source files being compiled as AP easily (sign up for *
as an annotation to get these), you can ask for all top level types in them, then filter it down to enums, then filter away all enums that do not implement DomainEnum
, and go from there.
Because the 'domain ID' is conveyed via an annotation parameter, the compiler itself will force the authors of any domain enums to make those constant values, thus eliminating the hacky example completely.
NB: Lombok hacks directly into the entire AST, but lombok isn't an annotation processor, and has multiple code paths for the various commonly used AST implementations out there. You probably don't want to go down this path, but if you really want to, you can fork lombok and add a lombok processor (you do not need an annotation; lombok plugins see all code and can therefore decide to just scan for enum types that implement DomainEnum
and look for it). But you'd still have to dictate as per documentation that all domain enums MUST have their domain ID as first parameter of each enum constant value. I'd go with the above plan, much simpler and better.