Java mit ANTLR

1. Übersicht

In diesem Tutorial geben wir einen kurzen Überblick über den ANTLR-Parser-Generator und zeigen einige reale Anwendungen.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) ist ein Tool zur Verarbeitung von strukturiertem Text.

Dazu erhalten wir Zugriff auf Grundelemente für die Sprachverarbeitung wie Lexer, Grammatiken und Parser sowie auf die Laufzeit, um Text gegen sie zu verarbeiten.

Es wird oft verwendet, um Tools und Frameworks zu erstellen. Beispielsweise verwendet Hibernate ANTLR zum Parsen und Verarbeiten von HQL-Abfragen und Elasticsearch verwendet es für Painless.

Und Java ist nur eine Bindung. ANTLR bietet auch Bindungen für C #, Python, JavaScript, Go, C ++ und Swift an.

3. Konfiguration

Zunächst fügen wir unserer pom.xml antlr-runtime hinzu :

 org.antlr antlr4-runtime 4.7.1 

Und auch das Antlr-Maven-Plugin:

 org.antlr antlr4-maven-plugin 4.7.1    antlr4    

Es ist die Aufgabe des Plugins, Code aus den von uns angegebenen Grammatiken zu generieren.

4. Wie funktioniert es?

Grundsätzlich müssen wir drei einfache Schritte ausführen, wenn wir den Parser mithilfe des ANTLR Maven-Plugins erstellen möchten:

  • Bereiten Sie eine Grammatikdatei vor
  • Quellen generieren
  • Erstellen Sie den Listener

Lassen Sie uns diese Schritte in Aktion sehen.

5. Verwenden einer vorhandenen Grammatik

Verwenden wir zuerst ANTLR, um Code auf Methoden mit schlechtem Gehäuse zu analysieren:

public class SampleClass { public void DoSomethingElse() { //... } }

Einfach ausgedrückt, wir überprüfen, ob alle Methodennamen in unserem Code mit einem Kleinbuchstaben beginnen.

5.1. Bereiten Sie eine Grammatikdatei vor

Das Schöne ist, dass es bereits mehrere Grammatikdateien gibt, die unseren Zwecken entsprechen.

Verwenden wir die Java8.g4-Grammatikdatei, die wir im Github-Grammatik-Repo von ANTLR gefunden haben.

Wir können das Verzeichnis src / main / antlr4 erstellen und dort herunterladen.

5.2. Quellen generieren

ANTLR generiert Java-Code, der den von uns angegebenen Grammatikdateien entspricht, und das Maven-Plugin macht es einfach:

mvn package

Standardmäßig werden mehrere Dateien im Verzeichnis target / generate-sources / antlr4 generiert :

