ラベル anakia の投稿を表示しています。 すべての投稿を表示
ラベル anakia の投稿を表示しています。 すべての投稿を表示

2010/03/19

Anakia:#set()の挙動について

このブログ記事や最近作成したIronPythonについての文書はAnakiaで生成するようにしています。 最近ではJSPとの比較でVelocityが語られる事はむしろ少なくなってきたのかもしれませんが、テンプレート言語として習得の容易さやスピードの面で優れていると思います。

ただ、しばらく使ってみてAnakiaで作成する原稿からは事前に設定された、主にXML文書のリソース、にアクセスできるだけで柔軟性に欠ける部分があると感じるようになりました。 つまり動的にマクロの内部から任意のXMLファイルへ、あるいはネットワーク越しにRSSなどのXML形式の外部データソースにアクセスする事はできません。

これはシンプルにするための割り切りなのかもしれませんが、プロセス上のフレームワークの作り手とドキュメントを作成するライターの役割分担を行なうことを意識しているようにも思えます。

いずれにしても本当に不満であれば、AnakiaTask.java辺りを書き換えれば簡単に拡張できそうです。 例えば$dateで参照可能なjava.util.Dateオブジェクトは次のようなコードで実装されています。

org/apache/anakia/AnakiaTask.javaより抜粋

...
context.put ("date", new java.util.Date() );
...

本題:Velocity #set()マクロの挙動について

さてさて、本題ですが、*.vslマクロファイルを書いている時に気がついたコーディングパターンがあります。 それは#setマクロでgetAttributeValue()メソッドを使った時に変数が初期化されないという点です。 結局のところ次のように空文字列で初期化をする1行が必要になりました。

最終的に作成したマクロ

#macro (find_nextpage $value)##
  #set($name = "")
  #set($name = $root.getChild("next").getAttributeValue("name"))##
  ...

このgetAttributeValue()メソッドを呼び出す前に$name変数を空文字列で初期化しているところが問題です。 こうしないと該当するAttributeが存在しない場合に、前のループでセットした$nameの値がそのまま維持されてしまうところです。 $nameの値はnullでもなく変化しません。

velocity.propertiesの設定でvelocimacro.context.localscope=falseとしているために、全ての変数が大域変数と同じ扱いになるため以前の呼び出しで設定された$nameの値を覚えているのは、しかたのないことです。 しかし代入しているのに、左辺値が変化しないのはちょっと感覚とずれているように感じます。

左辺値が変化しないのはgetAttributeValue()に限らず、#set()マクロの基本的な挙動のようです。 コードをざっとみたところleftReferenceがcontextから削除されているので、未定義なのかと思いきや以前の値が復活しているという少し謎な動きです。

getAttributeValue()を少し追ってみる

私がコーディングしたコードではgetChildren("name")のようなメソッドは、getName()で事前にタグが"name"だと判定しているので戻り値があると分かっていて呼ぶ場合がほとんどでした。 その反面getAttributeValue()はオプション扱いのattributeの存在を確認するために呼ぶ場合がほとんどだったため、戻り値が空文字列になると仮定して書いたコードは、とってもバギーになってしまいました。

そこでgetAttributeValue()周りのコードをシンプルにできないか少しコードを追ってみました。

もともとgetAttributeValue()メソッドはAnakiaが準備しているものではなく、 org.jdom.Elementで定義されているjdom由来のメソッドです。 この処理は次のように定義されています。

引数が1つの場合(org/jdom/Element.javaより抜粋)

    public String getAttributeValue(final String name) {
        return getAttributeValue(name, Namespace.NO_NAMESPACE);
    }

さらにNamespaceが指定されている場合に呼ばれるメソッドは次のようになります。

引数が2つでNamespaceを取る場合(org/jdom/Element.javaより抜粋)

    public String getAttributeValue(final String name, final Namespace ns) {
        return getAttributeValue(name, ns, null);
    }

最終的には次のように該当するattributeがない場合には、defに該当するnullが返されます。

