/*
 * XmlResource class.
 *
 * Copyright (C) 2007 SATOH Takayuki All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ts.util.resource;

import ts.util.text.StringSequence;
import ts.util.text.StringOperation;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Properties;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.Source;
import javax.xml.transform.Result;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Text;

/**
 * XMLファイルを対象とするリソースクラス。
 * <br>
 * XMLファイルをロードして、その内容から指定されたキーに対する値を取得する。
 * また、指定されたキーに対して値を設定し、その内容をXMLファイルに保存する。
 * 名前空間の対応については、名前空間を特定してロードしたり保存したりすること
 * はできないが、名前空間を使ったXMLファイルでも、名前空間を使用しない場合と
 * 同様にロードして値を取得したり、値を設定して保存したりできるようにしている。
 * <p>
 * XMLファイルの記述と、リソースオブジェクトのキーとの対応は、例えば以下のよう
 * になる。
 * <pre>
 *  〜 XMLファイル 〜
 *    &lt;aaa&gt;
 *      &lt;bbb&gt;
 *        &lt;ddd&gt;あああ&lt;/ddd&gt;
 *        &lt;eee&gt;いいい&lt;/eee&gt;
 *        &lt;eee&gt;ううう&lt;/eee&gt;
 *      &lt;/bbb&gt;
 *      &lt;ccc&gt;
 *        &lt;fff id="F1"&gt;えええ&lt;/fff&gt;
 *        &lt;fff id="F2"&gt;おおお&lt;/fff&gt;
 *      &lt;/ccc&gt;
 *    &lt;/aaa&gt;
 *   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 *    res : XmlResource
 *
 *    res.getFirstValue("aaa.bbb.ddd") → あああ
 *    res.getFirstValue("aaa.bbb.eee") → いいい
 *    res.getFirstValue("aaa.bbb.fff") → えええ
 *
 *    res.getValues("aaa.bbb.ddd")     → { あああ }
 *    res.getValues("aaa.bbb.eee")     → { いいい, ううう }
 *    res.getValues("aaa.bbb.fff")     → { えええ, おおお }
 *
 *    res.getFirstValue("aaa.bbb.eee(id=F1)") → えええ
 *    res.getFirstValue("aaa.bbb.eee(id=F2)") → おおお
 *
 *    res.getValues("aaa.bbb.eee(id=F2)") → { おおお }
 *
 *    res.getFirstAttribute("aaa.bbb.fff", "id")        → F1
 *    res.getFirstAttribute("aaa.bbb.fff(id=F2)", "id") → F2
 *
 *    res.getAttributes("aaa.bbb.fff", "id")        → F1
 *    res.getAttributes("aaa.bbb.fff(id=F2)", "id") → F2
 * </pre>
 *
 *
 * @author  佐藤隆之. 
 * @version $Revision: 1.4 $, $Date: 2011-08-01 15:29:32 $
 */
public class XmlResource extends AbstractResource
{
  /** シリアル・バージョン番号。 */
  static final long serialVersionUID = -2547359772464689709L;

  /** XMLデータを保持するドキュメントオブジェクト。 */
  private Document document_ ;

  /** XMLの基底ノード。 */
  private Node baseNode_ ;

  /** XMLの正当性チェックを行うかどうかを示すフラグ。 */
  private boolean isValidating_ = true;

  /**
   * デフォルトコンストラクタ。
   */
  public XmlResource()
  {
    renew();
  }

  /**
   * XMLファイルのパスを引数にとるコンストラクタ。
   *
   * @param  path XMLファイルのパス。
   * @throws FileNotFoundException 指定したパスのファイルが見つからない場合。
   * @throws IOException ファイルの入出力中に例外が発生した場合。
   */
  public XmlResource(String path) throws FileNotFoundException, IOException
  {
    load(path);
  }

