SAX XML インターフェイスの使用

要約:
  • SAX は、XML パーサーおよび XML パーサー機能のための共通のインターフェイスです。
  • この章では、SAX インターフェイスを使用して簡単な XML パーザーを開発する方法を説明します。

SAX 概要

ODBC が、異なるリレーショナル データベースおよびそれに関連する機能として提供されているものに実装される共通インターフェイスであるように、SAX (Simple API for XML) は、異なる XML パーサーおよび XML パーサー機能に実装される汎用インターフェイスです。これは、XML-DEV メーリング リストのメンバによって共同開発されました。本ガイドおよび関連する『API リファレンス』の各セクションの説明は、「SAX プロジェクト Web ページ (英語)」のドキュメントを土台としています。
SAX API は、Java SAX 2.0 API を Curl® 言語にマッピングしたものです。SAX 2 で使用禁止の SAX 1 クラスのサポート、あるいは SAX 2 との互換性を SAX 1 に提供するクラスのサポートは行なわれていません。クラス名は、Java SAX のクラス名と同じです。メソッド名は Curl 言語の命名規則に従います。つまり、オーバーロードされた Java™ プログラミング言語メソッドは、異なる名前で Curl 言語メソッドにマップされます。オーバーロードされた Java プログラミング言語コンストラクタは、キーワード引数でコンストラクタにマップされています。詳細は『API リファレンス』を参照してください。
Curl 言語で記述された アプレットは、パッケージ CURL.XML.SAX.PARSER をインポートすることによって SAX API にアクセスできます。この章では、プログラム内で SAX を使用する Curl 言語開発者の方を対象に、クイック スタート チュートリアルを用意しています。

ドキュメントの構文解析

要約:
  • SAX パーサー ドライバを使用するために SAXParser のインスタンスを作成します。
  • set-content-handler および set-error-handler を使用してイベントハンドラを登録します。
DefaultHandler を拡張するクラスの作成から始めましょう。
{curl 8.0 applet}
{import * from CURL.XML.SAX.PARSER}

{define-class public MySAXApp {inherits DefaultHandler}

  || passed in on constructor, used for parse output
  field private output:VBox
  || count element nesting for indenting parse output
  field private nesting-depth:int

  {constructor public {default parse-output:VBox}
    set self.output = parse-output
    {construct-super}
  }
}
パーサーのドライバを実装する Curl 言語クラスは SAXParser です。既定のコンストラクタを呼び出して、ドライバの新しいインスタンスを作成します。
{let xr:XMLReader = {SAXParser}}
SAXParser インスタンスを使用して XML ドキュメントを構文解析することができます。ただし、最初に XMLReader.set-content-handler および XMLReader.set-error-handler メソッドを使用して、パーサーが情報をレポートするために使用できるイベント ハンドラを登録する必要があります。実際のアプリケーションではハンドラが個別のオブジェクトになるのが一般的ですが、このチュートリアルでは、ハンドラをトップ レベルのクラスにバンドルして、クラスをインスタンス化して XML ドライバとともにそれを登録するだけで済むようにしてあります。
{let xr:XMLReader = {SAXParser}}
{let parse-output:VBox = {VBox}}
{let handler:MySAXApp = {MySAXApp parse-output}}
{xr.set-content-handler handler}
{xr.set-error-handler handler}
このコードによって、XML 構文解析イベントを受け取る MySAXApp のインスタンスを作成し、通常のコンテンツ イベントまたはエラー イベント (その他の種類のイベントもありますがほとんど使用されません) 用にそのインスタンスを XML リーダーとともに登録します。ここでは、解析出力のためにVBox を作成し、引数としてそれを MySAXApp コンストラクタに渡します。
次に parse-and-display と呼ばれるプロシージャを作成します。非 NULL 入力が指定されている場合、このプロシージャでは SAXParser.parse メソッド呼び出しを実行してから出力を表示します。
解析メソッド呼び出しの周囲に try /catch ブロックをインクルードします。SAXParseException をキャッチすると解析出力 (parse output) が表示されるように、VBox にエラー メッセージ テキストを追加します。
|| This code is called when the user has selected an xml file to parse
|| The argument "input-name" is the URL of the file to parse
{define-proc {parse-and-display input-name:Url}:void
    || clear the parse output of any previous xml file
    {parse-output.clear}

    {try
        || Parse from what is found by opening input-name
        {xr.parse {InputSource system-id = input-name.name}}

     catch file-exception:IOException do
        || Handle any exception thrown due to the file read
        {parse-output.add
            {text color="red",
                A problem occurred while reading the file:
                {value file-exception}
            }
        }
     catch parse-exception:SAXParseException do
        || Handle any exception thrown while parsing the XML
        {parse-output.add
            {text color="red",
                A problem occurred parsing the XML file:
                {error-message parse-exception}
            }
        }
    }
}
CommandButton により、読み取るファイルを選択するためのダイアログをユーザーが開けるようになります。このボタンは、choose-location を呼び出してダイアログ ボックスを表示します。ユーザーがファイルを選択しない場合、choose-location プロシージャは null を返します。この場合、エラー メッセージが出力領域に追加されます。それ以外の場合は、parse-and-display を呼び出します。
hrule 呼び出しは、CommandButton の下に水平線を引きます。解析出力 (parse output) は、この線の下に表示されます。
{let command:CommandButton =
    {CommandButton
        label = "Choose File",
        {on Action do
            let f:#Url = {choose-location
                             title="Select an XML file to parse"
                         }
            {if-non-null f then
                {parse-and-display f}
             else
                {parse-output.clear}
                {parse-output.add
                    {text color="red",
                        Please select a file to parse.
                    }
                }
            }
        }
    }
}

