S-JIS[2011-08-15/2015-07-27] 変更履歴

Asakusa Frameworkデータモデルドライバー

Asakusa Framework0.7.3の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の実験

まずはSPIで自分のDriverが呼び出せる状態にする必要がある。


プロジェクトの作成

最初に、Driverを記述する為のEclipseのプロジェクトを新たに作成する。[2015-07-19]

  1. ワークスペースのディレクトリーの下にプロジェクト名(今回はafw-driver)のディレクトリーを作成する。
  2. その下にbuild.gradleを新規に用意する。以下のような感じ。
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'maven'
    
    defaultTasks 'jar'
    
    group = 'com.example.asakusafw.dmdl'
    version = '0.1-SNAPSHOT'
    
    sourceCompatibility = 1.7
    targetCompatibility = 1.7
    
    def defaultEncoding = 'UTF-8'
    [compileJava, compileTestJava]*.options*.encoding = defaultEncoding
    
    repositories {
        mavenCentral()
        maven { url 'http://asakusafw.s3.amazonaws.com/maven/releases' }
    }
    
    dependencies {
        compile group: 'com.asakusafw', name: 'asakusa-dmdl-java', version: "0.7.3-hadoop1"
        runtime group: 'org.apache.hadoop', name: 'hadoop-core', version: "1.2.1"
    
        testCompile group: 'ch.qos.logback', name: 'logback-classic', version: "1.1.3"
    }
    
    task wrapper(type: Wrapper) {
        gradleVersion '2.2.1'
        jarFile file('.buildtools/gradlew.jar')
    }
    
    eclipse.classpath.file {
        whenMerged { classpath ->
            classpath.entries.findAll { entry -> entry.kind == 'output' }*.path = 'classes'
        }
    }
  3. Gradleコマンドを実行して、Eclipseプロジェクトを生成する。
    $ gradle wrapper
    $ mkdir -p src/main/java
    $ mkdir -p src/main/resources
    $ ./gradle eclipse
  4. 生成されたプロジェクトをEclipseにインポートする。

テスト時のログ出力をコンソールに出力する為、以下のファイルを用意しておく。[2015-07-27]

src/test/resources/logback.xml:

<configuration>
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<target>System.out</target>
		<encoder>
			<pattern>%d{yyyy/MM/dd HH:mm:ss} %-5level %msg%n</pattern>
		</encoder>
	</appender>

	<root>
		<level value="INFO" />
		<appender-ref ref="STDOUT" />
	</root>
</configuration>

Driverクラスの作成

まずは空っぽのDriverを作成してみる。

afw-driver/src/main/java/jp/hishidama/asakusafw/dmdl/EmptyDriver.java:

package jp.hishidama.asakusafw.dmdl;
import java.io.IOException;

import com.asakusafw.dmdl.java.emitter.EmitContext;
import com.asakusafw.dmdl.java.spi.JavaDataModelDriver;
import com.asakusafw.dmdl.semantics.ModelDeclaration;
public class EmptyDriver extends JavaDataModelDriver {
	@Override
	public void generateResources(EmitContext context, ModelDeclaration model) throws IOException {
		System.out.println("+++EmptyDriver#generateResources\t" + model);
	}
}

JavaDataModelDriverにはアノテーション定義用に呼ばれるメソッドやフィールド定義用に呼ばれるメソッド・メソッド定義用に呼ばれるメソッド等があるが、必ず1回だけ呼ばれるメソッドとしてgenerateResourcesメソッドが用意されている。[/2015-07-25]
どんなものが引数で渡されるのか、とりあえずコンソールに表示してみる。


SPIのファイルの作成

SPIを使う際は、META-INF/servicesというディレクトリーの下に親クラスの名前をしたテキストファイルを作成し、その中に具象クラスを記述する。

afw-driver/src/main/resources/META-INF/services/com.asakusafw.dmdl.java.spi.JavaDataModelDriver:

jp.hishidama.asakusafw.dmdl.EmptyDriver

実行方法

モデルの生成にはcom.asakusafw.dmdl.java.Mainを実行する。
これを実行する為の「実行の構成」は以下の通り。

