KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

Introduction to Antlr! Pt.4


shimoda01

こんにちは。下田です。以前から気になっていたけど混み過ぎで行けなかった流行りのハンバーガー屋のシェイクシャックに行ってきました。どうせ食べるならと言う事で、ハンバーガー2個頼んだのですが、それでも少し物足りなかったので、自分のお腹が思った以上に拡大していることに気づきがっかりしました。
はい、では、前回の記事から随分と日にちが経っていますが、 Antlr の続きを進めていきたいと思います。前回までで、 Antlr の基本的な部分は説明しましたので、今回は基本的な機能を応用して小さいプログラムを作っていきたいと思います。

では、前回使っていた MySQL の構文を持ってきます。これに以前説明した戻り値を追加していきます。今回扱いたいデータは、カラム情報だけなのでカラム情報周辺に戻り値を追加しています。

grammar MyMySQL;

create_table: CREATE TABLE tbl_name LPAREN create_definition (COMMA create_definition)* RPAREN SEMICOLON ;

create_definition:
    col_name column_definition
  | index_definition
  ;

column_definition:
  data_type column_option*
  ;

column_option:
    NOT NULL
  | DEFAULT (NULL | StringLiteral)
  | AUTO_INCREMENT
  | COLUMN_FORMAT (FIXED|DYNAMIC|DEFAULT)
  | STORAGE (DISK|MEMORY|DEFAULT)
  | COMMENT StringLiteral
  ;

data_type:
    INT (LPAREN length RPAREN)? (UNSIGNED)? (ZEROFILL)?
  | TINYINT (LPAREN length RPAREN)? (UNSIGNED)? (ZEROFILL)?
  | BIGINT (LPAREN length RPAREN)? (UNSIGNED)? (ZEROFILL)?
  | DATE
  | DATETIME
  | CHAR (LPAREN length RPAREN)? (BINARY)? (CHARACTER SET charset_name)? (COLLATE charset_name)?
  | VARCHAR (LPAREN length RPAREN)? (BINARY)? (CHARACTER SET charset_name)? (COLLATE charset_name)?
  | MEDIUMTEXT (BINARY)? (CHARACTER SET charset_name)? (COLLATE charset_name)?
  | TEXT (BINARY)? (CHARACTER SET charset_name)? (COLLATE charset_name)?
  ;

index_definition:
    UNIQUE (KEY)? col_name LPAREN col_name (COMMA col_name)* RPAREN
  | PRIMARY KEY LPAREN col_name (COMMA col_name)* RPAREN
  | KEY col_name LPAREN col_name (COMMA col_name)* RPAREN
  ;

length: DIGIT|DIGITS ;

tbl_name: ID ;

col_name: ID ;

charset_name: ID;

