XPath拡張関数を実装したxfyコンポーネントの開発

XPath式に使用できる関数を実装したxfyコンポーネントを開発する方法を解説します。

1. はじめに

xfy technologyでは、開発者が独自のXPath関数をxfyコンポーネントに組み込むことができます。このxfyコンポーネントから作成したxfyプラグインをxfyユーザーエージェントの実行環境へ配置すると、作成したXPath関数はxfyユーザーエージェントでXML文書を処理するXPath式に使用できます。こうした関数をXPath拡張関数と呼びます。

XPath拡張関数を作成するには、次の方法があります。

この文書では、XPath拡張関数をJava言語で作成してxfyコンポーネントに組み込む方法を説明します。XVCDのユーザー定義関数については、「VC中核リファレンス:要素・属性編」を参照してください。

この文書内のサンプルコードは、xfy Developer's Toolkit 配布パッケージ内のdoc/samples/developers/tutorial/java_function_componentに収録されています。xfy Developer's Toolkitには、このほかにも豊富なサンプルが用意されています。ぜひ、それらのサンプルもご覧ください。

1.1. XPath拡張関数作成の概要

XPath拡張関数をxfyコンポーネントに組み込んでxfyプラットフォームで利用できるようにするには、次の2つのクラスを作成します。

  • XPath拡張関数を実装したクラス
  • 上記のクラスをxfyプラットフォームで使えるようにするためのクラス(XPath関数サービスプロバイダ)

XPath拡張関数を実装したクラスは、1つのxfyコンポーネントに複数存在できます。XPath関数サービスプロバイダはXPath関数のセットをxfyプラットフォームで使えるようにします。

作成したXPath関数サービスプロバイダの情報は、xfyコンポーネントから作成するxfyプラグインのプラグインマニフェストファイルに記述します。従って、XPath拡張関数をxfyプラットフォームで利用できるようにするには、次のファイルが必要です。

  • XPath拡張関数を実装したクラス(ひとつ以上)
  • XPath関数サービスプロバイダ
  • プラグインマニフェストファイル

XPath拡張関数を実装したクラスをコンパイルするときは、XfyCore.jarCategories.jarが必要です。そのため、XPath拡張関数を作成する環境では、クラスパスにXfyCore.jarcommon/com.xfy/Categories.jarを追加しておきます。これらのjarファイルは、xfyユーザーエージェントの実行環境の基点フォルダ内にあるbinフォルダ以下にあります。

1.2. xfyプラットフォームのXPath関数評価の仕組み

xfyプラットフォームでは、XPath関数はFunctionインターフェイスを実装したクラスのインスタンスとして表現されます。

public interface Function {

    public Value call(Arguments args, Context context) throws ValueException;

}

xfyプラットフォームのXPath評価エンジンは、XPath関数を評価するときに、XPath関数を実装したクラスのインスタンス(関数オブジェクト)を取得します。関数オブジェクトは、XPathFunctionServiceインターフェイスを実装したサービスプロバイダに問い合わせて取得します。XPathFunctionServiceインターフェイスはXPath拡張関数サービスカテゴリを表します。XPathFunctionServiceサービスプロバイダに問い合わせるときには関数名を使用します。xfyプラットフォームは、関数オブジェクトが取得できるまで、管理しているXPathFunctionServiceサービスプロバイダに、順次、問い合わせます。すべてのXPathFunctionServiceサービスプロバイダに問い合わせても関数オブジェクトを取得できないときは、エラーとして処理します。

XPathFunctionServiceインターフェイスは、次のように定義されています。

public interface XPathFunctionService extends Service {

    public Function lookup(Name name);

}

プラットフォーム拡張関数のfunction:average関数を評価する場合を例に、関数オブジェクトを取得する流れを説明します。function:average関数は、次のように記述されます。

select="function:average(/sample:list/sample:item))"
        ^^^^^^^^^^^^^^^^関数名

この場合、「function:avarage」という関数名を格納したNameオブジェクトを引数にして、管理しているXPathFunctionServiceサービスプロバイダのlookupメソッドを、順次、呼び出します。この関数はプラットフォーム拡張関数なので、XPathFunction.jarXPathFunctionProviderAverageFunctionクラスのオブジェクトを返します。