|| Determine the location of the example XML file.
{let example-url:Url = {url "../../default/support/brillig.xml"}}

{VBox
        {heading XML Example},
        {text Select an XML file to be read. Hint: try
            {bold {value example-url.local-filename}}
        },
        command,
        {hrule},
        {ScrollBox height=4.5in, parse-output}
    }

これまでのデモ クラスの内容全体を次に示します。
{curl 8.0 applet}
{import * from CURL.XML.SAX.PARSER}

{define-class public MySAXApp {inherits DefaultHandler}

  || passed in on constructor, used for parse output
  field private output:VBox
  || count element nesting for indenting parse output
  field private nesting-depth:int

  {constructor public {default parse-output:VBox}
    set self.output = parse-output
    {construct-super}
  }
}
{let xr:XMLReader = {SAXParser}}
{let parse-output:VBox = {VBox}}
{let handler:MySAXApp = {MySAXApp parse-output}}
{xr.set-content-handler handler}
{xr.set-error-handler handler}


{define-proc {parse-and-display input-name:Url}:void
    || clear the parse output of any previous xml file
    {parse-output.clear}

    {try
        || Parse from what is found by opening input-name
        {xr.parse {InputSource system-id = input-name.name}}

     catch file-exception:IOException do
        || Handle any exception thrown due to the file read
        {parse-output.add
            {text color="red",
                A problem occurred while reading the file:
                {value file-exception}
            }
        }
     catch parse-exception:SAXParseException do
        || Handle any exception thrown while parsing the XML
        {parse-output.add
            {text color="red",
                A problem occurred parsing the XML file:
                {error-message parse-exception}
            }
        }
    }
}

{let command:CommandButton =
    {CommandButton
        label = "Choose File",
        {on Action do
            let f:#Url = {choose-location
                             title="Select an XML file to parse"
                         }
            {if-non-null f then
                {parse-and-display f}
             else
                {parse-output.clear}
                {parse-output.add
                    {text color="red",
                        Please select a file to parse.
                    }
                }
            }
        }
    }
}
|| Determine the location of the example XML file.
{let example-url:Url = {url "../../default/support/brillig.xml"}}

{VBox
    {heading XML Example},
    {text Select an XML file to be read. Hint: try
        {bold {value example-url.local-filename}}
    },
    command,
    {hrule},
    {ScrollBox height=4.5in, parse-output}
}
このコードをコンパイルして実行できますが、アプリケーションを設定して SAX イベントを処理していないため、存在しない URL を指定しない限り、ほとんど何も起こりません。

イベントの処理

要約:
  • start-document および end-document メソッドを使用して start-document および end-document イベントを処理します。
  • start-element および end-element が要素イベントを処理します。
  • 通常の文字データは、 character メソッドによって処理されます。
XML 構文解析イベントに応答するメソッドの実装を開始しましょう。前のセクションで、XML 構文解析イベントを受け取るクラスを登録したことを思い出してください。最も重要なイベントは、ドキュメントの開始および終了、要素の開始および終了、および文字データです。
ドキュメントの開始および終了を検出するために、クライアント アプリケーションは DefaultHandler.start-document および DefaultHandler.end-document メソッドを MySAXApp クラスの一部として実装します。
{method public {start-document}:void
    set self.nesting-depth = 0
    {self.output.add
        {text color="purple", Start document}
    }
}

