目次へ

4. XML サポート

2007.01.18 株式会社四次元データ 鈴木 圭

本章では Mustang における XML サポートについて解説を行います。JAXB、JAX-WS の新バージョンがサポートされたほか、StAX(Streaming API for XML)と XML Digital Signature API が新しくサポートされました。StAX は DOM や SAX に続く第三の XML パーサです。XML Digital Signature API は XML デジタル署名を扱うための API です。

4.1. StAX 1.0

Mustang では StAX(Streaming API for XML)のバージョン 1.0 が新しくサポートされました。StAX とは JSR 173 で仕様制定されている XML のためのストリーミング API のことで、SAX(Simple API for XML)や DOM(Document Object Model)と同じく XML の読み込みや書き込みを行うための API です。StAX はイテレータのようなインタフェースによって XML の読み書きを行います。そのため DOM のように大量のメモリを消費することなく、SAX のようにコールバックによる完全に受け身のスタイルでもない、ちょうど SAX と DOM の中間のような特徴を持ちます。

StAX のイテレータ方式による XML のパージングは、プログラムの制御によって要素の取得やスキップを行うことから「Pull 型」や「Pull モデル」などと呼ばれます。他の Pull 型の XML パーサには XPP(XML Pull Parser)CyberNeko Pull Parser などがあります。

StAX では XML を読み書きするために Cursor API と Event Iterator API という二種類の API が提供されています。どちらもイテレータのような使い方であることは同じですが、Cursor API は低レベル API であり、Event Iterator API は Cursor API の上位レイヤの API という位置付けです。

以下に Cursor API と Event Iterator API それぞれを用いた場合のコードの断片を示します:

Cursor API Event Iterator API
XMLStreamReader reader;
...
while(reader.hasNext()) {
    int eventCode = reader.next();
    ...
}
XMLEventReader reader;
...
while(reader.hasNext()) {
    XMLEvent event = reader.nextEvent();
    ...
}

詳しくは後述しますが、Cursor API を用いた場合は int 型で表されるイベントのコードを受け取りますが、Event Iterator API を用いた場合はイベントを表すオブジェクトを受け取るという違いがあります。

Cursor API

Cursor API では、XML の読み取りには javax.xml.stream.XMLStreamReader インタフェース、書き込みには javax.xml.stream.XMLStreamWriter インタフェースを用います。

Cursor API による読み取り

XML データの読み取りに使用する XMLStreamReader インタフェースのインスタンスを取得するには javax.xml.stream.XMLInputFactory の createXMLStreamReader メソッドを使用します。

XMLStreamReader を用いると、XML データの先頭から順番に読み取り専用アクセスを行うことができます。XMLStreamReader の使い方は、読み取ることができる要素が残っていることを hasNext メソッドで判定し、next メソッドで次の要素へ移動する、その他のメソッドで現在の要素の値などを取得する、という具合です。

以下に XMLStreamReader の動作を調べるためのサンプルとして、sample-for-stax.xml というファイルから XML データを読み込み、出現したイベントの種類を書き出すプログラムを示します:

import java.io.FileReader;
import java.io.IOException;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

public class NewXMLStreamReaderMain {
    private static final String[] eventTypeNames = { "", "START_ELEMENT",
            "END_ELEMENT", "PROCESSING_INSTRUCTION", "CHARACTERS", "COMMENT",
            "SPACE", "START_DOCUMENT", "END_DOCUMENT", "ENTITY_REFERENCE",
            "ATTRIBUTE", "DTD", "CDATA", "NAMESPACE", "NOTATION_DECLARATION",
            "ENTITY_DECLARATION", };

    public static void main(String[] arguments) throws IOException {
        try {
            XMLInputFactory factory = XMLInputFactory.newInstance();
            XMLStreamReader reader =
                factory.createXMLStreamReader(new FileReader("sample-for-stax.xml"));

            while (reader.hasNext()) {
                System.out.println(eventTypeNames[reader.next()]);
            }
            reader.close();
        } catch (XMLStreamException exception) {
            exception.printStackTrace();
        }
    }
}

入力に使用する sample-for-stax.xml の内容は以下のものを使用しました:

<person><name>taro</name><age>10</age></person>

以下にプログラムの出力結果と XML データの対応を示します:

START_ELEMENT    ... <person>
START_ELEMENT    ... <name>
CHARACTERS       ... taro
END_ELEMENT      ... </name>
START_ELEMENT    ... <age>
CHARACTERS       ... 10
END_ELEMENT      ... </age>
END_ELEMENT      ... </person>
END_DOCUMENT     ... XML データの終端

プログラムの全体の流れは、最初に XMLInputFactory#newInstance メソッドで XMLInputFactory インスタンスを取得しています。次に XMLInputFactory#createXMLStreamReader メソッドで XMLStreamReader インスタンスを取得しています。XMLInputFactory#createStreamReader メソッドにはいくつかのオーバーロードが存在しますが、今回は java.io.Reader を引数に取るバージョンを使用しています。あとは while ループで XML データを読み取り、対応するイベントの種類を出力します。また、重大なエラーが発生した場合は javax.xml.stream.XMLStreamException が投げられます(重大でないエラーに関しては javax.xml.stream.XMLReporter インタフェースを介してエラー報告が行われます)。

Cursor API による書き出し

XML データの書き出しに使用する XMLStreamWriter インタフェースのインスタンスを得るには、javax.xml.stream.XMLOutputFactory クラスの createXMLStreamWriter メソッドを使用します。XMLStreamWriter には名前空間の宣言をはじめ、DTD やコメントなど XML を構築するために必要なメソッドが含まれています。

以下に XMLStreamWriter によって標準出力に XML データを出力する例を示します:

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

