関数型のテンプレートエンジンを作ってみる(0.3) - 条件分岐・繰り返しの導入
前回追加したdefineタグ、insertタグに加えて、今回は条件分岐タグ(ifタグ)と繰り返しタグ(foreachタグ)を導入する。
ifタグは以下のように、条件が満たされた時にその内部がレンダリングされる。
// 会員のみメッセージを表示 {if condition="customer.isMember"} ようこそ、会員{insert value="customer.firstName"/} {/if}
foreachタグは以下のように、itemsで渡されたCollectionのそれぞれについて、varで指定したシンボルに要素を設定し、内部をレンダリングする。
// customersのfirstNameをカンマ区切りで出力 {foreach items="customers" var=customer} {insert value="customer.firstName"/}, {/foreach}
タグ属性のExpression(「"」で囲われた部分)には、ELを書けるものとする。ELには、今回MVELを使ってみた。*1
前回からの変更点: Contextに値を入れる
まず大きな変更点としては、今までTemplateしか入れられなかったContextに、リストやboolean値など一般的な値を入れられるようにする。これはforeachでコレクションを取ったり、ifでboolean値を取ったりするのに必要。
値を直に入れるのではなく、Valueという型で包むようにする。また、ValueとTemplateの共通の親としてAssociableというインタフェースを定義する。
public interface Associable<V, T extends Template<V, T>> { /** @return Templateならtrueを、Valueならfalseを戻す。*/ boolean isTemplate(); /** @return テンプレートとしての値を戻す */ T asTemplate(); /** @return Valueとしての値を戻す */ Value<V, T> asValue(); }
public interface Value<V, T extends Template<V, T>> extends Associable<V, T> { V value(); }
TemplateインタフェースもAssociableをextendsするように変更する。
public interface Template<V, T extends Template<V, T>> extends Associable<V, T> { TemplatePair<V, T> apply(Context<V, T> context); boolean isReducible(); }
Contextのget()は、今までのようにTemplateを返すのではなく、Associableを返すようにする。
public interface Context<V, T extends Template<V, T>> { Associable<V, T> get(Symbol name); }
これに伴いContextに関するユーティリティクラスの実装も変わるけど省略。
ついでに、Template、Valueの実装クラスを用意する。
Valueの実装クラスはSimpleValueとする。単にObject型の値を保持する。
Templateとして評価された場合はSimpleLiteralを戻す。
public class SimpleValue implements Value<Object, SimpleTemplate> { private final Object value; public SimpleValue(Object value) { this.value = value; } @Override public Object value() { return value; } @Override public boolean isTemplate() { return false; } @Override public Value<Object, SimpleTemplate> asValue() { return this; } @Override public SimpleTemplate asTemplate() { return new SimpleLiteral(value); } }
Templateの実装クラスはAbstractSimpleTemplateとし、他のSimpleXX系のTemplate実装クラスはこれを継承する。
Valueとして評価された場合はthisをSimpleValueで包んで戻す。
public abstract class AbstractSimpleTemplate implements SimpleTemplate { @Override public boolean isTemplate() { return true; } @Override public Value<Object, SimpleTemplate> asValue() { return new SimpleValue(this); } @Override public SimpleTemplate asTemplate() { return this; } }
asTemplate()/asValue()は単にキャストではなくて、相互に値の変換になっている。
前回SimpleSourceインタフェース(タグの属性値の評価方法を切り替える為のインタフェース)は属性値の内容を取得するのにgetTemplate()というメソッドを定義していたが、これをgetAssociable()という名前に変更し、戻り値もTemplateではなくAssociableに変更する。
public interface SimpleSource { SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context); }
↓↓↓
public interface SimpleSource { Associable<Object, SimpleTemplate> getAssociable(Context<Object, SimpleTemplate> context); }
SimpleSourceの実装クラスや使用しているテンプレートのクラスも変わるけど省略。
前回からの変更点: ELを扱えるようにする。
前回は、タグの属性値で""を使った場合、SimpleExpressionというSimpleSourceの実装クラスを生成していた。
このSimpleExpressionの内容取得メソッドを変更し、「"で囲われた部分をリテラルTemplateとして取り出す」から「"で囲われた部分をELとして評価し、その値を取り出す」に変更する。
public class SimpleExpression implements SimpleSource { private final String expression; public SimpleExpression(String expression) { this.text = expression; } @Override public SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context) { return new SimpleLiteral(expression); } }
↓↓↓
public class SimpleExpression implements SimpleSource { private final String expression; public SimpleExpression(String expression) { this.expression = expression; } // 内容をcontextコンテキストでMVELとして評価し、値がAssociable型でなければSimpleValueで包んで戻す。 // 内容が評価できない場合はnullを戻す。 @Override public Associable<Object, SimpleTemplate> getAssociable(Context<Object, SimpleTemplate> context) { try { Object value = evaluate(context); if (!(value instanceof Associable)) { value = new SimpleValue(value); } return (Associable<Object, SimpleTemplate>) value; } catch (PropertyAccessException e) { return null; } } private Object evaluate(Context<Object, SimpleTemplate> context) { return MVEL.eval(expression, new HotplateVariableResolverFactory(context)); }
HotplateVariableResolverFactoryはContextをMVELのコンテキストに読み替える為のクラスとして用意した。
実装方法が合ってるかはあまり良くわからないけど、とりあえずgetVariableResolver(String)でコンテキストの中身を戻すようにした。
public static class HotplateVariableResolverFactory extends BaseVariableResolverFactory { private static final long serialVersionUID = 1L; private Context<Object, SimpleTemplate> context; public HotplateVariableResolverFactory(Context<Object, SimpleTemplate> context) { this.context = context; } @Override public VariableResolver getVariableResolver(String name) { Associable<Object, SimpleTemplate> value = context.get(Symbol.of(name)); if (value != null) { return new SimpleValueResolver(value.asValue().value()); } else { return null; } } @Override public boolean isTarget(String name) { return context.get(Symbol.of(name)) != null; } @Override public boolean isResolveable(String name) { return context.get(Symbol.of(name)) != null; } @Override public VariableResolver createVariable(String name, Object value) { throw new IllegalStateException(); } @Override public VariableResolver createVariable(String name, Object value, Class<?> type) { throw new IllegalStateException(); } } }
この変更に伴って、既存のタグの仕様が少し変わる。
Expressionで文字列リテラルを表したい場合には、""で囲むだけではなく、中でさらに''で囲む必要がある
{define name=firstName value="花子" /}
↓↓↓
{define name=firstName value="'花子'" /}
Ifタグの定義
実行時にconditionタグの値を評価し、trueならタグの中身を評価して戻すようなタグを追加する。trueで無ければSimpleNopを戻す。
パース時のTagHandler、ProcessorPrototypeと実行時のProcessorを定義する。
public class IfTagHandler implements TagHandler { @Override public String[] tagNames() { return new String[] {"if"}; } @Override public boolean requireSingleTag(String tagName) { return false; // シングルタグは取らない } @Override public boolean requireContainerTag(String tagName) { return true; } @Override public SimpleTemplatePrototype handleSingleTag(String tagName, List<Attribute> attributes) { throw new IllegalStateException("If cannot use as a single tag."); } @Override public SimpleTemplatePrototype handleContainerTag(String tagName, List<Attribute> attributes, List<SimpleTemplatePrototype> elements) { // conditionタグの値を取得 Attribute conditionAttribute = Attribute.findAttribute("condition", attributes); return new SimpleIfProcessorPrototype( TagHandlerUtils.makeSource(conditionAttribute), new SimpleContainerPrototype(elements)); } }
public class SimpleIfProcessorPrototype implements SimpleTemplatePrototype { private final SimpleSource condition; private final SimpleTemplatePrototype contents; public SimpleIfProcessorPrototype(SimpleSource condition, SimpleTemplatePrototype contents) { super(); this.condition = condition; this.contents = contents; } public SimpleTemplate instantiate(Context<Object, SimpleTemplate> lexicalContext) { return new SimpleIfProcessor( lexicalContext, condition, contents.instantiate(lexicalContext)); } }
public class SimpleIfProcessor extends AbstractSimpleTemplate implements SimpleTemplate { private final Context<Object, SimpleTemplate> lexicalContext; private final SimpleSource condition; private final SimpleTemplate content; public SimpleIfProcessor(Context<Object, SimpleTemplate>lexicalContext, SimpleSource condition, SimpleTemplate contents) { super(); this.lexicalContext = lexicalContext; this.condition = condition; this.content = content; } @Override public TemplatePair<Object, SimpleTemplate> apply(final Context<Object, SimpleTemplate> context) { Context<Object, SimpleTemplate> merged = ContextUtils.merge(context, lexicalContext); // conditionの値を取得 Associable<Object, SimpleTemplate> conditionAssociable = this.condition.getAssociable(merged); if (conditionAssociable == null) { // 定義が見つからない場合は自分自身を戻す=未評価 return TemplatePair.<Object, SimpleTemplate>pairOf(this); } Object conditionValue = conditionAssociable.asValue().value(); if (conditionValue != null && conditionValue.equals(true)) { // conditionValueがtrueならばcontentを評価して戻す。 return TemplatePair.pairOf(content.apply(context).template()); } else { // conditionValueがfalseならばSimpleNopを戻す。contentは捨てる。 return TemplatePair.<Object, SimpleTemplate>pairOf(new SimpleNop()); } } @Override public boolean isReducible() { return true; } @Override public String getString() { throw new IllegalStateException("Unevaluated if:" + this); } }
Foreachタグの定義
実行時にitemsタグの値を評価し、コレクションならその要素の一つずつについて、varで指定されたシンボルで要素をcontextに追加してタグの内容を評価し、評価結果を連結して戻すようなタグを追加する。
パース時のTagHandler、ProcessorPrototypeと実行時のProcessorを定義する。
public class ForeachTagHandler implements TagHandler{ @Override public String[] tagNames() { return new String[] {"foreach"}; } @Override public boolean requireSingleTag(String tagName) { return false; // シングルタグは取らない } @Override public boolean requireContainerTag(String tagName) { return true; } @Override public SimpleTemplatePrototype handleSingleTag(String tagName, List<Attribute> attributes) { throw new IllegalStateException("Foreach cannot use as a single tag."); } @Override public SimpleTemplatePrototype handleContainerTag(String tagName, List<Attribute> attributes, List<SimpleTemplatePrototype> elements) { // itemsタグの値を取得 Attribute itemsAttribute = Attribute.findAttribute("items", attributes); // varタグの値を取得 Attribute varAttribute = Attribute.findAttribute("var", attributes); // itemsタグの値はSimpleSourceとして、varの値はSymbolとして渡す return new SimpleForeachProcessorPrototype( TagHandlerUtils.makeSource(itemsAttribute), varAttribute.getValue().getSymbol(), new SimpleContainerPrototype(elements)); } }
public class SimpleForeachProcessorPrototype implements SimpleTemplatePrototype { private final SimpleSource items; private final Symbol var; private final SimpleTemplatePrototype contents; public SimpleForeachProcessorPrototype(SimpleSource items, Symbol var, SimpleTemplatePrototype contents) { super(); this.items = items; this.var = var; this.contents = contents; } public SimpleTemplate instantiate(Context<Object, SimpleTemplate> lexicalContext) { return new SimpleForeachProcessor( lexicalContext, items, var, contents.instantiate(lexicalContext)); } }
public class SimpleForeachProcessor extends AbstractSimpleTemplate implements SimpleTemplate { private final Context<Object, SimpleTemplate> lexicalContext; private final SimpleSource items; private final Symbol var; private final SimpleTemplate content; public SimpleForeachProcessor(Context<Object, SimpleTemplate>lexicalContext, SimpleSource items, Symbol var, SimpleTemplate content) { super(); this.lexicalContext = lexicalContext; this.items = items; this.var = var; this.content = content; } @SuppressWarnings("unchecked") @Override public TemplatePair<Object, SimpleTemplate> apply(final Context<Object, SimpleTemplate> context) { Context<Object, SimpleTemplate> merged = ContextUtils.merge(context, lexicalContext); // itemsの値を取得 Associable<Object, SimpleTemplate> itemsAssociable = this.items.getAssociable(merged); if (itemsAssociable == null) { // itemsが見つからない場合は自分自身を戻す=未評価 return TemplatePair.<Object, SimpleTemplate>pairOf(this); } Object itemsValue = itemsAssociable.asValue().value(); if (itemsValue == null || !(itemsValue instanceof Collection)) { // Collectionでなければエラー throw new IllegalStateException("'items' is not collection"); } List<SimpleTemplate> elements = new ArrayList<SimpleTemplate>(); for (Object item : (Collection<Object>) itemsValue) { // itemの値をvarとしてセットし、タグ内を評価する。 Context<Object, SimpleTemplate> innerContext = ContextUtils.put(context, var, new SimpleValue(item)); elements.add(content.apply(innerContext).template()); } // タグ内のすべての評価結果をSimpleContainerとして戻す。 return TemplatePair.<Object, SimpleTemplate>pairOf(new SimpleContainer(elements)); } @Override public boolean isReducible() { return true; } @Override public String getString() { throw new IllegalStateException("Unevaluated foreach:" + this); } }
パーサーにTagHandler
忘れずに追加
public final class ParserFactory { ... private void registerHandlers() { registerHandler(new DefineTagHandler()); registerHandler(new InsertTagHandler()); registerHandler(new IfTagHandler()); // NEW!! registerHandler(new ForeachTagHandler()); // NEW!! } ...
実行サンプル(Ifタグ)
会員(isMember==true)の場合のみ、「ようこそ、会員<会員のファーストネーム>」と表示する。
public static class Customer { public String firstName; public String lastName; public boolean isMember; } @Test public void testIf() { Customer customer = new Customer(); customer.firstName = "トン吉"; customer.isMember = true; Context<Object, SimpleTemplate> context = new ContextBuilder<Object, SimpleTemplate>() .put(Symbol.of("customer"), new SimpleValue(customer)) .context(); SimpleTranslator translator = new SimpleTranslator(); SimpleTemplate template = translator.toTemplate("" + "{if condition=\"customer.isMember\"}" + "ようこそ、会員{insert value=\"customer.firstName\"/}" + "{/if}" + ""); String output = translator.fromTemplate(template.apply(context).template()); System.out.println(output); String expected = "ようこそ、会員トン吉"; assertEquals(expected, output); }
実行サンプル(Foreachタグ)
顧客情報のListを元に、ファーストネームを「、」区切りで表示する。
@Test public void testForeach() { List<Customer> customers = new ArrayList<Customer>(); customers.add(new Customer(){{firstName="Boo";}}); customers.add(new Customer(){{firstName="Hoo";}}); customers.add(new Customer(){{firstName="Woo";}}); Context<Object, SimpleTemplate> context = new ContextBuilder<Object, SimpleTemplate>() .put(Symbol.of("customers"), new SimpleValue(customers)) .context(); SimpleTranslator translator = new SimpleTranslator(); SimpleTemplate template = translator.toTemplate("" + "{foreach items=\"customers\" var=customer}" + "{insert value=\"customer.firstName\"/}、" + "{/foreach}" + ""); String output = translator.fromTemplate(template.apply(context).template()); System.out.println(output); String expected = "Boo、Hoo、Woo、"; assertEquals(expected, output); }
これでdefine/insert/if/foreachタグが出来たので、次回は「関数型テンプレートエンジンに可能なこと」を実例を元に挙げていく。
*1:MVELは使ったことが無かったので試しに使ってみた。ELの役割は非常に重要なのでそのうち自家製のものに換える。