XSLTからJavaを使う

はじめに

XML+XSLTでトップページを作成できるようになったので,歳時記の個々の日記を書いているファイルもXML+XSLTにしよう.

このページは他のページと違い,前後の日付に移動するためのアクセスメニューを見出しタグ<h1>の横につけている.タグの付加はXSLTでできるので,前後の日付をXMLに書いておくのが手っ取り早いが,それでは芸がない.前後の日付は自動で算出したいものだ.

XSLTでは日付の計算はできそうにないので,Javaを使うことにした.

XMLファイルの作成

トップページ同様,新しいタグは作らない.1月1日を実験台とする.前日が12月31日,翌日が1月2日になるはずだ.

<!-- 0101.htmlから作った0101.xml -->
<root>
  <h1>1月1日</h1>
  <h2>2002年(火)</h2>
  <p>
    帰ってきた弟家族と一緒に,雨が降る中近所のお宮に初詣.父親が手術をしたばかりなので,九州への三社参りは今年はなし.一日テレビを見て過ごす正月となった.
  </p>

  <p>
    猪木祭りを見る.ドン・フライとミルコ・クロコップは強かったが,それ以外はつまらん試合ばかりだった.
  </p>

  <h2>2001年(月)</h2>
  <p>
    母方のばあさんが死んだので,今年は初詣なし.そう言えば去年も何か理由をつけていかなかった記憶が.日本人の信仰心の希薄さがこんなところにも.しかし,雑煮は食べる.
  </p>

  <p>
    午後,親戚が集り近所の国民宿舎へ.正月は上げ膳据え膳,が世の母親の夢なのだそうだ.
  </p>
</root>

Javaプログラムの作成

指定した日の前後の日付をURLの一部として返すJavaプログラムを作成した."1/1"を渡すと"../12/1231.html"と"../01/0102.html"を返す.コンパイルしてSCal.classを作っておこう.

import.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class SCal{
  private String date;

  private String get(int i){
    final SimpleDateFormat inputFormatter=new SimpleDateFormat("M/d");
    final SimpleDateFormat outputFormatter=new SimpleDateFormat("../MM/MMdd");
    final Calendar cal=Calendar.getInstance();

    cal.setTime(inputFormatter.parse(this.date,new ParsePosition(0)));
    cal.add(Calendar.DATE,i);
    return outputFormatter.format(cal.getTime())+".html";
  }

  public SCal(String date){
    this.date=date;
  }
  public String prev(){
    return get(-1);
  }
  public String next(){
    return get(+1);
  }

  public static void main(String[] args){
    SCal s=new SCal("1/1");
    System.out.println("Prev: "+s.prev());
    System.out.println("Next: "+s.next());
  }
}

XSLTを作る

XSLTの中からJavaを使う際の記述方法は,XTとXalan-Javaでほぼ同じ.<xsl:stylesheet xmlns:~>で名前空間を宣言して,<xsl:variable name="返却値" select="メソッド"/>と使う.XTとXalan-Javaでは指定する名前空間が異なる.まず,XTの場合,名前空間はhttp://www.jclark.com/xt/java/クラス名となる.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:scal="http://www.jclark.com/xt/java/SCal" <!-- 名前空間を宣言 -->
  exclude-result-prefixes="scal"
  version="1.0">

  <xsl:include href="framework.xsl"/>

  <xsl:variable name="today" select="concat(/root/h2,'年',/root/h1)"/>
  <xsl:variable name="scal" select="scal:new(string($today))"/> <!-- インスタンスを生成して変数scalへ入れる -->
  <xsl:variable name="prev" select="scal:prev($scal)"/> <!-- 前日のURLをprevに取得.メソッドの第1引数にインスタンス$scalを指定 -->
  <xsl:variable name="next" select="scal:next($scal)"/> <!-- 翌日のURLをnextに取得.メソッドの第1引数にインスタンス$scalを指定 -->

  <xsl:template name="date">
    <h1><xsl:call-template name="title"/></h1>
    <div class="DateMenu">
      <a href="{$prev}">
        <xsl:value-of select="number(substring($prev,7,2))"/>
        <xsl:text>月</xsl:text>
        <xsl:value-of select="number(substring($prev,9,2))"/>
        <xsl:text>日</xsl:text>
      </a>
      <xsl:call-template name="menu_div"/>
      <a href="{$next}">
        <xsl:value-of select="number(substring($next,7,2))"/>
        <xsl:text>月</xsl:text>
        <xsl:value-of select="number(substring($next,9,2))"/>
        <xsl:text>日</xsl:text>
      </a>
    </div>
  </xsl:template>

  <<省略>>

