scala用のテンプレートエンジン、Scalateをplay2から使おうとして小一時間ハマってたので共有します。
Scalateはググっても情報が少なくて寂しいです。みなさんScalaでAPIサーバ書いてるからテンプレートエンジンはあまり使わないのでしょうか。
ハマり1: 数値に勝手にカンマが挿入される
たとえば、以下のように出力したいときに、
<Ad id="9000">
次のように3桁毎にカンマが挿入されます
<Ad id="9,000">
余計なお世話だよ!!!
原因: java.text.DecimalFormatの仕業
ScalateのRenderContextが数値出力時のフォーマットに抽象クラスのNumberFormatを使うようになっており、実体はサブクラスのDecimalFormatが使われてる(多分。ロケール依存)
/////////////////////////////////////////////////////////////////////
//
// custom object rendering
//
/////////////////////////////////////////////////////////////////////
private[this] val _numberFormat = new Lazy(NumberFormat.getNumberInstance(locale))
private[this] val _percentFormat = new Lazy(NumberFormat.getPercentInstance(locale))
private[this] val _dateFormat = new Lazy(DateFormat.getDateInstance(DateFormat.FULL, locale))
以下はjava8のDecimalFormatのAPIリファレンスからの抜粋
グループ区切り子は一般に1000ごとに区切るために使用しますが、国によっては10000ごとに使用するところもあります。グループ区切りのサイズとは、100,000,000の場合は3、1,0000,0000の場合は4というように、グループ区切り文字間の一定の桁数です。複数のグループ区切り文字を持つパターンを指定すると、最後の区切り文字と末尾の整数との間が、この間隔として使用されます。したがって、"#,##,###,####" == "######,####" == "##,####,####"となります。
setGroupingUsedメソッドにfalseを設定し、グループ化をoffにする必要があるらしい
解決方法: NumberFormatを自分で設定する
以下のようにTemplateEngineを継承したカスタムクラスを作り、RenderContextを生成するcreateRenderContextメソッドをオーバーライドし、あとはよしなにって感じです。
class CustomTemplateEngine extends TemplateEngine {
override protected def createRenderContext(uri: String, out: PrintWriter): RenderContext = {
val context = new DefaultRenderContext(uri, this, out)
val df = new DecimalFormat()
df.setGroupingUsed(false)
context.numberFormat = df
context
}
}
ハマり2: レイアウト機能が効かない
Layoutsのことです。大体のテンプレートエンジンにはあると思うんですが、骨組みとなるテンプレートを作っておいて、別のテンプレートから骨組みテンプレートを呼び出すと、自身のレンダリング結果を埋め込んでもらえる的なやつ。
骨組みテンプレートを指定しているのにも関わらず、骨組みに埋め込まれず、そのままレンダリングされてしまいます。
これはググっても全然でてこなかったので、デバッガでソースを追ったところ、以下のソースにたどり着きました
var layoutStrategy: LayoutStrategy = NullLayoutStrategy
Nullだと。。。!!!!
以下がNullLayoutStrategyの定義
/**
* A <code>LayoutStrategy</code> that renders the given template without
* using any layout.
*/
object NullLayoutStrategy extends LayoutStrategy {
def layout(template: Template, context: RenderContext) = template.render(context)
}
レイアウト使わずにそのままレンダリングするとかいってますね。フザケンナ
解決方法:DefaultLayoutStrategyに差し替える
engine.layoutStrategy = new DefaultLayoutStrategy(engine)
layoutStrategy変数はvarで定義されており、あとから差し替えることが可能なので、DefaultLayoutStrategyに変更してしまいます。
どこでやるかはいろいろパターンあると思うんですが、私はPlay2のDIコンテナ(Guice)に登録する用のモジュールの中でやりました。
感想
自分のアプリや、フレームワークの中にScalateを組み込むことをEmbeddingというようですが、今回の件がEmbedding用途での注意事項としてドキュメントに特に記載がないのがちょっと厳しいと感じました。(NumberFormatの件はJavaのAPIのデフォルト挙動なので、Scalateに責任は無いけどw)
ハマり1はskinny-frameworkのソースを見て気づき、ハマり2はいったんデバッガで見つけたあとに、play-scalateプラグインの中で同様のことをやってるのに気づいたんですが、これらが無かったら+1時間はハマってたでしょうね。
先達に感謝。
(play-scalateプラグインそのまま使いたかったのですが、プラグイン機能は2.6でオミット。。。。)