Automatisch generierte Enum Converter (2)

„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>

Schreibe einen Kommentar