  /**
   * コピーコンストラクタ。
   *
   * @param  res コピー元のXMLリソースオブジェクト。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected XmlResource(XmlResource res)
  {
    super(res);

    document_ = res.document_ ;
    baseNode_ = res.baseNode_ ;
  }

  /**
   * XMLファイルを読み込む際にXMLの正当性をチェックするかどうかを設定する。
   * <br>
   * デフォルトでは<tt>false</tt>である。
   */
  public void setValidating(boolean isValidating)
  {
    isValidating_ = isValidating;
  }

  /**
   * XMLドキュメントオブジェクトを取得する。
   *
   * @return XMLドキュメントオブジェクト。
   */
  protected Document getDocument()
  {
    return document_ ;
  }

  /**
   * このオブジェクトが示すドキュメントオブジェクトの基底ノードを取得する。
   *
   * @return 基底ノード。
   */
  protected Node getBaseNode()
  {
    return baseNode_ ;
  }

  /**
   * 基底要素の名前を取得する。
   * <br>
   * 要素名に名前空間が含まれている場合は、ローカル名だけを返す。
   *
   * @return 基底要素の名前。
   */
  public String getBaseElementName()
  {
    return getBaseNode().getLocalName();
  }

  /**
   * 指定されたキーに最初に該当するノードを取得する。
   * <br>
   * 該当するノードが一つも存在しない場合はヌルを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー。
   * @return 引数のキーに最初に対応するノード。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected Node findFirstNode(String key) throws IllegalKeyException
  {
    assert (key != null) : "@param:key is null.";

    if (StringOperation.isEmpty(key)) {
      return getBaseNode();
    }

    List<String> keyElemLst = splitKey(key);
    return findFirstNode(getBaseNode(), keyElemLst, 0);
  }

  /**
   * キー要素リストに最初に該当する下位ノードを再帰的に探索する。
   *
   * @param  node  ノード。
   * @param  keyElemLst キー要素リスト。 
   * @param  index 現在探索中のキー要素のインデックス。
   * @return キー要素リストに該当するノード。
   * @throws NullPointerException 引数がヌルの場合。
   * @throws IndexOutOfBoundsException キー要素のインデックスが負の値の場合。
   */
  private Node findFirstNode(Node node, List<String> keyElemLst, int index)
    throws NullPointerException, IndexOutOfBoundsException
  {
    if (index >= keyElemLst.size()) {
      return node;
    }

    String keyElem = keyElemLst.get(index);
    String elemName = getNameOfKeyElement(keyElem);
    List<String[]> attrLst = listAttributesOfKeyElement(keyElem);

    NEXT_NODE:
    for (Node n = node.getFirstChild(); n != null; n = n.getNextSibling()) {
      if (n.getNodeType() != Node.ELEMENT_NODE) {
        continue;
      }

      // for namespace support.
      //if (! n.getNodeName().equals(elemName)) {
      if (! elemName.equals("*") && ! elemName.equals(n.getLocalName())) {
        continue;
      }

      if (attrLst.size() > 0) {
        NamedNodeMap attrNodeMap = n.getAttributes(); 
        for (String[] attr : attrLst) {
          Node attrNode = attrNodeMap.getNamedItem(attr[0]);
          if (attrNode == null || ! attr[1].equals(attrNode.getNodeValue())) {
            continue NEXT_NODE;
          }
        }
      }

      Node child = findFirstNode(n, keyElemLst, index + 1);
      if (child != null) {
        return child;
      }
    }

    return null;
  }