public class XMLStreamWriterMain
{
    public static void main(String[] arguments)
    {
        try {
            XMLOutputFactory factory = XMLOutputFactory.newInstance();
            factory.setProperty("javax.xml.stream.isRepairingNamespaces", Boolean.TRUE);

            XMLStreamWriter writer = factory.createXMLStreamWriter(System.out);
            writer.writeStartDocument();
            writer.writeStartElement("example", "hoge", "http://example.com");
            writer.writeAttribute("piyo", "fuga");
            writer.writeCharacters("gofu");
            writer.writeEndElement();
            writer.writeEndDocument();
            writer.close();
        } catch(XMLStreamException exception) {
            exception.printStackTrace();
        }
    }
}

出力例:

<?xml version="1.0" ?><example:hoge xmlns:example="http://example.com" piyo="fuga">gofu</example:hoge>

手順としては XMLOutputFactory#newInstance メソッドで XMLOutputFactory インスタンスを取得します。factory.setProperty の部分では出力する XML に名前空間の宣言(「xmlns:example="http://example.com"」)を含めるようにプロパティを設定しています。そして、先ほど説明したとおり XMLOutputFactory#createXMLStreamWriter メソッドで XMLStreamWriter インスタンスを作成しています。残りの部分では、作成した XMLStreamWriter のメソッド(writeStartDocument や writeAttribute など)で XML データを出力しています。

このサンプルでは出てきませんでしたが、入れ子要素を持たない要素(<br /> など)を出力する場合は XMLStreamWriter#writeEmptyElement(String) を使用します。

Event Iterator API

Event Iterator API はパースした要素を int 型のイベント・コードではなく javax.xml.stream.events.XMLEvent オブジェクトとして得ることができるため、Cursor API よりも手軽に利用することができます。Event Iterator API では XML データの読み込みには javax.xml.stream.XMLEventReader インタフェース、書き込みには javax.xml.stream.XMLEventWriter インタフェースを用います。

Event Iterator API による読み込み

XML データの読み込みに使用する XMLEventReader インタフェースのインスタンスは、XMLInputFactory クラスの createXMLEventReader メソッドによって取得します。

XMLEventReader では hasNext メソッドで読み取ることができる要素が残っていることを判定し、nextEvent で XMLEvent オブジェクトを取得します。XMLEvent のサブインタフェースには開始要素を表す StartElement やコメントを表す Comment、文字列を表す Characters(いずれも javax.xml.stream.events パッケージ)などがあります。XMLEvent インタフェースの isXXXXX メソッド(isAttribute や isStartElement など)でイベントの種類を判定、asXXXXX メソッド(asStartElement や asCharacters など)で実際のインタフェースにキャストされたインスタンスを得ることができます。

以下に、Event Iterator API によって XML データを読み取るプログラムを示します。動作は先ほどの Cursor API の時と同様 sample-for-stax.xml というファイルを読み取り、出現するイベントを表示する、というものです:

import java.io.FileReader;
import java.io.IOException;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;

public class NewXMLEventReaderMain
{
    public static void main(String[] arguments) throws IOException
    {
        try {
            XMLInputFactory factory = XMLInputFactory.newInstance();
            XMLEventReader  reader  =
                factory.createXMLEventReader(new FileReader("sample-for-stax.xml"));

            while(reader.hasNext()) {
                XMLEvent event = reader.nextEvent();
                System.out.println(event);
            }
            reader.close();
        } catch(XMLStreamException exception) {
            exception.printStackTrace();
        }
    }
}

入力ファイル sample-for-stax.xml の内容は先ほどと同じです:

<person><name>taro</name><age>10</age></person>

出力結果:

<?xml version="1.0" encoding='null' standalone='no'?>
<person>
<name>
taro
</name>
<age>
10
</age>
</person>
ENDDOCUMENT

Event Iterator API による書き出し

XML データの書き出しに使用する XMLEventWriter インタフェースのインスタンスは、XMLOutputFactory クラスの createXMLEventWriter メソッドによって取得します。

XMLEventWriter の add(XMLEvent) メソッドを使用することで、新しいイベントを追加することができます。XMLEventWriter#add(XMLEvent) メソッドに渡すオブジェクトは javax.xml.stream.XMLEventFactory クラスの createXXXXX メソッド(createStartElement や createCharacters など)を用いて作成します。XMLEventFactory のインスタンスは XMLEventFactory.newInstance メソッドで取得します。

以下に XMLEventWriter を用いて XML データを標準出力に出力する簡単なサンプルを示します:

import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;

public class XMLEventWriterMain
{
    public static void main(String[] arguments)
    {
        try {
            XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
            outputFactory.setProperty("javax.xml.stream.isRepairingNamespaces", Boolean.TRUE);

            XMLEventFactory eventFactory = XMLEventFactory.newInstance();
            XMLEventWriter  writer       = outputFactory.createXMLEventWriter(System.out);
            writer.add(eventFactory.createStartDocument());
            writer.add(eventFactory.createStartElement("example", "http://example.com", "hoge"));
            writer.add(eventFactory.createAttribute("piyo", "fuga"));
            writer.add(eventFactory.createCharacters("gofu"));
            writer.add(eventFactory.createEndElement("example", "http://example.com", "hoge"));
            writer.add(eventFactory.createEndDocument());
            writer.close();
        } catch(XMLStreamException exception) {
            exception.printStackTrace();
        }
    }
}

出力例:

<?xml version="1.0"?><example:hoge xmlns:example="http://example.com" piyo="fuga">gofu</example:hoge>

処理としては Cursor API による書き出しのサンプルと同じ XML データを出力しています。

4.2. JAXB 2.0

