この章では、Oracle XML Developer's Kit (XDK)に含まれているJavaライブラリを使用して2つのExtensible Markup Language (XML)入力間の差分を求める方法について説明します。
内容は次のとおりです。
Java XML diffライブラリには、oracle.xml.diff
パッケージのXmlUtils
クラスに、XML入力の差分検出メソッド、ハッシング・メソッドおよび等価比較メソッドが含まれています。oracle.xml.diff
パッケージのOptions
クラスには、XmlUtils
クラスの各メソッドで入力を処理する方法を制御するオプションが用意されています(「Java XML diffライブラリのユーザー・オプション」を参照)。サポートされているオプションの1つは、空白文字の正規化で、これはデフォルトで有効化されています。
XML diffメソッドで使用されるアルゴリズムは、最小の差分が要求されない、2つの大容量XML文書(5MB以上)の差分を数秒以内に検出するユースケース向けに設計されています。最小の差分とは可能なかぎり小さい変更であり、これを最初のXML入力に適用すると、2番目のXML入力と同等の(まったく同じ)出力が生成されます。既知の最小差分検出アルゴリズムでは、数MBの入力を処理するために、とてつもない量のメモリー容量と時間が必要になります。XML diffメソッドで使用されるアルゴリズムでは、XML入力内に同一サブツリーが繰り返し出現することのない、最高品質の(可能なかぎり最小に近い)差分検出が可能になります。
Java XML diffライブラリでは、各メソッドに複数の等価バリアントが用意されており、Document Object Model (DOM)ノード、ファイル、入力ストリームなど様々な形式でのXML入力が可能になっています。内部的には、差分検出、ハッシングおよび等価比較はDOMツリーで行われます。DOMツリー・フォーム以外の入力は、内部でDOMツリーに変換されます。計算のオーバーヘッドを削減するために、可能な場合は直接DOMに渡すことが推奨されます。
Java XML diffライブラリには、diff出力を、DOM文書またはそれぞれがdiff操作を表すオブジェクトのリストとして戻すメソッドが含まれます。2つめのオプションを使用すると、XML文書生成のオーバーヘッドを回避できます。最初のオプションを使用すると、「Diff出力スキーマ」に記載されたXMLスキーマに準拠する文書が戻されます。最初のオプションは、将来参照するためにdiff出力をログとして保存しなければならない場合などに便利です。
Java XML diffライブラリで提供されるhashメソッドでは、XML入力のハッシュ値が計算されます。2つのXML入力のハッシュ値が同数の場合、これらは非常に高い確率で同一です。
Java XML diffライブラリで提供されるequalメソッドでは、2つの入力の等価性が比較されます。
Java XML diffライブラリを使用するには、アプリケーションがJavaバージョン1.6以降で実行されていて、任意のDOMが実装されている必要があります。
注意:
この章で説明されているアプリケーション・プログラミング・インタフェース(API)コンポーネントは、Javaパッケージoracle.xml.diff
内に含まれています。簡潔にするために、完全修飾名は、混乱を避ける必要がある場合にのみ使用されます。
oracle.xml.diff
パッケージの詳細は、Oracle Database XML Java APIリファレンスを参照してください。
Java XML diffライブラリでは、oracle.xml.diff
パッケージのOptions
クラスの各メソッドを使用して設定可能な、2つのオプションがサポートされています。起動するたびに、diffメソッド、hashメソッドおよびequalメソッドにOptions
オブジェクトが直接渡されます。
テキスト・ノードの正規化(デフォルトで有効)
テキスト・ノードは、diff、hashおよびequalメソッドが動作するDOMツリーで正規化されます。テキスト・ノードの正規化では、隣接するテキスト・ノードが結合された後に、結合されたノードから先頭および末尾の空白が削除されます。単一テキスト・ノードの先頭および末尾の空白は削除されます。空白のみのテキスト・ノードは削除されます。
正規化は、提供されたXML入力を変更することなく、最小限の追加領域のあるライブラリ内で実行されます。
DOM入力に対して独自の正規化を実行してからライブラリに渡すには、Options
オブジェクトでnormalizeTextNodes(false)
メソッドを起動して、デフォルトの正規化を無効にする必要があります。
なんらかのタイプの正規化(デフォルトまたは独自)を実行せずにdiffメソッドを起動することは、お薦めできません。同一の空白のテキスト・ノードがある(XML文書ではよくある)とdiffの品質は低下します。
名前空間接頭辞の差分を無視(デフォルトで有効)
diff、hashおよびequalメソッドでは、XML名前空間接頭辞の差分は無視されます。たとえば、2つのDOMノードが異なる接頭辞を持つ以外同一である場合、これらは同一とみなされます(2つの異なる接頭辞が同じ名前空間のUniversal Resource Identifier (URI)にマップされる場合でも)。同じURIにマップされる場合でも、異なる名前空間接頭辞をまったく異なるものとして扱うようにライブラリを構成するには、Options
オブジェクトでignorePrefixDifferences(false)
メソッドを起動して、名前空間接頭辞のデフォルトの動作を無効にします。
関連項目:
Options
クラスの各メソッドの詳細は、Oracle Database XML Java APIリファレンスを参照してください
Java XML dffライブラリには、oracle.xml.diff
パッケージのXmlUtils
クラスに、様々なdiff
およびdiffToDoc
メソッドが用意されています。これらのメソッドを使用して2つのXML入力を比較し、両者の間に差分があるかどうかを判断できます。
diffToDoc
メソッドは、「Diff出力スキーマ」に記載されているスキーマに準拠したDOM文書として出力を戻します。Java XML diffライブラリには、これらのメソッドの複数の等価バリアントが含まれており、様々なフォーム(DOMノード、ファイル、入力ストリームなど)での入力が可能になっています。
Java XML diffライブラリには、diff操作オブジェクトのリストとして戻されるdiff出力の使用を可能にする、等価のdiff
メソッドのセットが含まれています。
差分を示すDOM文書を構築する必要がないため、diff
メソッドを使用する方がdifftoDoc
メソッドを使用するよりも効率的です。差分をXMLフォームで示す必要がない場合は、これらのメソッドの使用を検討してください。diff
メソッドを使用するには、DiffOpReceiver
インタフェースの実装を作成し、それをパラメータとしてdiff
メソッドに渡す必要があります。DiffOpReceiver.receiveDiff
メソッドは、差分をDiffOp
オブジェクトのリストとして受け取ります。
差分の結果は、それがDOM文書として戻される場合でもDiffOps
オブジェクトのリストとして戻される場合でも、一連の差分操作として解釈できます。実行できる差分操作は次のとおりです。
append-node
insert-node-before
delete-node
最初のDOMツリーに一連の差分操作を適用すると、ツリーは2番目のDOMツリーと同等になります。たとえば、次の2つのXML入力を使用するとします。
最初の入力: <a><b/></a>
2番目の入力: <a><c/></a>
最初の入力と2番目の入力を比較した差分結果のリストが、次の2つの差分操作とともに示されます。
delete-node /a[1]/b[1] append-node <c/> to /a[1]
最初の入力でXPath式/a/b
で表されるノードを削除してから、最初の入力でXPath式/a
で表されるノードに<c/>
を追加すると、<a><c/></a>
という結果が生成され、これは2番目の入力と同等です。
domToDoc(…)
メソッドによって差分操作の出力がDOM文書に生成される場合は、XPath式に依拠してノードの位置が示されます。これらのXPathの位置は、元の最初の入力のノード位置を参照します。適用した差分操作は反映されません。
注意:
Java XML diffライブラリでは、属性ノードに対するappend-node、insert-node-beforeおよびdelete-node操作はサポートしていません。このため、ノードのいずれかの属性が変更された場合、ノード全体の削除、およびそれに続く変更されたノードへの新規ノードの挿入または付加として変更が示されます。
たとえば、次の2つの入力があるとします。
最初の入力: <a attr1="val1"><b/></a>
2番目の入力: <a attr2="val2"><b/</a>
差分は、次の2つの差分操作で構成されます。
insert <a attr2="val2"><b/></a> before /a[1] delete /a[1]
内容は次のとおりです。
注意:
この項では、XML文書の出力を使用して、各差分操作を説明します。ここでは説明しませんが、プログラムで戻される差分操作の結果も同等です。
関連項目:
DiffOpReceiver
インタフェースの詳細およびXmlUtils
クラスのメソッドの詳細は、Oracle Database XML Java APIリファレンスを参照してください
append-node操作は、指定したノードを、指定した最初の入力ノードの最後の子として追加するよう指定します。例12-1に、強調表示されたノード<enumeration value="FL"/>
を文書に追加するappend-node操作を示します。
元の文書(強調表示された変更なしの)および変更された文書を入力として、diffToDoc(…)
メソッドを起動すると、次の出力が生成されます。
<xd:append-node xd:parent-xpath="/schema[1]/simpleType[1]/restriction[1]" xd:node-type="element"> <xd:content> <enumeration value="FL"/> </xd:content> </xd:append-node>
前述の出力では、append-node操作は<append-node>
要素によって表されています。この要素は、指定したタイプのノードを、指定した最初の入力親ノードの最後の子として追加するよう指定します。parent-xpath
属性は、親ノードを指定します。node-type
属性は、追加するノードのタイプを指定します。<content>
子要素は、追加するノードを指定します。
diff(…)
メソッドを使用する場合、DiffOpReceiver.receiverDiff(…)
メソッドでDiffOp
オブジェクトとしてappend-node操作にアクセスすることもできます。この場合、この操作は、差分操作に含まれる2つのDOMツリー内のノードへの実際の参照を戻します。最初の入力の親ノードへの参照は、DiffOp
のgetParent()
メソッドを起動して戻されます。2番目の入力から追加するノードへの参照は、DiffOp
のgetNew()
メソッドを起動して戻されます。
例12-1 ノードの追加
<schema>
…
<simpleType name="USState">
<restriction base="string">
<enumeration value="NY"/>
<enumeration value="TX"/>
<enumeration value="CA"/>
<enumeration value="FL"/>
</restriction>
</simpleType>
…
</schema>
insert-node-before操作は、指定したノードを、最初の入力の特定ノードの前に挿入するよう指定します。例12-2に、文書で強調表示されたノード<!-- A type representing US States -->
をノード<simpleType name="USState">
の前に挿入するinsert-node-before操作を示します。
元の文書(強調表示された変更なしの)および変更された文書を入力として、diffToDoc(…)
メソッドを起動すると、次の出力が生成されます。
<xd:insert-node-before xd:node-type="comment" xd:xpath="/schema[1]/simpleType[1]"> <xd:content> <!-- A type representing US States --> </xd:content> </xd:insert-node-before>
前述の出力では、insert-node-before操作は<insert-node-before>
要素によって表されています。この要素は、指定したタイプのノードを、指定した最初の入力ノードの前に挿入するよう指定します。xpath
属性は、最初の入力ノードの位置を指定します。node-type
属性は、挿入するノードのタイプを指定します。<content>
子要素は、挿入するノードを指定します。
diff(…)
メソッドを使用する場合、DiffOpReceiver.receiverDiff(…)
メソッドでDiffOp
オブジェクトとしてinsert-node-before操作にアクセスすることもできます。この場合、この操作は、差分操作に含まれる2つのDOMツリー内のノードへの実際の参照を戻します。最初の入力でノードを挿入する前のノードへの参照は、DiffOp
のgetSibling()
メソッドを起動して戻されます。2番目の入力から挿入するノードへの参照は、DiffOp
のgetNew()
メソッドを起動して戻されます。
例12-2 ノードの挿入
<schema>
…
<!-- A type representing US States -->
<simpleType name="USState">
<restriction base="string">
<enumeration value="NY"/>
<enumeration value="TX"/>
<enumeration value="CA"/>
</restriction>
</simpleType>
…
</schema>
delete-node操作は、最初の入力の特定ノード(およびそのサブツリー)を削除するよう指定します。例12-3に、強調表示されたノード<element name="LineItems" maxOccurs="unbounded">
を文書から削除するdelete-node操作を示します。
元の文書(強調表示された変更なしの)および変更された文書を入力として、diffToDoc(…)メソッドを起動すると、次の出力が生成されます。
<xd:delete-node xd:node-type="element" xd:xpath= "/schema[1]/element[1]/complexType[1]/sequence[1]/element[1]/element[1]"/>
前述の出力では、delete-node操作は<delete-node>
要素によって表されています。この要素は、指定したタイプのノードを削除するよう指定します。xpath
属性は、最初の入力ノードの位置を指定します。node-type
属性は、削除するノードのタイプを指定します。
diff(…)
メソッドを使用する場合、DiffOpReceiver.receiverDiff(…)
メソッドでDiffOp
オブジェクトとしてdelete-node操作にアクセスすることもできます。この場合、この操作は、最初の入力DOMツリー内のノードへの実際の参照を戻します。最初の入力から削除するノードへの参照は、DiffOp
のgetCurrent()
メソッドを起動して戻されます。
例12-3 ノードの削除
<schema>
…
<element name="PurchaseOrder">
<complexType>
<sequence>
<element name="PO-Number" type="string">
<element name="LineItems" maxOccurs="unbounded">
…
</schema>
この項の例では、Javaアプリケーションからdiff
およびdiffToDoc
メソッドを起動して、2つの入力間で差分検出を実行する方法を示します。
例12-4に、diffToDoc
メソッドを使用して入力ファイルdoc
とdoc1
を比較する方法を示します。
引き続きこの例を使用します。2つの入力ファイルf1.xml
とf2.xml
には例12-1と同じデータが含まれます。
次のサンプル・コードは、f1.xml
の内容を示しています。
<schema> <simpleType name="USState"> <restriction base="string"> <enumeration value="NY"/> <enumeration value="TX"/> <enumeration value="CA"/> </restriction> </simpleType> </schema>
また、次のサンプル・コードはf2.xml
の内容を示しています。
<schema> <simpleType name="USState"> <restriction base="string"> <enumeration value="NY"/> <enumeration value="TX"/> <enumeration value="CA"/> <enumeration value="FL"/> </restriction> </simpleType> </schema>
textDiff.java
および入力ファイルは現在のディレクトリにあるとします。次のコマンドを入力して、サンプルをコンパイルして実行します。
javac -classpath "xml.jar" textDiff.java java –classpath “xml.jar:." textDiff f1.xml f2.xml
生成されたdiffAsDom
文書をシリアライズすると、次の出力が生成されます。
<xd:xdiff xmlns:xd="http://xmlns.oracle.com/xdb/xdiff.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.oracle.com/xdb/xdiff.xsd http://xmlns.oracle.com/xdb/xdiff.xsd"> <?oracle-xmldiff operations-in-docorder="true" output-model="snapshot" diff-algorithm="greedy-heuristic"?> <xd:append-node xd:node-type="element" xd:parent-xpath="/schema[1]/simpleType[1]/restriction[1]"> <xd:content> <enumeration value="FL"/> </xd:content> </xd:append-node> </xd:xdiff>
例12-5に、DiffOpReceiver
インタフェースの実装を使用して、2つのXML入力の比較からDiffOp
オブジェクトのリストとして戻された差分を処理する方法を示します。
次のコマンドを入力して、サンプルをコンパイルして実行します。
javac -classpath "xml.jar" progDiff.java java –classpath “xml.jar:." progDiff f1.xml f2.xml
次の出力が生成されます。
APPENDING NODE: <enumeration value="FL"/> TO THE PARENT NODE: <restriction base="string"> <enumeration value="NY"/> <enumeration value="TX"/> <enumeration value="CA"/> </restriction>
例12-4 Javaアプリケーションから差分を文書として取得
import oracle.xml.diff.XmlUtils; import oracle.xml.diff.Options; import java.io.File; import org.w3c.dom.Node; import org.w3c.dom.Document; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilder; public class textDiff { public static void main(String[] args) throws Exception { XmlUtils xmlUtils = new XmlUtils(); //Parse the two input files DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); dbFactory.setNamespaceAware(true); DocumentBuilder docBuilder = dbFactory.newDocumentBuilder(); Node doc = docBuilder.parse(new File(args[0])); Node doc1 = docBuilder.parse(new File(args[1])); //Run the diff try { Document diffAsDom = xmlUtils.diffToDoc(doc, doc1, new Options()); } catch (Exception e) { e.printStackTrace(); } } }
例12-5 DiffOpReceiverを使用したJavaアプリケーションからの差分の取得
import oracle.xml.diff.DiffOp; import oracle.xml.diff.DiffOpReceiver; import java.util.List; import java.util.Properties; import java.io.File; import org.w3c.dom.Node; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilder; public class progDiff { public static void main(String[] args) throws Exception { XmlUtils xmlUtils = new XmlUtils(); //Parse the two input files DocumentBuilderFactory dbFac = DocumentBuilderFactory.newInstance(); dbFac.setNamespaceAware(true); DocumentBuilder docBuilder = dbFac.newDocumentBuilder(); Node doc = docBuilder.parse(new File(args[0])); Node doc1 = docBuilder.parse(new File(args[1])); Options opt = new Options(); //Instantiate the DiffOpReceiver. This is the object that //will receive DiffOps, ie diff operations that the XmlDiff //outputs. Each object represents either deletion or insert //or append of a node. In this DiffOpReceiverImpl //implementation (see below) of the DiffOpReceiver //interface, we simply print out each diff operation. DiffOpReceiver diffOpRec = new progDiff().new DiffOpReceiverImpl(); xmlUtils.diff(doc, doc1, diffOpRec, opt); } class DiffOpReceiverImpl implements DiffOpReceiver { public void receiveDiff(List<DiffOp> diffOps) { try { for (int i = 0; i < diffOps.size(); i++) { DiffOp diffOperation= diffOps.get(i); //Delete operation, print out the deleted // node from the first tree if (diffOperation.getOpName() == DiffOp.Name.DELETE) System.out.println ("DELETING NODE:\n" + XmlUtils.nodeToString(diffOperation.getCurrent(), false)); //Insert operation. Print out the node //from the second tree to be inserted, //and the node from the first tree //before which the insertion will happen else if (diffOperation.getOpName() == DiffOp.Name.INSERT_BEFORE_NODE) System.out.println ("INSERTING NODE:\n" + XmlUtils.nodeToString(diffOperation.getNew(), false) + "BEFORE NODE:\n" + XmlUtils.nodeToString(diffOperation.getSibling(), false)); //Append as the last node of the parent. //Print out the node from the second tree //that will be appended, and the parent //node from the first tree to which the //former node will be appended as the //last child. else if (diffOperation.getOpName() == DiffOp.Name.INSERT_BY_APPENDING) System.out.println ("APPENDING NODE:\n" + XmlUtils.nodeToString(diffOperation.getNew(), false) + "TO THE PARENT NODE:\n" + XmlUtils.nodeToString(diffOperation.getParent(), false)); } } catch (Exception e) { System.err.println ("Error while printing out the diff result:" + e.getMessage()); } } } }
Java XML diffライブラリには、高い確率で入力を一意に識別するハッシュ値を計算するためのhash
メソッドが用意されています。非常に低い確率でハッシュ衝突が発生することがあるため、ハッシュ値が一致する場合でも2つの入力が同一であるという保証はありません。2つの入力が本当に同一であると断定できることを確認するには、equal
メソッドを使用します。equal
メソッドは、2つの入力の絶対的な等価性を確認しながら、両者を同時に処理します。
Java XML diffライブラリには、hash
メソッドおよびequal
メソッドの複数の等価バリアントが用意されており、様々なフォーム(DOMノード、ファイルなど)での入力を受け入れることができます。
関連項目:
XmlUtils
クラスのhash
およびequal
メソッドの詳細は、Oracle Database XML Java APIリファレンスを参照してください
例12-6に、Java XML diffライブラリが準拠するDiff出力スキーマ(xdiff.xsd
)を示します。
例12-6 Diff出力スキーマ: xdiff.xsd
<schema targetNamespace="http://xmlns.oracle.com/xdb/xdiff.xsd" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:xd="http://xmlns.oracle.com/xdb/xdiff.xsd" version="1.0" elementFormDefault="qualified" attributeFormDefault="qualified"> <annotation> <documentation xml:lang="en"> Defines the structure of XML documents that capture the difference between two XML inputs. Changes that are not supported by Oracle XmlDiff may not be expressible in this schema. 'oracle-xmldiff' PI: We use 'oracle-xmldiff' PI to describe certain aspects of the diff. This should be the first element of top level xdiff element. version-number: version number of the XML diff schema output-model: output model for representing the diff. Currently, only the "snapshot" model is supported. Snapshot model: Each operation uses XPaths as if no operations have been applied to the input document. Default and works for both Xmldiff and XmlPatch. <!-- Example: <?oracle-xmldiff version-number = "1.0" output-model = "snapshot"?> --> </documentation> </annotation> <!-- Enumerate the supported node types --> <simpleType name="xdiff-nodetype"> <restriction base="string"> <enumeration value="element"/> <enumeration value="text"/> <enumeration value="cdata"/> <enumeration value="processing-instruction"/> <enumeration value="comment"/> </restriction> </simpleType> <element name="xdiff"> <complexType> <choice minOccurs="0" maxOccurs="unbounded"> <element name="append-node"> <complexType> <sequence> <element name="content" type="anyType"/> </sequence> <attribute name="node-type" type="xd:xdiff-nodetype"/> <attribute name="parent-xpath" type="string"/> </complexType> </element> <element name="insert-node-before"> <complexType> <sequence> <element name="content" type="anyType"/> </sequence> <attribute name="xpath" type="string"/> <attribute name="node-type" type="xd:xdiff-nodetype"/> </complexType> </element> <element name="delete-node"> <complexType> <attribute name="node-type" type="xd:xdiff-nodetype"/> <attribute name="xpath" type="string"/> </complexType> </element> </choice> </complexType> </element> </schema>