Java med ANTLR

1. Oversigt

I denne vejledning laver vi et hurtigt overblik over ANTLR-parsergeneratoren og viser nogle applikationer i den virkelige verden.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) er et værktøj til behandling af struktureret tekst.

Det gør dette ved at give os adgang til sprogbehandling primitiver som lexers, grammatik og parsers samt runtime til at behandle tekst mod dem.

Det bruges ofte til at bygge værktøjer og rammer. For eksempel bruger dvale ANTLR til parsing og behandling af HQL-forespørgsler, og Elasticsearch bruger det til smertefri.

Og Java er kun en binding. ANTLR tilbyder også bindinger til C #, Python, JavaScript, Go, C ++ og Swift.

3. Konfiguration

Lad os først og fremmest starte med at tilføje antlr-runtime til vores pom.xml:

 org.antlr antlr4-runtime 4.7.1 

Og også antlr-maven-plugin:

 org.antlr antlr4-maven-plugin 4.7.1 antlr4 

Det er plugins opgave at generere kode fra de grammatikker, vi specificerer.

4. Hvordan fungerer det?

Dybest set, når vi vil oprette parseren ved hjælp af ANTLR Maven-pluginet, skal vi følge tre enkle trin:

  • udarbejde en grammatikfil
  • generere kilder
  • oprette lytteren

Så lad os se disse trin i aktion.

5. Brug af en eksisterende grammatik

Lad os først bruge ANTLR til at analysere kode til metoder med dårlig kabinet:

