このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
DOM は XML 文書などをプログラムで扱うために提供される、オブジェクト指 向型の API です。 もともとは、 Netscape 社が提案した Dynamic HTML という技術が発端です。 これはブラウザが読み込んだ HTML を Javascript 言語が取り扱えるようにす るために、HTML 文書に API を与えたものです。 すぐに Microsoft 社が DHTML と JScript というよく似ているけど、互換性 があまりない形態で追従しました。 これらは現在では DOM Level 0 と呼ばれていますが、これは規格化されたも のではありません。
W3C が制定している DOM は Level 1, 2, 3 があります。 これらはプログラミング言語には中立ということで、 OMG IDL(ISO14750) というインターフェイス記述言語により定義されています。 但し、規格書の中では Java と Javascript(ECMA Script) の両方の記述も含 まれています。
現在の主なブラウザは DOM Level 2 まで準拠していると言えます。 また、 Java 6 からは、 DOM Level 3 Core に対応しています。
前回の XML 文書の例に対して、下記のようなオブジェクトを生成することが 目標となります。
<class name="データ構造とアルゴリズム II" > <teacher name="坂本直志" /> <time day="tue" period="2" /> </class>
基本的には、要素間の関係は含む含まないの関係となり、さらに、ある要素を 直接含む要素は一つだけなので、木構造になります。 したがって、コンポジットデザインパターンを使用し、各要素を順に取り出す ことができます。
この他、DOM の検索 API として、次が org.w3c.dom.Document に用意されて います。
DOM Level 3 Core は XML 用のインタフェイスを記述しています。 これから説明するのは、 org.w3c.dom パッケージの内容です。 この節では主要なインタフェイスについて説明します。
Node は DOM のすべてのオブジェクトが実装しているインタフェイスです。
まず、Node は名前と、値を持ちます。 値に関しては setter も用意されています。
package org.w3c.dom;
public interface Node {
public String getNodeName();
public String getNodeValue() throws DOMException;
public void setNodeValue(String nodeValue) throws DOMException;
...
ノードの種類によって、番号が割り当てられていて、 getNodeType で 番号を取り出せます。
public short getNodeType();
public static final short ELEMENT_NODE = 1;
public static final short ATTRIBUTE_NODE = 2;
public static final short TEXT_NODE = 3;
public static final short CDATA_SECTION_NODE = 4;
public static final short ENTITY_REFERENCE_NODE = 5;
public static final short ENTITY_NODE = 6;
public static final short PROCESSING_INSTRUCTION_NODE = 7;
public static final short COMMENT_NODE = 8;
public static final short DOCUMENT_NODE = 9;
public static final short DOCUMENT_TYPE_NODE = 10;
public static final short DOCUMENT_FRAGMENT_NODE = 11;
public static final short NOTATION_NODE = 12;
また 子ノードを持つので、それに関するメソッドが定義されています。
public Node getParentNode();
public NodeList getChildNodes();
public Node getFirstChild();
public Node getLastChild();
public Node getPreviousSibling();
public Node getNextSibling();
public Node insertBefore(Node newChild, Node refChild) throws DOMException;
public Node replaceChild(Node newChild, Node oldChild) throws DOMException;
public Node removeChild(Node oldChild) throws DOMException;
public Node appendChild(Node newChild) throws DOMException;
public boolean hasChildNodes();
また、 子ノードの他に、Attribute に関するメソッドもあります。
public boolean hasAttributes();
public NamedNodeMap getAttributes();
なお、上記のメソッドで示されていますが、子ノードは NodeList 形式で集め られ、 Attibute は NamedNodeMap という形式で集められています。
NodeList の定義は以下の通りです。これは抜粋ではなく、すべてです。
package org.w3c.dom;
public interface NodeList {
public Node item(int index);
public int getLength();
}
このように、 NodeList 型は単に番号により Node が取り出せるだけの仕組み しか持ってなく、 iterator や for each 構文などもそのままでは使えません。
NamedNodeMap の定義は以下がすべてです。
package org.w3c.dom;
public interface NamedNodeMap {
public Node item(int index);
public int getLength();
public Node getNamedItem(String name);
public Node setNamedItem(Node arg) throws DOMException;
public Node removeNamedItem(String name) throws DOMException;
public Node getNamedItemNS(String namespaceURI, String localName);
public Node setNamedItemNS(Node arg) throws DOMException;
public Node removeNamedItemNS(String namespaceURI, String localName)
throws DOMException;
}
NodeList と同様に番号で呼び出すメソッドもありますが、 getNamedItem と いうメソッドで名前から Node を取り出すことができます。 setNamedItem では Node の名前を使って登録します。 これが void 型でないのは、置き換えられる Node がある場合に、それが返る ようになっているからです。 また、名前空間を指定するメソッドもあります。
これから、Node を継承した各データタイプの定義を見ていきます。 各データタイプにおいて、 nodeName, nodeValue, attributes の解釈が変わっ てきます。 始めに、 org.w3c.dom.Node のマニュアルにに記載されている表を抜粋します。
インタフェース | nodeName | nodeValue | attributes |
---|---|---|---|
Attr |
Attr.name と同じ |
Attr.value と同じ |
null |
CDATASection |
"#cdata-section" |
CharacterData.data (CDATA セクションの内容) と同じ |
null |
Comment |
"#comment" |
CharacterData.data (コメントの内容) と同じ |
null |
Document |
"#document" |
null |
null |
DocumentFragment |
"#document-fragment" |
null |
null |
DocumentType |
DocumentType.name と同じ |
null |
null |
Element |
Element.tagName と同じ |
null |
NamedNodeMap |
Entity |
エンティティー名 | null |
null |
EntityReference |
参照されるエンティティーの名前 |
null |
null |
Notation |
表記法名 |
null |
null |
ProcessingInstruction |
ProcessingInstruction.target と同じ |
ProcessingInstruction.data と同じ |
null |
Text |
"#text" |
CharacterData.data (テキストノードの内容) と同じ |
null |
nodeType = 1 である、 ELEMENT_NODE のクラスは、値、名前、属性すべてを 持っています。 なお、 getNodeName と getTagName は同じ値になります。 また、 getAttribute(文字列) は、 getAttributes().getNamedItem(文字列) と同じです。 なお、setAttribute(文字列, 文字列) は自動的に属性オブジェクトを生成し て要素を追加してくれるので便利ですが、CDATA の検査などはしないとマニュ アルに注釈があります。
属性を示すノードは org.w3c.dom.Attr インタフェースです。 Element で説明しましたが、Element では文字列を介して属性値を操作可能に するメソッドが追加されてました。 そのため、このノードを使用しなくても属性値の操作は可能です。
Text, Comment, CDATASection は Node を直接継承せず、 CharacterData イ ンタフェースを継承しています。 CharacterData インタフェースは下記のように、文字列に関するメソッドを追 加しています。 なお、CharacterData インタフェースの使用する文字は UTF-16 で記述されていると 仮定されています。 そのため、文字列操作においては 16 bit が基準になっています。
package org.w3c.dom;
public interface CharacterData extends Node {
public String getData() throws DOMException;
public void setData(String data) throws DOMException;
public int getLength();
public String substringData(int offset, int count) throws DOMException;
public void appendData(String arg) throws DOMException;
public void insertData(int offset, String arg) throws DOMException;
public void deleteData(int offset, int count) throws DOMException;
public void replaceData(int offset, int count, String arg) throws DOMException;
}
この CharacterData インタフェースをそれぞれ継承するのが Text, Comment, CDATASection ですが、 Text に splitText メソッドが追加される他は、メソッ ドは追加されません。
package org.w3c.dom;
public interface Text extends CharacterData {
public Text splitText(int offset) throws DOMException;
}
package org.w3c.dom;
public interface Comment extends CharacterData {
}
package org.w3c.dom;
public interface CDATASection extends Text {
}
Document は DOM の根の Node になります。 そのため、管理的なメソッドが定義されています。 Doctype, 根の Element が取得できます。
public interface Document extends Node {
public DocumentType getDoctype();
public Element getDocumentElement();
...
また、文書型定義にしたがって要素や属性などのファクトリが定義されていま す。 これを用いて様々な Node を作り、加えるなど操作を行います。
public Element createElement(String tagName) throws DOMException;
public DocumentFragment createDocumentFragment();
public Text createTextNode(String data);
public Comment createComment(String data);
public CDATASection createCDATASection(String data) throws DOMException;
public ProcessingInstruction createProcessingInstruction(String target,
String data) throws DOMException;
public Attr createAttribute(String name) throws DOMException;
public EntityReference createEntityReference(String name) throws DOMException;
さらに、 XML 文書から特定のタグや ID を用いて要素を抜き出すメソッドも あります。
public NodeList getElementsByTagName(String tagname);
public Element getElementById(String elementId);
この他、名前空間を指定するものなどもあります。
XML 文書を DOM に変換するには、javax.xml.parsers パッケージを使います。 ここにある、DocumentBuilder クラスに実装されている parse メソッドは、 java.io.File オブジェクト、または java.io.InputStream オブジェクトを受 け取り、 org.w3c.dom.Document オブジェクトを返します。 但し、 DocumentBuilder には public なコンストラクタがありませんので、 ファクトリクラスである、 DocumentBuilderFactory を使用する必要がありま す。 これにも public なコンストラクタは無く、 newInstance() とい う static メソッドを呼んでオブジェクトを作った後、 newDocumentBuilder メソッドを呼ぶ必要があります。 つまり、次のようにします。
final FileInputStream f = new FileInputStream(ファイル名);
final DocumentBuilderFactory dbf
= DocumentBuilderFactory.newInstance();
final DocumentBuilder db = dbf.newDocumentBuilder();
final Document doc = db.parse(f);
一方、 DOM オブジェクトを XML 文書にするには、 java.xml.transform.Transformer を使用します。 これは、汎用の変換オブジェクトで、 transform メソッドを使って特定の型 から特定の型へと変換します。 transform メソッドの引数は javax.xml.transform.Source 型と、 javax.xml.transform.Result 型です。 Source 型には XML を入力しますが、 DOM を入力するには javax.xml.transform.dom.DOMSource 型のオブジェクトを与えます。 これはコンストラクタに Node のインスタン スを与えて作成します。
一方、 Result 型ですが、標準出力などの Stream にするには、 javax.xml.transform.stream.StreamResult のオブジェクトに与えます。 これも同様にコンストラクタに File, OutputStream, Writer のオブジェクトを与えます。 なお、文字コードを指定する場合は、 java.util.Properties オブジェクトに encoding として文字コードを与え、 Transform オブジェクトの setOutputProperties メソッドに与えます。 なお、この setOutputProperties メソッドで与えられる Property に許され ている属性値は javax.xml.transform.OutputKeys に定義されています。 indent 属性を yes にすると、表示が整形されます。
sample.xml ファイルを XML 文書を DOM に変換した後、DOM を標準出力に出力 するプログラムです。
import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
class Rei {
private static Document getDocument(InputStream is) throws Exception {
final DocumentBuilderFactory dbf
= DocumentBuilderFactory.newInstance();
final DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(is);
}
private static void printDocument(Document doc) throws Exception {
final TransformerFactory tfactory = TransformerFactory.newInstance();
final Transformer transformer = tfactory.newTransformer();
final Properties prop = new Properties();
prop.setProperty("encoding", "Shift_JIS");
prop.setProperty("indent", "yes");
transformer.setOutputProperties(prop);
transformer.transform(new DOMSource(doc),
new StreamResult(System.out));
}
public static void main(String[] args) throws Exception {
final Document doc = getDocument(new FileInputStream("sample.xml"));
printDocument(doc);
}
}
Eclipse で実行する際には、プロジェクトの直下に sample.xml というファイ ルを作成し、ソースタブの画面で下記をペーストします。
<?xml version="1.0" encoding="shift_jis" ?> <class name="データ構造とアルゴリズム II" > <teacher name="坂本直志" /> <time day="tue" period="2" /> </class>
次に、 DOM をメソッドにより一から作成し、 XML 文書として出力する例を示 します。 DocumentBuilder でファイルを読まず、 newDocument メソッドで新しい Document オブジェクトを作成します。 その後、この Document オブジェクトを Element のファクトリとして使用し、作っ た Element を appendChild メソッドで次々に加えていきます。 また、各 Element に対して属性を与えるため、 setAttribute を使います。
import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
class Rei {
private static Document getNewDocument() throws Exception {
final DocumentBuilderFactory dbf
= DocumentBuilderFactory.newInstance();
final DocumentBuilder db = dbf.newDocumentBuilder();
return db.newDocument();
}
private static void printDocument(Document doc) throws Exception {
final TransformerFactory tfactory = TransformerFactory.newInstance();
final Transformer transformer = tfactory.newTransformer();
final Properties prop = new Properties();
prop.setProperty("encoding", "Shift_JIS");
prop.setProperty("indent", "yes");
transformer.setOutputProperties(prop);
transformer.transform(new DOMSource(doc),
new StreamResult(System.out));
}
public static void main(String[] args) throws Exception {
final Document doc = getNewDocument();
final Element root = doc.createElement("class");
root.setAttribute("name","データ構造とアルゴリズム II");
doc.appendChild(root);
final Element teacher = doc.createElement("teacher");
teacher.setAttribute("name","坂本直志");
root.appendChild(teacher);
final Element time = doc.createElement("time");
time.setAttribute("day","tue");
time.setAttribute("period","2");
root.appendChild(time);
printDocument(doc);
}
}
次に、読み込んだ XML 文書に対して、すべての Element に ID をつけること を考えます。
<a> <b /> <b /> </a> |
→ | <a id="0"> <b id="1" /> <b id="2" /> </a> |
但し、ID 属性は文書中で唯一でなければなりません。 そのため、もともと存在している ID に対しては重複しないようにします。
<a> <b id="0" /> <b /> </a> |
→ | <a id="1"> <b id="0" /> <b id="2" /> </a> |
このためには、一旦すべて読み込んでから、重複のチェックができるようにし ます。 そのため、これを行うために、getElementsByTagName メソッドを使用します。 なお、これにはワイルドカード * が使用できます。 したがって、これを使うことで、すべての Element にアクセスできます。
import java.io.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import java.util.*;
class Rei {
private static Document getDocument(InputStream is) throws Exception {
final DocumentBuilderFactory dbf
= DocumentBuilderFactory.newInstance();
final DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(is);
}
private static void printDocument(Document doc) throws Exception {
final TransformerFactory tfactory = TransformerFactory.newInstance();
final Transformer transformer = tfactory.newTransformer();
final Properties prop = new Properties();
prop.setProperty("encoding", "Shift_JIS");
prop.setProperty("indent", "yes");
transformer.setOutputProperties(prop);
transformer.transform(new DOMSource(doc),
new StreamResult(System.out));
}
private static HashSet<String> getIdSet(NodeList list){
final HashSet<String> idset = new HashSet<String>();
for(int i=0; i<list.getLength(); i++){
final Element e = (Element) list.item(i);
if(e.hasAttribute("id")){
idset.add(e.getAttribute("id"));
}
}
return idset;
}
private static void appendId(Document doc){
final NodeList list= doc.getElementsByTagName("*");
final HashSet<String> idset = getIdSet(list);
int id=0;
for(int i=0; i<list.getLength(); i++){
final Element e = (Element) list.item(i);
if(!e.hasAttribute("id")){
String idstr;
do{
idstr=String.valueOf(id++);
}while(idset.contains(idstr));
e.setAttribute("id",idstr);
}
}
}
public static void main(String[] args) throws Exception {
final Document doc = getDocument(new FileInputStream("sample.xml"));
appendId(doc);
printDocument(doc);
}
}
<?xml version="1.0" encoding="shift_jis" ?> <class name="データ構造とアルゴリズム II" > <teacher name="坂本直志" /> <time id="0" day="tue" period="2" /> </class>
最後にテキストファイルを読んで XML ファイルを出力する例を示します。 ここでは、 Excel などで作成できる CSV(Comma Separated Values) を読んで、それに相当する XML を作成します。
CSV は次のようなファイルです。
"品名","単価","個数","合計" "りんご",200,3,600 "みかん",100,5,500 "もも",300,1,300
これは次のような文法で示せます。
CSV ::= (line)* line ::= data (, data)* data ::= number | """ string """
これを JavaCC で厳密に解釈しても良いですが、もっと簡略な方法を取ります。 つまり、入力の構文が常に正しいと仮定して、 単純に ,(カンマ) と "(ダブルクォート) を含まない文字列を取り出すという正規文法で要素を取り出すこと にします。
まず、次の正規文法により、一回の操作で要素を取り出すことを考えます。
[^,"]+
しかし、この方法では「1,,3」など空の項目が存在する時に、要素数が 2 個しかな いという誤動作をします。
従って、ちゃんと構文解析するには、[^,]*
で要素を取り出した後、
[^"]*
でデータを取り出すという二段階の操作の必要があります。
なお、これを、二段階に分けずに
[^,"]*
で取り出すと、「1,"2",3」というデータに対して、
「(1),()"(2)"(),(3)」とカッコで囲んだように 5 箇所にマッチしてしまいま
す。
次に上記のファイルに対して生成する XML の DTD あるいは XMLSchema を与 えます。 これを考えるにあたり、まず、上記の入力ファイルの例から XML 文書の例を 作成してみます。 最初の行は各データの項目名が含まれていて、次の行からはその項目名に対す るデータが含まれているとします。 例えば、上記のデータは次のように変換されるようにしたいと考えます。
<itemlist> <item> <data name="品名" value="りんご" /> <data name="単価" value="200" /> <data name="個数" value="3" /> <data name="合計" value="600" /> </item> <item> <data name="品名" value="みかん" /> <data name="単価" value="100" /> <data name="個数" value="5" /> <data name="合計" value="500" /> </item> <item> <data name="品名" value="もも" /> <data name="単価" value="300" /> <data name="個数" value="1" /> <data name="合計" value="300" /> </item> </itemlist>
これの DTD と XML Schema の定義を示します。 但し、一行目にメタデータが含まれていて、二行目以降はデータということだ け決め、データがいくつあるかどうかなどは問わないとします。 つまり、決めなければいけないのは、行単位でデータが存在することと、各デー タに名前がついていることにします。
<!ELEMENT itemlist (item*)> <!ELEMENT item (data*)> <!ELEMENT data EMPTY> <!ATTLIST data name CDATA #REQUIRED value CDATA #REQUIRED>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="itemlist" type="itemlistType"/> <xsd:complexType name="itemlistType"> <xsd:sequence> <xsd:element name="item" type="itemType"/> </xsd:sequence> </xsd:complexType> <xsd:complexType name="itemType"> <xsd:sequence> <xsd:element name="data" type="dataType"/> </xsd:sequence> </xsd:complexType> <xsd:complexType name="dataType"> <xsd:attribute name="name" type="xsd:string"/> <xsd:attribute name="value" type="xsd:string"/> </xsd:complexType> </xsd:schema>
さて、上記の構文解析とDTDに基づいて、この変換プログラムを作成します。
import java.io.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import java.util.*;
import java.util.regex.*;
class CSVMatcher {
private String[] array;
private int index;
public CSVMatcher(String line){
array = line.split(",");
index = 0;
}
public boolean find(){
return index < array.length;
}
public String group(){
return array[index++].replaceAll("\"","");
}
public static ArrayList<String> getDataName(String line){
final ArrayList<String> dataName = new ArrayList<String>();
final CSVMatcher m = new CSVMatcher(line);
while(m.find()){
dataName.add(m.group());
}
return dataName;
}
}
class Rei {
private static Document getDocument() throws Exception {
final DocumentBuilderFactory dbf
= DocumentBuilderFactory.newInstance();
final DocumentBuilder db = dbf.newDocumentBuilder();
return db.newDocument();
}
private static void printDocument(Document doc) throws Exception {
final TransformerFactory tfactory = TransformerFactory.newInstance();
final Transformer transformer = tfactory.newTransformer();
final Properties prop = new Properties();
prop.setProperty("encoding", "Shift_JIS");
prop.setProperty("indent", "yes");
transformer.setOutputProperties(prop);
transformer.transform(new DOMSource(doc),
new StreamResult(System.out));
}
private static Element createData(String name, String value){
final Element data = doc.createElement("data");
data.setAttribute("name",name);
data.setAttribute("value",value);
return data;
}
private static Element getItem(List<String> dataName, String line){
final Element item = doc.createElement("item");
final CSVMatcher m = new CSVMatcher(line);
int index=0;
while(m.find()){
item.appendChild(createData(dataName.get(index++),m.group()));
}
return item;
}
private static Document doc;
public static void main(String[] args) throws Exception {
doc = getDocument();
final Element root = doc.createElement("itemlist");
doc.appendChild(root);
final BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("sample.csv"),"Shift_JIS"));
String line;
if((line=br.readLine())==null){
return ;
}
final ArrayList<String> dataName = CSVMatcher.getDataName(line);
while((line=br.readLine())!=null){
root.appendChild(getItem(dataName,line));
}
printDocument(doc);
}
}
OMG は Object Management Group という団体です。 この団体が提案したコンピュータネットワーク上で使用する分散オブジェクト のための規格 CORBA(Common Object Request Broker Architecture)で使用す るインターフェイス記述言語が OMG IDL になります。 言語仕様は CORBA の仕様書の http://www.omg.org/technology/documents/formal/corba_2.htm 3 章に割り当てられていて、ダウンロード可能です。