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
データモデルドライバーの実行方法と同じ。