JavaとANTLR

ANTLRを使用したJava

1. 概要

このチュートリアルでは、ANTLRパーサジェネレータの概要を簡単に説明し、実際のアプリケーションをいくつか紹介します。

2. ANTLR

ANTLR(他の言語認識ツール)は、構造化テキストを処理するためのツールです。

これは、レクサー、文法、パーサーなどの言語処理プリミティブと、それらに対してテキストを処理するランタイムへのアクセスを提供することで実現します。

ツールやフレームワークを構築するためによく使用されます。 たとえば、HibernateはHQLクエリの解析と処理にANTLRを使用し、Elasticsearchはそれを痛みのないために使用します。

また、Javaは1つのバインディングにすぎません。 ANTLRは、C#、Python、JavaScript、Go、C ++、Swiftのバインディングも提供します。

3. 設定

まず、pom.xmlantlr-runtimeを追加することから始めましょう。


    org.antlr
    antlr4-runtime
    4.7.1

また、antlr-maven-plugin


    org.antlr
    antlr4-maven-plugin
    4.7.1
    
        
            
                antlr4
            
        
    

指定した文法からコードを生成するのはプラグインの仕事です。

4. 仕組み

基本的に、ANTLR Maven pluginを使用してパーサーを作成する場合は、次の3つの簡単な手順に従う必要があります。

  • 文法ファイルを準備する

  • ソースを生成する

  • リスナーを作成する

それでは、これらの手順を実際に見てみましょう。

5. 既存の文法の使用

まず、ANTLRを使用して、大文字と小文字が正しくないメソッドのコードを分析しましょう。

public class SampleClass {

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

簡単に言うと、コード内のすべてのメソッド名が小文字で始まることを検証します。

5.1. 文法ファイルを準備する

すばらしいのは、私たちの目的に合う文法ファイルがすでにいくつかあることです。

ANTLR’s Github grammar repoで見つけたthe Java8.g4 grammar fileを使用してみましょう。

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

それらのファイルの名前がare based on the name of the grammar fileであることに注意してください。

後でテストするときに、Java8Lexer ファイルとJava8Parserファイルが必要になります。 ただし、今のところ、MethodUppercaseListenerを作成するにはJava8BaseListenerが必要です。

5.3. MethodUppercaseListenerの作成

使用したJava8文法に基づいて、Java8BaseListenerにはオーバーライドできるいくつかのメソッドがあり、それぞれが文法ファイルの見出しに対応しています。

たとえば、文法はメソッド名、パラメータリスト、およびthrows句を次のように定義します。

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

したがって、Java8BaseListener は、このパターンが検出されるたびに呼び出されるメソッドenterMethodDeclarator を共有します。

それでは、enterMethodDeclaratorをオーバーライドし、Identifierを引き出して、次のチェックを実行しましょう。

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. 文法ファイルを準備する

まず、ファイル内の各ログ行がどのように見えるかについてのメンタルマップを作成できるかどうかを見てみましょう。

<日時> <レベル> <メッセージ>

または、もう1つ深いレベルに進むと、次のようになります。

<日時>:= <年> <ダッシュ> <月> <ダッシュ> <日>…

等々。 これを考慮して、テキストを解析する粒度のレベルを決定できるようにすることが重要です。

文法ファイルは、基本的にはレクサーとパーサーのルールのセットです。 Simply put, lexer rules describe the syntax of the grammar while parser rules describe the semantics.

reusable building blocks for lexer rules.であるフラグメントを定義することから始めましょう

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の詳細を追加します。

timestamp : DATE ' ' TIME;

levelの場合:

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

そして、messageの場合:

message : (TEXT | ' ')+;

以上です! 文法はすぐに使用できます。 以前と同様に、src/main/antlr4ディレクトリに配置します。

6.2. ソースを生成する

これは単なるmvn packageであり、文法の名前に基づいて、LogBaseListenerLogParserなどのいくつかのファイルが作成されることを思い出してください。

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;

current は現在のログ行を保持します。これは、文法に基づいてlogEntry, againを入力するたびに再初期化できます。

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

次に、enterTimestampenterLevel,、およびenterMessageを使用して、適切な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);
    }

ちなみに、LogListener はスレッドセーフではないことに注意してください。

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を使用して独自の言語のカスタムパーサーを作成する方法に焦点を当てました。

また、既存の文法ファイルを使用して、それらをコードリンティングなどの非常に簡単なタスクに適用する方法も確認しました。

いつものように、ここで使用されているすべてのコードはover on GitHubで見つけることができます。