関数オブジェクトを取得したxfyプラットフォームは、関数オブジェクトのcallメソッドを呼び出します。このとき、callメソッドの引数には、XPath拡張関数の引数とコンテキスト情報を設定します。呼び出したcallメソッドからは、関数の評価結果としてValueオブジェクトが返されます。

1.3. 実装例について

以降の説明では、下記の仕様を仮定したXPath拡張関数の実装例を記載しています。

名前空間URI参照
http://xmlns.example.com/developer/function
名前空間接頭辞
example
Javaパッケージ名
com.example.developer.function.*
サンプルXPath関数の仕様
  • example:hello-world()
    "Hello World!" という文字列を返す関数です。引数はありません。
  • example:hello(string)
    引数で与えられた文字列の前に"Hello "を、後ろに"!"を付加した文字列を返す関数です。必須の引数が1つあります。

2. XPath拡張関数のサービスプロバイダクラスの実装

最初に、XPath拡張関数のサービスプロバイダクラスを実装します。XPath拡張関数のサービスプロバイダクラスは、XPathFunctionServiceインターフェイスを実装したクラスです。

2.1. XPathFunctionServiceインターフェイス

XPathFunctionServiceインターフェイスは、XPath拡張関数サービスカテゴリを表すJavaインターフェイスです。XPathFunctionServiceインターフェイスは、次のようなJavaインターフェイスです。

インターフェイスの完全修飾名
com.xfy.common.xpath.function.XPathFunctionService
インターフェイスが含まれるプラグインJARファイル
common/com.xfy/Categories.jar
インターフェイスの定義
package com.xfy.common.xpath.function;

import com.xfy.place.service.Service;
import com.xfy.place.xml.Name;

public interface XPathFunctionService extends Service {

    public Function lookup(Name name);

}

2.2. XPathFunctionServiceインターフェイスの実装

XPath拡張関数のサービスプロバイダクラスでは、XPathFunctionServiceインターフェイスを次のように実装します。

  • lookupメソッドの実装
    独自の関数名に対応する関数オブジェクトを返すように実装します。lookupメソッドの引数には、NameクラスでXPath関数の名前が渡されます。Nameクラスは、XML名前空間URI参照とローカル名から構成される、XMLでの名前を表すクラスです。名前空間URI参照とローカル名を調べて自分が知っている関数名ならば、その関数のインスタンスを返します。知らない関数名の場合はnullを返します。

example:hello()関数とexample:hello-world(string)関数を提供するXPath拡張関数のサービスプロバイダクラスMyXPathFunctionProviderを実装した例を、次に示します。

package com.example.developer.function;

import com.xfy.common.xpath.function.Function;
import com.xfy.common.xpath.function.XPathFunctionService;
import com.xfy.place.XfyObject;
import com.xfy.place.xml.Name;

public class MyXPathFunctionProvider extends XfyObject implements XPathFunctionService {

    // example:hello-world 関数
    private Function helloWorldFunction = new HelloWorldFunction();

    // example:hello 関数
    private Function helloFunction = new HelloFunction();

    // 名前に対応した XPath 関数のインスタンスを返す
    public Function lookup(Name name) {

        // 名前空間を調べる
        String namespaceUri = name.getNamespaceUri();
        if ("http://xmlns.example.com/developer/function".equals(namespaceUri)) {
            // 次にローカル名を調べる
            String localName = name.getLocalName();
            if ("hello-world".equals(localName)) {
                return this.helloWorldFunction;
            } else if ("hello".equals(localName)) {
                return this.helloFunction;
            }
        }

        // 知らない名前の場合は null を返す
        return null;
    }

}

XPathFunctionServiceインターフェイスを実装するときのポイントは、次のとおりです。

  • implements XPathFunctionProvider
  • extends XfyObject
    XPath 関数サービスプロバイダを作成するときは、xfyプラットフォームでオブジェクトを管理するためのXfyObjectクラスを継承する必要があります。
  • 名前空間URI参照とローカル名を調べて、対応する関数のインスタンスを返します。
  • 知らない名前の場合はnullを返します。

3. プラグインマニフェストファイルの記述