StringLiteral: '\'' StringCharacters? '\'' ;
fragment StringCharacters: StringCharacter+ ;
fragment StringCharacter: ~['] | '\'\'' ;

CREATE: 'create' ;
TABLE: 'table' ;
LPAREN: '(' ;
RPAREN: ')' ;
SEMICOLON: ';' ;
COMMA: ',' ;
NOT: 'not' ;
NULL: 'null' ;
DEFAULT: 'default' ;
AUTO_INCREMENT: 'auto_increment' ;
COLUMN_FORMAT: 'column_format' ;
COMMENT: 'comment' ;
DISK: 'disk' ;
DYNAMIC: 'dynamic' ;
FIXED: 'fixed' ;
KEY: 'key' ;
INDEX: 'index' ;
MEMORY: 'memory' ;
PRIMARY: 'primary' ;
STORAGE: 'storage' ;
UNIQUE: 'unique' ;
INT: 'int' ;
BIGINT: 'bigint' ;
TINYINT: 'tinyint' ;
UNSIGNED: 'unsigned' ;
ZEROFILL: 'zerofill' ;
DATE: 'date' ;
DATETIME: 'datetime' ;
CHAR: 'char' ;
VARCHAR: 'varchar' ;
TEXT: 'text' ;
MEDIUMTEXT: 'mediumtext' ;
BINARY: 'binary' ;
CHARACTER: 'character' ;
COLLATE : 'collate' ;
SET: 'set' ;
DIGIT: [0-9] ;
DIGITS: [0-9]+ ;
ID: [a-z0-9_]+ ;
WS : [ \t\r\n]+ -> skip;

まずは、 data_type の戻り値に Java に対応するクラスを設定して返すようにしてみます。

data_type returns[Class cls]:
    INT (LPAREN length RPAREN)? (UNSIGNED)? (ZEROFILL)? {$cls = Integer.class;}
  | TINYINT (LPAREN length RPAREN)? (UNSIGNED)? (ZEROFILL)? {$cls = Integer.class;}
  | BIGINT (LPAREN length RPAREN)? (UNSIGNED)? (ZEROFILL)? {$cls = Long.class;}
  | DATE {$cls = java.util.Date.class;}
  | DATETIME {$cls = java.util.Date.class;}
  | CHAR (LPAREN length RPAREN)? (BINARY)? (CHARACTER SET charset_name)? (COLLATE charset_name)? {$cls = String.class;}
  | VARCHAR (LPAREN length RPAREN)? (BINARY)? (CHARACTER SET charset_name)? (COLLATE charset_name)?  {$cls = String.class;}
  | MEDIUMTEXT (BINARY)? (CHARACTER SET charset_name)? (COLLATE charset_name)? {$cls = String.class;}
  | TEXT (BINARY)? (CHARACTER SET charset_name)? (COLLATE charset_name)? {$cls = String.class;}
  ;

それぞれの型の定義はパイプでつながっているので、その末尾にそれぞれのクラスを設定するだけです。
戻り値に、 Java の Class オブジェクトを返すよう定義しています。そして各行は、 MySQL の型と、 Java の型に対応するマッピングを記載しています。これで、 MySQL と Java の型のマッピングが完成しました。

次に、 column_definition の戻り値を設定したいと思います。しかし、こちらの戻り値は、データ型とオプションを返したいのですが、 Java では複数の戻り値を受け取れませんので、複数データを保持できるクラスを用意してそれに詰めたものを返したいと思います。

column_definition:
  data_type column_option*
  ;
package a.b;
import java.util.List;
public class MyAntlrColumnOption {
    private Class<?> type;
    private List<String> option;
    public Class<?> getType() {
        return type;
    }
    public void setType(Class<?> type) {
        this.type = type;
    }
    public List<String> getOption() {
        return option;
    }
    public void setOption(List<String> option) {
        this.option = option;
    }
}

用意したオブジェクトはこんな感じになっています。 type は、data_type の戻り値からセットしたものを、そして、 option は、 column_option からの戻り値をセットする想定です。実際に設定する処理の部分はこちらになります。

column_definition returns[a.b.MyAntlrColumnOption def]:
  dt=data_type op+=column_option*
  {
    $def = new a.b.MyAntlrColumnOption();
    $def.setType($dt.cls);
    List<String> list = new ArrayList<String>();
    for (Column_optionContext t : $op) {
      list.add(t.getText());
    }
    $def.setOption(list);
  }
  ;

まずは、戻り値の定義ですが、フルパッケージ名を設定しています。 Antlr の生成する Parser には、独自で作ったクラスの場所を解決する仕組みがありません。よって、フルパッケージ名を記載するか、parser の import を変更する仕組みを使って import 文を追加する必要があります。今回はそこまで大量のクラスを使うことはないので、フルパッケージ名で利用します。次の行の変数のバインディングですが、単数の場合は ‘=’ を使い、複数の場合は ‘+=’ を利用しリストとして受け取るようにします。受け取った変数は、括弧の中で ‘$’ を使い参照します。 data_type からの戻り値は単純な Class オブジェクトなので、単純にセットしていますが、 column_option の方は何も設定していません。その場合は、 Column_optionContext という自動で作られるクラスとして扱いそのコンテキストの中にある文字列を取り込みます。

この要領で、 create_definition も作成していきます。

package a.b;
public class MyAntlrColumn {
    private String name;
    private MyAntlrColumnOption columnOption;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public MyAntlrColumnOption getColumnOption() {
        return columnOption;
    }
    public void setColumnOption(MyAntlrColumnOption columnOption) {
        this.columnOption = columnOption;
    }
}
create_table returns[List<a.b.MyAntlrColumn> columns]:
  CREATE TABLE tbl_name LPAREN cd=create_definition (COMMA cds+=create_definition)* RPAREN SEMICOLON
  {
    $columns = new ArrayList<a.b.MyAntlrColumn>();
    $columns.add($cd.column);
    for (Create_definitionContext c : $cds) {
      $columns.add(c.column);
    }
  }
  ;

create_definition returns[a.b.MyAntlrColumn column]:
    c=col_name cd=column_definition
    {
      $column = new a.b.MyAntlrColumn();
      $column.setColumnOption($cd.def);
      $column.setName($c.text);
    }
  | index_definition
  ;

create_table の場所では、下層で作られた MyAntlrColumn を集めて List にして戻しているだけです。それでは以下のSQLを取り込んで、 MyAntlrColumn の List が取れるか確認してみます。

create table analytics ( a_id int(25) not null auto_increment, a_blog varchar(200) default null, a_user_id int(25) default null, a_ip varchar(30) default null, a_time varchar(20) default null, a_cockie varchar(100) default null, a_rq_p_id int(25) default null, a_request_url varchar(100) default null, primary key (a_id), key a_blog (a_blog), key a_blog_2 (a_blog), key a_user_id (a_user_id), key a_rq_p_id (a_rq_p_id));

テスト用のクラスは以前使ったものと大して変わりません。

package a;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;

import a.antlr.MyMySQLLexer;
import a.antlr.MyMySQLParser;
import a.antlr.MyMySQLParser.Create_tableContext;
import a.b.MyAntlrColumn;

public class Main3 {
    public static void main(String[] args) {
        try {
            List<String> readAllLines = Files.readAllLines(Paths.get("src/test/resources/test1.txt"));
            String sql = readAllLines.get(0);
            MyMySQLLexer lexer = new MyMySQLLexer(new ANTLRInputStream(sql.toLowerCase()));
            CommonTokenStream tokenStream = new CommonTokenStream(lexer);
            MyMySQLParser parser = new MyMySQLParser(tokenStream);
            Create_tableContext tableContext = parser.create_table();
            for (MyAntlrColumn c : tableContext.columns) {
                if (c != null) {
                    System.out.println(c);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

前回と違うところといえば、 Parser から取得するコンテキストが create_table のものになるだけです。そのコンテキストの中には、戻り値として指定した、 リストの ‘columns’ が参照できるようになっています。そのリストを表示しているだけですが、今回のテストで使った SQL には、 Index の定義があるため、 その箇所だけ MyAntlrColumn が null になります。それを取り除いた結果が以下のようになります。

MyAntlrColumn [name=a_id, columnOption=MyAntlrColumnOption [type=class java.lang.Integer, option=[notnull, auto_increment]]]
MyAntlrColumn [name=a_blog, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]]
MyAntlrColumn [name=a_user_id, columnOption=MyAntlrColumnOption [type=class java.lang.Integer, option=[defaultnull]]]
MyAntlrColumn [name=a_ip, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]]
MyAntlrColumn [name=a_time, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]]
MyAntlrColumn [name=a_cockie, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]]
MyAntlrColumn [name=a_rq_p_id, columnOption=MyAntlrColumnOption [type=class java.lang.Integer, option=[defaultnull]]]
MyAntlrColumn [name=a_request_url, columnOption=MyAntlrColumnOption [type=class java.lang.String, option=[defaultnull]]]

はい、想定通りキレイに MySQL の構文を解析した結果が格納されています。次回はこれを使って更に機能を追加していきたと思います。

【関連記事】
Splunkに株価を取り込んでみた ft. Fujikawa
Introduction to Antlr!
Introduction to Antlr! Pt.2
JavaPoetで簡単コード生成!
Introduction to Antlr! Pt.3
モニタリングルーター Sensu
マイクロフレームワーク Spark