Java dengan ANTLR

1. Gambaran keseluruhan

Dalam tutorial ini, kami akan membuat tinjauan ringkas penjana penghurai ANTLR dan menunjukkan beberapa aplikasi dunia nyata.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) adalah alat untuk memproses teks berstruktur.

Ia melakukan ini dengan memberi kita akses ke primitif pemprosesan bahasa seperti lexer, tatabahasa, dan penghurai serta runtime untuk memproses teks terhadapnya.

Ia sering digunakan untuk membina alat dan kerangka kerja. Sebagai contoh, Hibernate menggunakan ANTLR untuk menghuraikan dan memproses pertanyaan HQL dan Elasticsearch menggunakannya untuk Tidak menyakitkan.

Dan Java hanya satu yang mengikat. ANTLR juga menawarkan pengikatan untuk C #, Python, JavaScript, Go, C ++ dan Swift.

3. Konfigurasi

Pertama sekali, mari mulakan dengan menambahkan antlr-runtime ke pom.xml kami :

 org.antlr antlr4-runtime 4.7.1 

Dan juga plugin antlr-maven:

 org.antlr antlr4-maven-plugin 4.7.1    antlr4    

Ini adalah tugas plugin untuk menghasilkan kod dari tatabahasa yang kita tentukan.

4. Bagaimana Ia Berfungsi?

Pada asasnya, apabila kita ingin membuat penghurai dengan menggunakan pemalam ANTLR Maven, kita perlu mengikuti tiga langkah mudah:

  • sediakan fail tatabahasa
  • menjana sumber
  • buat pendengar

Oleh itu, mari kita lihat langkah-langkah ini.

5. Menggunakan Tatabahasa Sedia Ada

Mari kita gunakan ANTLR untuk menganalisis kod kaedah dengan casing yang buruk:

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

Ringkasnya, kami akan mengesahkan bahawa semua nama kaedah dalam kod kami bermula dengan huruf kecil.

5.1. Sediakan Fail Tatabahasa

Apa yang menyenangkan ialah terdapat beberapa fail tatabahasa di luar sana yang sesuai dengan tujuan kita.

Mari gunakan fail tatabahasa Java8.g4 yang kami dapati di repo tatabahasa Github ANTLR.

Kita boleh membuat direktori src / main / antlr4 dan memuat turunnya di sana.

5.2. Menjana Sumber

ANTLR berfungsi dengan menghasilkan kod Java yang sesuai dengan fail tatabahasa yang kami berikan, dan plugin maven memudahkan:

mvn package

Secara lalai, ini akan menghasilkan beberapa fail di bawah direktori target / dihasilkan-sumber / antlr4 :

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

Perhatikan bahawa nama fail tersebut berdasarkan nama fail tatabahasa .

Kami memerlukan fail Java8Lexer dan Java8Parser kemudian semasa kami menguji. Namun, buat masa ini, kami memerlukan Java8BaseListener untuk membuat MethodUppercaseListener kami .

5.3. Membuat KaedahUppercaseListener

Berdasarkan tata bahasa Java8 yang kami gunakan, Java8BaseListener memiliki beberapa metode yang dapat kami ganti, masing-masing sesuai dengan tajuk dalam file tatabahasa.

Contohnya, tatabahasa menentukan nama kaedah, senarai parameter, dan klausa lontaran seperti:

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

Oleh itu Java8BaseListener mempunyai metode enterMethodDeclarator yang akan dipanggil setiap kali corak ini ditemui.

Oleh itu, mari kita ganti enterMethodDeclarator , tarik Pengecam , dan lakukan pemeriksaan kami:

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. Ujian

Sekarang, mari kita lakukan beberapa ujian. Pertama, kita membina lexer:

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

Kemudian, kami menunjukkan penghurai:

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

Kemudian, pejalan kaki dan pendengar:

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

Terakhir, kami memberitahu ANTLR untuk mengikuti kelas sampel kami :

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

6. Membina Tatabahasa Kami

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

Dalam artikel ini, kami memfokuskan pada cara membuat penghurai khusus untuk bahasa sendiri menggunakan ANTLR.

Kami juga melihat bagaimana menggunakan fail tatabahasa yang ada dan menerapkannya untuk tugas-tugas yang sangat mudah seperti penyisipan kod.

Seperti biasa, semua kod yang digunakan di sini boleh didapati di GitHub.