DevDevデブ!!

プログラミングのこととか書きます。多分。。。

scalateでVAST形式のxmlを出力したい人生だった。。。

Qiitaに書いたけど、自分のブログにも書いておく

qiita.com

ことのあらまし

動画広告の標準フォーマットであるVAST形式のxmlをテンプレートから出力したくて、Scalateを使おうとしたんだけど、小一時間ハマってた

なんでTwirlじゃなくてScalate使おうとしたかは。。。。忘れた。

(今確認したらTwirlは普通にhtml以外も出力できるじゃん。。。)

ハマり1: 数値を出力すると、三桁毎にカンマが自動挿入される

たとえば、以下のようなscalateのssp形式のテンプレートがあったとする

<Ad id="${advertiserId}">

以下のように出力されるのを期待しているんだけど、

<Ad id="9000">

次のようにカンマが挿入されてしまう

<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の件はJavaAPIのデフォルト挙動なので、Scalateに責任は無いけどw)

ハマり1はskinny-frameworkのソースを見て気づき、ハマり2はいったんデバッガで見つけたあとに、play-scalateプラグインの中で同様のことをやってるのに気づいたんですが、これらが無かったら+1時間はハマってたでしょうね。

先達に感謝。

(play-scalateプラグインそのまま使いたかったのですが、プラグイン機能は2.6でオミット。。。。)