Xalan-Javaはhttp://xml.apache.org/xslt/java/SCalとなる.後はXTと同じだ.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:scal="http://xml.apache.org/xslt/java/SCal"
                version="1.0">

Xalan-Javaの場合は,名前空間にクラスまで指定しないやり方もある.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:java="http://xml.apache.org/xslt/java"
                version="1.0">
                <!-- クラスSCalまで指定しない.
                     最後にスラッシュをつける
                     (xmlns:java="http://xml.apache.org/xslt/java/")と
                    ‘Unknown error in XPath’発生. -->

  <<省略>>

  <xsl:variable name="today" select="concat(/root/h2,'年',/root/h1)"/>
  <xsl:variable name="scal" select="java:SCal.new(string($today))"/> 
  <!-- インスタンス生成時にクラス名(SCal)まで指定 -->
  <xsl:variable name="prev" select="java:prev($scal)"/>
  <xsl:variable name="next" select="java:next($scal)"/>

インスタンスを作らずにクラスメソッドだけ使う

簡単なプログラムなので,インスタンスを作らずにクラスメソッドのみで実装することもできる.

import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class SCal{
  private static String get(String date,int i){
    final SimpleDateFormat inputFormatter=new SimpleDateFormat("M/d");
    final SimpleDateFormat outputFormatter=new SimpleDateFormat("../MM/MMdd");
    final Calendar cal=Calendar.getInstance();

    cal.setTime(inputFormatter.parse(date,new ParsePosition(0)));
    cal.add(Calendar.DATE,i);
    return outputFormatter.format(cal.getTime())+".html";
  }

  public static String prev(String date){
    return get(date,-1);
  }

  public static String next(String date){
    return get(date,+1);
  }

  public static void main(String[] args){
    System.out.println("Prev: "+SCal.prev("1/1"));
    System.out.println("Next: "+SCal.next("1/1"));
  }
}

その場合,XSLTからインスタンスの生成がなくなる.まずXTの場合.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:scal="http://www.jclark.com/xt/java/SCal"
                version="1.0">

<<省略>>

<xsl:variable name="xml_date" select="/root/h1"/>
<xsl:variable name="month" select="substring-before($xml_date,'月')"/>
<xsl:variable name="day" select="substring-before(substring-after($xml_date,'月'),'日')"/>
<xsl:variable name="prev" select="scal:prev(concat($month,'/',$day))"/>
<xsl:variable name="next" select="scal:next(concat($month,'/',$day))"/>
<!-- インスタンスを生成しないでprev/nextメソッドが使える -->

Xalan-Javaで名前空間にクラス名まで含める場合,変るのは名前空間の宣言部分だけだ.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:scal="http://xml.apache.org/xslt/java/SCal"
                version="1.0">

<<省略>>

<xsl:variable name="xml_date" select="/root/h1"/>
<xsl:variable name="month" select="substring-before($xml_date,'月')"/>
<xsl:variable name="day" select="substring-before(substring-after($xml_date,'月'),'日')"/>
<xsl:variable name="prev" select="scal:prev(concat($month,'/',$day))"/>
<xsl:variable name="next" select="scal:next(concat($month,'/',$day))"/>

Xalan-Javaで名前空間にクラス名まで含めない場合は以下の通り.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:java="http://xml.apache.org/xslt/java"
                version="1.0">

<<省略>>

<xsl:variable name="xml_date" select="/root/h1"/>
<xsl:variable name="month" select="substring-before($xml_date,'月')"/>
<xsl:variable name="day" select="substring-before(substring-after($xml_date,'月'),'日')"/>
<xsl:variable name="prev" select="java:SCal.prev(concat($month,'/',$day))"/> <!-- メソッドにクラス名をつけて呼び出す -->
<xsl:variable name="next" select="java:SCal.next(concat($month,'/',$day))"/>

classpathの指定

XSLTプロセッサで変換を行う前に,XSLTの中で使うJavaのクラスがあるディレクトリをclasspathに追加する必要がある.追加しないと,以下のエラーが発生する.

# XTの場合
implementation of extention namespace not avairable

# Xalan-Javaの場合
file:date.xsl; Line 13; Column 59; XSLT Error (javax.xml.transform.TransformerException): javax.xml.transform.TransformerException: java.lang.ClassNotFoundException: SCal

# こんなエラーの場合もある
file:daily.xsl; Line 63; Column 72; XSLT Error (javax.xml.transform.TransformerException): java.lang.NoSuchMethodException: For extension function, could not find method java.lang.String.new([ExpressionContext,] ).

もし,カレントにあるならset classpath="$classpath$;."とする.XTはclasspathに指定がなくてもカレントディレクトリは参照してくれるようだ.