タブ 設定内容 備考
メイン プロジェクト(P) afw-driver 自分のプロジェクト
メイン・クラス(M) com.asakusafw.dmdl.java.Main 実行するクラス名
引数 プログラムの引数(A) -output C:/cygwin/tmp/afw/
-package com.example.modelgen
-source ../afw-wordcount/src/main/dmdl
sourceで指定した場所にあるdmdlファイルを読み込み、
outputで指定した場所にJavaソースを出力する。
参考: DMDLユーザーガイド#DMDLコンパイラの実行

※普通、SPIを使う際はjarファイル化するのだが、今回はsrc/main/resourcesにSPIのファイルを入れてある(厳密には、それがclassesディレクトリーに移送され、classesがクラスパスに含まれている)のでjarファイルを作らなくても動作する。[/2015-07-19]

実行結果:

〜
+++EmptyDriver#generateResources	RECORD word_count_model
+++EmptyDriver#generateResources	RECORD line_model

dmdlファイル内のモデル毎にDriverが呼ばれている。

Gradleコマンドによるモデル生成時に自分のドライバーを実行させる方法


com.asakusafw.utils.java.modelを使ったJavaソースの生成

空っぽのJavaソースを生成してみる。

afw-driver/src/jp/hishidama/asakusafw/dmdl/SimpleDriver.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.asakusafw.utils.java.model.syntax.Attribute;
import com.asakusafw.utils.java.model.syntax.ClassDeclaration;
import com.asakusafw.utils.java.model.syntax.Javadoc;
import com.asakusafw.utils.java.model.syntax.ModelFactory;
import com.asakusafw.utils.java.model.syntax.SimpleName;
import com.asakusafw.utils.java.model.syntax.Type;
import com.asakusafw.utils.java.model.syntax.TypeBodyDeclaration;
import com.asakusafw.utils.java.model.syntax.TypeParameterDeclaration;
import com.asakusafw.utils.java.model.util.AttributeBuilder;
import com.asakusafw.utils.java.model.util.JavadocBuilder;

0.2.1の頃com.ashigeru.langパッケージを使っていたが、いつの間にかcom.asakusafw.utilsに替わっていた。[2015-07-19]


このソースは、ModelInputDriverを参考にしている。
なぜなら、Input(やOutput)はモデルとは別のソースファイルを生成しているから。
今回は新しいソースを生成したい。

/**
 * @see com.asakusafw.dmdl.java.emitter.driver.ModelInputDriver
 */
public class SimpleDriver extends JavaDataModelDriver {

