„Manchmal zeigt sich der Weg erst, wenn man anfängt ihn zu gehen.“
Paul Coelho
Im vorherigen Beitrag wurde ein Annotation Processor vorgestellt, mit dem AttributeConverter
für Enum Klassen automatisch generiert werden können. Kaum war dieser fertig gestellt, bahnten sich schon die ersten Änderungen an. Die WithEnumConverter
Annotation erhält die neuen Attribute ordinal
, nullKeyForbidden
, exceptionIfMissing
und verliert das Attribut representation
.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface WithEnumConverter { boolean ordinal() default true; boolean autoApply() default false; boolean nullKeyForbidden() default false; boolean exceptionIfMissing() default false; }
Das Attribut representation
diente dazu, den Typ für das Persistieren in der Datenbank zu definieren. Da nur die beiden Typen String
und Integer
verwendet werden, wird es durch das boolean
Attribut ordinal
ersetzt.
Die anderen beiden Attribute nullKeyForbidden
und exceptionIfMissing
ändern das Verhalten der generierten AttributeConverters
. Das Attribute nullKeyForbidden
verbietet das Konvertieren von null
Werten. Stattdessen wir eine IllegalArgumentException
geworfen. Das Attribute exceptionIfMissing
sorgt für eine IllegalArgumentException
, wenn das Ergebnis der Konvertierung null
ist.
Die Verhaltensanpassung für den AttributeConverter
erfolgt innerhalb des FreeMarker Templates.
@Override public ${databaseTypeName} convertToDatabaseColumn(${enumTypeName} value) { if (value == null) { <#if nullKeyForbidden> throw new IllegalArgumentException("null key is forbidden"); <#else> return null; </#if> } <#if exceptionIfMissing> return check(value, toDatabaseColumn.get(value)); <#else> return toDatabaseColumn.get(value); </#if> } @Override public ${enumTypeName} convertToEntityAttribute(${databaseTypeName} value) { if (value == null) { <#if nullKeyForbidden> throw new IllegalArgumentException("null key is forbidden"); <#else> return null; </#if> } <#if exceptionIfMissing> return check(value, toEntityAttribute.get(value)); <#else> return toEntityAttribute.get(value); </#if> } <#if exceptionIfMissing> private <K,V> V check(K key, V value) { if (value == null) { throw new IllegalArgumentException("null value is forbidden: " + key); } return value; } </#if>
Der erste Freemarker If-Block fügt für nullKeyForbidden
entweder eine throw
Anweisung in den Sourcecode ein oder eine return null;
Anweisung. Der erste If-Block für exceptionIfMissing
fügt einen zusätzlichen Aufruf für eine check Methode ein. Innerhalb der check
Methode wird eine IllegalArgumentException
für null
Werte geworfen.
@WithEnumConverter(ordinal=false, nullKeyForbidden=true, exceptionIfMissing=true) enum TestEnum { A, B, C }
Das hier dargestellte Beispiel mit dem TestEnum
produziert nun die folgenden drei Methoden.
@Override public String convertToDatabaseColumn(TestEnum value) { if (value == null) { throw new IllegalArgumentException("null key is forbidden"); } return check(value, toDatabaseColumn.get(value)); } @Override public TestEnum convertToEntityAttribute(String value) { if (value == null) { throw new IllegalArgumentException("null key is forbidden"); } return check(value, toEntityAttribute.get(value)); } private <K,V> V check(K key, V value) { if (value == null) { throw new IllegalArgumentException("null value is forbidden: " + key); } return value; }
Eine weitere kleine didaktische Änderung erfahren die ValueHolder
. Sie erhalten ein plain
Flag, dass nur dann auf true
gesetzt wird, wenn die Enum Konstante nicht annotiert ist.
private List<ValueHolder> createValueHolders(TypeElement enumType, boolean ordinal) { AtomicInteger index = new AtomicInteger(); List<ValueHolder> list = enumType.getEnclosedElements().stream() .filter(x -> x.getKind() == ElementKind.ENUM_CONSTANT).map(x -> convert(x, index, ordinal)) .filter(Objects::nonNull).toList(); if (list.stream().allMatch(ValueHolder::isPlain)) { printMessage(Kind.WARNING, ordinal ? "use @Enumerated" : "use @Enumerated(EnumType.STRING)"); } Map<String, List<String>> values = list.stream().map(ValueHolder::getValue).flatMap(List::stream) .collect(Collectors.groupingBy(String::toString)); List<String> doublettes = values.values().stream().filter(v -> v.size() != 1).flatMap(Collection::stream).toList(); if (!doublettes.isEmpty()) { throw new IllegalArgumentException("doublettes defined: " + doublettes); } printMessage(Kind.NOTE, "values: " + list); return list; }
Wenn die Flags aller ValueHolder
auf true
stehen, dann gibt es keinen offensichtlichen Grund einen AttributeConverter
zu verwenden. Weil nicht alle Kollegen diesem Minimalismus frönen, teilt ihnen der EnumConverterProcessor
die richtige Lösung als Compiler Warning mit.
Wer den EnumConverterProcessor
einmal ausprobieren möchte. Einfach die nachfolgende Dependency verwenden und die eigene Enum annotieren.
<dependency> <groupId>de.schegge</groupId> <artifactId>enum-converter-generator</artifactId> <version>1.0.2</version> </dependency>