サービスカテゴリXPathFunctionServiceを実装したサービスプロバイダをxfyプラグインに含めるときは、プラグインマニフェストファイルに実装したクラスの情報を記述します。MyXPathFunctionProviderサービスプロバイダを例にすると、次のようにプラグインマニフェストファイルに記述します。

<manifest ...>

  <provider class="com.example.developer.function.MyXPathFunctionProvider">
    <category class="com.xfy.common.xpath.function.XPathFunctionService" />
  </provider>

</manifest>

この記述は、次の情報を表します。

4. XPath拡張関数本体となるクラスの実装

続いて、XPath拡張関数本体となるクラスを実装します。XPath拡張関数本体となるクラスは、Functionインターフェイスを実装したクラスです。

4.1. Functionインターフェース

FunctionインターフェイスはXPath拡張関数の実体を表すクラスです。

インターフェイスの完全修飾名
com.xfy.common.xpath.function.Function
インターフェイスが含まれるプラグイン JAR ファイル
common/com.xfy/Categories.jar
インターフェイスの定義
package com.xfy.common.xpath.function;

import com.xfy.common.xpath.context.Context;
import com.xfy.common.xpath.value.Value;
import com.xfy.common.xpath.value.ValueException;

public interface Function {

    public Value call(Arguments args, Context context) throws ValueException;

}

4.2. Functionインターフェイスの実装

XPath拡張関数本体となるクラスでは、Functionインターフェイスを次のように実装します。

  • callメソッドの実装
    callメソッドは、XPath 関数が評価されるタイミングで呼び出されるメソッドです。callメソッドの仕様は、次のとおりです。
    /**
     * 関数を評価する。
     *
     * @param args 関数引数
     * @param context コンテキスト
     * @return 評価結果(nullは許されない)
     * @throws ValueException 関数引数の個数が不正な場合など、関数評価エラーの場合
     */
    public Value call( Arguments args, Context context ) throws ValueException;
    callメソッドには、XPath関数への引数(Arguments args )とXPath関数の評価時のコンテキスト情報(Context context )が渡されます。callメソッドは評価結果のValueクラスのオブジェクトを返します。
    callメソッド内の処理は、おおよそ、次の順に行います。
    1. 引数の数や設定されている値をチェックする。
    2. 引数やコンテキストを元に、XPath関数を実装する目的の処理を行う。
    3. 結果をValueオブジェクトとして返す。
    callメソッド内では、必要に応じて例外処理をします。また、必要に応じて評価結果のValueオブジェクトにValueWatcherオブジェクトを設定します。

4.3. XPath関数の引数 - Argumentsクラス

Functionインターフェイスのcallメソッドには、XPath関数の引数情報としてArgumentsクラスのオブジェクトが渡されます。Argumentsオブジェクトからは、XPath関数の引数の数と、XPath関数の引数それぞれの値を取得できます。Argumentsクラスの仕様は、次のとおりです。

/**
 * 関数引数
 */
public interface Arguments {

    /**
     * 引数の個数を取得する。
     * @return 引数個数
     */
    public int getCount();

    /**
     * 引数を取得する。
     * @param index 引数位置(0〜)
     * @return 引数
     */
	public Value getArgument( int index );

}

XPath関数の引数それぞれの値は、XPath関数の評価値を表すValueクラスのオブジェクトとして取得できます。

4.4. XPath関数評価時のコンテキスト情報 - Contextクラス

XPath関数を評価するときのコンテキスト情報として、Contextクラスのオブジェクトが渡されます。Context オブジェクトからは、コンテキストノード、コンテキストサイズ、コンテキストポジション、変数一覧、変数の値などを取得できます。Contextクラスの仕様は、次のとおりです。

/**
 * XPath式評価コンテキストを表すインターフェイス
 */
public interface Context {

    /**
     * コンテキストノード
     */
    public Node getNode();

    /**
     * コンテキストポジションを持った値(先頭位置が1であることに注意)。
     */
    public Value getPosition();

    /**
     * コンテキストサイズを持った値(> 0)。
     */
    public Value getSize();

    /**
     * 指定された名前の変数があるかどうかを返す
     * 
     * @param name 変数の有修飾名
     * @return あればtrue、なければfalse
     */
    public boolean hasVariable(Name name);

    /**
     * 変数名から直接値を得る
     * 
     * @param name 変数名
     * @return 変数の値
     */
    public Value getVariable(Name name);