  /**
   * 指定されたキーに該当する全てのノードを取得する。
   * <br>
   * 該当するノードが一つも存在しない場合は空のリストを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー。
   * @return 引数のキーに対応する全てのノードを格納するリスト。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected List<Node> findAllNodes(String key) throws IllegalKeyException
  {
    assert (key != null) : "@param:key is null.";

    List<Node> nodeLst = new LinkedList<Node>();

    if (StringOperation.isEmpty(key)) {
      nodeLst.add(getBaseNode());
      return nodeLst;
    }

    List<String> keyElemLst = splitKey(key);
    findAllNodes(nodeLst, getBaseNode(), keyElemLst, 0);
    return nodeLst;
  }

  /**
   * キー要素リストに該当する全ての下位ノードを再帰的に探索して、リストに
   * 格納する。
   *
   * @param  nodeLst  該当するノードを格納するリスト。
   * @param  node  ノード。
   * @param  keyElemLst キー要素リスト。 
   * @param  index 現在探索中のキー要素のインデックス。
   * @throws NullPointerException 引数がヌルの場合。
   * @throws IndexOutOfBoundsException インデックスが負の値の場合。
   */
  private void findAllNodes(
    List<Node> nodeLst, Node node, List<String> keyElemLst, int index
  ) throws NullPointerException, IndexOutOfBoundsException
  {
    if (index >= keyElemLst.size()) {
      nodeLst.add(node);
      return;
    }

    String keyElem = keyElemLst.get(index);
    String elemName = getNameOfKeyElement(keyElem);
    List<String[]> attrLst = listAttributesOfKeyElement(keyElem);

    NEXT_NODE:
    for (Node n = node.getFirstChild(); n != null; n = n.getNextSibling()) {
      if (n.getNodeType() != Node.ELEMENT_NODE) {
        continue;
      }

      // for namespace support.
      //if (! n.getNodeName().equals(elemName)) {
      if (! elemName.equals("*") && ! elemName.equals(n.getLocalName())) {
        continue;
      }

      if (attrLst.size() > 0) {
        NamedNodeMap attrNodeMap = n.getAttributes(); 
        for (String[] attr : attrLst) {
          Node attrNode = attrNodeMap.getNamedItem(attr[0]);
          if (attrNode == null || ! attr[1].equals(attrNode.getNodeValue())) {
            continue NEXT_NODE;
          }
        }
      }

      findAllNodes(nodeLst, n, keyElemLst, index + 1);
    }
  }

  /**
   * キー要素リストに最初に該当する下位ノードを探索して、存在すればそれを返し、
   * 存在しなければ作成して返す。
   * <br>
   * 引数のキーが不正な場合は、例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに最初に対応する、又は作成されたノード。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected Node findOrCreateFirstNode(String key) throws IllegalKeyException
  {
    assert (key != null) : "@param:key is null.";

    if (StringOperation.isEmpty(key)) {
      return getBaseNode();
    }

    List<String> keyElemLst = splitKey(key);

    List<String> singleKeyL = new ArrayList<String>(1);
    singleKeyL.add("");

    Node node = getBaseNode();
    int i = 0;
    for (; i<keyElemLst.size(); i++) {
      singleKeyL.set(0, keyElemLst.get(i));
      Node n = findFirstNode(node, singleKeyL, 0);
      if (n == null) {
        break;
      }
      node = n;
    }

    for (; i<keyElemLst.size(); i++) {
      String keyElem = keyElemLst.get(i);
      String elemName = getNameOfKeyElement(keyElem);
      List<String[]> attrLst = listAttributesOfKeyElement(keyElem);

      Element newElem = getDocument().createElementNS(
        node.getNamespaceURI(), elemName);

      for (String[] attr : attrLst) {
        newElem.setAttribute(attr[0], attr[1]);
      }
      node.appendChild(newElem);
      node = newElem;
    }
    return node;
  }

  /**
   * 指定されたノードのテキスト値を取得する。
   * <br>
   * 引数がヌルの場合は空文字列を返す。
   *
   * @param  node ノードオブジェクト。
   * @return 引数のノードのテキスト値。
   */
  protected String getNodeText(Node node)
  {
    if (node == null) {
      return "";
    }

    StringBuilder buf = new StringBuilder();

    NodeList nodeLst = node.getChildNodes();
    if (nodeLst.getLength() > 0) {
      Node n = nodeLst.item(0);
      switch (n.getNodeType()) {
      case Node.CDATA_SECTION_NODE :
        buf.append(n.getNodeValue());
        break;
      case Node.TEXT_NODE :
        buf.append(shortenWhitespace(n.getNodeValue()));
        break;
      }

      for (int i=1; i<nodeLst.getLength(); i++) {
        n = nodeLst.item(i);

        String s;
        switch (n.getNodeType()) {
        case Node.CDATA_SECTION_NODE :
          s = n.getNodeValue();
          if (! StringOperation.isEmpty(s)) {
            buf.append(" ").append(s);
          }
          break;
        case Node.TEXT_NODE :
          s = shortenWhitespace(n.getNodeValue());
          if (! StringOperation.isEmpty(s)) {
            buf.append(" ").append(s);
          }
          break;
        }
      }
    }

    return buf.toString();
  }