{method public {end-document}:void
    {self.output.add
        {text color="purple",End document}
    }
}
開始 / 終了ドキュメント イベント ハンドラは引数を持ちません。SAX ドライバがドキュメントの先頭を検出すると、start-document メソッドを 1 回呼び出します。ドキュメントの終わりを検出すると end-document メソッドを 1 回呼び出します。エラーが発生すると end-document は呼び出されない可能性があります。
これらのメソッドは、コンストラクタに渡されメンバ出力に保存されている VBox に紫色のメッセージを追加します。ただし、実際のアプリケーションではこれらのハンドラに任意のコードを含めることができます。数種類のメモリ内ツリーの構築、出力の生成、データベースのポピュレート、または XML ストリームからの情報の抽出などをコードによって実行するのが最も一般的です。
SAX ドライバは同じ方法で要素の開始および終了を検出してメソッドを呼び出しますが、さらにいくつかのパラメータを DefaultHandler.start-element および DefaultHandler.end-element メソッドに渡します。
{method public {start-element
                   uri:String,
                   name:String,
                   qname:String,
                   atts:Attributes
               }:void
    set self.nesting-depth = self.nesting-depth + 1
    {self.output.add
        {HBox
            {Fill width = self.nesting-depth * 0.5in},
            {text color="navy", Start element: },
            {text color="fuchsia",
                {if uri != ""
                 then
                    "[" & uri & "]"
                 else
                    ""
                }
            },
            {text color="fuchsia", {value name}}
        }
    }
}

{method public {end-element
                   uri:String,
                   name:String,
                   qname:String
               }:void
    {self.output.add
        {HBox
            {Fill width = self.nesting-depth * 0.5in},
            {text color="navy", End element: },
            {text color="fuchsia",
                {if uri != ""
                 then
                    "[" & uri & "]"
                 else
                    ""
                }
            },
            {text color="fuchsia", {value name}}
        }
    }
    set self.nesting-depth = self.nesting-depth - 1
}
要素が開始または終了するたびに、要素のローカル名の前にある中カッコ内の名前空間の URL を使用して、テキストを VBox に追加します。Fill は、要素がネストされている位置に応じてメッセージをインデントします。start-element ごとに要素のネストの深さを増加し、end-element ごとに減少します。
最後に、SAXParser は、characters メソッドよって通常の文字データを送信します。次の実装はすべての文字データを解析出力 (parse output) に印刷します。
{method public {characters
                   ch:StringBuf,
                   start:int,
                   length:int
               }:void
    {self.output.add
        {HBox
            {Fill width = self.nesting-depth * 0.5in},
            {text color="teal", Characters: },
            {text color="lime",
                {ch.substr start, length}}
        }
    }
}
SAX ドライバは任意に文字データをまとめてしまうので、単一の characters イベントに到着する要素の文字データの内容をそのまま当てにすることはできません。

エラーの処理

SAX ドライバが警告やエラーを検出すると、次の 3 つのメソッド、errorfatal-error、または warning のいずれかを呼び出します。この例では set-error-handler を呼び出すので、エラーを報告するためにこれらのメソッドを実装する必要があります。SAX ドライバは、SAXParseException 型の引数を使用してこれらのメソッドを呼び出します。説明を簡単にするために、ここではこれらの 3 つのメソッド (errorfatal-error および warning) をすべて同じ方法で実装します。例外を再スローし、解析呼び出しの周りに try-catch ブロックを置きます。
エラー、致命的エラー、および警告メソッドの実装例を次に示します。
{method public {error exception:SAXParseException}:void
    {throw exception}
}

{method public {fatal-error exception:SAXParseException}:void
    {throw exception}
}

{method public {warning exception:SAXParseException}:void
    {throw exception}
}
最後に、表示するエラー メッセージに SAXParseException を返すために使用するプロシージャを次に示します。
{define-proc {error-message parse-exception:SAXParseException}:String
    let sb:StringBuf={StringBuf "Error detected"}
    {if {parse-exception.get-line-number} != 0
     then
        {sb.concat " at line " & {parse-exception.get-line-number}}
        {if {parse-exception.get-system-id} != null
         then
            {sb.concat " of " & {parse-exception.get-system-id}}
        }
    }
    {sb.concat ": " & {parse-exception.get-message}}
    {return {sb.to-String}}
}
       

サンプル SAX アプリケーション

