Ein ANTRL 4 Setup mit Maven

ANTRL – ANother Tool for Language Recognition – ist ein leistungsfähiges Werkzeug um Lexer und Parser aus einer Grammatik zu generieren, mit denen sich dann die so definierte Sprache verarbeiten lässt. ANTLR kann den Parsercode in verschiedenen Zielsprachen generieren, ist aber selbst in Java geschrieben. Es sollte sich daher direkt in ein Java Projekt mit Maven Build integrieren lassen. 

Da es mich dann doch einige Zeit mit Google und ChatGPT gekostet und am Ende noch einen Blick in den Quellcode erfordert hat um herauszufinden, wie das genau funktioniert, will ich das hier einmal aufschreiben. Aber zunächst zwei Video Empfehlungen:

Wie soll mein Maven Projekt aussehen

Es soll ein JAR aus dem Projekt herausfallen, welches als Abhängigkeit in einem größeren Projekt genutzt wird. Ziel ist es dabei, den aus der Grammatik erzeugten Parser direkt mit Code zu verknüpfen, der ihn konkret einsetzt. Also muss der ANTLR Code im Idealfall im Verzeichnis liegen, welches Maven für die Quellcodes verwendet. Das Projekt hat grundsätzlich eine Standardstruktur mit separaten Verzeichnissen für die ANTLR Bestandteile:

ANTLRProjekt
└ src/
  └ main/
    └ java/
      └ hen/bru/antlr/
        └ App.java
        └ aappro/
          └ <generierte ANTLR Klassen>
    └ antlr/
      └ <ANTLR Grammatik,.g4-Datei>
  └ test/
└ target/

Der Begriff ‚aappro‘ kommt aus dem konkreten Anwendungsfall, der mit der Approbationsordnung für Ärzte (ÄApprO) zu tun hat.

Anlage der Grammatik

Für die Grammatikdatei legt man unter main/ einfach den Ordner antlr/ an und darin AApprOAusdruck.g4:

...
    └ antlr/
      └ AApprOAusdruck.g4
...

Wenn man in seiner IDE ein Tool wie den ANTLR4 language support for Visual Studio Code hat legt dieses ggf. dann schon los und erzeugt an einer Stelle außerhalb des CLASSPATH generierte Dateien, die man zur Vorschau verwenden kann.

ANTLR Maven Plugin ergänzen

Um den Maven Prozess nutzen zu können, wird in der pom.xml eine Konfiguration in der hier gezeigten Art ergänzt. Zusammen mit den Abhängigkeiten erhält man z. B. das:

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>hen.bru.antrl</groupId>
  <artifactId>AntlrTest</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>AntlrTest</name>
  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>org.antlr</groupId>
      <artifactId>antlr4-runtime</artifactId>
      <version>4.13.1</version>
    </dependency>
    <dependency>
      <groupId>org.antlr</groupId>
      <artifactId>antlr4</artifactId>
      <version>4.13.1</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <release>17</release>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4-maven-plugin</artifactId>
        <version>4.13.1</version>
        <executions>
          <execution>
            <id>antlr</id>
            <goals>
              <goal>antlr4</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Wenn man nun maven compile aufruft wird gleich die Generierung der Klassen aus der Grammatik durchgeführt. Aber hier tritt das erste Problem auf: Die Klassen werden per Default im /target-Verzeichnis abgelegt, wo sie VS Code zumindest nicht ohne weitere Konfiguration für die Entwicklung von darauf aufbauendem Code im Projekt erkennt.

Den Pfad für die generierten Dateien anpassen

Um den Defaultpfad zu ändern muss im plugin-Abschnitt eine configuration ergänzt werden:

...
      <plugin>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4-maven-plugin</artifactId>
        <version>4.13.1</version>
        <executions>
          <execution>
            <id>antlr</id>
            <configuration>
              <outputDirectory>${project.build.sourceDirectory}/hen/bru/antlr/aappro</outputDirectory>
            </configuration>
            <goals>
              <goal>antlr4</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
...

Damit wird das gewünschte outputDirectory spezifiziert und nun werden die generierten Quellcodes dort abgelegt, wo sie die IDE – und auch Maven weiterhin – findet. Aber in einem eigenen Package, damit sie sich ggf. einfach auf einen Schlag löschen lassen. Das ist manchmal der schnellste Weg, wenn sich die Toolchain verknotet hat.

Das fehlende Package im Java Code

Nun tritt aber das nächste Problem auf: Die generierten Java Klassen haben keine package-Definition und der Compiler beschwert sich darüber zurecht. Ein Irrweg zur Lösung waren die header-Definitionen, die man in der Grammatikdatei einfügen kann:

grammar AApprOAusdruck;

@parser::header { package hen.bru.antlr.aappro; }
@lexer::header { package hen.bru.antlr.aappro; }
...

Damit bringt man zwar die Paketdefinition in die Klassen, die zu Lexer und Parser gehören, aber nicht in die Listener- und Visitorklassen. Das hat früher mal so funktioniert, wurde aber in Zuge eines Bugfixes abgestellt.

Generell hat es mich überrascht, wie kompliziert es schien diese doch eigentlich generell übliche Vorgehensweise – wer legt schon seine Java Klassen ganz an die Spitze seiner Pakethierarchie!? – umzusetzen. Der entscheidende Hinweis war dann, dass sich in der Kommandozeilenversion des Tools ein -package-Parameter befindet, der genau diese umfassende Wirkung hat.

Was ich dann erst mit Blick in den Quellcode – zum Glück möglich bei Open Source Software – verstanden habe ist, wie man in Maven diesen Parameter setzt. Und zwar so:

...
      <plugin>
        <groupId>org.antlr</groupId>
        <artifactId>antlr4-maven-plugin</artifactId>
        <version>4.13.1</version>
        <executions>
          <execution>
            <id>antlr</id>
            <configuration>
              <outputDirectory>${project.build.sourceDirectory}/hen/bru/antlr/aappro</outputDirectory>
              <visitor>true</visitor>
              <listener>false</listener>
              <arguments>-package,hen.bru.antlr.aappro</arguments>
            </configuration>
            <goals>
              <goal>antlr4</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
...

Das entscheidende ist hier der arguments-Parameter und das man hier -package und den Paketnamen nicht per Leerzeichen wie auf der Kommandozeile, sondern per Komma trennen muss. Dann funktioniert es perfekt. Die beiden anderen Optionen steuern nun noch die Generierung der Listener-Klassen (hier unterdrückt) der Visitor-Klassen (hier gewünscht).

Und damit läuft es dann endlich rund: Nach jeder Änderung der Grammatik erzeugt ein Maven Lauf die neuen, generierten Klassen und die lassen sich nahtlos im selbst erstellten Code verwenden.

ihbrune

1991-1996: Studium der Naturwissenschaftlichen Informatik an der Universität Bielefeld. Abschluss mit der Diplomarbeit zum Thema 'Analyse von ein- und mehrdimensionalen Zeitreihen mit der Karhunen-Loève- und Wavelet Transformation' || 1996-1997: Wissenschaftlicher Mitarbeiter am Lehrstuhl Prof. A. Knoll in der Technischen Fakultät der Universität Bielefeld im Projekt 'LANeCo: Local Area Net Configuration' || 1998- 2018: Tätigkeit im BIS - Bielefelder Informationssystem an der Universität Bielefeld || Seit Oktober 2018: Leitung der Abteilung Informationssysteme und Prozessunterstützung im BITS