Рубрики
Без рубрики

Java с ANTLR

Узнайте, как анализировать и обрабатывать структурированный текст и языки с помощью ANTLR на Java

Автор оригинала: baeldung.

1. Обзор

В этом уроке мы сделаем краткий обзор генератора ANTLR синтаксического анализатора и покажем некоторые реальные приложения.

2. АНТЛР

ANTLR (еще один инструмент для распознавания языков) – это инструмент для обработки структурированного текста.

Это достигается за счет предоставления нам доступа к примитивам обработки языка, таким как лексеры, грамматики и синтаксические анализаторы, а также к среде выполнения для обработки текста на их основе.

Он часто используется для создания инструментов и фреймворков. Например, 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. Подготовьте файл грамматики

Что приятно, так это то, что уже существует несколько файлов грамматики, которые могут соответствовать нашим целям.

Давайте использовать файл грамматики Java 8.g4 , который мы нашли в репозитории грамматики Github ANTLR .

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

5.2. Генерировать Источники

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

mvn package

По умолчанию это приведет к созданию нескольких файлов в каталоге target/generated-sources/antlr4 :

  • По умолчанию это приведет к созданию нескольких файлов в каталоге
  • target/generated-sources/antlr4
  • :
  • По умолчанию это приведет к созданию нескольких файлов в каталоге
  • target/generated-sources/antlr4
  • :
  • По умолчанию это приведет к созданию нескольких файлов в каталоге
  • target/generated-sources/antlr4

Обратите внимание, что имена этих файлов основаны на имени файла грамматики .

Нам понадобятся файлы Java 8 Lexer и Java8Parser позже, когда мы будем тестировать. На данный момент, однако, нам нужен Javabaselistener для создания нашего Прослушивателя верхнего регистра метода .

5.3. Создание MethodUppercaseListener

Основываясь на грамматике Java 8, которую мы использовали, Java8BaseListener имеет несколько методов, которые мы можем переопределить, каждый из которых соответствует заголовку в файле грамматики.

Например, грамматика определяет имя метода, список параметров и предложение throws следующим образом:

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

И поэтому Javabaselistener имеет метод введите MethodDeclarator , который будет вызываться каждый раз при обнаружении этого шаблона.

Итак, давайте переопределим введите MethodDeclarator , вытащим Идентификатор и выполним нашу проверку:

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. Построение Нашей Грамматики

Теперь давайте попробуем что-нибудь немного более сложное, например, разбор файлов журналов:

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

Поскольку у нас есть пользовательский формат журнала, нам сначала нужно будет создать нашу собственную грамматику.

6.1. Подготовьте файл грамматики

Во-первых, давайте посмотрим, сможем ли мы создать мысленную карту того, как выглядит каждая строка журнала в нашем файле.

<<дата и время> <уровень> <сообщение>

Или, если мы углубимся еще на один уровень, мы могли бы сказать:

<дата и время>:= <год><тире><месяц><тире><день> …

И так далее. Важно учитывать это, чтобы мы могли решить, на каком уровне детализации мы хотим анализировать текст.

Файл грамматики-это в основном набор правил лексера и синтаксического анализа. Проще говоря, правила лексера описывают синтаксис грамматики, в то время как правила синтаксического анализа описывают семантику.

Давайте начнем с определения фрагментов, которые являются многоразовыми строительными блоками для правил лексера.

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

Далее давайте определим правила лексера остатков:

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

Имея эти строительные блоки на месте, мы можем построить правила синтаксического анализа для базовой структуры:

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

А затем мы добавим детали для метки времени :

timestamp : DATE ' ' TIME;

Для уровня :

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

И для сообщения :

message : (TEXT | ' ')+;

И это все! Наша грамматика готова к использованию. Мы поместим его в каталог src/main/antlr4 , как и раньше.

6.2. Генерировать Источники

Напомним , что это всего лишь быстрый mvn-пакет , и что это создаст несколько файлов , таких как LogBaseListener , LogParser и так далее, на основе названия нашей грамматики.

6.3. Создайте Наш Прослушиватель Журналов

Теперь мы готовы реализовать наш прослушиватель, который мы в конечном итоге будем использовать для разбора файла журнала на объекты Java.

Итак, давайте начнем с простого класса модели для записи в журнале:

public class LogEntry {

    private LogLevel level;
    private String message;
    private LocalDateTime timestamp;
   
    // getters and setters
}

Теперь нам нужно подкласс LogBaseListener , как и раньше:

public class LogListener extends LogBaseListener {

    private List entries = new ArrayList<>();
    private LogEntry current;

текущий сохранит текущую строку журнала, которую мы можем повторно инициализировать каждый раз, когда вводим запись в журнале, снова на основе вашей грамматики:

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

Далее мы будем использовать введите метку времени , введите уровень, и введите сообщение для настройки соответствующей Записи в журнале свойств:

    @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()));
    }

И, наконец, давайте используем метод exitEntry для создания и добавления нашего нового LogEntry :

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

Заметьте, кстати, что наши Составитель списка журналов это небезопасно для потоков!

6.4. Тестирование

И теперь мы можем проверить еще раз, как и в прошлый раз:

@Test
public void whenLogContainsOneErrorLogEntry_thenOneErrorIsReturned()
  throws Exception {
 
    String logLine ="2018-May-05 14:20:24 ERROR Bad thing happened";

    // 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. Заключение

В этой статье мы сосредоточились на том, как создать пользовательский синтаксический анализатор для собственного языка с помощью ANTLR.

Мы также увидели, как использовать существующие файлы грамматики и применять их для очень простых задач, таких как компоновка кода.

Как всегда, весь код, используемый здесь, можно найти на GitHub .