Mustang では JAXB(Java Architecture for XML Binding)の新バージョンである JAXB 2.0(JSR 222)のサポートが行われました。JAXB とは Java オブジェクトと XML のバインディングを行うための技術で、Java オブジェクトを XML に書き出すことや、XML から Java オブジェクトを復元することができます。また、XML Schema による Validation にも対応しています。なお、JAXB 1.x に関しては「J2SE(Java2 Standard Edition) -> XML DOM XSLT(javax.xml.parsers他) -> 9. JAXB(1)」にて解説していますので、参考にしてください。

以下に JAXB 2.0 の主な変更点を示します:

  • XML Schema の完全なサポート
    - JAXB 1.x では XML Schema の一部の機能がサポートされませんでしたが、JAXB 2.0 では XML Schema を完全にサポートしています。
  • Java と XML Schema の双方向のバインディング
    - JAXB 1.x では XML Schema を基にして Java クラスを生成することしかできませんでしたが、JAXB 2.0 では Java クラスから XML Schema を生成することも可能になりました。
  • アノテーションによるバインディングの指定
    - Java クラスから XML Schema を作成するときのパラメータを指定するためのアノテーションが導入され、バインディングの指定を簡単に行うことができるようになりました。
  • パーシャル・バインディング
    - アプリケーションに関係のある XML ドキュメントの一部をマッピングする機能が加わりました。

この中でも特に、Java クラスから XML Schema を生成できるようになったことが大きな改善であるといえるでしょう。以前は手作業で XMLSchema を作成する作業が高いハードルであり、また Java クラスの変更された場合の XMLSchema の修正が煩雑でもありました。しかし、JAXB 2.0 では schemagen というツールによって XMLSchema を生成することが可能であるだけではなく、アノテーションによって柔軟にバインディング方法を指定することができるため、以前よりも JAXB を利用する敷居が下がったと言えます。

コマンドライン・ツール

以前は XML Schema から Java クラスを作成する xjc だけが提供されていましたが、新しく Java クラスから XML Schema を生成する schemagen というコマンドライン・ツールが追加されました。これらのコマンドライン・ツールは(JDK をインストールしたディレクトリを JAVA_HOME として)JAVA_HOME/bin ディレクトリに配置されています。

  • xjc
    - XML Schema から Java クラスを作成する。
  • schemagen
    - Java クラスから XML Schema を作成する。

schemagen の使い方は以下のようになっています:

Usage: schemagen [-options ...] <java files>
Options:
    -d <path>         :  出力先ディレクトリ
    -cp <path>        :  ユーザ定義ファイルの検索パス
    -classpath <path> :  ユーザ定義ファイルの検索パス
    -version          :  バージョン情報

例えば、src ディレクトリにある全ての Java ソースファイルを対象に XML Schema ファイルを生成するには以下のように使用します:

schemagen src/*.java

主なアノテーション

Java クラスから XML Schema を作成することができるようになりました。しかし、Java クラスのフィールドを XML の要素とするか属性とするかなど、 XML Schema を生成する際に曖昧さがあります。そこで、どのような XML Schema を生成するか指定するためのアノテーションが導入されました。導入されたアノテーションは javax.xml.bind.annotation パッケージに含まれており、全部で三十個近くあります。ここではその中からいくつかピックアップして紹介します:

  • XmlSchema
    - パッケージ名を XML 名前空間にマッピングします。
  • XmlRootElement
    - トップ・レベルのクラスや列挙型を XML 要素にマッピングします。
  • XmlElement
    - JavaBean プロパティやフィールドを XML 要素にマッピングします。
  • XmlAttribute
    - JavaBean プロパティやフィールドを XML 要素にマッピングします。
  • XmlEnum
    - 列挙型を XML 表現にマッピングします。
  • XmlID
    - JavaBean プロパティやフィールドを XML ID にマッピングします。
  • XmlSchemaType
    - JavaBean プロパティやフィールド、パッケージを XML Schema の組み込み型にマッピングします。
  • XmlType
    - トップ・レベルのクラスや列挙型を XML Schema タイプにマッピングします。

それでは実際にアノテーションを使用した Java クラスの作成から XML Schema の生成、それを用いた Java オブジェクト/XML 間の相互変換を行うサンプルを示します。

アノテーションの使用

アノテーションの使用例として、人を表す Person クラス(Person.java)を示します:

// Person.java
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="人")
public class Person
{
    public enum Language
    {
        C, CPlusPlus, D, CSharp, Scheme, StandardML, Lisp, Basic,
        ObjectiveCaml, Python, Haskell, Perl, Delphi, Prolog,
        JavaScript, VisualBasic, ActionScript, Icon, Ruby, PHP
    }

    private String name;
    private int age;
    private Language[] languages;

    @XmlAttribute(name="年齢")
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @XmlAttribute(name="名前")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @XmlElementWrapper(name="使用言語")
    @XmlElement(name="言語")
    public Language[] getLanguages() {
        return languages;
    }

    public void setLanguages(Language... languages) {
        this.languages = languages;
    }
}

この Person クラスは{名前,年齢,使用言語}という三つの情報を保持します。また、いくつかのアノテーションを使用しています。使用しているアノテーションは以下の四種類です:

  • XmlRootElement
  • XmlAttribute
  • XmlElementWrapper
  • XmlElement

XmlRootElement はトップ・レベルのクラスや列挙型に対して使用するアノテーションです。パラメータには name(XML でのローカル名)や namespace(XML 要素の名前空間)を指定可能です。ここでは name 属性に「"人"」を指定しています。

XmlAttribute は JavaBean プロパティやフィールドから XML の属性へのマッピングを指定するためのアノテーションです。パラメータには name(XML での属性名)、namespace(属性の名前空間)、required(属性が必須であるかどうか)を指定することができます。ここでは getAge 及び getName メソッドに XmlAttribute を使用しています(それぞれ属性名を「年齢」及び「名前」とするために)。

getLanguages メソッドには XmlElementWrapper と XmlElement という二つのアノテーションを使用しています。XmlElement は JavaBean プロパティやフィールドから XML 要素へのマッピングを指定するためのアノテーションです。パラメータには name(XML でのローカル名)や defaultValue(デフォルト値)、required(要素が必須であるかどうか)などを指定することができます。XmlElementWrapper は XmlElement などと一緒に使用するアノテーションで、生成される XML 要素の上位要素を生成するために使用します。getLanguages メソッドの戻り値は Language の配列となっているため、XmlElementWrapper を指定しないと生成される XML は

  ...
  <言語>C</言語>
  <言語>CPlusPlus</言語>
  <言語>D</言語>
  <言語>CSharp</言語>
  ...

という具合になります。一方、XmlElementWrapper を使用すると

  ...
  <使用言語>
    <言語>C</言語>
    <言語>CPlusPlus</言語>
    <言語>D</言語>
    <言語>CSharp</言語>
  </使用言語>
  ...

という具合に、複数の要素を一つにまとめる要素を生成させることができます。このように XmlElementWrapper は配列やコレクションなどを一つにまとめる XML 要素を生成させる場合に使用します。

XML Schema の生成

作成した Person クラス(Person.java)から XML Schema を生成するには、前述の通り schemagen というコマンドライン・ツールを使用します。今回の場合は以下のコマンドを実行します:

>schemagen Person.java

実行に成功すると以下のようなメッセージが出力されます:

注:Writing schema1.xsd

出力メッセージの通り、XML Schema ファイルは schema1.xsd という名前で作成されます。schema1.xsd の内容は以下の通りです:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="&#20154;" type="person"/>
             <!-- 「&#20154;」は「人」を XML エスケープしたもの -->

  <xs:complexType name="person">

    <xs:sequence>
      <xs:element name="&#20351;&#29992;&#35328;&#35486;" minOccurs="0">
                 <!-- 「&#20351;&#29992;&#35328;&#35486;」は「使用言語」を XML エスケープしたもの -->
        <xs:complexType>
          <xs:sequence>
            <xs:element name="&#35328;&#35486;" type="language" maxOccurs="unbounded" minOccurs="0"/>
                       <!-- 「&#35328;&#35486;」は「言語」を XML エスケープしたもの -->
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:sequence>
    <xs:attribute name="&#24180;&#40802;" type="xs:int" use="required"/>
                  <!-- 「&#24180;&#40802;」は「年齢」を XML エスケープしたもの -->
    <xs:attribute name="&#21517;&#21069;" type="xs:string"/>
                  <!-- 「&#21517;&#21069;」は「名前」を XML エスケープしたもの -->
  </xs:complexType>

  <xs:simpleType name="language">
    <xs:restriction base="xs:string">
      <xs:enumeration value="PHP"/>
      <xs:enumeration value="Ruby"/>
      <xs:enumeration value="Icon"/>
      <xs:enumeration value="ActionScript"/>
      <xs:enumeration value="VisualBasic"/>
      <xs:enumeration value="JavaScript"/>
      <xs:enumeration value="Prolog"/>
      <xs:enumeration value="Delphi"/>
      <xs:enumeration value="Perl"/>
      <xs:enumeration value="Haskell"/>
      <xs:enumeration value="Python"/>
      <xs:enumeration value="ObjectiveCaml"/>
      <xs:enumeration value="Basic"/>
      <xs:enumeration value="Lisp"/>
      <xs:enumeration value="StandardML"/>
      <xs:enumeration value="Scheme"/>
      <xs:enumeration value="CSharp"/>
      <xs:enumeration value="D"/>
      <xs:enumeration value="CPlusPlus"/>
      <xs:enumeration value="C"/>
    </xs:restriction>
  </xs:simpleType>
</xs:schema>

Java オブジェクトと XML の相互変換

作成した Person クラスと XML Schema ファイル(schema1.xsd)を用いて、Java オブジェクトと XML の相互変換を行うプログラムは以下のようになります:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.xml.sax.SAXException;

public class Main
{
    public static void main(String[] args)
        throws JAXBException, SAXException, IOException
    {
        // Person クラスのためのコンテキストを作成.
        JAXBContext context = JAXBContext.newInstance(Person.class);

        // 使用する Schema オブジェクトを作成.
        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema        schema  = factory.newSchema(new File("schema1.xsd"));

        // Java オブジェクトから XML に変換するための Marshaller を作成.
        Marshaller marshaller = context.createMarshaller();
        marshaller.setSchema(schema);
        marshaller.setProperty(Marshaller.JAXB_ENCODING, "Shift_JIS");
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

        // XML に変換する Java オブジェクト (Person) を作成.
        Person person = new Person();
        person.setName("太郎");
        person.setAge(77);
        person.setLanguages(Person.Language.C, Person.Language.CPlusPlus);

        // XML に変換.
        marshaller.marshal(person, new FileOutputStream("person.xml"));

        // XML から Java オブジェクトに変換するための Unmarshaller を作成.
        Unmarshaller unmarshaller = context.createUnmarshaller();
        unmarshaller.setSchema(schema);

        // XML から Java オブジェクトに変換.
        Person person2 = (Person)unmarshaller.unmarshal(new FileInputStream("person.xml"));

        // 内容を表示.
        System.out.println("name     : " + person2.getName());
        System.out.println("age      : " + person2.getAge());
        for(Person.Language language : person2.getLanguages()) {
            System.out.println("language : " + language);
        }
    }
}

実行結果(出力):

name     : 太郎
age      : 77
language : C
language : CPlusPlus

処理の流れはプログラム中のコメントで示したとおり、最初に Java オブジェクトを XML に変換し、次に XML から Java オブジェクトに変換(オブジェクトを復元)しています。

Java オブジェクトと XML の相互変換に関しては JAXB 1.x と同じなので、要点だけをまとめます:

  • 最初に JAXBContext を作成する
  • Java オブジェクトから XML への変換には Marshaller の marshal メソッドを使用する
  • XML から Java オブジェクトへの変換には Unmarshaller の unmarshal メソッドを使用する
  • XML Schema を使用するには、Marshaller や Unmarshaller の setSchema メソッドを使用する

4.3. JAX-WS 2.0 / Web Services Metadata

JAX-WS 2.0(JSR 224)は JAX-RPC を前身とする Web サービスのための API です(JAX-WS のバージョンは 2.0 から始まります)。JAX-PRC からの違いとして、データ・バインディングに JAXB を使用することや、Web サービス・メタデータ(JSR 181)で定義されているアノテーションを用いて開発することができるようになりました。通常のクラスに WebService というアノテーションを付加することで、そのクラスの持つメソッドを Web サービス経由で呼び出すことができるようになります。また、コマンドライン・ツールのサポートにより WSDL(Web Service Description Language:Web サービスの情報を記述するための XML ベースの言語)について詳しく知らなくても Web サービスの開発/利用を行うことができます。JAX-WS の API は javax.xml.ws 以下のパッケージ、Web サービス・メタデータの API は javax.jws 以下のパッケージに配置されています。

アノテーション

JAX-WS には以下のアノテーションなどが含まれます(javax.xml.ws パッケージ):

  • WebEndpoint
    - 生成される WSDL ファイルの wsdl:port 要素
  • WebServiceProvider
    - Web サービスの提供者を表す Provider インタフェースの実装クラスであることを示すアノテーションです。
  • WebServiceClient
    - 生成されたサービス・インタフェースであることを表すアノテーションです。
  • WebFault
    - サービス定義の例外クラスであることを表すアノテーションです。

Web サービス・メタデータ(javax.jws パッケージ)では以下に示すアノテーションなどが含まれます:

  • WebService
    - Web サービスの実装クラスであることを指定するためのアノテーションです。
  • WebMethod
    - Web サービスのオペレーションとして公開するメソッドをカスタマイズするためのアノテーションです。
  • WebParam
    - Web サービスのメッセージ・パートと XML 要素とのパラメータのマッピングをカスタマイズするためのアノテーションです。
  • WebResult
    - WSDL パートと XML 要素との戻り値のマッピングをカスタマイズするためのアノテーションです。

この中で WebService アノテーションが特に重要です。Web サービスとして公開したい処理を含むクラスを WebService アノテーションで修飾し、wsgen を利用して必要なファイルを自動生成すれば、Web サービスとして公開するためのファイルは全て揃います。

コマンドライン・ツール

JAX-WS 2.0 では wsgen と wsimport という二つのコマンドライン・ツールが提供されています:

  • wsgen
    - wsgen は JAX-WS を用いた Web サービスの配備/実行を行うために必要なファイルを生成するためのコマンドライン・ユーティリティです。wsgen によって生成されたファイルは基本的に変更せずに使用します。
  • wsimport
    - wsimport は Web サービスのクライアント・プログラムを作成するときに利用するコマンドライン・ツールです。引数に指定された WSDL ファイルを読み取り、Web サービスを利用するために必要な Java クラスを生成します。wsimport によって生成されたファイルを利用することで、Web サービスのクライアント・プログラムを簡単に作成することができます。

wsgen と wsimport を使用することで、WSDL について詳しく知らなくても、Web サービスを提供するプログラムや利用するプログラムを作成することができます。

Web サービスの作成

それでは実際に Web サービスを作成する方法を順を追って説明します。作成するのは BMI(肥満指数)を求めるサービスです:

// MyWebService.java
package com.example.mustang.ws;

import javax.jws.WebService;

@WebService
public class MyWebService
{
    /**
     *
     *  BMI 指数を求める.
     *
     *  @param height 身長(m)
     *  @param weight 体重(kg)
     *
     */
    public double calculateBmi(double height, double weight)
    {
        return weight / (height * height);
    }
}