offentlig klasse SampleClass {public void DoSomethingElse () {// ...}}

Kort sagt, vi validerer, at alle metodenavne i vores kode starter med små bogstaver.

5.1. Forbered en grammatikfil

Hvad der er pænt er, at der allerede er flere grammatikfiler derude, der kan passe til vores formål.

Lad os bruge Java8.g4 grammatikfilen, som vi fandt i ANTLRs Github grammatik repo.

Vi kan oprette src / main / antlr4 bibliotek og download det der.

5.2. Generer kilder

ANTLR fungerer ved at generere Java-kode svarende til de grammatikfiler, vi giver den, og maven-pluginet gør det let:

mvn-pakke

Som standard genererer dette flere filer under mål / genererede kilder / antlr4 vejviser:

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

Bemærk, at navnene på disse filer er baseret på navnet på grammatikfilen.

Vi har brug for Java8Lexer og Java8Parser filer senere, når vi tester. For nu har vi dog brug for Java8BaseListener for at skabe vores MetodeUppercaseListener.

5.3. Opretter MetodeUppercaseListener

Baseret på den Java8-grammatik, som vi brugte, Java8BaseListener har flere metoder, som vi kan tilsidesætte, hver svarer til en overskrift i grammatikfilen.

For eksempel definerer grammatikken metodens navn, parameterliste og kaste-klausul som sådan:

methodDeclarator: Identifier '(' formalParameterList? ')' dæmpes? ;

Også Java8BaseListener har en metode enterMethodDeclarator som vil blive påberåbt hver gang dette mønster opstår.

Så lad os tilsidesætte enterMethodDeclarator, træk ud Identifikator, og udfør vores kontrol:

offentlig klasse UppercaseMethodListener udvider Java8BaseListener {private List-fejl = ny ArrayList (); // ... getter for fejl @ Override public void enterMethodDeclarator (Java8Parser.MethodDeclaratorContext ctx) {TerminalNode node = ctx.Identifier (); String methodName = node.getText (); hvis (Character.isUpperCase (methodName.charAt (0))) {String error = String.format ("Method% s isercased!", methodName); error.add (fejl); }}}

5.4. Testning

Lad os nu lave nogle test. Først konstruerer vi lexeren:

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

Derefter instantierer vi parseren:

CommonTokenStream tokens = ny CommonTokenStream (lexer); Java8Parser parser = ny Java8Parser (tokens); ParseTree-træ = parser.compilationUnit ();

Og så rullatoren og lytteren:

ParseTreeWalker walker = ny ParseTreeWalker (); UppercaseMethodListener lytter = ny UppercaseMethodListener ();

Endelig beder vi ANTLR om at gå gennem vores prøveklasse:

walker.walk (lytter, træ); assertThat (listener.getErrors (). størrelse (), er (1)); assertThat (listener.getErrors (). get (0), is ("Metode DoSomething er store bogstaver!"));

6. Opbygning af vores grammatik

Lad os nu prøve noget bare lidt mere komplekst, som parsing af logfiler:

2018-maj-05 14:20:18 INFO opstod der en fejl 2018-maj-05 14:20:19 INFO endnu en fejl 2018-maj-05 14:20:20 INFO en eller anden metode startede 2018-maj-05 14:20 : 21 DEBUG anden metode startede 2018-maj-05 14:20:21 DEBUG indtastede fantastisk metode 2018-maj-05 14:20:24 FEJL Dårligt skete der

Fordi vi har et brugerdefineret logformat, skal vi først oprette vores egen grammatik.

6.1. Forbered en grammatikfil

Lad os først se, om vi kan oprette et mentalt kort over, hvordan hver loglinje ser ud i vores fil.

Eller hvis vi går endnu et niveau dybt, kan vi måske sige:

:= …

Og så videre. Det er vigtigt at overveje dette, så vi kan beslutte, på hvilket granularitetsniveau vi vil analysere teksten.

En grammatikfil er grundlæggende et sæt lexer- og parserregler. Kort sagt, lexer-regler beskriver syntaks for grammatikken, mens parser-regler beskriver semantikken.

Lad os starte med at definere fragmenter, der er genanvendelige byggesten til lexer-regler.

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

Lad os derefter definere de resterende lexerregler:

DATO: TWODIGIT TWODIGIT '-' BREVBREV '-' TWODIGIT; TID: TWODIGIT ':' TWODIGIT ':' TWODIGIT; TEKST: LETTER +; CRLF: '\ r'? '\ n' | '\ r';

Med disse byggesten på plads kan vi oprette parserregler for den grundlæggende struktur:

log: post +; post: tidsstempel '' niveau '' meddelelse CRLF;

Og så tilføjer vi detaljerne for tidsstempel:

tidsstempel: DATO '' TID;

Til niveau:

niveau: 'FEJL' | 'INFO' | 'FEJLFINDE';

Og for besked:

besked: (TEKST | '') +;

Og det er det! Vores grammatik er klar til brug. Vi sætter det under src / main / antlr4 katalog som før.

6.2.Generer kilder

Husk at dette bare er en hurtig mvn-pakke, og at dette vil skabe flere filer som LogBaseListener, LogParser, og så videre, baseret på navnet på vores grammatik.

6.3. Opret vores log-lytter

Nu er vi klar til at implementere vores lytter, som vi i sidste ende bruger til at analysere en logfil i Java-objekter.

Så lad os starte med en simpel modelklasse til logindgangen:

offentlig klasse LogEntry {privat LogLevel-niveau; privat streng besked; privat LocalDateTime tidsstempel; // getters og setters}

Nu skal vi underklasse LogBaseListener som før:

offentlig klasse LogListener udvider LogBaseListener {private Listeindgange = ny ArrayList (); privat LogEntry nuværende;

nuværende holder fast i den aktuelle loglinje, som vi kan geninitialisere hver gang vi indtaster en logEntry, igen baseret på vores grammatik:

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

Dernæst bruger vi enterTimestamp, enterLevel, og enterMessage til indstilling af passende Logindgang ejendomme:

 @ Overstyr offentlig ugyldig enterTimestamp (LogParser.TimestampContext ctx) {this.current.setTimestamp (LocalDateTime.parse (ctx.getText (), DEFAULT_DATETIME_FORMATTER)); } @ Overstyr offentlig ugyldig enterMessage (LogParser.MessageContext ctx) {this.current.setMessage (ctx.getText ()); } @ Overstyr offentlig tomrum enterLevel (LogParser.LevelContext ctx) {this.current.setLevel (LogLevel.valueOf (ctx.getText ())); }

Og endelig, lad os bruge exitEntry metode for at oprette og tilføje vores nye Logindgang:

 @ Overstyr offentlig ugyldigt exitLogEntry (LogParser.EntryContext ctx) {this.entries.add (this.current); }

Bemærk forresten, at vores LogListener er ikke trådsikker!

6.4. Testning

Og nu kan vi teste igen som sidste gang:

@Test offentlig ugyldig nårLogContainsOneErrorLogEntry_thenOneErrorIsReturned () kaster Undtagelse {String logLine; // instantier lexeren, parseren og rullatoren LogListener-lytteren = ny LogListener (); walker.walk (lytter, logParser.log ()); LogEntry-indgang = listener.getEntries (). Get (0); assertThat (entry.getLevel (), er (LogLevel.ERROR)); assertThat (entry.getMessage (), er ("Dårlig ting skete")); assertThat (entry.getTimestamp (), er (LocalDateTime.of (2018,5,5,14,20,24))); }

7. Konklusion

I denne artikel fokuserede vi på, hvordan man opretter den brugerdefinerede parser til eget sprog ved hjælp af ANTLR.

Vi så også, hvordan man bruger eksisterende grammatikfiler og anvender dem til meget enkle opgaver som kodeforing.

Som altid kan al den kode, der bruges her, findes på GitHub.