該当するattributeがない場合に第3引数が返される

    public String getAttributeValue(final String name, final Namespace ns, final String def) {
        final Attribute attribute = (Attribute) attributes.get(name, ns);
        if (attribute == null) {
            return def;
        }

        return attribute.getValue();
    }

しかし.getAttributeValue("name", "", "")をようなマクロを作成しても呼ばれるのはGetAttributeValue("name")のままで、defにはnullが入ってしまうため回避する方法はなさそうです。

別のメソッドを呼び出す方法がないか考えましたが、良い方法は思い付きません。 最初の例を別の方法で書き直すなら、次のような方法でしょうか。

別の解決策

  #if($root.getChild("next").getAttributeValue("name"))##
    #set($name = $root.getChild("next").getAttributeValue("name"))##
  #else##
    #set($name = "")##
  #end##

いずれにしても良い方法ではないので、繰り返し呼ぶマクロ内部では変数の初期化とgetAttributeValue()の呼び出しは対で必要そうです。

2009/12/30

Anakia 1.0: 出力フォーマットのTips

Anakiaに限らずVelocityを使うと、テンプレートと出力されたファイルの間に予期しない改行コードが埋め込まれるなどの違いが生まれます。 今回はVelocityの MLに投稿された記事を元に対策をメモしておきます。

問題の現象について

MLなどでも度々問題になっているようですが、Velocityでは命令の前に埋め込まれた空白などがそのまま出力されてしまいます。

例えばanakiaを使っているとして、次のようなテンプレートを準備します。

vslテンプレート

<?xml version="1.0" encoding="UTF-8" ?>
<html>
#document()
</html>
#macro (document)
  #set ($allSections = $root.getChildren())
  #foreach ( $section in $allSections )##
    #if ($section.getName().equals("p"))##
    <p>
      $section.getContent()
    </p>
    #end
  #end
#end

出力は次のようになります。

入力ファイル

<?xml version="1.0" ?>

<blog>
  <p>sample messages</p>
</blog>

vslテンプレートを適用した出力例

<?xml version="1.0" encoding="UTF-8" ?>
<html>
            <p>
        sample messages
      </p>
      </html>

期待する出力は、pの開始タグと閉じタグの先頭が縦に揃っていて、内部にコンテンツが少し字下げされて表示されているものなのですが、少し違います。

期待する出力

<?xml version="1.0" encoding="UTF-8" ?>
<html>
      <p>
        sample messages
      </p>
</html>

VTL命令行にある空白の出力抑制

Velocityのテンプレート言語(VTL)の命令はスクリプト言語なので、 書く時には適当にインデントを追加しないと人間には読みずらくなります。

しかし、そのインデントに使われた空白がそのまま出力されてしまうと意図しない空白となる場合があります。

修正版のvslテンプレート

出力をみながらテンプレートを修正します。

修正版 vslテンプレート

<?xml version="1.0" encoding="UTF-8" ?>
<html>
#document()
</html>
#macro (document)
#set ($allSections = $root.getChildren())
#foreach ( $section in $allSections )
#if ($section.getName().equals("p"))
      <p>
        $section.getContent()
      </p>
#end
#end
#end

出力例

<?xml version="1.0" encoding="UTF-8" ?>
<html>
      <p>
        sample messages
      </p>
</html>

出力は意図したようになりましたが、 書かれたテンプレートはかなり人間には読めないものになってしまいました。

vslファイルの自動生成

人間に読めなくて大変なら、人間が編集するファイルとanakiaが使うファイルを分ければ良いという発送で、'#'で始まる行は先頭の空白を取り除くようにしようと思います。

build.xmlは次のようになっていて、 prop/blog.vsl ファイルを自動生成します。

build.xmlファイルから抜粋

<anakia basedir="${docs.src}" 
   destdir="${docs.dest}"
   extension=".html"
   style="prop/blog.vsl"

簡単にできるのでMakefileを使う事にしました。

Makefileの内容

all: prop/blog.vsl build

prop/blog.vsl: prop/blog.vsl.template
	sed -e 's/^ *#/#/' $< > $@

build:
	ant -f prop/build.xml