最低限必要なことは、Web サービスとして公開するメソッドを含むクラスに WebService アノテーションを付加することです。ここでは BMI を求めるメソッド(calculateBmi)を持つ MyWebService クラスを作成しました。

次に、作成した MyWebService を公開するための main メソッドを含むクラス MyWebServiceMain を作成します。Web サービスを公開するには、それを公開するサーバが必要となりますが、ここでは javax.xml.ws.Endpoint クラスを利用することにします。Endpoint は Web サービスのエンド・ポイントを表すクラスであり、static メソッドの create や publish によって Endpoint インスタンスを作成することができます。create メソッドや publish メソッドには引数の異なるいくつかの形式がありますが、ここでは publish(String address, Object implementor) という形式の publish メソッドを使用して、以下のように MyWebServiceMain クラスを実装します:

// MyWebServiceMain.java
package com.example.mustang.ws;

import javax.xml.ws.Endpoint;

public class MyWebServiceMain
{
    public static void main(String[] arguments)
    {
        Endpoint.publish("http://localhost:8080/MyWebService",
                         new MyWebService());
    }
}

publish(String address, Object implementor) メソッドの第一引数には Web サービスにアクセスするための URI、第二引数には Web サービスの実装クラス(WebService アノテーションを付加したクラス)のインスタンスを指定します。

ここまでの作業が終了したら、一度プログラムをコンパイルします:

javac com/example/mustang/ws/*.java

次に wsgen を用いて Web サービスとして公開するために必要なファイルを生成します:

wsgen -cp . com.example.mustang.ws.MyWebService

wsgen の引数には、生成されるファイルの出力先ディレクトリと、WebService アノテーションを付加したクラスを指定します。出力先は -cp オプションで指定します(カレントディレクトリに出力する場合でも明示的に指定しなければならないようです)。このコマンドの実行で com.example.mustang.ws.jaxws パッケージにいくつかのファイルが生成されます。

これで Web サービスを提供するために必要な準備が整いましたので、試しに MyWebServiceMain を実行してみましょう:

java com.example.mustang.ws.MyWebServiceMain

MyWebServiceMain を実行したままの状態で以下の URL にアクセスすると、WSDL ファイルを見ることができます:

http://localhost:8080/MyWebService?wsdl

クライアント・プログラムの作成

クライアント・プログラム(Web サービスを利用するプログラム)の作成は、wsimport を用いて、WSDL ファイルから Web サービスを利用するためのファイルを生成し、それを使用してクライアント・プログラムを作成します(というやり方がクライアント・プログラムを作成する一つの方法です)。

wsimport の使い方は、引数に WSDL ファイルのパスまたは URI を指定します。例えば先ほどの MyWebServiceMain を実行した状態で以下のように wsimport を実行すると、MyWebService を利用するためのファイルがカレントディレクトリに生成されます:

wsimport http://localhost:8080/MyWebService?wsdl

-d オプションで生成されるファイルの出力先を指定することもできます。このコマンドの実行で com.example.mustang.ws パッケージに以下のファイルが生成されます:

  • CalculateBmi.class
  • CalculateBmiResponse.class
  • MyWebService.class
  • MyWebServiceService.class
  • ObjectFactory.class
  • package-info.class

生成されたファイルを利用して Web サービスを利用したクライアント・プログラム MyWebServiceClientMain.java を作成します。wsimport によって生成されたファイルは多いですが、全てを直接利用するわけではありません。MyWebServiceClientMain.java の実装は以下のようになります:

// MyWebServiceClientMain.java
package com.example.mustang.ws;

import java.util.Scanner;

public class MyWebServiceClientMain
{
    public static void main(String[] arguments) throws Exception
    {
        Scanner scanner = new Scanner(System.in);
        System.out.print("身長(cm)>> ");
        double height = scanner.nextDouble();
        System.out.print("体重(kg)>> ");
        double weight = scanner.nextDouble();

        MyWebServiceService service = new MyWebServiceService();
        MyWebService port = service.getMyWebServicePort();
        double bmi = port.calculateBmi(height/100, weight);

        System.out.println("あなたの BMI 指数は " + bmi + " です");
    }
}

生成されたファイルの中の MyWebServiceService が Web サービスを利用するためのクラスです。MyWebServiceService#getMyWebServicePort メソッドから得られる MyWebService インスタンスによって Web サービスを利用することができます。

プログラム全体としては

  1. 身長、体重を入力する
  2. Web サービス経由で BMI を求める
  3. 求めた BMI を表示する

という処理を行っています。以下に実行例を示します(太字は入力項目):

身長(cm)>> 179
体重(kg)>> 65
あなたの BMI 指数は 20.286507911738084 です

4.4. XML Digital Signature API

XML Digital Signature API は JSR 105 で仕様制定されている、XML デジタル署名のための API です。W3C による勧告 XML-Signature Syntax and Processing に則り XML デジタル署名の生成及び検証を行うことができます。JSR 105 ではさらに、W3C 勧告 XML-Signature XPath Filter 2.0 及び Exclusive XML Canonicalization Version 1.0 もサポートすべき(必須ではない)としています。XML-Signature XPath Filter 2.0 は XML の一部分だけに対して署名を行うために XPath を使用するための仕様、Exclusive XML Canonicalization Version 1.0 は 署名された XML を他の XML に埋め込む場合などに生じる問題を回避するための XML 正規化の仕様です。

JSR 105 では「特定の XML 処理メカニズム(DOM など)に依存してはならない」「サードパーティによる実装がプラグイン可能でなければならない」など、柔軟性や拡張性に関する考慮が行われています。しかし、現状ではデフォルトの処理メカニズムとして DOM のサポートを必須とし、また、高レベル API はサポートしないという内容になっています。

XML デジタル署名

XML デジタル署名は、その名前から XML データ専用のデジタル署名であると思われるかもしれませんが、作成した署名データが XML であるというだけで、一般のデジタル署名と同様、任意のデータに対して署名を行うことができます。とはいっても、XML 正規化(XML データの表記上の揺らぎに対処するための処理)や XML 文書の一部に対して署名を行う機能など、XML データに特化した仕様が含まれているため、XML データを扱うことが第一に考えられているといえます。XML デジタル署名の利用例としては、XML 形式でメッセージのやり取りを行う SOAP(Simple Object Access Protocol)などが挙げられます。

XML デジタル署名での署名対象と署名データの保存の仕方には、以下に示す三種類があります:

XML デジタル署名の保存形式
  • Detached Signature
    - 署名と署名対象が別々に保管される形式。
  • Enveloping Signature
    - 署名の XML データの中に署名対象が包含される形式。
  • Enveloped Signature
    - 署名対象の XML データの中に署名が包含される形式。

どの形式で署名を保存するかは、用途に応じて決定します。

XML デジタル署名における署名の手順は、大きく二つに分けることができます:

  1. 署名対象のダイジェスト値を求める
  2. 署名自体のダイジェスト値を求める

最初に署名対象のダイジェスト値を求めます。これによって、後から署名対象の改ざんが行われていないことを検証することができます。次に署名自体が改ざんされていないことを後から検証可能とするために、署名自体のダイジェスト値を求めます。

実際に作成される署名データは、大まかには以下のような形式をしています:

<Signature>
  <SignedInfo>
    <CanonicalizationMethod />
    <SignatureMethod />
    <Reference URI="...">
      <DigestMethod>...</DigestMethod>
      <DigestValue>...</DigestValue>
      ...
    </Reference>
  </SignedInfo>
  <SignatureValue>...</SignatureValue>
  <KeyInfo>...</KeyInfo>
  <Object>...</Object>
  ...
</Signature>

Signature が署名のルート要素です。その中の SignedInfo 以下には署名対象のダイジェスト値やダイジェスト値を求めるときに使用したアルゴリズムなどの情報が含まれます。また、SignatureValue には SignedInfo のダイジェスト値が含まれます。KeyInfo には SignatureValue を求めるときに使用した鍵の情報(通常は公開鍵)が含まれます。Object は署名対象を内部に取り込む場合に使用します。

SignedInfo 以下の CanonicalizationMethod 及び SignatureMethon は(署名対象のリソースではなく)SignedInfo 以下のダイジェスト値を求めるときに必要な情報で、それぞれ XML 正規化のアルゴリズムとダイジェスト値を求めるときに使用したアルゴリズムを指定します(SignedInfo 以下の XML データを CanonicalizationMethod で指定されたアルゴリズムで正規化したもののダイジェスト値を SignatureMethod で指定されたアルゴリズムで求めた値が SignatureValue の値となります)。XML 正規化については簡単に説明すると、XML では空白の扱いや空要素タグの記述方法など、記述に関していくらかの自由度があるため、XML 文書の意味は同じでもバイナリ表現が異なることがあります。そこで XML 正規化という処理を行ってからダイジェスト値を求めるという作業が行われます。

署名対象に関する情報は、SignedInfo の子要素である Reference 以下に記述されます。Reference の属性 URI で署名対象を指定し、DigestMethod、DigestValue でダイジェスト値を求めるときに使用したアルゴリズムとその値を指定します。

以上、XML デジタル署名について簡単に説明をしてきましたが、より詳細な情報は W3C 勧告 XML-Signature Syntax and Processing や関連文書を参照してください。

署名を行うサンプル

以下に XML Digital Signature API を用いて署名を作成する例を示します:

import java.security.InvalidAlgorithmParameterException;
import java.security.KeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;

import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;

public class GenerateDetachedSignatureMain
{
    public static void main(String[] arguments) throws Exception
    {
        // XML デジタル署名を作成するためのファクトリ.
        XMLSignatureFactory signatureFactory = XMLSignatureFactory.getInstance();

        // 鍵を作成.
        KeyPair keyPair;
        {
            KeyPairGenerator generator = KeyPairGenerator.getInstance("DSA");
            generator.initialize(512);
            keyPair = generator.generateKeyPair();
        }

        // 署名を作成.
        XMLSignature signature = createXMLSignature(signatureFactory, keyPair);

        // document に署名データを書き込む.
        Document document;
        {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            document = factory.newDocumentBuilder().newDocument();
            signature.sign(new DOMSignContext(keyPair.getPrivate(), document));
        }

        // 署名をファイル "dsig.xml" に出力.
        {
            TransformerFactory factory = TransformerFactory.newInstance();
            Transformer transformer = factory.newTransformer();
            transformer.transform(
                    new DOMSource(document), new StreamResult("dsig.xml"));
        }
    }

    private static XMLSignature createXMLSignature(
            XMLSignatureFactory signatureFactory, KeyPair keyPair)
        throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyException
    {
        // 署名の対象となる文書の URI.
        String referenceUri = "http://www.w3.org/TR/xmldsig-core/";

        // メッセージ・ダイジェストを求めるアルゴリズム.
        DigestMethod digestMethod =
            signatureFactory.newDigestMethod(DigestMethod.SHA1, null);

        // XML 正規化のアルゴリズム.
        CanonicalizationMethod canonicalizationMethod =
            signatureFactory.newCanonicalizationMethod(
                    CanonicalizationMethod.INCLUSIVE,
                    (C14NMethodParameterSpec)null);

        // 署名を求めるアルゴリズム.
        SignatureMethod signatureMethod =
            signatureFactory.newSignatureMethod(SignatureMethod.DSA_SHA1, null);

        // <KeyInfo>
        KeyInfoFactory factory = signatureFactory.getKeyInfoFactory();
        KeyValue keyValue = factory.newKeyValue(keyPair.getPublic());
        KeyInfo keyInfo = factory.newKeyInfo(Collections.singletonList(keyValue));

        // <Reference>
        Reference reference =
            signatureFactory.newReference(referenceUri, digestMethod);

        // <SignedInfo>
        SignedInfo signedInfo =
            signatureFactory.newSignedInfo(
                    canonicalizationMethod, signatureMethod,
                    Collections.singletonList(reference));

        // 署名を作成.
        return signatureFactory.newXMLSignature(signedInfo, keyInfo);
    }
}

このプログラムを実行すると、http://www.w3.org/TR/xmldsig-core/ のデータに対する署名を作成し、dsig.xml というファイルに出力します(出力例:dsig.xml)。署名データを単独のファイルとして出力するので、前述の Detached Signature にあたります。プログラムを簡単にするため、使用するアルゴリズムなどは全て定数として指定しています。

全体の構成は、createXMLSignature メソッドで署名の作成を行い、main メソッドでは署名の作成に必要な鍵の生成や、作成した署名のファイルへの出力を行います。

main メソッドでは、署名の作成に必要な様々なクラスのファクトリである javax.xml.crypto.dsig.XMLSignatureFactory を XMLSignatureFactory#getInstance メソッドによって取得します。引数を取らない XMLSignatureFactory#getInstance はデフォルトのメカニズムである DOM による実装クラスのインスタンスを返します。次に署名に使用する鍵のペアを作成しています。ここでは W3C 勧告で実装が必須とされている DSA(Digital Signature Algorithm)を指定しています。

鍵の生成が終わると、createXMLSignature メソッドによって署名の作成を行っています。メソッドの前半では署名を作成するときに使用するアルゴリズムを表すオブジェクト、後半では作成する署名データに含まれる KeyInfo や Reference などの要素を表すオブジェクトの作成を行っています。必要なオブジェクトの生成は XMLSignatureFactory のメソッド newXXXXX(newSignedInfo や newSignatureMethod など)によって行っています。ここではダイジェスト値を求めるアルゴリズムには SHA1、XML 正規化アルゴリズムには Canonical XML(without comments)、署名を求めるアルゴリズムには DSS(DSAwithSHA1)として署名を作成しています。

main メソッドに戻ると、作成した署名データから org.w3c.dom.Document を構築し、それをファイルに出力します。

検証を行うサンプル

署名を行うサンプルでは http://www.w3.org/TR/xmldsig-core/ のデータに対する署名を作成し、dsig.xml というファイルに出力しました。今度は、このファイルが改ざんされていないことを検証するプログラムを示します:

import java.security.Key;
import java.security.KeyException;
import java.security.PublicKey;

import javax.xml.crypto.AlgorithmMethod;
import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.XMLValidateContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

public class ValidateDetachedSignatureMain
{
    public static void main(String[] arguments) throws Exception
    {
        // document に署名データを読み込む.
        Document document;
        {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            document = factory.newDocumentBuilder().parse("dsig.xml");
        }

        // 検証に関するコンテキスト情報.
        XMLValidateContext validateContext;
        {
            NodeList nodeList =
                document.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
            validateContext =
                new DOMValidateContext(new MyKeySelector(), nodeList.item(0));
        }

        // XMLSignature インスタンスを再構築.
        XMLSignature signature;
        {
            XMLSignatureFactory factory = XMLSignatureFactory.getInstance();
            signature = factory.unmarshalXMLSignature(validateContext);
        }

        // 検証.
        boolean validity = signature.validate(validateContext);

        // 結果を通知.
        if(validity) {
            System.out.println("successful.");
        } else {
            System.out.println("failure.");
        }
    }

    // KeyInfo から鍵情報を選択する.
    private static class MyKeySelector extends KeySelector
    {
        @Override
        public KeySelectorResult select(
                KeyInfo keyInfo, KeySelector.Purpose purpose,
                AlgorithmMethod method, XMLCryptoContext context)
            throws KeySelectorException
        {
            try {
                for(Object element : keyInfo.getContent()) {
                    if(element instanceof KeyValue) {
                        PublicKey publicKey = ((KeyValue)element).getPublicKey();

                        // 見つかった公開鍵は method で指定されるアルゴリズムのものであるかチェックする.
                        if(isSameAlgorithm(publicKey.getAlgorithm(), method.getAlgorithm())) {
                            return new MyKeySelectorResult(publicKey);
                        }
                    }
                }
            } catch(KeyException exception) {
                throw new KeySelectorException(exception);
            }
            return null;
        }

        // アルゴリズムの名前と URI から同じアルゴリズムであるか判定する.
        // 現状では DSA と RSA のみ対応.
        private boolean isSameAlgorithm(String algorithmName, String algorithmUri) {
            if("DSA".equalsIgnoreCase(algorithmName)
                    && SignatureMethod.DSA_SHA1.equals(algorithmUri)) {
                return true;
            }
            if("RSA".equalsIgnoreCase(algorithmName)
                    && SignatureMethod.RSA_SHA1.equals(algorithmUri)) {
                return true;
            }
            return false;
        }
    }

    private static class MyKeySelectorResult implements KeySelectorResult
    {
        private Key key;

        public MyKeySelectorResult(Key key) {
            this.key = key;
        }

        public Key getKey() {
            return key;
        }
    }
}

実行結果:

successful.

最初に署名データを Document に読み込み、次に検証のコンテキスト情報を作成しています。検証のコンテキスト情報は javax.xml.crypto.dsig.XMLValidateContext インタフェース及びその実装クラスによって表されます。ここでは DOM による実装である javax.xml.crypto.dsig.dom.DOMValidateContext をインスタンス化しています。DOMValidateContext のコンストラクタにはいくつかの形式がありますが、ここでは DOMValidateContext(javax.xml.crypto.KeySelector, org.w3c.dom.Node) という形式のコンストラクタを使用しています。最初の引数の KeySelector は指定した DOM ノードから鍵情報を選択する役割を持つインタフェースであり、ここでは独自に作成した実装クラス MyKeySelector を渡しています。二番目の引数の Node には、鍵情報を探す対象となる DOM ノードを指定します。

DOM ノードから鍵情報を選択する処理は MyKeySelector#select メソッドで行います。メソッド内部では KeyInfo の要素から公開鍵の値を検索しています。見つかった公開鍵は isSameAlgorithm メソッドで method で指定されるアルゴリズムのものであるか判定します。同じアルゴリズムである場合は、それを戻り値とします。select メソッドの戻り値の javax.xml.crypto.KeySelectorResult は選択された鍵を表すインタフェースであり、ここでは独自に作成した MyKeySelectorResult クラスに鍵をラップして返しています。

DOMValidatorContext の作成が終わると、それをもとに XMLSignature を構築します。そして XMLSignature#validate メソッドによって署名の検証を行っています。

まとめ

今回は Mustang の XML サポートということで

  • StAX 1.0
  • JAXB 2.0
  • JAX-WS 2.0 / Web Services Metadata
  • XML Digital Signature API

について解説しました。StAX や XML Digital Signature API の追加という新機能のほか、JAXB 2.0 では XML Schema、JAX-WS 2.0 では WSDL を直接記述しなくても扱えるようになるなど、EoD(Ease of Development)の促進も行われました。

さて、次回はスクリプティング機能について解説します。スクリプティング機能は Java とスクリプト言語の間のやり取りをサポートし、EoD を促進します。

↑このページの先頭へ

こちらもチェック!

PR
  • XMLDB.jp
  • シナジーマーケティング研究開発グループブログ