  • Java8.interp
  • Java8Listener.java
  • Java8BaseListener.java
  • Java8Lexer.java
  • Java8Lexer.interp
  • Java8Parser.java
  • Java8.tokens
  • Java8Lexer.tokens

Beachten Sie, dass die Namen dieser Dateien auf dem Namen der Grammatikdatei basieren .

Wir werden die Java8Lexer- und die Java8Parser- Dateien später beim Testen benötigen . Für den Moment benötigen wir jedoch den Java8BaseListener zum Erstellen unseres MethodUppercaseListener .

5.3. Erstellen MethodUppercaseListener

Basierend auf der von uns verwendeten Java8-Grammatik verfügt Java8BaseListener über mehrere Methoden, die wir überschreiben können. Jede Methode entspricht einer Überschrift in der Grammatikdatei.

Zum Beispiel definiert die Grammatik den Methodennamen, die Parameterliste und die Throws-Klausel wie folgt:

methodDeclarator : Identifier '(' formalParameterList? ')' dims? ;

Und so Java8BaseListener hat eine Methode enterMethodDeclarator , die jedes Mal aufgerufen wird dieses Muster angetroffen wird .

Überschreiben wir also enterMethodDeclarator , ziehen den Bezeichner heraus und führen unsere Prüfung durch:

public class UppercaseMethodListener extends Java8BaseListener { private List errors = new ArrayList(); // ... getter for errors @Override public void enterMethodDeclarator(Java8Parser.MethodDeclaratorContext ctx) { TerminalNode node = ctx.Identifier(); String methodName = node.getText(); if (Character.isUpperCase(methodName.charAt(0))) { String error = String.format("Method %s is uppercased!", methodName); errors.add(error); } } }

5.4. Testen

Lassen Sie uns nun einige Tests durchführen. Zuerst konstruieren wir den Lexer:

String javaClassContent = "public class SampleClass { void DoSomething(){} }"; Java8Lexer java8Lexer = new Java8Lexer(CharStreams.fromString(javaClassContent));

Dann instanziieren wir den Parser:

CommonTokenStream tokens = new CommonTokenStream(lexer); Java8Parser parser = new Java8Parser(tokens); ParseTree tree = parser.compilationUnit();

Und dann der Wanderer und der Zuhörer:

ParseTreeWalker walker = new ParseTreeWalker(); UppercaseMethodListener listener= new UppercaseMethodListener();

Zuletzt weisen wir ANTLR an, unsere Beispielklasse zu durchlaufen :

walker.walk(listener, tree); assertThat(listener.getErrors().size(), is(1)); assertThat(listener.getErrors().get(0), is("Method DoSomething is uppercased!"));

6. Aufbau unserer Grammatik

Now, let's try something just a little bit more complex, like parsing log files:

2018-May-05 14:20:18 INFO some error occurred 2018-May-05 14:20:19 INFO yet another error 2018-May-05 14:20:20 INFO some method started 2018-May-05 14:20:21 DEBUG another method started 2018-May-05 14:20:21 DEBUG entering awesome method 2018-May-05 14:20:24 ERROR Bad thing happened

Because we have a custom log format, we're going to first need to create our own grammar.

6.1. Prepare a Grammar File

First, let's see if we can create a mental map of what each log line looks like in our file.

Or if we go one more level deep, we might say:

:= …

And so on. It's important to consider this so we can decide at what level of granularity we want to parse the text.

A grammar file is basically a set of lexer and parser rules. Simply put, lexer rules describe the syntax of the grammar while parser rules describe the semantics.

Let's start by defining fragments which are reusable building blocks for lexer rules.

fragment DIGIT : [0-9]; fragment TWODIGIT : DIGIT DIGIT; fragment LETTER : [A-Za-z];

Next, let's define the remainings lexer rules:

DATE : TWODIGIT TWODIGIT '-' LETTER LETTER LETTER '-' TWODIGIT; TIME : TWODIGIT ':' TWODIGIT ':' TWODIGIT; TEXT : LETTER+ ; CRLF : '\r'? '\n' | '\r';

With these building blocks in place, we can build parser rules for the basic structure:

log : entry+; entry : timestamp ' ' level ' ' message CRLF;

And then we'll add the details for timestamp:

timestamp : DATE ' ' TIME;

For level:

level : 'ERROR' | 'INFO' | 'DEBUG';

And for message:

message : (TEXT | ' ')+;

And that's it! Our grammar is ready to use. We will put it under the src/main/antlr4 directory as before.

6.2.Generate Sources

Recall that this is just a quick mvn package, and that this will create several files like LogBaseListener, LogParser, and so on, based on the name of our grammar.

6.3. Create Our Log Listener

Now, we are ready to implement our listener, which we'll ultimately use to parse a log file into Java objects.

So, let's start with a simple model class for the log entry:

public class LogEntry { private LogLevel level; private String message; private LocalDateTime timestamp; // getters and setters }

Now, we need to subclass LogBaseListener as before:

public class LogListener extends LogBaseListener { private List entries = new ArrayList(); private LogEntry current;

current will hold onto the current log line, which we can reinitialize each time we enter a logEntry, again based on our grammar:

 @Override public void enterEntry(LogParser.EntryContext ctx) { this.current = new LogEntry(); }

Next, we'll use enterTimestamp, enterLevel, and enterMessage for setting the appropriate LogEntry properties:

 @Override public void enterTimestamp(LogParser.TimestampContext ctx) { this.current.setTimestamp( LocalDateTime.parse(ctx.getText(), DEFAULT_DATETIME_FORMATTER)); } @Override public void enterMessage(LogParser.MessageContext ctx) { this.current.setMessage(ctx.getText()); } @Override public void enterLevel(LogParser.LevelContext ctx) { this.current.setLevel(LogLevel.valueOf(ctx.getText())); }

And finally, let's use the exitEntry method in order to create and add our new LogEntry:

 @Override public void exitLogEntry(LogParser.EntryContext ctx) { this.entries.add(this.current); }

Note, by the way, that our LogListener isn't threadsafe!

6.4. Testing

And now we can test again as we did last time:

@Test public void whenLogContainsOneErrorLogEntry_thenOneErrorIsReturned() throws Exception { String logLine; // instantiate the lexer, the parser, and the walker LogListener listener = new LogListener(); walker.walk(listener, logParser.log()); LogEntry entry = listener.getEntries().get(0); assertThat(entry.getLevel(), is(LogLevel.ERROR)); assertThat(entry.getMessage(), is("Bad thing happened")); assertThat(entry.getTimestamp(), is(LocalDateTime.of(2018,5,5,14,20,24))); }

7. Conclusion

In diesem Artikel haben wir uns darauf konzentriert, wie der benutzerdefinierte Parser für die eigene Sprache mithilfe der ANTLR erstellt wird.

Wir haben auch gesehen, wie vorhandene Grammatikdateien verwendet und für sehr einfache Aufgaben wie Code-Flusen angewendet werden.

Wie immer ist der gesamte hier verwendete Code auf GitHub zu finden.