blog.vsl.template はインデントを使って編集して、makeコマンドで blog.vsl を生成しています。 Makefileに限定する必要もないのですが、 blog.vslblog.vsl.template より新しい場合には生成過程がスキップされるので無駄が省く事ができます。 大した時間じゃないですけどね。

改行コードの出力を抑制する

さて今回使ったのは簡単な例ですが、場合によっては本来1行に収めたいものを複数行に渡って書きたい場合があるかもしれません。 その場合には"##"を行末に入れる事で、改行の出力を抑制する事ができます。

vslテンプレート

<?xml version="1.0" encoding="UTF-8" ?>
<html>
#document()
</html>
#macro (document)
  #set ($allSections = $root.getChildren())
  #foreach ( $section in $allSections )
    #if ($section.getName().equals("p"))
      <p>##
        $section.getContent()##
      </p>
    #end
  #end
#end

改行を抑制した例

<?xml version="1.0" encoding="UTF-8" ?>
<html>
      <p>        sample messages      </p>
</html>

これはp開始タグと getContent() が出力された後の改行が抑制されています。 sample messages前後の空白を抑制したい場合には次の2つのパターンが考えられると思います。

例1

      <p>$section.getContent()</p>

例2

      <p>##
$section.getContent()##
</p>

まとめ

今回は2つのTipsを書きましたが、Velocityを使うと出力されるHTMLコードが汚なくなると感じた場合には有効だと思います。

これぐらいならVelocityのTutorialにあっても良さそうなものですが、 知らないとどうしようもないですよね…。 出力に気を配る人はそんなにいないのかなぁ。

2009/12/14

ひさしぶりにコンテンツを作ってみた

簡単なXML→HTML変換器を作成したので、調子に乗ってテストを兼ねて気になっていた話題で文書を作成してみました。 実際に使ってみるといろいろ不備がみつかったのですが、その場で修正していって、自分が書きたいものに特化しているけれど、そこそこ使えそうな形になってきたと思います。

きっとベースは同じでも書きたいものによって、少しずつ実装を変化させていくと思います。 書く量を減らしつつ、便利なものを短時間で作っていく、そんな事を目指しています。 対象によってタグが違うのは不便そうですが、記述する対象によってボキャブラリーやタグ付けの粒度は変化すると思うんですよね。

このブログでは読書感想文的なものは単純なリンクを含む程度で、 本文で必要なものは 強調 ぐらいです。 またコードや端末への入力・出力といった技術的な要素を多く含むので、codeタグといったXHTMLへ直接マッピングされるタグの他にコメント用の"note"タグという独自のタグも含んでいます。

今回使ったVelocityベースのAnakiaフレームワークはXSLTのように標準化がされていないけれど、 小さくまとまっていますから自分のツールボックスには入れておこうと思います。

いくつか実装した機能として、本文中に直接anchorとhrefを埋め込む事はせずに、外部のXMLファイルに情報をまとめて、本文からは簡単な名前だけを書き、Anakiaの中で変換するようにしています。 XMLファイルは、build.xmlの中でcontextタグの中で指定して、参照用のマクロをsite.vslで定義しています。 応用すれば、いろいろな情報と本文との結び付きを弱く保つ事ができそうです。

build.xmlでの指定例

<anakia>
...
  <context name="anc" file="../etc/proj_anchor.xml" />
</anakia>

ここで指定した"proj_anchor.xml"ファイルの中身は次のようになっています。

<anchor>
  <item name="ibm.jp" href="http://www.ibm.com/jp"/>
</anchor>

build.xmlの中で指定したcontextには次のようなVelocityマクロを準備しました。

#macro (find_anc $name $value)
  #set ($href = $anc.selectNodes("/item[@name='$name']/@href"))
  <a $href>$value</a>
#end

丁度このブログ記事を生成するツールには、この仕組みを入れていなかったのでバックポートしてみました。 ブログの中にあるリンクを一元管理するのは良いのか、悪いのか、どうなんだろうな。