XSLTプロセッサで変換

classpathの指定を正しく行っていれば,変換はJavaを使わない時と同じである.

なんでだろう

単独のJavaクラスなら動くのにXTやXalan-Javaから使うと動かない場合がいくつかあった.回避はできたが,動かない原因は不明である.

日付のフォーマットに‘月日’を使うと当日日付になるのはなんでだろう

以下のプログラムは単独のJavaのクラスとして動かすと,予想通り../12/1231.htmlと../01/0102.htmlを返す.

import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class SCal{
  private String date;

  private String get(int i){
    final SimpleDateFormat inputFormatter=new SimpleDateFormat("M月d日"); // 受取る日付のフォーマットはM/dではなくM月d日
    final SimpleDateFormat outputFormatter=new SimpleDateFormat("../MM/MMdd");
    final Calendar cal=Calendar.getInstance();

    cal.setTime(inputFormatter.parse(this.date,new ParsePosition(0)));
    cal.add(Calendar.DATE,i);
    return outputFormatter.format(cal.getTime())+".html";
  }

  public SCal(String date){
    this.date=date;
  }
  public String prev(){
    return get(-1);
  }
  public String next(){
    return get(+1);
  }

  public static void main(String[] args){
    SCal s=new SCal("1月1日");
    System.out.println("Prev: "+s.prev());
    System.out.println("Next: "+s.next());
  }
}

実行結果
$ java SCal
Prev: ../12/1231.html
Next: ../01/0102.html

しかし,XT・Xalan-Javaから使うと1月1日ではなく当日の日付が処理対象となる.XSLTプロセッサからJavaクラスへは‘1月1日’で値が渡っているが,Javaプログラムの中でなぜか当日となっているようだ.文字コードはXSLTもJavaもUTF-8である.XSLTプロセッサからJavaクラスへ渡す日付のフォーマットを‘1月1日’から‘1/1’に変更すれば回避可能.

SimpleDateFormatのシングルクォートでエラーになるのはなんでだろう

以下のプログラムも単独のJavaのクラスとして動かすと../12/1231.htmlと../01/0102.htmlを返す.

import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class SCal{
  private String date;

  private String get(int i){
    final SimpleDateFormat inputFormatter=new SimpleDateFormat("M/d");
    final SimpleDateFormat outputFormatter=new SimpleDateFormat("../MM/MMdd.'html"); // URLをここで作る.‘h’は‘時(hour)’を表すのでシングルクォートでエスケープ
    final Calendar cal=Calendar.getInstance();

    cal.setTime(inputFormatter.parse(this.date,new ParsePosition(0)));
    cal.add(Calendar.DATE,i);
    return outputFormatter.format(cal.getTime());
  }

  public SCal(String date){
    this.date=date;
  }
  public String prev(){
    return get(-1);
  }
  public String next(){
    return get(+1);
  }

  public static void main(String[] args){
    SCal s=new SCal("1/1");
    System.out.println("Prev: "+s.prev());
    System.out.println("Next: "+s.next());
  }
}

実行結果
$ java SCal
Prev: ../12/1231.html
Next: ../01/0102.html

XTの場合こんなエラーが出る.Xalan-Javaはエラーにはならない.

java.lang.IllegalArgumentException: Illegal pattern character '

SimpleDateFormatでは.htmlをつけず,文字列として連結すれば回避可能である.

