Asakusa Framework0.2.1のJavaDataModelDriverのメモ。
(→最新のJavaDataModelDriverのメモ)
|
|
Asakusa Frameworkでは、DMDL(dmdlファイル)でデータモデルを記述すると、Modelクラス(やio/Input・io/Outputクラス)のJavaソースファイルが生成される。
これらはcom.asakusafw.dmdl.java.spi.JavaDataModelDriverを継承したクラスで生成されている。
(特にInput・OutputクラスはModelInputDriver・ModelOutputDriverで作られている。ModelクラスはConcreteModelEmitterを始めとする色々なDriverで分散して作っている模様)
これらのドライバーはJavaのサービスプロバイダー(SPI)の仕組みを使って呼ばれている。
したがって、自分でドライバーを作ってSPIに入れれば、独自のクラス(Javaソース)を生成することが出来る。
AsakusaFWのソースコードリーディングでSPIを使っていると聞いていたので、AsakusaFWのjarファイルの中からSPIのファイルを探したら それらしいクラス(JavaDataModelDriver)を見つけたので、試してみたもの。
まずはSPIで自分のDriverが呼び出せる状態にする必要がある。
Driverを記述する為のEclipse(ワークスペース)のプロジェクトを新たに作成し、まずは空っぽのDriverを作成してみる。
package jp.hishidama.asakusafw.dmdl;
import java.io.IOException; import java.util.List; import com.asakusafw.dmdl.java.emitter.EmitContext; import com.asakusafw.dmdl.java.spi.JavaDataModelDriver; import com.asakusafw.dmdl.semantics.ModelDeclaration; import com.asakusafw.dmdl.semantics.PropertyDeclaration; import com.ashigeru.lang.java.model.syntax.Annotation; import com.ashigeru.lang.java.model.syntax.MethodDeclaration; import com.ashigeru.lang.java.model.syntax.Type;
public class EmptyDriver extends JavaDataModelDriver {
@Override public List<Annotation> getTypeAnnotations(EmitContext context, ModelDeclaration model) throws IOException { System.out.println("+++EmptyDriver#getTypeAnnotations\t" + model); return super.getTypeAnnotations(context, model); } @Override public List<Type> getInterfaces(EmitContext context, ModelDeclaration model) throws IOException { System.out.println("+++EmptyDriver#getInterfaces\t" + model); return super.getInterfaces(context, model); } @Override public List<Annotation> getMemberAnnotations(EmitContext context, PropertyDeclaration property) throws IOException { System.out.println("+++EmptyDriver#getMemberAnnotations\t" + property); return super.getMemberAnnotations(context, property); } @Override public List<MethodDeclaration> getMethods(EmitContext context, ModelDeclaration model) throws IOException { System.out.println("+++EmptyDriver#getMethods\t" + model); return super.getMethods(context, model); } }
JavaDataModelDriverで定義されているメソッドは4つ。
(0.7.3ではgetFieldsというメソッドもある。[2015-07-25])
どんなものが引数で渡されるのか、とりあえずコンソールに表示してみる。
(各メソッドで親クラスのメソッドを呼び出しているが、何も処理しない(空のリストを返している))
このソースをコンパイルするのに必要なjarファイルは以下の通り。
asakusa-dmdl-java-0.2.1.jar asakusa-dmdl-core-0.2.1.jar |
DMDL関連 |
asakusa-runtime-0.2.1.jar | AsakusaFWのランタイム |
java-dom-0.1.0.jar | Javaソースの構築用 |
適当にインポート文を書けばコンパイルエラーになるので、Eclipseのクイック・フィックスで「プロジェクト・セットアップの修正」を選べば候補となるjarファイルを探してくれるので楽。
SPIを使う際は、META-INF/servicesというディレクトリーの下に親クラスの名前をしたテキストファイルを作成し、その中に具象クラスを記述する。
jp.hishidama.asakusafw.dmdl.EmptyDriver
Driverを実行するにはモデルの生成を行う(モデルの生成を行うとDriverを読み込んで実行してくれる)が、これを実行する為に以下のjarファイルが必要。(ビルドパスに追加しておく)
commons-cli-1.2.jar | コマンドラインの解析 |
slf4j-api-1.6.1.jar logback-classic-0.9.28.jar logback-core-0.9.28.jar |
ログ出力 |
simple-graph-0.1.0.jar | たぶんグラフの作成 |
hadoop-core-0.20.2-cdh3u0.jar | Hadoopのコア(Writable等) |
まぁ、プロジェクトの下の.classpathファイルを書き換えるのが早そう。
(環境によってバージョンが違う可能性もあるので、自分のAsakusaFW用プロジェクトからコピーしてくるのが確実そう)
<classpathentry kind="var" path="M2_REPO/com/asakusafw/asakusa-dmdl-core/0.2.1/asakusa-dmdl-core-0.2.1.jar" sourcepath="M2_REPO/com/asakusafw/asakusa-dmdl-core/0.2.1/asakusa-dmdl-core-0.2.1-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/com/asakusafw/asakusa-dmdl-java/0.2.1/asakusa-dmdl-java-0.2.1.jar" sourcepath="M2_REPO/com/asakusafw/asakusa-dmdl-java/0.2.1/asakusa-dmdl-java-0.2.1-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/com/asakusafw/asakusa-runtime/0.2.1/asakusa-runtime-0.2.1.jar" sourcepath="M2_REPO/com/asakusafw/asakusa-runtime/0.2.1/asakusa-runtime-0.2.1-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/com/ashigeru/lang/java/java-dom/0.1.0/java-dom-0.1.0.jar" sourcepath="M2_REPO/com/ashigeru/lang/java/java-dom/0.1.0/java-dom-0.1.0-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/commons-cli/commons-cli/1.2/commons-cli-1.2.jar" sourcepath="M2_REPO/commons-cli/commons-cli/1.2/commons-cli-1.2-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/org/slf4j/slf4j-api/1.6.1/slf4j-api-1.6.1.jar" sourcepath="M2_REPO/org/slf4j/slf4j-api/1.6.1/slf4j-api-1.6.1-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/com/ashigeru/util/simple-graph/0.1.0/simple-graph-0.1.0.jar" sourcepath="M2_REPO/com/ashigeru/util/simple-graph/0.1.0/simple-graph-0.1.0-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/org/apache/hadoop/hadoop-core/0.20.2-cdh3u0/hadoop-core-0.20.2-cdh3u0.jar" sourcepath="M2_REPO/org/apache/hadoop/hadoop-core/0.20.2-cdh3u0/hadoop-core-0.20.2-cdh3u0-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/ch/qos/logback/logback-classic/0.9.28/logback-classic-0.9.28.jar" sourcepath="M2_REPO/ch/qos/logback/logback-classic/0.9.28/logback-classic-0.9.28-sources.jar"/>
<classpathentry kind="var" path="M2_REPO/ch/qos/logback/logback-core/0.9.28/logback-core-0.9.28.jar" sourcepath="M2_REPO/ch/qos/logback/logback-core/0.9.28/logback-core-0.9.28-sources.jar"/>
モデルの生成にはcom.asakusafw.dmdl.java.Mainを実行する。
これを実行する為の「実行の構成」は以下の通り。
タブ | 設定内容 | 備考 | |
---|---|---|---|
メイン | プロジェクト(P) | afw-driver |
自分のプロジェクト |
メイン・クラス(M) | com.asakusafw.dmdl.java.Main |
実行するクラス名 | |
引数 | プログラムの引数(A) | -output C:/cygwin/tmp/afw/ |
sourceで指定した場所にあるdmdlファイルを読み込み、 outputで指定した場所にJavaソースを出力する。 参考: DMDLユーザーガイド#DMDLコンパイラの実行 |
※普通、SPIを使う際はjarファイル化するのだが、今回はclassesがパスに含まれているのでjarファイルを作らなくても動作する。
〜 16:36:58.312 [main] DEBUG c.a.dmdl.analyzer.DmdlAnalyzer - Resolving attributes: word_count_model 16:36:58.312 [main] DEBUG c.a.dmdl.analyzer.DmdlAnalyzer - Resolving attributes: line_model 16:36:58.312 [main] INFO com.asakusafw.dmdl.java.GenerateTask - 2個のモデルからJavaデータモデルクラスを生成します 16:36:58.312 [main] INFO com.asakusafw.dmdl.java.GenerateTask - データモデルクラスを生成しています: word_count_model +++EmptyDriver#getTypeAnnotations RECORD word_count_model +++EmptyDriver#getInterfaces RECORD word_count_model +++EmptyDriver#getMemberAnnotations word_count_model.word : TEXT +++EmptyDriver#getMemberAnnotations word_count_model.count : INT +++EmptyDriver#getMethods RECORD word_count_model 16:36:58.890 [main] INFO com.asakusafw.dmdl.java.GenerateTask - データモデルクラスを生成しています: line_model +++EmptyDriver#getTypeAnnotations RECORD line_model +++EmptyDriver#getInterfaces RECORD line_model +++EmptyDriver#getMemberAnnotations line_model.text : TEXT +++EmptyDriver#getMethods RECORD line_model 16:36:58.984 [main] INFO com.asakusafw.dmdl.java.GenerateTask - データモデルクラスの生成が完了しました
dmdlファイル内のモデル毎にDriverが呼ばれている。
一番最初に呼ばれるメソッドはgetTypeAnnotations()らしい。
→Mavenコマンドによるモデル生成時に自分のドライバーを実行させる方法
では、とりあえず空っぽのJavaソースを生成してみる。
package jp.hishidama.asakusafw.dmdl;
import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.asakusafw.dmdl.java.emitter.EmitContext; import com.asakusafw.dmdl.java.spi.JavaDataModelDriver; import com.asakusafw.dmdl.semantics.ModelDeclaration; import com.ashigeru.lang.java.model.syntax.Annotation; import com.ashigeru.lang.java.model.syntax.Attribute; import com.ashigeru.lang.java.model.syntax.ClassDeclaration; import com.ashigeru.lang.java.model.syntax.Javadoc; import com.ashigeru.lang.java.model.syntax.ModelFactory; import com.ashigeru.lang.java.model.syntax.SimpleName; import com.ashigeru.lang.java.model.syntax.Type; import com.ashigeru.lang.java.model.syntax.TypeBodyDeclaration; import com.ashigeru.lang.java.model.syntax.TypeParameterDeclaration; import com.ashigeru.lang.java.model.util.AttributeBuilder; import com.ashigeru.lang.java.model.util.JavadocBuilder;
このソースは、ModelInputDriverを参考にしている。
なぜなら、Input(やOutput)はモデルとは別のソースファイルを生成しているから。
今回は新しいソースを生成したい。
/** * @see com.asakusafw.dmdl.java.emitter.driver.ModelInputDriver */ public class SimpleDriver extends JavaDataModelDriver {
@Override public List<Annotation> getTypeAnnotations(EmitContext context, ModelDeclaration model) throws IOException { System.out.println("+++SimpleDriver#getTypeAnnotations\t" + model); generate(context, model); return Collections.emptyList(); }
JavaDataModelDriverの中で最初に呼ばれるのがgetTypeAnnotations()なので、この中で新しいソースファイルを作るよう設定する。
private void generate(EmitContext context, ModelDeclaration model) throws IOException { String categoryName = "simple"; String typeNamePattern = "{0}Simple"; EmitContext next = new EmitContext(context.getSemantics(), context.getConfiguration(), model, categoryName, typeNamePattern); Generator.emit(next, model); }
ここで新しく生成するソースファイルを設定している。
categoryName(
カテゴリー名)は、パッケージ名の一部。
パッケージ名は「Mainの引数で指定されたパッケージ名」+「dmdl」+「カテゴリー名」になるので、今回は「sample.dmdl.simple
」になる。
typeNamePatternはクラス名のパターン。
「{0}」がモデル名に置換される。今回だと「{0}Simple」なので、「LineModelSimple」「WordCountModelSimple」というクラスが生成される。
実際にクラスの中身を定義するのがGenerator。
特にemit()で まずクラスそのものの定義を行っている。
private static class Generator { private final EmitContext context; private final ModelDeclaration model; private final ModelFactory f; private Generator(EmitContext context, ModelDeclaration model) { assert context != null; assert model != null; this.context = context; this.model = model; this.f = context.getModelFactory(); } static void emit(EmitContext context, ModelDeclaration model) throws IOException { assert context != null; assert model != null; Generator emitter = new Generator(context, model); emitter.emit(); } private void emit() throws IOException { Javadoc javadoc = new JavadocBuilder(f).text("とりあえず{0}のクラスを生成してみる。", model.getName()).toJavadoc(); List<Attribute> modifiers = new AttributeBuilder(f).Public().toAttributes(); SimpleName name = context.getTypeName(); List<TypeParameterDeclaration> typeParameters = Collections.emptyList(); Type superClass = null; List<Type> superInterfaceTypes = Collections.emptyList(); ClassDeclaration decl = f.newClassDeclaration( javadoc, //クラスのJavadoc。不要な場合はnull modifiers, //修飾子。publicやstatic・final等 name, //クラス名 typeParameters, //ジェネリクスの型引数。無い場合は空リスト superClass, //extendsしている親クラス。無い場合はnull superInterfaceTypes, //implementsしているインターフェース。無い場合は空リスト createMembers() //クラス内の定義(フィールドやメソッド)。無い場合は空リスト ); context.emit(decl); } // クラス内のフィールドやメソッドを定義する private List<TypeBodyDeclaration> createMembers() { List<TypeBodyDeclaration> results = new ArrayList<TypeBodyDeclaration>(); // results.add(createParserField()); // results.add(createConstructor()); // results.add(createReader()); // results.add(createCloser()); return results; } } }
基本的に、ModelFactoryの「newなんちゃら」メソッドを呼び出してJavaの構文を生成していく。
SPI用のservicesにSimpleDriverを追加しておく。
#jp.hishidama.asakusafw.dmdl.EmptyDriver jp.hishidama.asakusafw.dmdl.SimpleDriver
↓生成されたファイル
package sample.modelgen.
dmdl.simple;
/**
* とりあえずword_count_modelのクラスを生成してみる。
*/
public class WordCountModelSimple {
}
SimpleDriverにメソッド生成の処理を加えてみる。
出力したいメソッドはこんな感じ。
public void method1() { System.out.println("Hello"); }
まず、createMembers()からメソッド生成サブルーチンを呼ぶように修正する。
// クラス内のフィールドやメソッドを定義する private List<TypeBodyDeclaration> createMembers() { List<TypeBodyDeclaration> results = new ArrayList<TypeBodyDeclaration>(); results.add(createMethod1()); return results; }
メソッドの生成処理を記述する。
private TypeBodyDeclaration createMethod1() {
Javadoc javadoc = null;
List<Attribute> modifiers = new AttributeBuilder(f).Public().toAttributes();
List<TypeParameterDeclaration> typeParameters = Collections.emptyList();
Type returnType = context.resolve(void.class);
SimpleName name = f.newSimpleName("method1");
List<FormalParameterDeclaration> formalParameters = Collections.emptyList();
List<Type> exceptionTypes = Collections.emptyList();
return f.newMethodDeclaration(
javadoc,
modifiers, //修飾子
typeParameters, //メソッドの型引数
returnType, //戻り値の型
name, //メソッド名
formalParameters, //メソッドの引数
0, //メソッド名の後ろに配列定義の「[]」を付ける個数(そんな書き方出来るなんて初めて知ったよ…)
exceptionTypes, //例外(throws)
f.newBlock(createMethod1Body()) //メソッド本体
);
}
private List<Statement> createMethod1Body() { List<Statement> results = new ArrayList<Statement>(); results.add( new ExpressionBuilder(f, f.newSimpleName("System")) .field("out") .method("println", f.newLiteral("\"Hello\"")) .toStatement() ); return results; }
やれやれ、ちょっとしたものを書くだけでもえらく大変そうだ(苦笑)
package sample.modelgen.
dmdl.simple;
/**
* とりあえずword_count_modelのクラスを生成してみる。
*/
public class WordCountModelSimple {
public void method1() {
System.out.println("Hello");
}
}
インデントは入れてくれるのだが、空行は入れてくれない^^;
生成するJavaソースは、構文解析した状態を表すクラスを組み合わせて表現する。[2011-08-16]
構文要素を扱うクラス | 説明 | |
---|---|---|
f | ModelFactory | 構文インスタンスを生成する為のメソッドが揃っている。 |
context | EmitContext | 全体的な情報を保持している。 |
model | ModelDeclaration | 生成対象のDMDLのモデル名やプロパティー(生成対象のフィールド)の名前・データ型などを保持している。 |
Models | FQCNの識別子を生成するのに使える。 | |
ExpressionBuilder | 式(演算)を構築する。 ModelFactoryにも式を生成するメソッドはあるけれど、ExpressionBuilderの方が(長くなるけど)分かりやすい。 |
どんな風に構築していくのか、例をちょっと挙げてみる。
Java 構文 |
生成方法 | 備考 | 例 |
---|---|---|---|
識別子 | f.newSimpleName |
単純名を生成する。(変数名やクラス名など) | SimpleName name = f.newSimpleName("value"); |
Models.toName |
識別子を生成する。(単純名または修飾名) | Name name = Models.toName("sample.Sample"); |
|
型 | context.resolve |
型を生成する。 これによって指定したクラスは、必要に応じてインポート文が自動的に生成される。 (ただ、pack.C1.E1といった内部enumを指定して「E1.V1」という使い方をした場合に インポート文が生成されないっぽい?) |
Type t1 = context.resolve(String.class); |
リテラル | f.newLiteral |
リテラル(定数)を生成する。 | Literal l1 = f.newLiteral("123"); |
フィールド定義 | f.newFieldDeclaration |
フィールドを定義する。(初期化も定義出来る) | |
メソッド定義 | f.newMethodDeclaration |
メソッドを定義する。 | |
ローカル変数定義 | f.newLocalVariableDeclaration |
ローカル変数を定義する。(初期化も定義出来る) | |
変数への代入 | f.newAssignmentExpression |
代入式を定義する。 | |
インスタンス生成 | f.newClassInstanceCreation |
newでインスタンスを生成する。 | f.newClassInstanceCreationExpression( |
メソッド呼び出し | ExpressionBuilder#method |
メソッドを呼び出す。 | new ExpressionBuilder(f,
f.newSimpleName("bean")) |
フィールドアクセス | ExpressionBuilder#field |
フィールドを指定する。 | new ExpressionBuilder(f,
f.newSimpleName("bean")) |
.class | f.newClassLiteral |
「.class」を指定する。 classはJavaの予約語なので、ExpressionBuilder#fieldでは指定できない。 |
f.newClassLiteral(context.resolve(String.class)) |
this super |
f.newThis |
「this」や「super」という識別子を取得する。 thisやsuperはJavaの予約語なので、f.newSimpleName()では取得できない。 |
|
取得 内容 |
生成方法 | 備考 | 例 |
モデル名 | model.getName |
モデル名を取得する。 | model.getName() |
モデルクラス名 | model.getSymbol |
モデルのクラス・クラス名を取得する。[/2011-08-28] | Type t = context.resolve(model.getSymbol()); |
SimpleName name = context.getTypeName(); |
|||
QualifiedName name = |
|||
パッケージ名 | configuration.getBasePackage |
モデルのベースパッケージ名を取得する。[2011-08-28] | Name name =
context.getConfiguration().getBasePackage(); |
プロパティー一覧 | model.getDeclaredProperties |
プロパティー一覧を取得する。 | for (PropertyDeclaration property :
model.getDeclaredProperties()) { 〜 } |
プロパティー名 | context.getFieldName |
プロパティー名を取得する。 | SimpleName name = context.getFieldName(property); |
データ型 | property.getType |
プロパティーのデータ型を取得する。 ただしこの型はDMDLの型であり、Java構文のTypeではない。 Javaのプリミティブ型およびラッパークラスを返してくれるものがあると便利なのだが。 |
|
ゲッター | context.getValueGetterName |
プロパティーのゲッターメソッド名を取得する。 TEXT型の場合のAsStringが付いたメソッドを直接取得する方法は無いようだ。 |
SimpleName getter =
context.getValueGetterName(property); |
セッター | context.getValueSetterName |
プロパティーのセッターメソッド名を取得する。 | SimpleName setter =
context.getValueSetterName(property); |
→HiveのSerDe・PigのStorageを生成するドライバーでjarファイルを公開しているが、その中にソースも含まれているので、参考になるかも。
これらのクラスはとてもよく出来ている。よくここまで網羅して作り込んだものだと、本当に感心する。[2011-08-16]
(ModelFactoryのメソッドを見れば、Javaで出来る構文が全て分かるのではないかと思う)
使用したクラスのインポート文が自動生成されるとか、面白い。
しかし、HiveのSerDe・PigのStorageを生成するドライバーを作ってみてよく分かったが、これを使って構築していくのは人間のやることじゃない^^;
手でやるのは生産性が悪すぎる。特に保守性は壊滅的だろう。
生成するJavaソースのほとんどは固定された文なので、ベタのテキストを出力するように(つまり、素のServletでHTMLを出力する感じに)なっている方が絶対楽。
(改行まで扱いたいので、Scalaの"""が使えると便利だなー)
実際、DMDLから各種ツール用の定義が作れると色々幅が広がると思う。
そしてその定義の記述方法はJavaとは限らないわけで、やはりモデルやプロパティーの情報提供機能だけで充分なような。
ドライバーを記述する人は、それらの情報だけ受け取って、ファイルへの出力はWriterとかPrintStreamで行えばいい。
つーか、getTypeAnnotations()が呼ばれた時点で自分でファイルをオープンして書き込んじゃえばいいのか?!(爆)
追記 [2011-08-17]
ベタのテキストで…というのは、新しいファイルを作ることを考えたときのやり方。
AsakusaFWではモデルクラス(Java)の生成自体を複数のドライバーで分担して処理している。
もしこれらの生成中に別々のクラスをインポートする必要があったら、インポート文は先に書く必要があるし、重複して同じクラスをインポートする事も出来ないので
どこかで集中管理する必要がある。
そういった事を踏まえて、AsakusaFWのデータモデルドライバーはこういう構文クラスを使う方式を採っているのかもしれない。
でもやっぱり“やりすぎ”感が否めない…^^;
先の例では、ドライバーを起動するMainクラスを直接実行して独自ドライバーを呼び出した。[2011-08-28]
通常、DMDLからモデルクラスを生成するにはMavenのコマンドでgenerate-resources等を実行する。
このときに自分のドライバーも実行させるには、Mavenに自分のドライバーを登録する。
これで、モデルクラスの生成を行ったときに自分のドライバーも呼ばれるようになる。
しかしMavenのリポジトリーをいじるのはちょっと面倒。
モデル生成の実行にはAntが使われているので、build.xmlを修正する方が簡単(というか、自分には馴染みがあるw)。
<java classname="com.asakusafw.dmdl.java.Main" classpath="${compile_classpath}" fork="true" failonerror="true"> 〜 <arg value="-plugin" /> <arg value="C:\workspace\sample-driver\classes" /> <!-- <arg value="C:\workspace\sample-driver\bin\sample-driver.jar" /> --> </java>
Mainクラスは「-plugin」オプションでドライバーを指定できるので、その指定を追加する。パスはjarファイルでもclassesディレクトリーでもよい。
パス区切り文字(Windowsではセミコロン「;
」、UNIXではコロン「:
」)で区切って複数のパスを指定できるようだ。
なお、独自のパラメーター(引数)を渡したい場合、Mainが使用する以外の引数を保持してくれないので自分のドライバーまで値が回ってこない。
今のところはシステムプロパティー「<vmarg value="-Dプロパティー=値" />
」を使うしか無さそう。