    /**
     * スコープに存在するすべての変数名を返す
     * 
     * @return すべての変数名を含む配列
     */
    public Name[] getVariableNames();

}

4.5. XPath評価値 - Valueクラス

XPath関数の引数を取得したり、XPath関数を評価した結果を返したりするときには、Valueクラスのオブジェクトを使用します。Valueオブジェクトは、XPath式での値を表します。Valueクラスは、Categories.jarcom.xfy.common.xpath.valueパッケージで宣言されています。

import com.xfy.common.xpath.value.Value;

XPathの基本型は以下のクラスで定義されています。

XPathの型 対応するValue継承クラス
boolean com.xfy.common.xpath.value.BooleanValue
number com.xfy.common.xpath.value.NumberValue
string com.xfy.common.xpath.value.StringValue
node-set com.xfy.common.xpath.value.NodeSetValue
object com.xfy.common.xpath.value.ObjectValue

Valueクラスでは、必要な型で値を取得するメソッドが定義されています。定義されているメソッドは次のとおりです。

/**
 * 真偽値として取得する
 * @return 真偽値
 */
public abstract boolean getAsBoolean() throws ValueException;

/**
 * 数値として取得する
 * 
 * @return 数値
 */
public abstract Number getAsNumber() throws ValueException;

/**
 * 文字列として取得する
 * @return 文字列
 */
public abstract String getAsString() throws ValueException;

/**
 * ノード集合型として取得する
 * 型がノード集合でない場合は、nullが返る
 * 
 * @return ノード集合
 */
public abstract NodeSet getAsNodeSet() throws ValueException;

/**
 * 型がノード集合型の場合、先頭のノードを取得する
 * 型がノード集合でない場合は、nullが返る
 * @return ノード
 */
public abstract Node getAsNode() throws ValueException;

4.6. 関数引数のチェック

callメソッドでは、最初にXPath関数の引数をチェックします。XPath関数の引数が正しくない場合、つまり開発者がXPath関数の記述をまちがえた場合は、例外ValueExceptionをスローします。例外をスローすることで、XVCD 開発者がXPath関数の使い方をまちがえたときときに、エラーダイアログボックスが表示されます。

XPath関数の引数には、次の2とおりのチェックをします。

  • 引数の数のチェック
    引数の数のチェックは、すべてのXPath関数で共通しています。
  • 引数の値のチェック
    引数の値のチェック内容は、それぞれのXPath関数ごとに異なります。

引数の数をチェックするために、FunctionUtilityクラスにメソッドを用意してます。FunctionUtilityクラスは、XPath関数を実装するときに役立つ便利なメソッドを集めたクラスです。引数の数をチェックするときは、FunctionUtilityクラスのcheckCountメソッドを使います。checkCountメソッドの仕様は、次のとおりです。なお、IllegalArgumentCountExceptionValueExceptionクラスから派生した例外クラスです。

/**
 * 関数引数の個数をチェックして、不正な場合、例外をスローする。
 * @param args 関数引数
 * @param expectCount 期待する個数
 * @throws IllegalArgumentCountException 引数個数が不正な場合
 */
public static void checkCount( Context context, Arguments args, int expectCount )
    throws IllegalArgumentCountException;

/**
 * 関数引数の個数をチェックして、不正な場合、例外をスローする。
 * @param args 関数引数
 * @param expectMinCount 期待する最小個数
 * @param expectMaxCount 期待する最大個数
 * @throws IllegalArgumentCountException 引数個数が不正な場合
 */
public static void checkCount( Context context, Arguments args,
                               int expectMinCount, int expectMaxCount )
    throws IllegalArgumentCountException;

/**
 * 関数引数の個数をチェックして、不正な場合、例外をスローする。
 * @param args 関数引数
 * @param expectMinCount 期待する最小個数
 * @throws IllegalArgumentCountException 引数個数が不正な場合
 */
public static void checkMinCount( Context context, Arguments args, int expectMinCount )
    throws IllegalArgumentCountException;

XPath関数の引数の数をチェックしたら、続いて、必要に応じて個々の引数の値をチェックします。引数の値をチェックする必要があるのは、例えば次のような場合です。

  • 特定の書式に従った文字列だけを、XPath関数の引数として受け付ける場合
  • 引数に指定できる数値に、上限や下限がある場合