  /**
   * 指定された文字列中の空白の並びを一つの空白に短縮する。
   * <br>
   * 但し、文字列の先頭と終端の空白は除去する。
   *
   * @param  s 文字列。
   * @return 空白の並びを一つの空白に短縮された文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected String shortenWhitespace(String s)
  {
    assert (s != null) : "@param:s is null.";

    StringBuilder buf = new StringBuilder(s.length());
    StringSequence seq = new StringSequence(s).skipWhitespaces();
    while (seq.validIndex()) {
      if (seq.index() != seq.skipWhitespaces().index()) {
        if (! seq.validIndex()) {
          break;
        }
        buf.append(" ");
      }
      buf.append(seq.character());
      seq.next();
    }
    return buf.toString();
  }

  /**
   * 指定されたキーに最初に該当する要素の値文字列を取得する。
   * <br>
   * 該当する要素が一つも存在しない場合は空文字列を返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに最初に該当する要素の値文字列。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized String getFirstValue(String key)
    throws IllegalKeyException
  {
    return getNodeText(findFirstNode(key));
  }

  /**
   * 指定されたキーに該当する全要素の値文字列のリストを取得する。
   * <br>
   * 該当する要素が一つも存在しない場合は空のリストを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに該当する全要素の値文字列のリスト。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数のヌルの場合（デバッグモードのみ）。
   */
  public synchronized List<String> getValues(String key)
    throws IllegalKeyException
  {
    List<Node> nodeLst = findAllNodes(key);

    List<String> valueLst = new ArrayList<String>(nodeLst.size());
    for (Node node : nodeLst) {
      valueLst.add(getNodeText(node));
    }

    return valueLst;
  }

  /**
   * 指定されたキーに最初に該当する要素の、指定された属性の値を取得する。
   * <br>
   * 該当する要素が存在しない場合は空文字列を返す。
   * 該当する要素に、指定された名前の属性が存在しない場合は空文字列を返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @param  attr 属性名。
   * @return 引数のキーに最初に該当する要素の、指定された属性の値。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized String getFirstAttribute(String key, String attr)
    throws IllegalKeyException
  {
    assert (attr != null) : "@param:attr is null.";

    Node node = findFirstNode(key);
    if (node == null) {
      return getNodeText(null);
    }

    NamedNodeMap attrMap = node.getAttributes();
    if (attrMap == null) {
      return getNodeText(null);
    }

    return getNodeText(attrMap.getNamedItem(attr));
  }

  /**
   * 指定されたキーに該当する全要素の、指定された属性の値のリストを取得する。
   * <br>
   * 該当する要素が一つも存在しない場合は空のリストを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @param  attr 属性名。
   * @return 引数のキーに該当する全要素の、指定された属性の値のリスト。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertError 引数がヌルの場合。
   */
  public synchronized List<String> getAttributes(String key, String attr)
    throws IllegalKeyException
  {
    assert (attr != null) : "@param:attr is null.";

    List<Node> nodeLst = findAllNodes(key);

    List<String> valueLst = new ArrayList<String>(nodeLst.size());
    for (Node node : nodeLst) {
      NamedNodeMap attrMap = node.getAttributes();
      if (attrMap == null) {
        valueLst.add(getNodeText(null));
      }
      else {
        valueLst.add(getNodeText(attrMap.getNamedItem(attr)));
      }
    }

    return valueLst;
  }

