Asakusa Framework0.7.3のAttributeDriverのメモ。
|
|
Asakusa Frameworkでは、DMDL(dmdlファイル)でデータモデルの記述に属性(@hogeのような名前)を付けることが出来る。
属性は、com.asakusafw.dmdl.spi.AttributeDriverを継承したクラス(実際には、データモデルに付ける属性はModelAttributeDriver、プロパティーに付ける属性はPropertyAttributeDriverを継承する)で管理されている。
これらのドライバーはJavaのサービスプロバイダー(SPI)の仕組みを使って呼ばれている。
したがって、自分でドライバーを作ってSPIに入れれば、独自のクラス(Javaソース)を生成することが出来る。
(データモデルドライバーと同様)
属性の管理(属性名の指定や値の取得)はAttributeDriver(ModelAttributeDriver・PropertyAttributeDriver)を継承したクラスで行う。
取得した属性の値は、Traitというクラスを用意し、そこで保持する。
データモデルからファイルを生成するJavaDataModelDriverで、Traitを指定して情報を取得し、ファイル生成に利用する。
以下のような属性が記述できる属性ドライバーを作ってみる。
@example.model(name = "abc")
model1 = {
@example.field(value = "zzz")
value1 : TEXT;
};
属性ドライバーを作成するJavaプロジェクトは、データモデルドライバーのプロジェクトと同じ。
最初に、属性に指定された値を保持するTraitクラスを作っておく。
まずは、モデルに付ける「@example.model」のTrait。
package jp.hishidama.asakusafw.dmdl.attribute; import com.asakusafw.dmdl.model.AstNode; import com.asakusafw.dmdl.semantics.Trait;
//@see com.asakusafw.dmdl.directio.csv.driver.CsvFormatTrait
public class ExampleModelTrait implements Trait<ExampleModelTrait> {
private final AstNode originalAst;
private final ExampleModelConfiguration configuration;
public ExampleModelTrait(AstNode originalAst, ExampleModelConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException("configuration must not be null"); //$NON-NLS-1$
}
this.originalAst = originalAst;
this.configuration = configuration;
}
@Override
public AstNode getOriginalAst() {
return originalAst;
}
public ExampleModelConfiguration getConfiguration() {
return configuration;
}
public static class ExampleModelConfiguration {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
originalAstは、データモデルを解析した結果の抽象構文木。
エラーがあった場合のエラー箇所を指定するのに使ったりするようだ。
configurationは、属性の値を保持するもの。(値を保持しない属性なら、不要)
このクラスは自分で用意する。単なるJavaBeansでよい。
こちらは、プロパティーに付ける「@example.field」のTrait。
package jp.hishidama.asakusafw.dmdl.attribute; import com.asakusafw.dmdl.model.AstNode; import com.asakusafw.dmdl.semantics.Trait;
//@see com.asakusafw.dmdl.directio.csv.driver.CsvFieldTrait
public class ExampleFieldTrait implements Trait<ExampleFieldTrait> {
private final AstNode originalAst;
private final ExampleFieldConfiguration configuration;
public ExampleFieldTrait(AstNode originalAst, ExampleFieldConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException("configuration must not be null"); //$NON-NLS-1$
}
this.originalAst = originalAst;
this.configuration = configuration;
}
@Override
public AstNode getOriginalAst() {
return originalAst;
}
public ExampleFieldConfiguration getConfiguration() {
return configuration;
}
public static class ExampleFieldConfiguration {
private String value;
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}
※使用する要素が少ない場合は、わざわざConfigurationクラスを用意せず、直接Trait内に保持してしまっても良い。
モデルに付ける「@example.model」を解析するドライバーを作成する。
package jp.hishidama.asakusafw.dmdl.attribute; import java.util.Map; import jp.hishidama.asakusafw.dmdl.attribute.ExampleModelTrait.ExampleModelConfiguration; import com.asakusafw.dmdl.Diagnostic; import com.asakusafw.dmdl.Diagnostic.Level; import com.asakusafw.dmdl.model.AstAttribute; import com.asakusafw.dmdl.model.AstAttributeElement; import com.asakusafw.dmdl.model.AstLiteral; import com.asakusafw.dmdl.model.LiteralKind; import com.asakusafw.dmdl.semantics.DmdlSemantics; import com.asakusafw.dmdl.semantics.ModelDeclaration; import com.asakusafw.dmdl.spi.ModelAttributeDriver; import com.asakusafw.dmdl.util.AttributeUtil;
//@see com.asakusafw.dmdl.directio.csv.driver.CsvFormatDriver
public class ExampleModelDriver extends ModelAttributeDriver {
/**
* The attribute name.
*/
public static final String TARGET_NAME = "example.model"; //$NON-NLS-1$
/**
* The element name of {@value}.
*/
public static final String ELEMENT_NAME = "name"; //$NON-NLS-1$
@Override
public String getTargetName() {
return TARGET_NAME;
}
getTargetNameメソッドで、属性の名前(今回の例では、「example.model」)を返す。
@Override
public void process(DmdlSemantics environment, ModelDeclaration declaration, AstAttribute attribute) {
Map<String, AstAttributeElement> elements = AttributeUtil.getElementMap(attribute);
ExampleModelConfiguration conf = analyzeConfig(environment, attribute, elements);
if (conf != null) {
declaration.putTrait(ExampleModelTrait.class, new ExampleModelTrait(attribute, conf));
}
}
processメソッドで属性値の取得・チェックを行い、Traitに登録する。
取得・チェック処理本体はanalyzeConfigメソッドで行っている。
private ExampleModelConfiguration analyzeConfig(DmdlSemantics environment, AstAttribute attribute, Map<String, AstAttributeElement> elements) {
AstLiteral name = take(environment, elements, ELEMENT_NAME, LiteralKind.STRING);
// 仕様外の要素名をエラーとして出力する
environment.reportAll(AttributeUtil.reportInvalidElements(attribute, elements.values()));
ExampleModelConfiguration result = new ExampleModelConfiguration();
if (name != null && checkNotEmpty(environment, ELEMENT_NAME, name)) {
result.setName(name.toStringValue());
}
return result;
}
takeメソッドで値を取得するようになっている。
想定していない要素名があったら、エラーにしている。
ついでに要素の値が空かどうかのチェックもしているが、これが必要かどうかは自分が作る属性の仕様次第。
private boolean checkNotEmpty(DmdlSemantics environment, String name, AstLiteral stringLiteral) {
assert environment != null;
assert name != null;
assert stringLiteral != null;
assert stringLiteral.kind == LiteralKind.STRING;
if (stringLiteral.toStringValue().isEmpty()) {
environment.report(new Diagnostic(Level.ERROR, stringLiteral, "@{0}({1}) must not be empty", TARGET_NAME, name));
return false;
}
return true;
}
private AstLiteral take(DmdlSemantics environment, Map<String, AstAttributeElement> elements, String elementName, LiteralKind kind) {
assert environment != null;
assert elements != null;
assert elementName != null;
assert kind != null;
AstAttributeElement element = elements.remove(elementName);
if (element == null) {
return null;
}
if (!(element.value instanceof AstLiteral)) {
environment.report(new Diagnostic(Level.ERROR, element, "@{0}({1}) must be a {2} literal", TARGET_NAME, elementName, kind));
return null;
}
AstLiteral literal = (AstLiteral) element.value;
if (literal.kind != kind) {
environment.report(new Diagnostic(Level.ERROR, element, "@{0}({1}) must be a {2} literal", TARGET_NAME, elementName, kind));
return null;
}
return literal;
}
}
checkNotEmptyやtakeメソッドはどの属性でも使う機能なのでユーティリティーで用意しておいて欲しい気もするが、エラーメッセージが属性によって変わるので、共通では用意していないのかもしれない。
プロパティーに付ける「@example.field」を解析するドライバーを作成する。
package jp.hishidama.asakusafw.dmdl.attribute; import java.util.Map; import jp.hishidama.asakusafw.dmdl.attribute.ExampleFieldTrait.ExampleFieldConfiguration; import com.asakusafw.dmdl.model.AstAttribute; import com.asakusafw.dmdl.model.AstAttributeElement; import com.asakusafw.dmdl.semantics.DmdlSemantics; import com.asakusafw.dmdl.semantics.PropertyDeclaration; import com.asakusafw.dmdl.spi.PropertyAttributeDriver; import com.asakusafw.dmdl.util.AttributeUtil;
//@see com.asakusafw.dmdl.directio.csv.driver.CsvFieldDriver
public class ExampleFieldDriver extends PropertyAttributeDriver {
/**
* The attribute name.
*/
public static final String TARGET_NAME = "example.field"; //$NON-NLS-1$
/**
* The element name of {@value}.
*/
public static final String ELEMENT_VALUE = "value"; //$NON-NLS-1$
@Override
public String getTargetName() {
return TARGET_NAME;
}
getTargetNameメソッドで、属性名(今回の例では、「example.field」)を返す。
@Override
public void process(DmdlSemantics environment, PropertyDeclaration declaration, AstAttribute attribute) {
Map<String, AstAttributeElement> elements = AttributeUtil.getElementMap(attribute);
ExampleFieldConfiguration conf = analyzeConfig(environment, attribute, elements);
if (conf != null) {
declaration.putTrait(ExampleFieldTrait.class, new ExampleFieldTrait(attribute, conf));
}
}
private ExampleFieldConfiguration analyzeConfig(DmdlSemantics environment, AstAttribute attribute, Map<String, AstAttributeElement> elements) {
String value = AttributeUtil.takeString(environment, attribute, elements, ELEMENT_VALUE, true);
// 仕様外の要素名をエラーとして出力する
environment.reportAll(AttributeUtil.reportInvalidElements(attribute, elements.values()));
ExampleFieldConfiguration result = new ExampleFieldConfiguration();
result.setValue(value);
return result;
}
}
AttributeUtilに要素の値を取り出すtakeStringメソッドがあるので、それを使っている。
最後の引数のboolean mandatoryをtrueにしておくと、必須チェック(要素が指定されていなかったらエラーにする)を行ってくれる。
データモデルドライバーで属性を取得し、使用する。
package jp.hishidama.asakusafw.dmdl.attribute; import java.io.IOException; import jp.hishidama.asakusafw.dmdl.attribute.ExampleModelTrait.ExampleModelConfiguration; 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;
//@see com.asakusafw.dmdl.directio.csv.driver.CsvFormatEmitter
public class ExampleEmitter extends JavaDataModelDriver {
@Override
public void generateResources(EmitContext context, ModelDeclaration model) throws IOException {
ExampleModelTrait modelTrait = model.getTrait(ExampleModelTrait.class);
if (modelTrait == null) {
return;
}
ExampleModelConfiguration config = modelTrait.getConfiguration();
System.out.printf("+++example.model(name = %s)%n", config.getName());
for (PropertyDeclaration property : model.getDeclaredProperties()) {
ExampleFieldTrait trait = property.getTrait(ExampleFieldTrait.class);
String value = (trait != null) ? trait.getConfiguration().getValue() : null;
System.out.printf("%s's value=%s%n", property.getName(), value);
}
}
}
ドライバーを実行させるために、SPIのservicesのファイルを用意する。
jp.hishidama.asakusafw.dmdl.attribute.ExampleModelDriver jp.hishidama.asakusafw.dmdl.attribute.ExampleFieldDriver
jp.hishidama.asakusafw.dmdl.attribute.ExampleEmitter
データモデルドライバーの実行方法と同じ。