完成したサンプル アプリケーションを次に示します (実際のアプリケーションでは、イベント ハンドラを個別のクラスで実装するのが一般的です)。この短いサンプル ドキュメントでさえ 25 のイベントが生成されることに注意してください。これらのイベントには、使用される 6 つの要素の開始と終了ごとのイベント、文字データの 11 のチャンクごとのイベント、および要素間の空白 (ドキュメントの開始と終了のイベント) が含まれます。

例: サンプル アプリケーション
{import * from CURL.XML.SAX.PARSER}

{define-class public MySAXApp {inherits DefaultHandler}

  field private output:VBox
  field private nesting-depth:int

  {constructor public {default parse-output:VBox}
    set self.output = parse-output
    {construct-super}
  }

  {method public {start-document}:void
    set self.nesting-depth = 0
    {self.output.add
        {text color="purple", Start document}
    }
  }

  {method public {end-document}:void
    {self.output.add
        {text color="purple",End document}
    }
  }

  {method public {start-element
                     uri:String,
                     name:String,
                     qname:String,
                     atts:Attributes
                 }:void
    set self.nesting-depth = self.nesting-depth + 1
    {self.output.add
        {HBox
            {Fill width = self.nesting-depth * 0.5in},
            {text color="navy", Start element: },
            {text color="fuchsia",
                {if uri != "" then
                    "[" & uri & "]"
                 else
                    ""
                }
            },
            {text color="fuchsia", {value name}}
        }
    }
  }

  {method public {end-element
                     uri:String,
                     name:String,
                     qname:String
                 }:void
    {self.output.add
        {HBox
            {Fill width = self.nesting-depth * 0.5in},
            {text color="navy", End element: },
            {text color="fuchsia",
                {if uri != "" then
                    "[" & uri & "]"
                 else
                    ""
                }
            },
            {text color="fuchsia", {value name}}
        }
    }
    set self.nesting-depth = self.nesting-depth - 1
  }
  
  {method public {characters
                     ch:StringBuf,
                     start:int,
                     length:int
                 }:void
    {self.output.add
        {HBox
            {Fill width = self.nesting-depth * 0.5in},
            {text color="teal", Characters: },
            {text color="lime", {ch.substr start, length}}
        }
    }
  }
  
  {method public {error exception:SAXParseException}:void
    {throw exception}
  }
  
  {method public {fatal-error exception:SAXParseException}:void
    {throw exception}
  }
  
  {method public {warning exception:SAXParseException}:void
    {throw exception}
  }
  
}

{define-proc {error-message parse-exception:SAXParseException}:String
    let sb:StringBuf={StringBuf "Error detected"}
    {if {parse-exception.get-line-number} != 0
     then
        {sb.concat " at line " & {parse-exception.get-line-number}}
        {if {parse-exception.get-system-id} != null
         then
            {sb.concat " of " & {parse-exception.get-system-id}}
        }
    }
    {sb.concat ": " & {parse-exception.get-message}}
    {return {sb.to-String}}
}

{let xr:XMLReader = {SAXParser}}
{let parse-output:VBox = {VBox}}
{let handler:MySAXApp = {MySAXApp parse-output}}
{xr.set-content-handler handler}
{xr.set-error-handler handler}

{define-proc {parse-and-display input-name:Url}:void
    || clear the parse output of any previous xml file
    {parse-output.clear}

    {try
        || Parse from what is found by opening input-name
        {xr.parse {InputSource system-id = input-name.name}}

     catch file-exception:IOException do
        || Handle any exception thrown due to the file read
        {parse-output.add
            {text color="red",
                A problem occurred while reading the file:
                {value file-exception}
            }
        }
     catch parse-exception:SAXParseException do
        || Handle any exception thrown while parsing the XML
        {parse-output.add
            {text color="red",
                A problem occurred parsing the XML file:
                {error-message parse-exception}
            }
        }
    }
}

{let command:CommandButton =
    {CommandButton
        label = "Choose File",
        {on Action do
            let f:#Url = {choose-location
                             title="Select an XML file to parse"
                         }
            {if-non-null f then
                {parse-and-display f}
             else
                {parse-output.clear}
                {parse-output.add
                    {text color="red",
                        Please select a file to parse.
                    }
                }
            }
        }
    }
}
|| Determine the location of the example XML file.
{let example-url:Url = {url "../../default/support/brillig.xml"}}

{VBox
    {heading XML Example},
    {text Select an XML file to be read. Hint: try
        {bold {value example-url.local-filename}}
    },
    command,
    {hrule},
    {ScrollBox height=4.5in, parse-output}
}