  /**
   * 指定されたキーに最初に該当する要素を基底要素とする子リソースを取得する。
   * <br>
   * 指定されたキーに該当する要素が存在しない場合は
   * {@link ts.util.resource.Resource#EMPTY}を返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに最初に該当する要素の子リソース。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized Resource getFirstChild(String key)
    throws IllegalKeyException
  {
    Node node = findFirstNode(key);
    if (node == null) {
      return Resource.EMPTY;
    }

    XmlResource res = new XmlResource(this);
    res.baseNode_ = node;
    return res;
  }

  /**
   * 指定されたキーに最初に該当する子リソースのリストを取得する。
   * <br>
   * 指定されたキーに該当する要素が存在しない場合は空のリストを返す。
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 引数のキーに該当する要素の子リソースのリスト。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized List<Resource> getChildren(String key)
    throws IllegalKeyException
  {
    List<Node> nodeLst = findAllNodes(key);

    List<Resource> resLst = new ArrayList<Resource>(nodeLst.size());
    for (Node n : nodeLst) {
      XmlResource res = new XmlResource(this);
      res.baseNode_ = n;
      resLst.add(res);
    }

    return resLst;
  }

  /**
   * 指定されたキーに最初に該当する要素に、値文字列を設定する。
   * <br>
   * 指定されたキーに該当する要素が存在しない場合は、その要素を作成して
   * 値を設定する。
   *
   * @param  key キー文字列。
   * @param  value 引数のキーに最初に該当する要素に設定する値。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized void setFirstValue(String key, String value)
    throws IllegalKeyException
  {
    assert (value != null) : "@param:value is null.";

    try {
      Node node = findOrCreateFirstNode(key);
      Text text = getDocument().createCDATASection(value);

      for (Node n = node.getFirstChild(); n != null; n = n.getNextSibling()) {
        if (n.getNodeType() == Node.TEXT_NODE
        ||  n.getNodeType() == Node.CDATA_SECTION_NODE) {
          node.removeChild(n);
        }
      }
      node.appendChild(text);
    }
    catch (IllegalKeyException e) {
      throw e;
    }
    catch (Exception e) {
      throw new IllegalKeyException(key, e);
    }
  }

  /**
   * 指定されたキーに最初に該当する要素を基底要素とする子リソースを取得する。
   * 指定されたキーに該当する要素が存在しない場合は作成して返す。
   * <br>
   * 引数のキーが不正な場合は例外をスローする。
   *
   * @param  key キー文字列。
   * @return 指定されたキーに最初に該当する要素を基底要素とする子リソース。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized Resource getOrCreateFirstChild(String key)
    throws IllegalKeyException
  {
    try {
      Node node = findOrCreateFirstNode(key);

      XmlResource res = new XmlResource(this);
      res.baseNode_ = node;
      return res;
    }
    catch (IllegalKeyException e) {
      throw e;
    }
  }

  /**
   * 指定されたキーに該当する全ての要素を削除する。
   * <br>
   * 指定されたキーに該当する要素が見つからなかった場合は、何もしない。
   *
   * @param  key キー文字列。
   * @throws IllegalKeyException キーが不正な場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized void removeChildren(String key)
    throws IllegalKeyException
  {
    List<Node> nodeLst = findAllNodes(key);
    for (Node node : nodeLst) {
      Node parent = node.getParentNode();
      if (parent != null) {
        parent.removeChild(node);
      }
    }

    if (StringOperation.isEmpty(key)) {
      renew();
    }
  }

  /**
   * 現在の基底要素の全てのテキスト値と、子要素のリソース・オブジェクトを格納
   * したリストを取得する。
   *
   * @return 現在の基底要素のテキスト値、及び子要素のリソース・オブジェクトを
   *         格納したリスト。
   */
  public synchronized List<Object> getValuesAndChildren()
  {
    List<Object> lst = new LinkedList<Object>();
    Node base = getBaseNode();
    for (Node n = base.getFirstChild(); n != null; n = n.getNextSibling()) {
      switch (n.getNodeType()) {
      case Node.ELEMENT_NODE :
        XmlResource res = new XmlResource(this);
        res.baseNode_ = n;
        lst.add(res);
        break;
      case Node.CDATA_SECTION_NODE :
        lst.add(n.getNodeValue());
        break;
      case Node.TEXT_NODE :
        String s = n.getNodeValue().trim();
        if (s.length() == 0) continue;
        lst.add(shortenWhitespace(s));
        break;
      }
    }
    return lst;
  }