XPath関数example:hello-world()example:hello()で実際に引数をチェックする例を、次に示します。

/** example:hello-world() */
public class HelloWorldFunction implements Function{

    public Value call(Arguments args, Context context) throws ValueException {
        // 関数引数の個数チェック
        FunctionUtility.checkCount(context, args, 0);
        :
    }

}
/** example:hello(string) */
public class HelloFunction implements Function {

    public Value call(Arguments args, Context context) throws ValueException {
        // 関数引数の個数チェック
        // example:hello(string) は引数 1 個
        FunctionUtility.checkCount(context, args, 1);
        :
    }

}

4.7. XPath関数の処理

引数をチェックし終えたら、引数やコンテキストから必要な情報を取り出して、XPath関数を実装する目的だった処理を作成します。XPath関数の処理のためにコンテキストノードを取得する必要があるときは、ContextクラスのgetNode()メソッドを使用します。ContextクラスのgetNode()メソッドの仕様は、次のとおりです。

Node node = context.getNode();

getNode()メソッドの返値に使用されているNodeクラスは、xfyプラットフォームでのDOMのノードを表すクラスです。完全修飾名はcom.xfy.common.dom.Nodeです。

XPath関数example:hello-world()example:hello()にXPath関数の処理を実装すると、次のようになります。これらのXPath関数ではコンテキストノードを使用しないため、ContextクラスのgetNode()メソッドは使用していません。

/** example:hello-world() */
public class HelloWorldFunction implements Function {

    public Value call(Arguments args, Context context) throws ValueException {
        // 関数引数の個数チェック
        FunctionUtility.checkCount(context, args, 0);

        // 関数固有の処理
        String message = "Hello World!";
        :
    }

}
/** example:hello(string) */
public class HelloFunction implements Function {

    public Value call(Arguments args, Context context) throws ValueException {
        // 関数引数の個数チェック
        // example:hello(string) は引数 1 個
        FunctionUtility.checkCount(context, args, 1);

        // 第1引数を文字列として評価
        Value v = args.getArgument(0);
        String s = v.getAsString();
        String message = "Hello " + s + "!";
        :
    }

}

4.8. 評価結果のValueオブジェクトの生成

XPath関数の目的とする処理を終えたら、その結果を表すValueオブジェクトを生成して返します。XPath関数example:hello-world()example:hello()Valueオブジェクトを生成する手順を追加すると、次のようになります。

/** example:hello-world() */
public class HelloWorldFunction implements Function {

    public Value call(Arguments args, Context context) throws ValueException {
        // 関数引数の個数チェック
        FunctionUtility.checkCount(context, args, 0);

        // 関数固有の処理
        String message = "Hello World!";

        // 結果を返す
        return new StringValue(message);
    }

}
/** example:hello(string) */
public class HelloFunction implements Function {

    public Value call(Arguments args, Context context) throws ValueException {
        // 関数引数の個数チェック
        // example:hello(string) は引数 1 個
        FunctionUtility.checkCount(context, args, 1);

        // 第1引数を文字列として評価
        Value v = args.getArgument(0);
        String s = v.getAsString();
        String message = "Hello " + s + "!";

        // 結果を返す
        return new StringValue(message);
    }

}

XPathの仕様にはJava言語のnullに対応する型は存在しないので、callメソッドではnullは返せません。処理結果が空文字列になる場合は、空文字列を表すStringValueオブジェクトを生成して返します。また、処理結果が空のノードセットになる場合は、空のノードセットを表すNodeSetValueオブジェクトを生成して返します。

// 空文字列
return new StringValue("");

// 空のノードセット
return new NodeSetValue(null);

NaN(Not a Number)の場合は、次のようにして返します。

// NaN
return new NumberValue(Double.NaN);

Valueオブジェクトを生成するときは、XPath関数で評価してValueオブジェクトを生成する元となったオブジェクトに合ったValueWatcherオブジェクトを設定します。

4.9. ValueWatcherオブジェクト

