倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

MarkupBuilder で RELAX NG スキーマを書いてみた

MarkupBuilder でスタイルシートを書いてみた』ついでに、MarkupBuidler で RELAX NG スキーマも書いてみました。

RELAX NG には、XML 形式でない簡略化された記法『RELAX NG Compact Syntax』というのがあるのであえて MarkupBuilder を使う必要はないかも知れませんが、Compact Syntax に対応していない Java ライブラリでもこの方法は使えるので一定のメリットはあるかと。

妥当性検証対象の XML データ


妥当性検証の対象として使用するサンプルの XML データは『MarkupBuilder でスタイルシートを書いてみた』で用いたものを使うことにします。

Groovy の Builder を用いた RELAX NG スキーマ


RELAX NG スキーマの構築も、以前のスタイルシートの構築とやってることは同じです。

まずは MarkupBuilder の準備。 ここでも RELAX NG スキーマを文字列に書き出すために StringWriter を用いています。

def writer = new StringWriter()
def builder = new groovy.xml.MarkupBuilder(writer)

ではスキーマの構築。 名前空間を使う必要がない場合はシングルクォート (') で囲む必要もないのでスタイルシートの場合よりも簡単。

builder.grammar(xmlns:XMLConstants.RELAXNG_NS_URI){

    start{
        element(name:'Groovy-Projects'){
            oneOrMore{ ref(name:'GroovyProject') }
        }
    }

    define(name:'GroovyProject'){
        element{
            anyName{}
            attribute(name:'latest-version'){}
            element(name:'url'){ text{} }
            element(name:'description'){ text{} }
        }
    }
}

RELAX NG Compact Syntax に比べて oneOrMore など(他にも zeroOrMore, optional など)がちょっと助長な感じもしますが。

後は構築したスキーマを用いて妥当性検証を行います。 以下の例では javax.xml.validation パッケージを用いて妥当性検証を行っています:

def schemaSource = new StreamSource(new StringReader(writer.toString()))
def schema = RelaxngSchemaFactory.newFactory().newSchema(schemaSource)
def validator = schema.newValidator()
validator.validate(new StreamSource("sample.xml"))
public class RelaxngSchemaFactory {

    public static SchemaFactory newFactory(){

        try{
            return SchemaFactory.newInstance(XMLConstants.RELAXNG_NS_URI);

        }catch(IllegalArgumentException ex){

            for(SchemaFactory factory: ServiceLoader.load(SchemaFactory.class)){

                if(factory.isSchemaLanguageSupported(XMLConstants.RELAXNG_NS_URI))
                    return factory;
            }

            throw ex;
        }
    }
}

まぁ、スタイルシートのときとあんまり変わらないですね。

Groovyイン・アクション

Groovyイン・アクション


Relax Ng

Relax Ng

MarkupBuilder でスタイルシートを書いてみた

XSLT スタイルシートは(特にロジックなどを)書くのが結構大変。 以前から RELAX NG Compact Syntax のような簡略記法がないかなぁと思っていましたが、Groovy の Builder を使うとこの目的が達せられるかと思いチョット試してみました。

変換対象の XML データ


まずは変換対象の XML データ。 簡単のため、名前空間は使用しないことにしましょう:

<?xml version="1.0"?>
<Groovy-Projects>
  <Groovy latest-version="1.7.2">
    <url>http://groovy.codehaus.org/</url>
    <description>An agile dynamic language for the Java Platform</description>
  </Groovy>

  <Gant latest-version="1.9.2">
    <url>http://gant.codehaus.org/</url>
    <description>A Groovy-based build system that uses Ant tasks, but no XML.</description>
  </Gant>

  <GMaven latest-version="1.2">
    <url>http://docs.codehaus.org/display/GMAVEN/Home</url>
    <description>GMaven provides integration of Groovy into Maven.</description>
  </GMaven>

  <Griffon latest-version="0.3.1">
    <url>http://griffon.codehaus.org/</url>
    <description>A Grails-like Rich Internet Framework</description>
  </Griffon>

  <Grails latest-version="1.3.1">
    <url>http://grails.org/</url>
    <description>a Groovy-based web framework inspired by Ruby on Rails</description>
  </Grails>
</Groovy-Projects>

Groovy の Builder を用いたスタイルシート


では肝心のスタイルシートを見ていきましょう。

まずは準備としての Builder のインスタンスを取得します。 ここでは結果のスタイルシートXML を内容とする文字列として取得するために StringWriter のインスタンスを作成してそれを MarkupBuilder に渡します。

def writer = new StringWriter()
def builder = new groovy.xml.MarkupBuilder(writer)

さて、次がスタイルシートを構築する Builder を使ったコードです:

builder.'xsl:stylesheet'(
        version:'1.0',
        xmlns:'http://www.w3.org/TR/xhtml1/strict',
        'xmlns:xsl':'http://www.w3.org/1999/XSL/Transform'){

    'xsl:template'(match:'/'){
        html{
            head{
                title('Hello, Groovy world !')
            }
            body{
                table(border:'1'){
                    tr{
                        th('Project Name')
                        th('Latest Version')
                        th('Description')
                    }
                    'xsl:apply-templates'(select:'*')
                }
            }
        }
    }

    'xsl:template'(match:'/Groovy-Projects/*'){
        tr{
            th{
                a{
                    'xsl:attribute'(name:'href'){
                        'xsl:value-of'(select:'url')
                    }
                    'xsl:value-of'(select:'local-name()')
                }
            }
            td{'xsl:value-of'(select:'@latest-version')}
            td{'xsl:value-of'(select:'description')}
        }
    }
}
  • XML 形式で書くのに比べれば、書くのも読むのも楽だと思いませんか?
  • チョット面倒なのは、名前空間接頭辞を使用するには名前にコロン (:) が必要なため、XSLT のタグを全てシングルクォート (') で囲む必要があるところでしょうか。 XSLT名前空間をデフォルト名前空間にしても(接頭辞を使わないようにしても)、HTML タグの方に名前空間を使わないといけなくなるので、あまり簡単にはなりません。 そもそも、XSLT のタグにはハイフン (-) を含むものがいくつもあるので、結局はシングルクォートで囲む必要はありますが。
  • if, choose などのロジックを書くときに簡潔さの有難味がもっとありそう。
  • Groovy の Builder は混合内容モデル*1を使えませんが、xsl:text 要素を使えば問題ないでしょう。

スタイルシートが取得できれば、あとは javax.xml.transform パッケージ内のクラスを用いて XSL 変換を実行します(細かいコードは省略):

def reader = new StringReader(writer.toString())
def stylesheet = new StreamSource(reader)

def trans = TransformerFactory.newInstance().newTransformer(stylesheet)
// Transformer を用いた変換処理
  • 「StreamSource」は javax.xml.transform.StreamSource
  • 「TransformerFactory」は javax.xml.transform.TransformerFactory

変換結果の HTML ファイル


上記の変換を施すと、結果として以下の HTML が得られます(見やすさのため、適当に整形しています):

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/TR/xhtml1/strict">

<head>
  <title>Hello, Groovy world !</title>
</head>

<body>
  <table border="1">
    <tr>
      <th>Project Name</th><th>Latest Version</th><th>Description</th>
    </tr>

    <tr>
      <th><a href="http://groovy.codehaus.org/">Groovy</a></th><td>1.7.2</td>
      <td>An agile dynamic language for the Java Platform</td>
    </tr>

    <tr>
      <th><a href="http://gant.codehaus.org/">Gant</a></th><td>1.9.2</td>
      <td>A Groovy-based build system that uses Ant tasks, but no XML.</td>
    </tr>

    <tr>
      <th><a href="http://docs.codehaus.org/display/GMAVEN/Home">GMaven</a></th>
      <td>1.2</td><td>GMaven provides integration of Groovy into Maven.</td>
    </tr>

    <tr>
      <th><a href="http://griffon.codehaus.org/">Griffon</a></th><td>0.3.1</td>
      <td>A Grails-like Rich Internet Framework</td>
    </tr>

    <tr>
      <th><a href="http://grails.org/">Grails</a></th><td>1.3.1</td>
      <td>a Groovy-based web framework inspired by Ruby on Rails</td>
    </tr>
  </table>
</body>
</html>

ブラウザで表示するとこんな感じ:















Project NameLatest VersionDescription
Groovy1.7.2An agile dynamic language for the Java Platform
Gant1.9.2A Groovy-based build system that uses Ant tasks, but no XML.
GMaven1.2GMaven provides integration of Groovy into Maven.
Griffon0.3.1A Grails-like Rich Internet Framework
Grails1.3.1a Groovy-based web framework inspired by Ruby on Rails

どうでしょう?
Groovyイン・アクション

Groovyイン・アクション


XSLT: Mastering XML Transformations

XSLT: Mastering XML Transformations

*1:ある要素下に子テキストと子要素が混在しているもの。 正式には何て言うんですっけ?

Java と XML と空白と (6):テキストノードの正規化(JDOM 編)

前回、「テキストの正規化」として Nux JavaDoc API に定義されている空白の扱いを見ました。

で、ふと JDOMJavaDoc を見ると、似たような「テキストの正規化」が定義されていました・・・ しかも、こちらの方がなじみやすそうな命名で(^ ^;) ってことで、今回は JDOM に定義されている「テキストの正規化」を見ていきます。

org.jdom.outputFormat.TextMode


JDOM で「テキストの正規化」を定義しているのは、org.jdom.outputFormat.TextMode クラスです(JDOM JavaDoc API) 種類は4つ:

  • PRESERVE
  • TRIM_FULL_WHITE
  • TRIM
  • NORMALIZE

以下、次のサンプル XML 文書についての適用結果と共に処理方法を見ていきましょう(前回と同じ):

<root>
  <text>
    If at first an idea does not sound absurd,
    then there is no hope for it.
        - ALBERT EINSTEIN
  </text>
</root>

テキストの処理


★PRESERVE(保存する)★

All content is printed in the format it was created, no whitespace or line separators are are added or removed.

何もしません。 @xml:space 属性が付いているのと同じです。 大抵の場合、これがアプリケーションのデフォルト処理になっています。

<root>
  <text>
    If at first an idea does not sound absurd,
    then there is no hope for it.
        - ALBERT EINSTEIN
  </text>
</root>

= nux.xom.pool XOMUtil.Normalizer.PRESERVE

★TRIM_FULL_WHITE(全てが空白のテキストを切り取る)★

Content between tags consisting of all whitespace is not printed. If the content contains even one non-whitespace character, it is printed verbatim, whitespace and all.

空白のみからなるテキストを取り除き、それ以外のテキストはそのままにします。

<root><text>
    If at first an idea does not sound absurd,
    then there is no hope for it.
        - ALBERT EINSTEIN
  </text></root>

= nux.xom.pool XOMUtil.Normalizer.STRIPE

★TRIM(切り取る)★

Same as TrimAllWhite, plus leading/trailing whitespace are trimmed.

先頭と後尾の空白を取り除きます。

<root><text>If at first an idea does not sound absurd,
    then there is no hope for it.
        - ALBERT EINSTEIN</text></root>

= nux.xom.pool XOMUtil.Normalizer.TRIM

★NORMALIZE(正規化する)★

Same as TextTrim, plus addition interior whitespace is compressed to a single space.

空白の連なりをスペース (' ') で置き換え、先頭と後尾の空白を String#trim() メソッドで取り除きます。 値が単語の XML プロパティファイルまたは XHTML に使うことが多いと思います。

<root><text>If at first an idea does not sound absurd, then there is no hope for it. - ALBERT EINSTEIN</text></root>

= nux.xom.pool XOMUtil.Normalizer.COLLAPSE

Java と XML と空白と (5):XML の空白と Java

今回は、XML の空白を Java で扱う方法を幾つか。

  • 正規表現
  • 属性値の正規化
  • テキストの正規化

正規表現


XML での空白は ' ', '\t', '\r', '\n' の4文字なので、Java での正規表現java.util.regex.Pattern の JavaDoc 参照)で XML空白文字 (whitespace character) を表現すると

[ \t\r\n]

となります。 同様に空白 (whitespace) (空白文字の1つ以上の連なり)は

[ \t\r\n]+

となります。

属性値の正規化


属性値はパーサによって「正規化 (normalization)」という処理が行われます。 属性値の型が CDATA でない場合、この「正規化」の処理は以下のことを行います:

  • 先頭と後尾の空白を取り除く
  • 連続する空白文字を1つの空白で置き換える

★先頭と後尾の空白を取り除く★

「先頭と後尾の空白を取り除く」処理には String#trim() が適していそうですが、このメソッドで空白とみなす文字は「文字コードが \u0020 以下の文字」となっています。

幸いにも(というか、そうなるようにしてあるのでしょうが)、XML の属性値の文字列に String#trim() を適用すると、意図した通りに「先頭と後尾の空白を取り除く」ことができます

これは、「文字コードが \u0020 以下の文字」の文字の内、XML の空白文字 (' ', '\t', '\r', 'n') 以外の文字は禁止されている(使用できる文字として定義されていない)ためです。 例えば、文字コードが \u000b の文字が属性値の先頭にあった場合、「正規化」によってこの文字が除去されてはいけませんが、String#trim() では除去してしまいます。 しかし、こういった文字は属性値の中には現れないことになっています*1

★連続する空白文字を1つの空白で置き換える★

この処理は、String#replaceAll() メソッドによって実現できます。 上で定義した XML の空白を表す正規表現にも注意:

String attValue = ...;
attValue.replaceAll("[ \\t\\r\\n]+", " ");  // 正規表現にエスケープが必要

★属性の正規化★

結局のところ、「属性値の正規化」を行うには、以下の処理を行えば良いことになります(必ずしも実装がこうなっているわけではありませんが):

public String normalize(String attValue){
    return attValue.trim().replaceAll("[ \\t\\r\\n]+", " ");
}

テキストの正規化


テキスト中の空白の扱いは処理する側のアプリケーションに任せられています(@xml:space 属性がない場合)。 とはいっても、よく使われる処理は決まっているかと思います。

Nux JavaDoc API の nux.xom.pool.XOMUtil.Normalizer クラスに空白の取り扱い方を列挙してくれているので、そちらを紹介しておきましょう。 種類は5つ:

  • PRESERVE
  • STRIPE
  • REPLACE
  • TRIM
  • COLLAPSE

以下、次のサンプル XML 文書についての適用結果と共に処理方法を見ていきましょう:

<root>
  <text>
    If at first an idea does not sound absurd,
    then there is no hope for it.
        - ALBERT EINSTEIN
  </text>
</root>

★PRESERVE(保存する)★

Whitespace normalization returns the string unchanged; hence indicates no whitespace normalization should be performed at all; This is typically the default for applications.

何もしません。 @xml:space 属性が付いているのと同じです。 大抵の場合、これがアプリケーションのデフォルト処理になっています。

<root>
  <text>
    If at first an idea does not sound absurd,
    then there is no hope for it.
        - ALBERT EINSTEIN
  </text>
</root>

★STRIP(はぐ)★

Whitespace normalization removes strings that consist of whitespace-only (boundary whitespace), retaining other strings unchanged.

空白のみからなるテキストを取り除き、それ以外のテキストはそのままにします。

<root><text>
    If at first an idea does not sound absurd,
    then there is no hope for it.
        - ALBERT EINSTEIN
  </text></root>

★REPLACE(置換する)★

Whitespace normalization replaces each whitespace character in the string with a ' ' space character.

空白文字をスペース (' ') で置換します。

<root>   <text>     If at first an idea does not sound absurd,     then there is no hope for it.         - ALBERT EINSTEIN   </text> </root>

ここでは、改行文字をスペースに変換しています*2

★TRIM(切り取る)★

Whitespace normalization removes leading and trailing whitespaces, if present, ala String.trim().

先頭と後尾の空白を String#trim() メソッドを用いて取り除きます。

<root><text>If at first an idea does not sound absurd,
    then there is no hope for it.
        - ALBERT EINSTEIN</text></root>

★COLLAPSE(崩す)★

Whitespace normalization replaces each sequence of whitespace in the string by a single ' ' space character; Further, leading and trailing whitespaces are removed, if present, ala String.trim().

空白の連なりをスペース (' ') で置き換え、先頭と後尾の空白を String#trim() メソッドで取り除きます。 REPLACE & COLLAPSE。 値が単語の XML プロパティファイルまたは XHTML に使うことが多いと思います。

<root><text>If at first an idea does not sound absurd, then there is no hope for it. - ALBERT EINSTEIN</text></root>

*1:これは、HTML ではうまくいかない場合があります。 それは、HTML の空白文字の中に \u200B が含まれているためです。

*2:XML 文書では、OS の種類にかかわらず、改行は '\n' 1文字で表されます。

Java と XML と空白と (4) : HTML, XML, XHTML の空白

今回は、幾つかのマークアップ言語について、空白の扱いを見ていきます。 ここで見ていくのは次の3つ:

正確にはそれぞれの仕様書を参照してください。

HTML 4.01


仕様書は『HTML 4.01 Specification』。 以下では次の項目を見ていきます:

  • 空白文字
  • 空白の扱い

★空白文字★

Unicode 名前 Java
&#x0020; ASCII space ' '
&#x0009; ASCII tab '\t'
&#x000C; ASCII form feed '\f'
&#x200B; Zero-width space '\u200B'
&#x000D; carriage return '\r'
&#x000A; line feed '\n'

★空白の扱い★

  • <pre> 要素下の空白はそのまま
  • <pre> 要素下以外の空白は「単語間の空白 (inter-word space)」に変換される

「単語*1間の空白」は、欧文では普通のスペース (&#x0020;) 、日本語や中国語ではなし、タイ語では「Zero-width word separator (&#x200B;)」だそうです(タイ語分からん)。 「単語間空白」が実際にどのように表示されるかはレンダリングするアプリケーションに依存します。

仕様書全てに目を通しては居ませんが、先頭や後尾の空白をどのように扱うかは見当たりませんでした(少なくとも空白の扱いを書いてある周辺には)。 まぁ、取り除くのが普通でしょうけど。

XML 1.0


仕様書は『Extensible Markup Language (XML) 1.0 (Fifth Edition)』。 以下では次の項目を見ていきます:

  • 空白文字
  • 空白の扱い
  • 行末の扱い
  • 属性値の正規化

★空白文字★

XML での空白文字は以下の4つです:

Unicode 名前 Java
#x20 space ' '
#x9 tab '\t'
#xD carriage return '\r'
#xA line feed '\n'

★空白の扱い★

XML 文書中のある要素内の空白を保存したい(空白のトリミングなどを行いたくない)ときには、その要素に @xml:space 属性を付加し、属性値を "preserve" に設定します:

<pre xml:space="preserve">
  ...
</pre>

@xml:space 属性が設定されていないとき、もしくは @xml:space 属性の値が "default" の場合は、その XML 文書を受け取るアプリケーションのデフォルトの処理が行われます*2。 要は、@xml:space 属性を付加すると、HTML の <pre> 要素と同じように空白を扱うようになると言うことでいいんでしょう。

★行末の扱い★

XML 文書のパーサは解析時に

  1. #xD#xA → #xA」つまり \r\n → \n
  2. #xD → #xA」つまり \r → \n

の置き換えをしなければいけないことになっています。 したがって、行末は「#xA (\n)」だけと思って扱って構わないようです。

★属性値の正規化★

属性値は、解析時に「正規化*3」という処理を行うことになっています。 正確な記述ではありませんが、この変換は概ね以下の処理を行います*4

  1. 文字参照 (&#xA; など)、実体参照(&lt; など)を参照している文字置き換える
  2. #xD, #xA, #x9 は空白 (#x20) に置き換える
  3. 属性の型がCDATA*5 でないなら、先頭と後尾の空白を取り除き、連続する空白文字を1つの空白に置き換える

XHTML 1.0


仕様書は『XHTML〓 1.0 The Extensible HyperText Markup Language (Second Edition)』。 以下では次の項目を見ていきます:

  • 属性値での空白の扱い
  • 属性値内での改行
  • HTML と XML での空白文字

★属性値での空白の扱い★

属性値での空白の扱いは、XML でのものと同じになっています:

  • 先頭と後尾の空白を除去する
  • 1つ以上の空白文字の連なりを「単語間の空白 (inter-word space) 」に変換する

原文短いので、そのまま載っけときます:

4.7. White Space handling in attribute values
When user agents process attributes, they do so according to Section 3.3.3 of [XML]:

  • Strip leading and trailing white space.
  • Map sequences of one or more white space characters (including line breaks) to a single inter-word space.

ただし、XML ではこれらの処理を行うのは「属性の型が CDATA でないとき」となってますが、XHTML ではこれを常に行うようです。 まぁ、HTML に出てくる属性に文章 (CDATA) はなさそうなので問題はないようですが、何か一言あってもいいじゃん?

★属性値内での改行★

属性内では、改行 (line break) や空白文字の連なり (multiple white space characters) を使ってはいけないようです。 原文:

C.5. Line Breaks within Attribute Values
Avoid line breaks and multiple white space characters within attribute values. These are handled inconsistently by user agents.

★HTML と XML での空白文字★

HTML では許されている文字が、XML (XHTML) では許されていない場合があるようです。 また、空白の定義も異なっているので注意が必要。 原文:

C.15. White Space Characters in HTML vs. XML
Some characters that are legal in HTML documents, are illegal in XML document. For example, in HTML, the Formfeed character (U+000C) is treated as white space, in XHTML, due to XML's definition of characters, it is illegal.

formfeed ('\f') は、HTML では空白文字として扱われますが、XML (XHTML) では使ってはいけない文字になっています。

*1:仕様書内での「単語」とは、空白文字以外の文字の連なり (sequence) と定義されています。

*2:正直、アプリケーションはどんなマークアップXML 文書が来るか知っているはずなので、こういった指定をする必要なく空白をどのように扱うか決定できると思うんですが。 もしくは、どう扱っていいか分かるようにマークアップ(タブ名)を決めるようにすべきかと。

*3:英語では『Normalization』。 同じように「正規化」と訳される語として『Canonicalization』というものがありますが、この日記中で出てくるときは「正準化」と記します。 一般には使われていませんが、物理学関連の人には分かって貰えるハズ。

*4:仕様書では、属性値の文字列の構築方法が書かれてますが、ここではそうはしていません。

*5:CDATA とは文字データ(Character Data)のこと。 単語やその列挙ではなく文章(テキスト)。

Java と XML と空白と (3) : String#trim() メソッド

前回Java での「whitespace」のややこしい扱いを見ましたが、哀しいかな、他にも「whitespace」のややこしい扱いがあります。 それは String#trim() です。

String#trim() の JavaDoc をみると、

Returns a copy of the string, with leading and trailing whitespace omitted.

と書かれていて、先頭と後尾の「whitespace」を取り除くと書いてあります。

ただし、これ以降を読むと、「whitespace」とは「文字コードが \u0020 以下の文字」つまり、文字コードが \u0000 〜 \u0020 の33文字*1と定義されています。 これは前回みた Java での「whitespace」の定義と異なっています。

\u0001 〜 \u0020 までの文字は以下のようになっています(\u0000 は省略):

Unicode 文字 Unicode 文字 Unicode 文字 Unicode 文字
\u0001 \u0009 "\t" \u0011 \u0019
\u0002 \u000a "\n" \u0012 \u001a
\u0003 \u000b \u0013 \u001b
\u0004 \u000c "\f" \u0014 \u001c
\u0005 \u000d "\r" \u0015 \u001d
\u0006 \u000e \u0016 \u001e
\u0007 \u000f \u0017 \u001f
\u0008 "\b" \u0010 \u0018 \u0020 " "


文字コードの名前は以下の通り(Unicode 5.1 を参考にしたので、Java での名前は少し違うかも):

Unicode 名前 Unicode 名前 Unicode 名前
\u0000 NULL \u000b LINE TABULATION
(VT)
\u0016 SYNCHRONOUS IDLE
\u0001 START OF HEADING \u000c FORM FEED
(FF "\f")
\u0017 END OF TRANSMISSION BLOCK
\u0002 START OF TEXT \u000d CARRIAGE RETURN
(CR "\r")
\u0018 CANCEL
\u0003 END OF TEXT \u000e SHIFT OUT \u0019 END OF MEDIUM
\u0004 END OF TRANSMISSION \u000f SHIFT IN \u001a SUBSTITUTE
\u0005 ENQUIRY \u0010 DATA LINK ESCAPE \u001b ESCAPE
\u0006 ACKNOWLEDGE \u0011 DEVICE CONTROL ONE \u001c INFORMATION SEPARATOR FOUR
\u0007 BELL \u0012 DEVICE CONTROL TWO \u001d INFORMATION SEPARATOR THREE
\u0008 BACKSPACE
("\b")
\u0013 DEVICE CONTROL THREE \u001e INFORMATION SEPARATOR TWO
\u0009 CHARACTER TABULATION
(HT "\t")
\u0014 DEVICE CONTROL FOUR \u001f INFORMATION SEPARATOR ONE
\u000a LINE FEED
(LF "\n")
\u0015 NEGATIVE ACKNOWLEDGE \u0020 SPACE
(" ")

String#trim() で「whitespace」とみなされる文字はの集合は、XMLJava正規表現で「whitespace」とみなされる文字を全て含んでいます。 一方、Character#isWhitespace() で定義される「whitespace」の文字集合との間に包含関係はありません。

{XML の空白} ⊂ {Java 正規表現の空白} ⊂ {Java String#trim() の空白}

この「whitespace」の定義の違いが、どの程度影響があるのかよく分かりませんが(あまりないのかも知れません*2)、JavaDoc を一見してそれが分からないこと自体が問題かと。

*1:16進数であることに注意

*2:全角スペース (\u3000) は Character#isWhitespace() では「whitespace」とみなされますが、String#trim() では「whitespace」とみなされない、というのが日本語にとって注意が必要なところでしょうか? あまり困ったことはありませんけど(笑)

Java と XML と空白と (2) : 正規表現

前回java.lang.Character クラスの static メソッドを用いて、Java での空白文字を見ました。 このとき

  • Character#isSpaceChar()
  • Character#isWhitespace()

という2つのメソッドを使って空白文字かどうかを判定していました。

哀しいかな Java には他の空白の定義があります。 それは正規表現です(他にもあるかも知れませんが)。 パッケージとしては java.util.regex です。 java.util.regex.Pattern クラスの JavaDoc を見ると、(正規表現での)空白文字にマッチするパターンは「\s」で表され*1、これは

[ \t\n\x0B\f\r]

と同じです。 つまり、正規表現での空白文字は「' ', '\t', '\n', '\x0B', '\f', '\r'」の6つだということです。 各文字をもう少し詳しく見ると下表のようになります:

名前 文字リテラル (Java) Unicode 文字 (Java)
スペース (Space) ' ' \u0020
水平タブ (HT) '\t' \u0009
改行 (LF) '\n' \u000a
垂直タブ (VT) '\u000b \u000b
フォームフィード (FF) '\f' \u000c
復帰キャリッジ・リターン (CR) '\r' \u000d


ちなみに、これらは全て Character#isWhitespace() で指定される空白文字に含まれ、XML の空白文字を含みます。 雰囲気で包含関係を書き下すと以下のようになります:

{XML の空白} ⊂ {Java 正規表現の空白} ⊂ {Java Character#isWhitespace() の空白}

*1:ちなみに Character#isWhitespace() メソッドで指定される空白文字に一致するパターンは「\p{javaWhitespace}」で表すことが出来ます。

Java と XML と空白と (1) : Java と XML の空白文字

Java での空白文字


Java で空白とみなされる文字は?と聞かれると、意外とよく分からない。 Character クラスの static メソッドに isSpaceChar()isWhitespace() とが定義されているけど、JavaDoc を読んでも全然分からない。

ということで、実際に Character#isSpaceChar(), Character#isWhitespace() を実行してみました。 引数として渡したのは 0 〜 0xffff (65535) です。 下表は

  • SC : isSpaceChar() の返り値が true なら「○」
  • WS : isWhitespace() の返り値が true なら「○」

としてます。

Unicode 文字 SC WS Unicode 文字 SC WS Unicode 文字 SC WS
\u0009 '\t' \u00a0 \u2007
\u000a '\n' \u1680 \u2008
\u000b \u180e \u2009
\u000c '\f' \u2000 \u200a
\u000d '\r' \u2001 \u200b
\u001c \u2002 \u2028
\u001d \u2003 \u2029
\u001e \u2004 \u202f
\u001f \u2005 \u205f
\u0020 ' ' \u2006 \u3000 ' '


\u3000 は「全角スペース」です。

XML での空白文字


XML での空白文字は次の4つです(「Extensible Markup Language (XML) 1.0 (Fifth Edition)」参照):

Unicode 文字 (XML) 名前 文字リテラル (Java) Unicode 文字 (Java)
#x20 スペース (Space) ' ' \u0020
#x9 水平タブ (HT) '\t' \u0009
#xD 復帰キャリッジ・リターン (CR) '\r' \u000d
#xA 改行 (LF) '\n' \u000a


これらは全て、Java でも Whitespace とみなされます*1

*1:一方、空白' ' 以外は SpaceChar とはみなされないようです。

org.w3c.dom.Document を java.io.Reader に変換する

以前の記事で org.w3c.dom.Document オブジェクトを java.io.Writer へ書き出す処理を見ました。

今回はそれを踏まえて、「org.w3c.dom.Document オブジェクトを java.io.Reader オブジェクトとして扱う」方法を考えます。 この目的のために java.io.Reader クラスを拡張した DOMDocumentReader というクラスを作成します。

使用方法


実装の前に DOMDocumentReader クラスの使用方法を見てみましょう。 まぁ、テストケースの一部って事で。

Document doc = ...;
Reader reader = new DOMDocumentReader(doc);

単に、コンストラクタに org.w3c.dom.Document オブジェクトをとるってだけで、あとは通常の Reader として使えるようにします。 結構便利そうではないですか*1

DOMDocumentReader の実装 (1):フィールドとコンストラクタ


まずはフィールドとコンストラクタ。 フィールドとしては

  • Reader クラスとしての処理を委譲するreader : java.io.Reader
  • org.w3c.dom.Document オブジェクトを書き出すスレッド executor : java.util.concurrency.ExecutorService
  • 書き出しの際に IOException が発生した場合に、それを格納しておく exception : java.io.Exception

を定義します。 コンストラクタは、書き出したい org.w3c.dom.Document オブジェクトをとるものを定義します。

public class DOMDocumentReader extends Reader{
    
    /** 処理を委譲される (Piped)Reader フィールド */
    private final Reader reader;

    /** Document オブジェクトを (Piped)Writer に書き出す ExecutorService フィールド */
    private final ExecutorService executor;
        
    /**
       * Document オブジェクトを書き出す際に IOException した場合に、
       * その例外を格納しておくフィールド
       */
    private IOException exception;
    
    public DOMDocumentReader(Document doc){
        PipedWriter writer = new PipedWriter();
        
        Reader r = null;
        try{
            r = new PipedReader(writer);
        }catch(IOException ex){
            this.exception = ex;
        }
        
        this.reader = r;
        this.executor = Executors.newSingleThreadExecutor();
        this.executor.execute(new WriteDownThread(doc, writer));
    }
}

この手の処理には java.io.PipedReader, java.io.PipedWriter を使うのが普通でしょうか? あまり使ったことないですが。 同様に java.util.concurrency パッケージもあまり使ったこと無いんですけど*2

DOMDocumentReader の実装 (2):書き下しスレッド


org.w3c.dom.Document を (Piped)Writer に書き出す処理は以前の記事と同じです。 ただし、例外処理がチョット面倒。 書き出し時に例外が発生した場合、DOMDocumentReader オブジェクトのフィールド exception (IOException) にその例外をセットするようにします。 これには同期が必要

public class DOMDocumentReader extends Reader{

    ...
    
    private static final TransformerFactory FACTORY = TransformerFactory.newInstance(); 
    
    /** Document オブジェクトを (Piped)Writer へ書き出すスレッド */
    private class WriteDownThread implements Runnable{

        private final Transformer transformer;
        private final Document source;
        private final Writer result;
        
        public WriteDownThread(Document doc, Writer writer){
            try{
                this.transformer = FACTORY.newTransformer();
                
            }catch(TransformerConfigurationException ex){
                throw new RuntimeException(ex);
            }
                
            this.source = doc;
            this.result = writer;
        }
        
        @Override public void run() {
            try{
                this.transformer.transform(
                        new DOMSource(this.source), 
                        new StreamResult(this.result));
                this.result.flush();
                
            }catch(IOException ex){
                setIOException(ex);
            }catch(TransformerException ex){
                setIOException(new IOException(ex));
            }finally{
                try{
                    if(this.result != null)
                        this.result.close();
                    
                }catch(IOException ex){
                    // close() 以外で例外が発生していればそちらを優先
                    setIOExceptionIfUnset(ex);
                }
            }
        }
    }
    
    /** 例外をセットする */
    private synchronized void setIOException(IOException ex){
        this.exception = ex;
    }
    
    /** まだ例外がセットされていなければ、例外をセットする */
    private synchronized void setIOExceptionIfUnset(IOException ex){
        if(this.exception == null)
            this.exception = ex;
    }
}

DOMDocumentReader の実装 (3):reader フィールドへの委譲メソッド


java.io.Reader オブジェクトに定義されているメソッドは、全て reader フィールドへ処理を委譲するようにオーバーライドします。 ただし、IOException を投げるメソッドは委譲前に、書き出し時に例外が発生していないかチェックします(checkIOException() メソッド)。  close() メソッドでは、加えて executor フィールドの後処理 ExecutorService#shutdown() メソッドを呼び出します。

public class DOMDocumentReader extends Reader{

    ...

    // Document の書き出し時に例外が発生していないかチェック
    private void checkIOException()throws IOException{
        if(this.exception != null)
            throw this.exception;
    }

    //********** reader フィールドへの委譲メソッド **********
    @Override public void close() throws IOException {
        // ExecutorService をシャットダウンする
        this.executor.shutdown();
        checkIOException();
        this.reader.close();
    }

    @Override public void mark(int readAheadLimit) throws IOException {
        checkIOException();
        this.reader.mark(readAheadLimit);
    }

    @Override public boolean markSupported() {
        return this.reader.markSupported();
    }

    @Override public int read() throws IOException {
        checkIOException();
        return this.reader.read();
    }

    @Override public int read(char[] cbuf, int off, int len) throws IOException {
        checkIOException();
        return this.reader.read(cbuf, off, len);
    }

    @Override public int read(char[] cbuf) throws IOException {
        checkIOException();
        return this.reader.read(cbuf);
    }

    @Override public int read(CharBuffer target) throws IOException {
        checkIOException();
        return this.reader.read(target);
    }

    @Override public boolean ready() throws IOException {
        checkIOException();
        return this.reader.ready();
    }

    @Override public void reset() throws IOException {
        checkIOException();
        this.reader.reset();
    }

    @Override public long skip(long n) throws IOException {
        checkIOException();
        return this.reader.skip(n);
    }
}

DOMDocumentReader の実装のまとめ

public class DOMDocumentReader extends Reader{

    private static final TransformerFactory FACTORY = TransformerFactory.newInstance(); 
    
    /** 処理を委譲される (Piped)Reader フィールド */
    private final Reader reader;

    /** Document オブジェクトを (Piped)Writer に書き出す ExecutorService フィールド */
    private final ExecutorService executor;
        
    /**
       * Document オブジェクトを書き出す際に IOException した場合に、
       * その例外を格納しておくフィールド
       */
    private volatile IOException exception;
    
    public DOMDocumentReader(Document doc){
        PipedWriter writer = new PipedWriter();
        
        Reader r = null;
        try{
            r = new PipedReader(writer);
        }catch(IOException ex){
            this.exception = ex;
        }
        
        this.reader = r;
        this.executor = Executors.newSingleThreadExecutor();
        this.executor.execute(new WriteDownThread(doc, writer));
    }
    
    /** Document オブジェクトを (Piped)Writer へ書き出すスレッド */
    private class WriteDownThread implements Runnable{

        private final Transformer transformer;
        private final Document source;
        private final Writer result;
        
        public WriteDownThread(Document doc, Writer writer){
            try{
                this.transformer = FACTORY.newTransformer();
                
            }catch(TransformerConfigurationException ex){
                throw new RuntimeException(ex);
            }
                
            this.source = doc;
            this.result = writer;
        }
        
        @Override public void run() {
            try{
                this.transformer.transform(
                        new DOMSource(this.source), 
                        new StreamResult(this.result));
                this.result.flush();
                
            }catch(IOException ex){
                setIOException(ex);
            }catch(TransformerException ex){
                setIOException(new IOException(ex));
            }finally{
                try{
                    if(this.result != null)
                        this.result.close();
                    
                }catch(IOException ex){
                    // close() 以外で例外が発生していればそちらを優先
                    setIOExceptionIfUnset(ex);
                }
            }
        }
    }
    
    /** 例外をセットする */
    private synchronized void setIOException(IOException ex){
        this.exception = ex;
    }
    
    /** まだ例外がセットされていなければ、例外をセットする */
    private synchronized void setIOExceptionIfUnset(IOException ex){
        if(this.exception == null)
            this.exception = ex;
    }

    /** Document の書き出し時に例外が発生していないかチェック */
    private synchronized void checkIOException()throws IOException{
        if(this.exception != null)
            throw this.exception;
    }

    //********** reader フィールドへの委譲メソッド **********
    @Override public void close() throws IOException {
        // ExecutorService をシャットダウンする
        this.executor.shutdown();
        checkIOException();
        this.reader.close();
    }

    @Override public void mark(int readAheadLimit) throws IOException {
        checkIOException();
        this.reader.mark(readAheadLimit);
    }

    @Override public boolean markSupported() {
        return this.reader.markSupported();
    }

    @Override public int read() throws IOException {
        checkIOException();
        return this.reader.read();
    }

    @Override public int read(char[] cbuf, int off, int len) throws IOException {
        checkIOException();
        return this.reader.read(cbuf, off, len);
    }

    @Override public int read(char[] cbuf) throws IOException {
        checkIOException();
        return this.reader.read(cbuf);
    }

    @Override public int read(CharBuffer target) throws IOException {
        checkIOException();
        return this.reader.read(target);
    }

    @Override public boolean ready() throws IOException {
        checkIOException();
        return this.reader.ready();
    }

    @Override public void reset() throws IOException {
        checkIOException();
        this.reader.reset();
    }

    @Override public long skip(long n) throws IOException {
        checkIOException();
        return this.reader.skip(n);
    }
}

*1:JDOM や dom4j にも似たようなクラスがあっていいと思うのですが。

*2:とりあえず、ExecutorService#shutdown() は DOMDocumentReader#close() メソッド内で呼び出してますが。

org.w3c.dom.Document を出力する

org.w3c.dom.Document オブジェクトを標準出力やファイルに出力するのに困ったことはありませんか? javax.xml.transform パッケージ内のクラスを使うと、ある程度簡単に Document オブジェクトを外部へ書き出すことが出来ます。

java.io.Writer へ書き出す

    public void outputDocumentToWriter(Document doc, Writer writer)
            throws TransformerException{

        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.transform(new DOMSource(doc), new StreamResult(writer));
        writer.flush();
    }

Transformer オブジェクトはインスタンス・フィールドなどとして保持しておく方が良いでしょう。 java.io.OutputStream へ書き出すメソッドも同様に書くことが出来ます。

リソースの解放はオブジェクトを生成した者が行う」ことにして、ここでは Writer オブジェクトの close() メソッドは呼び出していません。

ファイルへ書き出す


上記の「Writer へ書き出すメソッド」を用いれば、ファイルへ書き出すメソッドも書けます。 「リソースの解放」処理のために少々長くなってしまいますが。

    public void outputDocumentToFile(Document doc, String filename)
            throws IOException, TransformerException{
        
        Writer writer = null;
        try{
            writer = new FileWriter("output.xml");
            output(doc, writer);
            
        }finally{
            if(writer != null)writer.close();
        }
    }

リソースの解放はオブジェクトを生成した者が行う」ってことで、FileWriter オブジェクトを生成しているこのメソッド内で close() メソッドを呼び出しています。

標準出力へ書き出す


標準出力 System.out は「リソースの解放」をする必要がないので非常に簡単。

    public void outputDocumentToConsole(Document doc)
            throws TransformerException{
        
        output(doc, new OutputStreamWriter(System.out));
    }