JavaでABNFを扱うためのライブラリをなんとなく作ってみたので使えるように公開? JSONもABNFでできているので作ってみました。簡易マニュアル的なページです。
RFC でよく使われるバッカス・ナウア記法(Backus-Naur form : BNF)のひとつ(Augmented Backus-Naur form)でIETFがRFCを記述する際によく利用される。BNFは、メタ言語などといわれていて、言語を作るための言語のようなものです。他にはEBNFなどが一般的ですがEBNFにもいろいろあるようです。RFC 5234 とRFC 7405で定義されています。RFC 7405の拡張はあまり使われていません。
ABNFはメール(CMS)、HTML、IPv6アドレスなどの定義に使われています。XMLなども別のBNFで定義されています。
自作ツールはSoftLibABNFという名で、各種定義はSoftLibRFCとして別途まとめています。しばらく更新せず安定版ですが、現状バージョン付与していないソースコードのみです。
XMLに代わってRESTだとかWeb APIの主役になっているあれです。JavaScript Object Notation という名のとおりJavaScriptではよく使われている形式がWebの標準的なものとして拡大したものですが、Javaで直接扱うのは難しそうなので作ってみました。JavaでJSONを扱えるのは、他に標準API、Jakson、Json in Javaなどがあるようですが、いろいろ機能を盛っているので標準APIよりは便利かどうか。RFC 8259とECMA-404 2nd Editionが最終的なJSONの標準として落ち着いているようです。
自作ツールはSoftLibJSONという名です。現状バージョン付与していないソースコードのみです。
ABNFの説明はどこかのを見てください。SoftLibABNFではRFC 5234とRFC 7405に対応しています。
ABNFはrule という単位でできています。このライブラリでは、ruleはテキストで記述することもできればJavaっぽく組むこともできます。
ABNF element = ABNF.text("text");
ABNF element = ABNF.bin('a');
など。
1行を記述するには、ABNFReg というclassを使ってそこに構築していくので仮で作ってみます。
static ABNFReg REG = new ABNFReg(ABNF5234.BASE);
static ABNF line = REG.rule("line := *ALPHA CRLF");
サンプルでABNFの構文を使って line を定義しています。変数には代入しなくてもREGに登録されるので、初期化をstaticにするかしないかはどちらでもよいです。
static ABNF line2 = REG.rule("line2", ABNF5234.ALPHA.x().pl(ABNF5234.CRLF));
というふうにstaticで持っておくとJavaっぽく書くこともできます。ALPHAとCRLFは、class ABNF5234 で持っていたりします。
ABNF#x() などの演算子は、組み合わされた結果のABNFを返すので、そのまま次の演算に使えます。最終的にできたline, line2などの ABNF Objectを比較などに使います。
簡単な比較は line.is("exampletext") などで、この場合はCRLFがないので false が返されます。他後述。
ABNF というclass は、ABNFでいうところのelements、rule などに相当するものです。名前がついているものがrule、ついていないものがelements ぐらいでいいかもしれません。
ABNFのstatic methodで各種ABNFが作れるようになっています。
ABNFの書式で書けるものは、Javaの構文で記述することもできます。パースする手間がないぶん速いかもしれません。
だいたいこんな感じで参照も組み合わせていくとABNFをJavaの構文で構築できます。x()やix(),c()などに慣れるとABNFから気軽に書き換えられるはずです。
要素のコンストラクタは使わない方向でABNF.xxxx() という感じでまとめています。
SoftLibABNFでは、ABNFReg というクラスをひとつの名前空間として構築します。ABNFではrulelist相当です。class中にstaticで定義して、下に1行ずつ並べてもいいですし、テキスト形式で流し込んでもかまいません。rule, elementなどはABNF型で対応します。
class Test { static ABNFReg REG = new ABNFReg(); }
classひとつでひとつの名前空間を作るくらいの感じで利用しています。
ABNFReg REG = new ABNFReg();
ABNFReg(ABNFReg 引き継ぐ名前空間);
ABNFReg(ABNFReg 引き継ぐ名前空間, ABNFReg ABNF定義);
ABNFReg(URL abnfのURL, ABNFReg 引き継ぐ名前空間);
ABNFReg(URL abnfのURL, ABNFReg 引き継ぐ名前空間, ABNFReg ABNF定義);
初期化パラメータは3種類
ABNFにはALPHA,DIGITなどのcore rulesがありますが、それを引き継ぐにはnew ABNFReg(ABNFReg.BASE) とします。他のABNFRegを引き継ぐのも同様です。複製するのでループなどに配慮された複製可能なものがいいです。
ABNFReg.BASEには特殊なパーサは埋め込まれていないのでパース後もPacket形式です。
2つめのパラメータはABNFパーサ自体のABNFで、省略時はRFC 5234ですが差し替えが可能です。RFC 7405やHTTPなどABNF構文自体に特殊な定義がある場合に差し替えます。普段は気にしなくてもいいかもしれません。EBNFなどに差し替えができるようになるかもしれません。
ABNF定義として ABNF5234.REG や ABNF7405.REG が使えます。これ自体がABNFRegでできているため改変も可能です。RFC 7230ではABNFの拡張がされているため、SoftLibRFC のHTTP7230.PAR として拡張しています。
メモ : ABNFの構文解析で必要ですが、ABNFの定義をABNFで書くと無限ループになってしまいます。Javaで記述する場合は参照しないため、コアな部分は後述するJavaの形で記述しています。
rulelist(abnfのURL) で読み込むための値を初期値として渡しているだけです。
登録はREG#rule() または REG#rulelist() で。
rule() はrulenameと elements を分けてもあわせても記述できます。elementsはABNF型でもよいです。rulenameと elementsの間にparser classを指定することで特定のデータ型で返すこともできるので拡張が簡単にできます。
参照は REG.ref(String rulename) または REG.href(String rulename) で。
name() は、REGへの登録には反映されない?のでruleで名付けた方がいいかもしれません。
static ABNF a = REG.rule("a","b"); // String rulename と String elements を分ける static ABNF b = REG.rule("b := c"); // String rule として記述(改行省略) static ABNF c = REG.rule("c", b.or(a)); // ABNF elements を javaで記述
ABNFの rule をひとつ作るのは、名前空間(ABNFReg)からrule(String rulename, String/ABNF elements) か、rule(String/ABNF rule) で可能です。ABNF自身の定義では厳密にはruleには改行を含まなくてはいけませんが、rule()では最後の改行は省略できます。
ABNF型は名前がついているrule も名前のついていないelements も扱えます。名前がないものは式そのままっぽいもの(仮)を名前相当で扱います。
一括登録するには、rulelist(URL 定義一覧) が使えます。
文字コードはutf-8、改行コードはCRLFです。ABNFの決まり事なのでCRのみやLFのみでは読み込めません。
JavaからREG内の名前を参照するには ref(rulename) または href(rulename) が使えます。
ABNF rule1 = REG.ref(String rulename);
ABNF rule 2 = REG.href(String rulename):
定義にABNF変数を使うこともできます。書式ではなくオブジェクトで構築することもできます。(後述)
ABNFReg#ref(String rulename)では未定義のルールを参照してもかまいません。循環ルールの定義も可能です。
REG.rule("a", REG.ref("b") ); // bはまだ定義されていない
REG.rule("b", REG.href("a") );
各ABNFは、is() と eq() という比較演算子とfind()を用意してみました。Packetというクラスを使うと便利ですが、Stringでもかまいません。バイナリを直接読ませるパターンはまだ作っていないのでPacketをかませてください。
eq()とis(Striing)はtrueまたはfalseを返します。 a.is("data") というふうな。Packetの場合は一致した長さが返せたりします。
find() は先頭がABNFに一致した場合に構造の部分を抽出します。名前をつけた要素を抽出したい場合に使うのですが、返す構造に癖があるので直すかもしれません。
PacketやFrontPacketは個人的にbyte[] やファイルの代わりに可変長データとして使っているものなので、継ぎ接ぎを扱うための便利クラスです。
InputStreamに似ているのでread() で読み、使わない分をOutputtStreamを逆にしたような感じのbackWrite()で書き戻します。getInputStream() やgetBackOutputStream() で慣れた形にしてもかまいません。PacketではOutputStream,backInputStream相当もあります。
ABNF のrule.is(Packet) では、一致した部分を小分けにしたPacketとして返し、元のPacketからその部分を切り取ります。
長さは long length(), int size() が使えます。全体を配列にするには toByteArray() です。
ruleのis(Packet) で比較して、想定と違ったなどの場合は元のPacketにbackWrite(isのPacket.toByteArray()) などで書き戻します。
Packetは何世代か造り直して現在はPacketA という名の実体をよく使います。SoftLibの方を参照してみてください。FrontPacketはInputStreamと繋いで使うこともできたりします。
ABNFとPacketが先頭一致した場合、一致した部分を切り取って返します。pacからは切り取った分減っています。
Cは名前が適当なのであとでどうにかしたいところ。
findはpacがABNFと一致していることを判定するところまではisとおなじです。pacからsubのparserで指定された名前を手がかりにデータを切り取ります。名前だけで判定しているのであれです。
この場合のsubParserは、この後のABNFParserと構造はおなじですが少し違うので。
一般的なABNFパーサの機能はここで終わりですが、ABNFの定義にいろいろなパーサというかビルダというのか組み立て方法を埋め込むことができるので以下簡単に。
static ABNF rule = REG.rule( String rulename, Class<ABNFParser>.parserClass, Strubg elements );
というふうに net.siisise.abnf.parser.ABNFParser インターフェースを継承したものをパラメータとして渡すことで、ABNFでパースした結果を独自の(任意の)型として返すことができるようになります。
ABNFList, ABNFSelect, ABNFSub などいくつかの抽象的実装を用意してありますのでそこから継承します。コンストラクタの形式を統一することで埋め込みを楽にしているので、その周辺は多少癖があります。
public class XXXParser extends ABNFBaseParser<型1, 型2> { public XXXParser(ABNF rule, ABNFReg reg, ABNFReg base) { super(rule, reg, base); } public 型1 parse(FrontPacket pac) { // 判定 Packet p = rule.is(pac); if ( p == null ) { return null; } // 処理 } } public class XXXListParser extends ABNFList<型1,型2> { public XXXListParser(ABNF rule, ABNFReg reg, ABNFReg base) { super(rule, reg, base, "subのabnf名1", "subのabnf名2" ... ); } public 型1 parse(List<型2> list) { // 処理 } } public class XXXSelectParser extends ABNFSelect<型1> { public XXXSelectParser(ABNF rule, ABNFReg reg, ABNFReg base) { super(rule, reg, base, "subの選択肢1", "選択肢2", "選択肢3"... );
} public 型1 other(FrontPacket pac) { // 選択肢以外の処理 } }
型1はこのParserが返す型。型2はABNFListなどで使う parseに渡される子の型です。例外もありますがparseで加工したものを上位に渡して組み立てていきます。
コンストラクタの引数 ruleはこのABNFの定義、regは登録されているABNFReg、baseはRFC 5234準拠, RFC 7405準拠、HTTP拡張など、BNFの種類です。
ABNFBaseParser はそのまま加工する場合。型2は使わないかもしれません。ruleと一致するかどうかはparse(FrontPacket)の中で判定します。ABNFList,ABNFSelectなどは判定済みで加工されたデータです。
ABNFParserではPacketのデータは必要量だけ読み取り、処理しきれないものは戻しておく、というルールで扱います。parse内で該当しないと判定した場合はpacにデータを戻し、return でnullを返すことで未処理として扱います。基本的に rule.is(pac) か rule.find(pac, sub)で判定できるので簡単です。
ABNFListはABNFのrepeat "*a"など繰り返し要素をリストに分解してから加工する場合に使います。
subは基本的に1つですが、複数指定するとsuperに渡した順で繋いで渡すので、ABNFのconcatenation "a b c" などでも使おうと思えば使えます。登場順ではありません。各1つしかないものを受け取る場合にも使えます。
ABNFSelectは型を持ったParserが複数あってABNFのalternation "a / b / c / d"などの場合、aとbはparserがあるが、c, dにはないときa, bはaとbのparserで対応し、c, dはotherで対応します。superにはaとbの名前を渡します。
データはPacketや他のABNFParserで返された値、そのListなどで渡されます。それを組み立てて更に上流に返すことで全体の構造が完成します。
superに他ABNFをREGと名前で渡すのは、ABNFのRFC 5234とRFC 7405のような違いを簡単な組み替えで吸収できるようにするためです。
Object バイトから = REG.parse(String rulename, byte[] source);
Object 文字列から = REG.parse(String rulename, String source);
Object Packetから = REG.parse(String rulename, Packet source);
などでしょうか。rulename はREGに登録したABNFの名前です。JSONを実装した場合であればJSON-text、ABNFであればrulelistやruleなどの単位で指定するだけで階層的に組み上がった形で期待するものが返ってくるはずです。基本的に先頭一致だったかもしれません。一致しなくなったところ以降は捨てられるかもしれません。
Parserがない場合には使えないので、ABNFのis(),eq(),find()などと使い分けは必要です。
ABNFParserは、現状ABNFRegが持つ形になっています。
よくある構文解析の字句解析はABNFがやってくれます。あとは解析されたデータを組み立てるだけ、ということでこういう方向でいいんだと思うのですが、他の形でナニカよいものがあるのかな。SoftLibJSONのJSON Parserは、JSONのABNF定義とABNFParserを基にして構築しています。その他の構文解析は行っていません。
HTTPのヘッダ解析ではABNFを若干拡張しているので、そのような方式にも対応できるように拡張手段を追加しています。拡張自体はABNF Parser本体には含まれません。
今後、ABNFRegのparseとABNFのfindを統合したりしなかったりするかもしれません。
まずABNFRegのBASEとREGにJava構文で書かれた方のABNF基本構造を登録しています。登録することでParserが使えるようになります。
次に必要なRFCなどのABNFを読み込みます。たとえば JSONならJSONのParseができます。特定の型にする場合はJava内での記述とパース用class埋め込みになります。
JSONをJSON Parserで必要なString,Integer,List/Mapなどの形式に変換して読み込みます。JSON Parser程度のものは各パーツ単位のパーサを用意するだけで作れます。
必要な場合はParser内またはABNFとは別のところでJava Objectなどに変換しますが、これはJSON Parserとオブジェクトマッピングの組み方次第なのでお好みでどちらにでもできます。