private String get(int i){
  final SimpleDateFormat inputFormatter=new SimpleDateFormat("M/d");
  final SimpleDateFormat outputFormatter=new SimpleDateFormat("../MM/MMdd);
  final Calendar cal=Calendar.getInstance();

  cal.setTime(inputFormatter.parse(this.date,new ParsePosition(0)));
  cal.add(Calendar.DATE,i);
  return outputFormatter.format(cal.getTime())+".html";
}

Saxonを使ってみる(2005.10.4追記)

Saxonでもxsltの中でjavaを使うことができる.使用したバージョンは8.5である.

Saxonのインストールはダウンロードしたzipファイルを適当なディレクトリに展開するだけである.Saxonをインストールしたディレクトリを${saxon_home}と記述する.

まずは,簡単なサンプルのファイル(sam.xml,sam.xsl,Sam.java)を作ることにしよう.Sam.javaはあらかじめコンパイルしSam.classを作っておく.

sam.xslの<xsl:stylesheet>タグの属性にjavaを使うための名前空間

xmlns:名前空間名="java:クラス名"

を定義すれば,該当するクラスをxsltの中で使用することができる.下記の例で言うと,

xmlns:sam-saxon="java:Sam"

がそれに該当する.

<!--sam.xsl-->
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet 
  xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0'
  xmlns:sam-saxon="java:Sam"> <!-- Saxonでjavaを使うための名前空間の定義-->

  <xsl:template match="/">
    <xsl:variable name="sam" select="sam-saxon:new()"/>
    <xsl:value-of select="sam-saxon:message($sam)"/>
  </xsl:template>
</xsl:stylesheet>

<!--sam.xml-->
<?xml version="1.0" encoding="utf-8"?>
<root>
  sample
</root>

// Sam.java
public class Sam{
  String message;

  public Sam(){
    this(null);
  }

  public Sam(String message){
    this.message=message==null? "hello world.": message;
  }

  public String message(){
    return message;
  }

  public static void main(String[] args){
    Sam sam=new Sam();
    System.out.println(sam.message());
    sam=new Sam("foo");
    System.out.println(sam.message());
  }
}

まずはコマンドラインで動かしてみよう.${saxon_home}/doc/index.htmlを見ると,Saxonでxsltを使うにはコマンドラインから以下のように打込めばOKである.

java -jar ${saxon_home}/saxon8.jar sam.xml sam.xsl > sam.html

javaを使っていないxsltの場合は問題なく動く.しかし,xsltの中でjavaを使った場合,以下のエラーが発生する.

$ java -jar ${saxon_home}/saxon8.jar sam.xml sam.xsl
Error at xsl:variable on line 8 of file:/c:/Documents%20and%20Settings/foo/My%20Documents/ant/sam.xsl:
XPST0003: XPath syntax error at char 15 on line 8 in {sam-saxon:new()}:
Cannot find a matching 0-argument function named {java:Sam}new()
Error at xsl:value-of on line 9 of file:/c:/Documents%20and%20Settings/foo/My%20Documents/ant/sam.xsl:
XPST0003: XPath syntax error at char 23 on line 9 in {sam-saxon:message($sam)}
:
Cannot find a matching 1-argument function named {java:Sam}message()
Failed to compile stylesheet. 2 errors detected.

xsltの中で使用しているSam.classファイルがclasspathの中にないためである.classpathにSam.classファイルのパスを指定しても,-jarオプションをつけてjavaコマンドを使用した場合,classpathを無視してしまう.そこで,以下のように-jarオプションを使わないでSaxonを起動する.

java -cp "${saxon_home}/saxon8.jar;." net.sf.saxon.Transform sam.xml sam.xsl > sam.html

以下のような実行結果となる.

<?xml version="1.0" encoding="UTF-8"?>hello world.

まず,-cpオプションにsaxon8.jarのあるパスとSam.classのあるパス(カレントディレクトリ)を指定する.次に,saxon8.jarのメインメソッドのあるクラス(net.sf.saxon.Transform)を指定する.saxon8.jarのメインメソッドのあるクラスはsaxon8.jarの中にあるMETA-INF/MANIFEST.MFの中に書いてある.jarファイルはzipファイルなので,対応したアーカイバで中を見ることが可能である.

続いて,antを使ってみよう.build.xmlへの記述方法は<style>タスクを使用して以下の通りとなる.classpath属性にsaxon8.jarのあるパスとSam.classのあるパスを指定する.

<style style="sam.xsl" in="sam.xml" out="sam.html" classpath="${saxon_home}/saxon8.jar;."/>

SaxonはXTやxalan用に定義した名前空間でもjavaのクラスを動かすことができる.例えば,上述したsam.xslにXT用の名前空間を定義したとしよう.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet 
  xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='2.0'
  xmlns:sam-xt="http://www.jclark.com/xt/java/Sam"> <!-- XT用の名前空間 -->

  <xsl:template match="/">
    <xsl:variable name="sam" select="sam-xt:new()"/>
    <xsl:value-of select="sam-xt:message($sam)"/>
  </xsl:template>
</xsl:stylesheet>

実行結果はSaxon用の名前空間を定義した場合と同じである.

java -cp "${saxon_home}/saxon8.jar;." net.sf.saxon.Transform sam.xml sam.xsl > sam.html

以下のような実行結果となる.

<?xml version="1.0" encoding="UTF-8"?>hello world.

Saxonで使えなくなってた(2017.3.12追記)

Saxon-HE 9.7.0.15JでJavaを使うと以下のエラーが発生する.

Fatal Error! Cannot find a matching 1-argument function named {Javaクラス}メソッド. Reflexive calls to Java methods are not available under Saxon-HE

いつからか分からないが(SaxonがHE/PE/EEと別れてからか?),Saxon-HEではJavaの呼出しができなくなったようだ.

参考:Saxonica > Saxon > Extensibility