XSLTと異なり、xfy technologyで使用するXVCDでは、ソースの変更に応じて、表示をリアルタイムで更新する必要があります。ValueWatcherオブジェクトは、Valueオブジェクトを生成する元となった評価対象のオブジェクトを監視します。評価対象のオブジェクトの評価結果が変更されると、ValueWatcherオブジェクトはValueChangeListenerインターフェイスを実装したクラスに、Valueオブジェクトを再評価する必要があることを通知します。

例えば、XPathのロケーションパス式の評価結果を表すValueオブジェクトの場合、Valueオブジェクトのソースは主にボキャブラリコネクションのソースDOMツリーです。このソースに対応するValueWatcherオブジェクトは、ソースDOMのミューテーションイベントを監視します。そして、ロケーションパスの評価結果が変更される場合に、ValueChangeListnerインターフェイスを実装したクラスに通知します。

XPath関数を実装するときに設定するValueWatcherオブジェクトには、関数引数のValueWatcherオブジェクトと関数固有のValueWatcherオブジェクトがあります。

4.9.1. XPath関数の引数へのValueWatcherの設定

XPath関数の引数にValueWatcherオブジェクトを設定するために、Valueクラスには次のメソッドが用意されています。

    /**
     * 関数の引数のValueWatcherをこの値のValueWatcherとして設定する。
     * このメソッドはValueインスタンスに対して一度しか呼ぶことができません。
     * 
     * @param args
     *            関数の引数
     * @param types
     *            引数値を評価する値タイプ配列、またはnull
     * @throws IllegalArgumentException
     *             types != nullであり、argsで指定した引数の数とtypesの配列サイズが異なるとき
     * @throws RuntimeException
     *             同じインスタンスに対してこのメソッドが2度呼ばれたとき
     * @see Value#isArgumentWatcherFixed()
     */
    public void setArgumentWatchers(Arguments args, int[] types);

    /**
     * @return すでにsetArgumentWatchers()メソッドが呼ばれていればtrueを返す
     */
    public boolean isArgumentWatcherFixed();

setArgumentWatcher( Arguments args, int[] types ) メソッドの第2引数には、次の値を設定します。

Value.TYPE_BOOLEAN
引数の値に、boolean型で取得した値の変更を監視するValueWatcherを設定する。
Value.TYPE_NUMBER
引数の値に、Number型で取得した値の変更を監視するValueWatcherを設定する。
Value.TYPE_STRING
引数の値に、String型で取得した値の変更を監視するValueWatcherを設定する。
Value.TYPE_NODESET
引数の値に、NodeSet型で取得した値の変更を監視するValueWatcherを設定する。
Value.TYPE_OBJECT
引数の値に、Object型で取得した値の変更を監視するValueWatcherを設定する。
Value.TYPE_ALL
引数の値に、上記のすべての型で取得した値の変更を監視するValueWatcherを設定する。
Value.TYPE_NULL
引数の値にはValueWatcherを設定しない。

XPath関数の実装内でsetArgumentWatcher( Arguments args, int[] types ) メソッドが呼び出されなかった場合は、callメソッドを呼び出したクラスが代理で引数のValueWatcherを設定します。XPath関数の代理で設定される引数のValueWatcherは、次のようなコードで設定されます。

Function func = ...;
Arguments args = ...;
Context context = ...;

// 関数呼び出し
Value result = func.call(args, context);

// 引数のValueWatcherが未設定ならば代理として設定する
if (!result.isArgumentWatcherFixed()) {

  result.setArgumentWatchers(args, null);
  // 第2引数にnullを指定した場合、すべての関数引数からValue.TYPE_ALLでValueWatcherを取得して設定する

}

ただし、Value.TYPE_ALL で値の変更を監視すると、関数の評価値にまったく影響を与えない変更もValueChangeListenerに通知されます。その結果、パフォーマンスが劣化するおそれがあります。そのため、なるべく関数側で引数の値のValueWatcherを設定することを推奨します。

4.9.2. 実装するXPath関数に固有のValueWatcherの設定

引数から取得されるValueWatcherオブジェクトでは変更を通知できない更新イベントには、実装するXPath関数に固有のValueWatcherオブジェクトを作成して、評価した値に設定する必要があります。

com.xfy.common.xpath.valueで定義されているValueクラスには、ValueWatcherオブジェクトを引数にとるコンストラクタがあります。XPath関数に固有のValueWatcherオブジェクトを設定するときは、このコンストラクタを使用します。