Im vorherigen Beitrag wurde die grobe Struktur eines Generators für Hamcrest Matcher vorgestellt. In diesem Beitrag vervollständigen wir den Entwurf, in dem die losen Enden verknüpft und die tatsächlichen Matcher-Klassen generiert werden.
Da sich in der Zwischenzeit die Implementierung etwas verändert hat, wird die ganze Implementierung des HamcrestMatcherBuilderProcessor
besprochen.
Der Generator als Implementierung eines Annotation Processors verarbeitet in seiner process Methode alle von ihm unterstützten Annotationen. Im Fall des Generators ist dies die @Hamcrest
Annotation.
@Override public boolean process(Set< extends TypeElement> annotations, RoundEnvironment roundEnv) { return annotations.stream().allMatch(annotation -> processAnnotations(annotation, roundEnv)); }
Die erste Schritt der Generierung geschieht in der processAnnotations
Methode.
private boolean processAnnotations(TypeElement annotation, RoundEnvironment roundEnv) { return roundEnv.getElementsAnnotatedWith(annotation).stream() .map(TypeElement.class::cast).map(this::extractValueType) .filter(Objects::nonNull).map(m -> (TypeElement) processingEnv.getTypeUtils().asElement(m)) .allMatch(this::handleSourceType); }
Die tatsächlich annotierten Klassen werden mit roundEnv.getElementsAnnotatedWith
bestimmt und die daran annotierte Ziel-Klassen mit extractValueType
ausgelesen.
public TypeMirror extractValueType(TypeElement e) { return e.getAnnotationMirrors().stream() .filter(m -> Hamcrest.class.getName().equals(m.getAnnotationType().toString())) .map(m -> m.getAnnotationType().asElement().accept(new HamcrestValueVisitor(), m.getElementValues())) .findFirst().orElse(null); }
Da die Klasse auch mit anderen Annotationen versehen sein kann, müssen wir auf Hamcrest filtern. Falls keine @Hamcrest
Annotation gefunden wird, liefert die Methode null
zurück. Das kann nie passieren, weil wir diese Klasse nur wegen ihrer @Hamcrest
Annotation verarbeiten.
Die eigentliche Generierung geschieht in der handleSourceType
Methode. Hier wird die Ziel-Klasse ausgelesen und die Sourcen der Matcher-Klasse generiert.
private boolean handleSourceType(TypeElement sourceType) { try { List<ExecutableElement> methods = sourceType.getEnclosedElements().stream() .filter(element -> element.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast) .filter(this::filterModifiers).filter(this::filterGetter) .collect(toList()); String sourceName = sourceType.getSimpleName().toString(); String matcherTypeName = sourceName + "Matchers"; Name packageName = processingEnv.getElementUtils().getPackageOf(sourceType).getQualifiedName(); return createMatcherClassFile(methods, sourceName, matcherTypeName, packageName); } catch (IOException ex) { return false; } }
Im ersten Schritt werden alle Methoden per Reflections ausgelesen die der Java Beans Getter Konvention für Attribute entsprechen um dann im zweiten Schritt die Matcher-Klasse erzeugt.
Der Name und das Package der Matcher-Klasse wird vom Namen der Ziel-Klasse abgeleitet.
private boolean createMatcherClassFile(List<ExecutableElement> methods, String sourceName, String matcherTypeName, Name packageName) throws IOException { JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(matcherTypeName); if (builderFile.getLastModified() > 0) { return false; } try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { String classHeaderTemplate = properties.getProperty("class.header"); out.println(String.format(classHeaderTemplate, packageName, matcherTypeName, LocalDate.now())); methods.forEach(method -> createMatcherMethod(sourceName, out, method, packageName)); out.println(properties.getProperty("class.footer")); } return true; }
Die Klasse JavaFileObject
erzeugt die gewünschte Klasse im korrekten Unterordner, der sich aus dem Package-Namen ergibt. In Verbindung mit dem Maven Compiler Plugin auch im korrekten Maven Target-Verzeichnis.
Für die PersonMatchers Klasse also die Datei target/generated-classes/annotations/de/schegge/ancestors/PersonsMatcher.java
.
Der Inhalt der Datei gliedert sich in den Header-, den Footer- und den Methodenbereich. Für alle drei Bereiche stehen einfache Templates bereit, die mit den entsprechenden Werten gefüllt, den gewünschten Source-Code ergeben.
Der Footer-Bereich ist trivial, er besteht nur aus der schließenden Klammer der Klassendefinition. Im Header-Bereich stehen die Package Deklaration, die Import Statements und der Beginn der Klassen Deklaration und ein wenig Javadoc.
class.header=\ /**%n\ * @author Hamcrest Matcher Generator by Jens Kaiser%n\ * @version 1.0%n\ *%n\ * @see <a href="https://gitlab.com/schegge/hamcrest-matcher-generator">Hamcrest Matcher Generator</a>%n\ */%n\ package %1$s;%n\ %n\ import static org.hamcrest.Matchers.equalTo;%n\ %n\ import org.hamcrest.FeatureMatcher;%n\ import org.hamcrest.Matcher;%n\ import de.schegge.processor.OptionalMatchers;%n\ %n\ public final class %2$s {
Der Methodenbereich wird durch mehrere Templates für jede gesammelte Methode aus der Ziel-Klasse gefüllt.
private void createMatcherMethod(String sourceName, PrintWriter out, ExecutableElement method, Name packageName) { String methodName = method.getSimpleName().toString(); String attributeName = createArgumentName(methodName); DeclaredType declaredType = (DeclaredType) method.getReturnType(); String variableName = attributeName.substring(0, 1).toLowerCase() + attributeName.substring(1); TypeElement typeElement = (TypeElement) declaredType.asElement(); if (typeElement.toString().equals(Optional.class.getName())) { String returnTypeName = createReturnTypeName(declaredType.getTypeArguments().get(0).toString(), packageName); String methodTemplate1 = properties.getProperty("class.matcherMethod"); String methodTemplate2 = properties.getProperty("class.optionalMethod"); out.printf(methodTemplate1, sourceName, attributeName, declaredType.toString(), variableName, methodName); out.printf(methodTemplate2, sourceName, attributeName, returnTypeName, variableName, methodName); return; } String returnTypeName = createReturnTypeName(method.getReturnType().toString(), packageName); String methodTemplate1 = properties.getProperty("class.method"); String methodTemplate2 = properties.getProperty("class.matcherMethod"); out.printf(methodTemplate1, sourceName, attributeName, returnTypeName, variableName, methodName); out.printf(methodTemplate2, sourceName, attributeName, returnTypeName, variableName, methodName); }
Ein Template fügt eine Methode ein, die mit einem Matcher
fur den Ergebnistyp der Methode parametrisiert ist und ein anderes Template erstellt eine spezialisierte Convenience Methode parametrisiert mit dem Ergebnistyp der Ziel-Methode. Diese ruft die vorhergehende Methode mit dem Hamcrest IsEqual.equalTo
Matcher auf.
Für Attribute vom Typ Optional
gibt es eigene Templates. Da Optional
als Methoden-Parameter nicht gerne gesehen sind, gibt es eine Matcher-Methoden mit denen geprüft wird, ob das Optional
gesetzt oder leer ist und eine weitere Method zum Prüfen des Inhalts.
Damit ist der Hamcrest Matcher Generator auch schon fertig implementiert und kann zur Generierung eigener Matcher Klassen genutzt werden. Etwas Konfiguration oder der Einbau einer Template Engine sind noch denkbar aber der Inhalt zukünftiger Beiträge.
Für Interessierte ist der Sourcecode des Generators wieder auf GitLab zu finden.
1 thought on “Hamcrest Matcher Generator (Teil 2)”
Comments are closed.