	@Override
	public void generateResources(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」+「カテゴリー名」になるので、今回は「com.example.modelgen.dmdl.simple」になる。

typeNamePatternはクラス名のパターン。
「{0}」がモデル名に置換される。今回だと「{0}Simple」なので、「LineModelSimple」「WordCountModelSimple」というクラスが生成される。


実際にクラスの中身を定義するのがGenerator

Javaの文法要素を表すクラス群が用意されており、それを使ってクラス(の抽象構文木)を生成する。[2015-07-19]
各文法要素はModelFactoryを使って生成し、最終的にそれを出力する。


実行方法

SPI用のservicesSimpleDriverを追加しておく。

afw-driver/src/main/resources/META-INF/services/com.asakusafw.dmdl.java.spi.JavaDataModelDriver:

#jp.hishidama.asakusafw.dmdl.EmptyDriver
jp.hishidama.asakusafw.dmdl.SimpleDriver

↓生成されたファイル

C:/cygwin/tmp/afw/com/example/modelgen/dmdl/simple/WordCountModelSimple.java:

package com.example.modelgen.dmdl.simple;
/**
 * とりあえずword_count_modelのクラスを生成してみる。
 */
public class WordCountModelSimple {
}

PrintWriterを使ったJavaソースの生成

java.modelを使うソース生成方法だと、生成したいクラスの構文に沿ってインスタンスを色々用意しないといけないので、けっこう面倒。[2015-07-20]
(抽象構文木を直接扱うのが慣れている人にとっては便利なんだろうけど^^;)

そこで、(無理矢理かもしれないが)PrintWriterを使ってソースを生成してみる。

afw-driver/src/jp/hishidama/asakusafw/dmdl/SimpleDriver.java:

package jp.hishidama.asakusafw.dmdl;
import java.io.IOException;
import java.io.PrintWriter;

import com.asakusafw.dmdl.java.Configuration;
import com.asakusafw.dmdl.java.emitter.EmitContext;
import com.asakusafw.dmdl.java.spi.JavaDataModelDriver;
import com.asakusafw.dmdl.semantics.ModelDeclaration;
import com.asakusafw.utils.java.model.syntax.ModelFactory;
import com.asakusafw.utils.java.model.syntax.PackageDeclaration;
import com.asakusafw.utils.java.model.util.Emitter;
/**
 * @see com.asakusafw.dmdl.java.emitter.driver.ModelInputDriver
 */
public class SimpleDriver extends JavaDataModelDriver {
	@Override
	public void generateResources(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);
	}

↑ここまでは、java.modelを使う方法と同じ。

	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 {
			PackageDeclaration packageName = f.newPackageDeclaration(context.getQualifiedTypeName().getQualifier());
			String fileName = context.getTypeName().getToken() + ".java";

			Configuration config = context.getConfiguration();
			Emitter emitter = config.getOutput();
			try (PrintWriter writer = emitter.openFor(packageName, fileName)) {
				writer.printf("package %s;%n%n", packageName.getName().toNameString());

				writer.println("/**");
				writer.printf(" * %sのクラスを生成してみる。%n", model.getName().identifier);
				writer.println(" */");
				writer.printf("public class %s {%n", context.getTypeName().toNameString());
				writer.println("}");
			}
		}
	}
}

openForメソッドでPrintWriterを取得できる。
この出力先のルートパスは、DMDLのコンパイルを実行するMainクラスの-outputで指定したディレクトリーになる。

※属性の値を取得したい場合は、model.getTraitメソッドを使用する。[2015-07-25]
→独自の属性を使用するには、属性ドライバーを作成する。


実行方法は、java.modelの場合の実行方法と同じ。


リソース生成時に実行する方法

先の例では、ドライバーを起動するMainクラスを直接実行して独自ドライバーを呼び出した。[2011-08-28]

通常、DMDLからモデルクラスを生成するにはGradleのコマンドでcompileDMDL等を実行する。[/2015-07-19]
このときに自分のドライバーを実行させるには、Asakusaプロジェクトの依存関係に自分のドライバーを追加する。

  1. 自分のドライバーをjarファイル化し、Mavenのローカルリポジトリーに登録する。
    $ ./gradlew install
  2. AsakusaFWのプロジェクトのbuild.gradleの依存関係に自分のドライバーを追加する。[/2015-07-21]
    repositories {
        mavenLocal() // Mavenローカルリポジトリーを使用する
    }
    
    dependencies {
    〜
        compile group: 'com.example.asakusafw.dmdl', name: 'afw-driver', version: '0.1-SNAPSHOT'
    }
  3. (ドライバーを修正したら、jarファイル化とローカルリポジトリーの登録をやり直す)

これで、モデルクラスの生成を行ったときに自分のドライバーも呼ばれるようになる。


または、Mavenローカルリポジトリーを使用せず、生成されたjarファイルを直接指定することも出来る。[2015-07-21]

  1. 自分のドライバーをjarファイル化する。
    $ ./gradlew jar
    これで、ワークスペースのプロジェクトディレクトリーのbuild/libsの下にjarファイル(afw-driver-0.1-SNAPSHOT.jar)が作られる。
  2. AsakusaFWのプロジェクトのbuild.gradleの依存関係に自分のドライバーを追加する。
    dependencies {
    〜
        compile files('D:/workspace/afw-driver/build/libs/afw-driver-0.1-SNAPSHOT.jar')
    }
  3. (ドライバーを修正したら、jarファイル化をやり直す)

AsakusaFW目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま