Java с ANTLR

1. Общ преглед

В този урок ще направим бърз преглед на генератора на синтактичен анализатор ANTLR и ще покажем някои реални приложения.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) е инструмент за обработка на структуриран текст.

Това прави това, като ни дава достъп до примитиви за обработка на език като лексери, граматики и парсери, както и по време на изпълнение за обработка на текст срещу тях.

Често се използва за изграждане на инструменти и рамки. Например Hibernate използва ANTLR за анализиране и обработка на HQL заявки, а Elasticsearch го използва за безболезненост.

А Java е само едно свързване. ANTLR предлага и обвързвания за C #, Python, JavaScript, Go, C ++ и Swift.

3. Конфигурация

Първо, нека започнем с добавяне на antlr-runtime към нашия pom.xml :

 org.antlr antlr4-runtime 4.7.1 

А също и приставката antlr-maven:

 org.antlr antlr4-maven-plugin 4.7.1    antlr4    

Задачата на приставката е да генерира код от посочените от нас граматики.

4. Как работи

По принцип, когато искаме да създадем парсер с помощта на приставката ANTLR Maven, трябва да следваме три прости стъпки:

  • подгответе граматичен файл
  • генериране на източници
  • създайте слушателя

И така, нека да видим тези стъпки в действие.

5. Използване на съществуваща граматика

Нека първо използваме ANTLR за анализ на код за методи с лош корпус:

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

Просто казано, ще потвърдим, че всички имена на методи в нашия код започват с малка буква.

5.1. Подгответе граматичен файл

Хубавото е, че вече има няколко граматични файла, които могат да отговарят на нашите цели.

Нека използваме граматичния файл Java8.g4, който намерихме в репозитория за граматика на Github на ANTLR.

Можем да създадем директорията src / main / antlr4 и да я изтеглим там.

5.2. Генериране на източници

ANTLR работи, като генерира Java код, съответстващ на граматичните файлове, които му предоставяме, а приставката maven улеснява:

mvn package

По подразбиране това ще генерира няколко файла в директорията target / generated-sources / antlr4 :

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

Забележете, че имената на тези файлове се основават на името на граматичния файл .

Ще ни трябват Java8Lexer и Java8Parser файловете по-късно, когато тестваме. Засега обаче ни е необходим Java8BaseListener за създаване на нашия MethodUppercaseListener .

5.3. Създаване на MethodUppercaseListener

Въз основа на използваната от нас граматика Java8, Java8BaseListener има няколко метода, които можем да заменим, всеки от които съответства на заглавие в граматичния файл.

Например, граматиката определя името на метода, списъка с параметри и изхвърля клауза по следния начин:

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

И така Java8BaseListener има метод enterMethodDeclarator, който ще бъде извикан всеки път, когато се срещне този модел.

И така, нека да заменим enterMethodDeclarator , да извадим идентификатора и да извършим нашата проверка:

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. Тестване

Сега, нека направим малко тестване. Първо, ние конструираме лексера:

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

След това правим екземпляр на парсер:

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

И тогава, проходилката и слушателят:

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

И накрая, казваме на ANTLR да премине през нашия примерен клас :

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

6. Изграждане на нашата граматика

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

В тази статия се фокусирахме върху това как да създадем персонализиран парсер за собствения език, използвайки ANTLR.

Също така видяхме как да използваме съществуващи граматични файлове и да ги прилагаме за много прости задачи като свързване на кода.

Както винаги, целият код, използван тук, може да бъде намерен в GitHub.