しかし自分で作成したツールの仕様書をどの程度のレベルでまとめておくのか、少し悩んでいます。 okwaveでも似たような投稿は度々みますね。良い結論に至ったものは、まだなさそうですが。 まぁ基本的な取扱説明書と内部のデータ構造ぐらいは最低限かな。

今回作成したツールで書いた文書はこちら → http://www.yasundial.org/shell_script/

2009/12/11

Bloggerへの投稿を自動化してみた

HTMLファイルを生成する事はできるようになったので、Google Data ProtocolのPython Client Libraryに附属するBlogger用のサンプルスクリプトを修正してコマンドラインから投稿できるようにしました。

この投稿自体も自前のXMLファイルから一連のコマンドでブログに投稿しています。 また初投稿した時にタイトル名とblog_post_idを保存しておくようにしたので、 タイトルの変更や記事の更新もできるようになっています。

実行時の様子

動作は2段階でXMLからHTMLに変換する部分( build.sh )と、生成したHTMLファイルをリストして指定した記事を投稿する部分( run.sh )に分かれています。

記事生成スクリプトの実行

HTMLファイルを生成するといっても実体はxhtmlに合わせているのでXML文書です。 ANTタスクの前後にxmlvalidateタスクを実行するようにしているので、少なくともwell-formedなXML文書である事は最低限チェックしています。

Buildfile: prop/build.xml

validate_xml:
[xmlvalidate] 2 file(s) have been successfully validated.

docs:
   [anakia] Transforming into: /home/user01/blogpost/html
   [anakia] Input:  20091211.1.xml
   [anakia] Output: /home/user01/blogpost/html/20091211.1.html
[xmlvalidate] 2 file(s) have been successfully validated.

BUILD SUCCESSFUL
Total time: 0 seconds
投稿スクリプトの実行

[0]	Bloggerへの投稿を自動化してみた
[1]	Apache Anakiaを使った簡易CMSの作成
select article number: 0
find entry: 9106769796720729015
[update_article] file=20091211.1.html,title=Bloggerへの投稿を自動化してみた,labels=[u'web', u'\u30d7\u30ed\u30b0\u30e9\u30df\u30f3\u30b0', u'ubuntu']

さいごに

まだ記事を参照するためのリンクの仕組みはなく、編集を楽にするツール類の開発は必要だと思います。でもタイトルの変更と記事の更新ができるようになった事で最低限の機能は実装できました。

しばらく使ってみて、これをネタにアプリケーションの設計や文書化について勉強していこうと思います。

2009/12/10

Apache Anakiaを使った簡易CMSの作成

anakiaについて調べてみたので、このブログに投稿する記事を生成するための簡易CMSを作ってみました。 機能は少ないですし、bloggerにポストするところも手でコピー&ペーストしています。

設計方針

このブログでは端末への入力やその結果を表示したり、 アプリケーションコードの一部を書く事が多いです。

いままでは手動で&などとクォートしていたのですが、 量が多いと面倒な事になります。 リダイレクト(>)なんかは手で書いていると、向きを間違えそうですしね。

コードを記述する場合の例

パラグラフ'p'タグの外で'code'タグやら'kbd'タグを使う場合は、'pre'タグの中に入れています。 この場合は主にCDATAを使って生のコードをそのまま書いています。

変換前:元のテキスト

<code><![CDATA[
<body>
  <h1>title</h1>
  <p>本文</p>
</body>
</code>]] >

変換後:画面には次のように表示される

<body>
  <h1>title</h1>
  <p>本文</p>
</body>

'p'タグの中でも'code'タグや'kbd'タグを使うことが出来るようになっています。 コマンドの入力$ emacs 20091210.1.xml &、コードの断片int i=0;

anakiaを使う場合の注意

Apache Anakiaをダウンロードして中にあるサンプルを変換して試すと思うのですが、 試す前に挫折してしまうかもしれません。 コマンドラインで動かそうとするとCLASSPATHの設定や附属のbuild/build.xmlはanakiaのビルド用でドキュメントの変換用ではなかったりします。

あとはデフォルトの言語が"ISO-8859-1"なので、せめてUTF-8なら良いのですが環境に合わせて適切に設定する必要もあります。

