Java s ANTLR-om

1. Pregled

U ovom uputstvu napravit ćemo kratki pregled generatora analizatora ANTLR i prikazati neke stvarne aplikacije.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) alat je za obradu strukturiranog teksta.

To čini tako što nam daje pristup primitivima za obradu jezika poput leksera, gramatika i parsera, kao i vremenu izvođenja za obradu teksta prema njima.

Često se koristi za izgradnju alata i okvira. Na primjer, Hibernate koristi ANTLR za raščlanjivanje i obradu HQL upita, a Elasticsearch ga koristi za Bezbolno.

A Java je samo jedno povezivanje. ANTLR također nudi vezove za C #, Python, JavaScript, Go, C ++ i Swift.

3. Konfiguracija

Prije svega, krenimo dodavanjem antlr-runtimea u naš pom.xml:

 org.antlr antlr4-vrijeme izvođenja 4.7.1 

I također dodatak antlr-maven:

 org.antlr antlr4-maven-plugin 4.7.1 antlr4 

Zadatak dodatka je generiranje koda iz gramatika koje smo naveli.

4. Kako to djeluje?

U osnovi, kada želimo stvoriti parser pomoću dodatka ANTLR Maven, moramo slijediti tri jednostavna koraka:

  • pripremiti gramatičku datoteku
  • generirati izvore
  • stvoriti slušatelja

Dakle, pogledajmo ove korake na djelu.

5. Korištenje postojeće gramatike

Prvo upotrijebimo ANTLR za analizu koda za metode s lošim kućištem:

javna klasa SampleClass {public void DoSomethingElse () {// ...}}

Jednostavno rečeno, provjerit ćemo da sva imena metoda u našem kodu počinju malim slovom.

5.1. Pripremite datoteku gramatike

Lijepo je što već postoji nekoliko gramatičkih datoteka koje mogu odgovarati našim svrhama.

Upotrijebimo gramatičku datoteku Java8.g4 koju smo pronašli u repo gramatike Github-a ANTLR-a.

Možemo stvoriti src / main / antlr4 i tamo ga preuzmite.

5.2. Generirajte izvore

ANTLR djeluje generirajući Java kôd koji odgovara gramatičkim datotekama koje smo mu dali, a dodatak maven olakšava:

mvn paket

Prema zadanim postavkama ovo će generirati nekoliko datoteka pod cilj / generirani-izvori / antlr4 imenik:

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

Primijetite da su imena tih datoteka temelje se na nazivu gramatičke datoteke.

Trebat će nam Java8Lexer i Java8Parser datoteke kasnije kada testiramo. Za sada nam, međutim, treba Java8BaseListener za stvaranje našeg MethodUppercaseListener.

5.3. Stvaranje MethodUppercaseListener

Na temelju gramatike Java8 koju smo koristili, Java8BaseListener ima nekoliko metoda koje možemo nadjačati, a svaka odgovara naslovu u gramatičkoj datoteci.

Na primjer, gramatika definira ime metode, popis parametara i klauzulu klauzulu ovako:

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

I tako Java8BaseListener ima metodu enterMethodDeclarator koji će se pozivati ​​svaki put kad se susretne ovaj obrazac.

Dakle, poništimo enterMethodDeclarator, izvucite Identifikator, i izvršite našu provjeru:

javna klasa UppercaseMethodListener proširuje Java8BaseListener {greške privatnog popisa = novi ArrayList (); // ... getter za pogreške @Override public void enterMethodDeclarator (Java8Parser.MethodDeclaratorContext ctx) {Čvor TerminalNode = ctx.Identifier (); Niz metodeName = node.getText (); if (Character.isUpperCase (methodName.charAt (0))) {String error = String.format ("Metoda% s je velika slova!", methodName); error.add (pogreška); }}}

5.4. Testiranje

Ajmo sada testirati. Prvo, konstruiramo lexer:

Niz javaClassContent = "javna klasa SampleClass {void DoSomething () {}}"; Java8Lexer java8Lexer = novi Java8Lexer (CharStreams.fromString (javaClassContent));

Zatim instanciramo parser:

CommonTokenStream tokeni = novi CommonTokenStream (lexer); Java8Parser parser = novi Java8Parser (tokeni); Stablo ParseTree = parser.compilationUnit ();

A onda, šetač i slušatelj:

Hodač ParseTreeWalker = novi ParseTreeWalker (); UppercaseMethodListener listener = novi UppercaseMethodListener ();

Na kraju, kažemo ANTLR-u da prođe kroz našu klasu uzoraka:

walker.walk (slušatelj, drvo); assertThat (listener.getErrors (). size (), je (1)); assertThat (listener.getErrors (). get (0), je ("Metoda DoSomething je velika slova!"));

6. Izgradnja naše gramatike

Pokušajmo sada nešto malo složenije, poput raščlanjivanja datoteka dnevnika:

2018-svibanj-05 14:20:18 INFO dogodila se neka pogreška 2018-svibanj-05 14:20:19 INFO još jedna pogreška 2018-svibanj-05 14:20:20 INFO neka metoda započela 2018-svi-05 : 21 DEBUG započela druga metoda 2018.-05. svibnja 14:20:21 DEBUG ulazi u sjajnu metodu 2018.-05. 5. 14:20:24 POGREŠKA Loša stvar se dogodila

Budući da imamo prilagođeni format dnevnika, prvo ćemo morati stvoriti vlastitu gramatiku.

6.1. Pripremite datoteku gramatike

Prvo, pogledajmo možemo li stvoriti mentalnu mapu kako izgleda svaki redak dnevnika u našoj datoteci.

Ili ako zađemo još jednu razinu duboko, mogli bismo reći:

:= …

I tako dalje. Važno je to razmotriti kako bismo mogli odlučiti na kojoj razini detaljnosti želimo raščlaniti tekst.

Gramatička datoteka u osnovi je skup pravila lexera i parsera. Jednostavno rečeno, pravila leksera opisuju sintaksu gramatike, dok pravila raščlanjivanja opisuju semantiku.

Počnimo s definiranjem fragmenata koji jesu višekratni građevni blokovi za lexerova pravila.

fragment DIGIT: [0-9]; fragment TWODIGIT: CIFRENA CIJENA; ulomak SLOVO: [A-Za-z];

Dalje, definirajmo preostala lexer pravila:

DATUM: TWODIGIT TWODIGIT '-' PISMO PISMO PISMO '-' TWODIGIT; VRIJEME: TWODIGIT ':' TWODIGIT ':' TWODIGIT; TEKST: SLOVO +; CRLF: '\ r'? '\ n' | '\ r';

S ovim ugrađenim blokovima možemo izraditi pravila raščlanjivanja za osnovnu strukturu:

zapisnik: unos +; unos: vremenska oznaka '' razina '' poruka CRLF;

A onda ćemo dodati detalje za vremenska oznaka:

vremenska oznaka: DATE '' TIME;

Za nivo:

razina: 'POGREŠKA' | 'INFO' | 'DEBUG';

I za poruka:

poruka: (TEKST | '') +;

I to je to! Naša je gramatika spremna za upotrebu. Stavit ćemo ga pod src / main / antlr4 direktorij kao i prije.

6.2.Generirajte izvore

Podsjetimo da je ovo samo brzo mvn paketi da će ovo stvoriti nekoliko datoteka poput LogBaseListener, LogParser, i tako dalje, na temelju naziva naše gramatike.

6.3. Stvorite našeg slušatelja dnevnika

Sada smo spremni implementirati naš slušatelj koji ćemo u konačnici koristiti za raščlanjivanje datoteke dnevnika u Java objekte.

Počnimo s jednostavnom klasom modela za unos u dnevnik:

javna klasa LogEntry {privatna razina LogLevel; privatna string poruka; privatna vremenska oznaka LocalDateTime; // geteri i postavljači}

Sada moramo podrazrediti LogBaseListener kao prije:

javna klasa LogListener proširuje LogBaseListener {private list entries = new ArrayList (); private LogEntry current;

Trenutno zadržat će se na trenutnoj liniji dnevnika, koju možemo ponovno pokrenuti svaki put kad uđemo u logEntry, opet na temelju naše gramatike:

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

Dalje ćemo upotrijebiti enterTimestamp, enterLevel, i enterMessage za postavljanje odgovarajućeg LogEntry Svojstva:

 @Preuzmi javnu prazninu 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 javna praznina enterLevel (LogParser.LevelContext ctx) {this.current.setLevel (LogLevel.valueOf (ctx.getText ())); }

I na kraju, iskoristimo exitEntry metodu kako bismo stvorili i dodali naš novi LogEntry:

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

Uzmite u obzir da je naš LogListener nije sigurno!

6.4. Testiranje

I sada možemo ponovno testirati kao i prošli put:

@Test public void whenLogContainsOneErrorLogEntry_thenOneErrorIsReturned () baca iznimku {String logLine; // instancirati lexer, parser i walker LogListener listener = new LogListener (); walker.walk (slušatelj, logParser.log ()); LogEntry entry = listener.getEntries (). Get (0); assertThat (entry.getLevel (), je (LogLevel.ERROR)); assertThat (entry.getMessage (), is ("Loša stvar se dogodila")); assertThat (entry.getTimestamp (), je (LocalDateTime.of (2018,5,5,14,20,24))); }

7. Zaključak

U ovom smo se članku usredotočili na to kako stvoriti prilagođeni parser za vlastiti jezik pomoću ANTLR-a.

Također smo vidjeli kako koristiti postojeće gramatičke datoteke i primijeniti ih za vrlo jednostavne zadatke poput povezivanja koda.

Kao i uvijek, sav ovdje korišten kod može se pronaći na GitHubu.