  /**
   * このオブジェクトを再初期化する。
   */
  private void renew()
  {
    try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setIgnoringComments(true);
      factory.setNamespaceAware(true);
      factory.setValidating(false);
      DocumentBuilder builder = factory.newDocumentBuilder();

      document_ = builder.newDocument();
      baseNode_ = document_;
    }
    catch (Exception e) {}
  }

  /**
   * 引数の入力ストリームからXMLファイルの内容を読み込む。
   *
   * @param  stream 入力ストリーム。
   * @throws IOException 読み込み中に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  @Override
  public void load(InputStream stream) throws IOException
  { 
    assert (stream != null) : "@param:stream is null.";

    try {
      DocumentBuilder builder = createDocumentBuilder();

      document_ = builder.parse(stream);
      baseNode_ = document_;
    }
    catch (IOException e) {
      throw e;
    }
    catch (Exception e) {
      IOException exc = new IOException(e.getMessage());
      exc.setStackTrace(e.getStackTrace());
      throw exc;
    }
  }

  /**
   * XMLドキュメント構築オブジェクトを作成する。
   *
   * @return XMLドキュメント構築オブジェクト。
   * @throws ParserConfigurationException 要求された構成を満たすXMLドキュメント
   *           構築オブジェクトを生成できない場合
   */
  protected DocumentBuilder createDocumentBuilder()
    throws ParserConfigurationException
  {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setIgnoringComments(true);
    factory.setNamespaceAware(true);
    factory.setValidating(isValidating_);

    DocumentBuilder builder = factory.newDocumentBuilder();
    if (! factory.isValidating()) {
      builder.setEntityResolver(new EntityResolver() {
        public InputSource resolveEntity(String publicId, String systemId)
        throws SAXException, IOException {
          return new InputSource(new StringReader(""));
        }
      });
    }
    return builder;
  }

  /**
   * このオブジェクトが保持しているドキュメントの内容を、指定された
   * 出力ストリームに保存する。
   *
   * @param  stream 出力ストリーム。
   * @throws UnsupportedOperationException 子リソースを保存しようとした場合。
   * @throws IOException ファイルの保存中に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  @Override
  public void save(OutputStream stream) throws IOException
  {
    assert (stream != null) : "@param:stream is null.";

    if (getBaseNode() != getDocument()) {
      throw new UnsupportedOperationException(
        "XmlResource class cannot save a child resource.");
    }

    try {
      TransformerFactory factory = TransformerFactory.newInstance();
      Transformer t = factory.newTransformer();
      t.setOutputProperty(OutputKeys.INDENT, "yes");
      t.setOutputProperty(OutputKeys.METHOD, "xml");

      DocumentType doctype = getDocument().getDoctype();
      if (doctype != null) {
        t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
          (doctype.getSystemId() != null) ? doctype.getSystemId() : "");
        t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
          (doctype.getPublicId() != null) ? doctype.getPublicId() : "");
      }

      // 保存ファイルの文字エンコーディングを、読込ファイルと同じにする
      Properties prop = t.getOutputProperties();
      prop.setProperty(OutputKeys.ENCODING, getDocument().getXmlEncoding());
      t.setOutputProperties(prop);

      Source s = new DOMSource(getBaseNode());
      Result r = new StreamResult(stream);
      t.transform(s, r);
    }
    catch (Exception e) {
      IOException exc = new IOException(e.getMessage());
      exc.setStackTrace(e.getStackTrace());
      throw exc;
    }
  }
}