ディレクトリ構造

トップディレクトリは任意で、そこからの相対的なパスは次のようになっています。

  • apache-ant-1.7.1
  • anakia-1.0
  • xml/20091210.1.xml - 記事ファイル
  • build.sh - antを呼び出すラッパースクリプト
  • html - 出力用ディレクトリ
  • prop - 設定用ファイルを配置するディレクトリ
  • prop/build.xml - ant用設定ファイル
  • prop/site.vsl - 変換ルールを記述した
  • prop/project.xml - 今回は使わない共通テンプレートファイル
  • prop/velocity.properties - 日本語を使うならセットアップ必須
build.sh

CLASSPATHの設定が面倒なので、wrapperスクリプトを作成しました。

#!/bin/bash

BASEDIR="$(dirname $0)"
ANT_HOME="${BASEDIR}/apache-ant-1.7.1"
ANAKIA_HOME="${BASEDIR}/anakia-1.0"

## export essential variables
export ANT_HOME
export PATH="$ANT_HOME/bin:$PATH"

CP_ANT=$(find . $ANT_HOME/lib -name '*.jar' | tr '\n' ':')
CP_ANAKIA=$(find . $ANAKIA_HOME -name '*.jar' | tr '\n' ':')
export CLASSPATH=$CLASSPATH:$CP_ANT:$CP_ANAKIA

## main ##
cd "$BASEDIR"
ant -f prop/build.xml
prop/build.xml

マニュアルに附属のものをベースに変更しています。 書き方はいろいろやってみないと良く分からないですね。 "projectFile"は"${docs.src}"からの相対パスになりますが、"style"の方は先頭で指定した"${basedir}"からのパスになったりするのでパスの指定は何回かやり直しました。

anakiaに附属の例では"AnakiaTask"クラスがない場合にエラーにしていましたが、 ここではxmlファイルがwell-formedでない場合にはエラーを出すようにしています。

<project name="build-site" default="docs" basedir="..">
  <property name="docs.src" value="xml"/>
  <property name="docs.dest" value="html"/>

  <target name="validate_xml">
    <xmlvalidate failonerror="yes" lenient="yes" warn="yes"
		 classname="org.apache.xerces.parsers.SAXParser">
      <fileset dir="${docs.src}" includes="**/*.xml"/>
      <attribute name="http://xml.org/sax/features/validation" value="false"/>
      <attribute name="http://apache.org/xml/features/validation/schema"  value="false"/>
    </xmlvalidate>
  </target>
  <target name="docs" depends="validate_xml">
    <taskdef name="anakia"
	     classname="org.apache.anakia.AnakiaTask"/>
    <anakia basedir="${docs.src}" 
	    destdir="${docs.dest}"
	    extension=".html" style="prop/site.vsl"
	    projectFile="../prop/project.xml"
	    lastModifiedCheck="true"
	    velocityPropertiesFile="prop/velocity.properties">
    </anakia>
  </target>
</project>
prop/velocity.properties

linux上で動かしているのでUTF-8に設定しています。 WindowsであればShift_JISなどに変更するのが良いのでしょうね。

input.encoding=UTF-8
output.encoding=UTF-8

さいごに

anakiaは便利なんですけれど、含まれているsite.vslファイルの中では'source'タグを処理する時に'pre'開始タグがなくて、'/pre'閉じタグだけがあったり、急いでリリースしたのかなぁと思ったりします。

入出力の言語設定はvelocityの部分なので、anakiaのマニュアルには記述がありません。 それでもコードをみてvelocity.propertiesだなと当りが付けられるので、OSSで良かったです。

Velocityを直接操作しても、この程度のものは作れると思いますが、 このanakiaを触り始めて、この記事を書き上げるところまで睡眠時間を含めて通算15時間ぐらいですから、 まぁ自分の時間は節約できたと思います。 シンプルだけれど、フレームワークとしては初心者向けのはずなのに、説明不足な点が少しばかり残念です。

2009/12/10追記: 後から付けたすためのノートのテスト。