/*
 * DomDocument.java
 * Copyright (C) 1999,2000,2001,2004 The Free Software Foundation
 * 
 * This file is part of GNU JAXP, a library.
 *
 * GNU JAXP is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * GNU JAXP 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Linking this library statically or dynamically with other modules is
 * making a combined work based on this library.  Thus, the terms and
 * conditions of the GNU General Public License cover the whole
 * combination.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obliged to do so.  If you do not wish to do so, delete this
 * exception statement from your version. 
 */

package gnu.xml.dom;

import java.util.Iterator;
import javax.xml.XMLConstants;

import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.DocumentType;
import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.Entity;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Notation;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.w3c.dom.UserDataHandler;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.NodeIterator;
import org.w3c.dom.traversal.TreeWalker;
import org.w3c.dom.xpath.XPathEvaluator;
import org.w3c.dom.xpath.XPathException;
import org.w3c.dom.xpath.XPathExpression;
import org.w3c.dom.xpath.XPathNSResolver;

/**
 * <p> "Document" and "DocumentTraversal" implementation.
 *
 * <p> Note that when this checks names for legality, it uses an
 * approximation of the XML rules, not the real ones.  Specifically,
 * it uses Unicode rules, with sufficient tweaks to pass a majority
 * of basic XML conformance tests.  (The huge XML character tables are
 * hairy to implement.)
 *
 * @author David Brownell 
 * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
 */
public class DomDocument
  extends DomNode
  implements Document, DocumentTraversal, XPathEvaluator
{

  private final DOMImplementation implementation;
  private boolean checkingCharacters = true;
  boolean checkingWellformedness = true;

  boolean building; // if true, skip mutation events in the tree
  
  DomDocumentConfiguration config;

  String inputEncoding;
  String encoding;
  String version = "1.0";
  boolean standalone;
  String systemId;
  
  /**
   * Constructs a Document node, associating it with an instance
   * of the DomImpl class.
   *
   * <p> Note that this constructor disables character checking.
   * It is normally used when connecting a DOM to an XML parser,
   * and duplicating such checks is undesirable.  When used for
   * purposes other than connecting to a parser, you should
   * re-enable that checking.
   *
   * @see #setCheckingCharacters
   */
  public DomDocument()
  {
    this(new DomImpl());
  }
  
  /**
   * Constructs a Document node, associating it with the specified
   * implementation.  This should only be used in conjunction with
   * a specialized implementation; it will normally be called by
   * that implementation.
   *
   * @see DomImpl
   * @see #setCheckingCharacters
   */
  protected DomDocument(DOMImplementation impl)
  {
    super(DOCUMENT_NODE, null);
    implementation = impl;
  }

  /**
   * Sets the <code>building</code> flag.
   * Mutation events in the document are not reported.
   */
  public void setBuilding(boolean flag)
  {
    building = flag;
  }

  /**
   * Sets whether to check for document well-formedness.
   * If true, an exception will be raised if a second doctype or root
   * element node is added to the document.
   */
  public void setCheckWellformedness(boolean flag)
  {
    checkingWellformedness = flag;
  }

  /**
   * <b>DOM L1</b>
   * Returns the constant "#document".
   */
  final public String getNodeName()
  {
    return "#document";
  }

  /**
   * <b>DOM L1</b>
   * Returns the document's root element, or null.
   */
  final public Element getDocumentElement()
  {
    for (DomNode ctx = first; ctx != null; ctx = ctx.next)
      {
        if (ctx.nodeType == ELEMENT_NODE)
          {
            return (Element) ctx;
          }
      }
    return null;
  }

  /**
   * <b>DOM L1</b>
   * Returns the document's DocumentType, or null.
   */
  final public DocumentType getDoctype()
  {
    for (DomNode ctx = first; ctx != null; ctx = ctx.next)
      {
      if (ctx.nodeType == DOCUMENT_TYPE_NODE)
          {
            return (DocumentType) ctx;
          }
      }
    return null;
  }

  /**
   * <b>DOM L1</b>
   * Returns the document's DOMImplementation.
   */
  final public DOMImplementation getImplementation()
  {
    return implementation;
  }

  /**
   * <b>DOM L1 (relocated in DOM L2)</b>
   * Returns the element with the specified "ID" attribute, or null.
   *
   * <p>Returns null unless {@link Consumer} was used to populate internal
   * DTD declaration information, using package-private APIs.  If that
   * internal DTD information is available, the document may be searched for
   * the element with that ID.
   */
  public Element getElementById(String id)
  {
    DomDoctype doctype = (DomDoctype) getDoctype();
    
    if (doctype == null || !doctype.hasIds()
        || id == null || id.length() == 0)
      {
        return null;
      }
    
    // yes, this is linear in size of document.
    // it'd be easy enough to maintain a hashtable.
    Node current = getDocumentElement();
    Node temp;
    
    if (current == null)
      {
        return null;
      }
    while (current != this)
      {
        // done?
        if (current.getNodeType() == ELEMENT_NODE)
          {
            DomElement element = (DomElement) current;
            DTDElementTypeInfo info =
              doctype.getElementTypeInfo(current.getNodeName());
            if (info != null &&
                id.equals(element.getAttribute(info.idAttrName)))
              {
                return element;
              }
            else if (element.userIdAttrs != null)
              {
                for (Iterator i = element.userIdAttrs.iterator();
                     i.hasNext(); )
                  {
                    Node idAttr = (Node) i.next();
                    if (id.equals(idAttr.getNodeValue()))
                      {
                        return element;
                      }
                  }
              }
          }
        
        // descend?
        if (current.hasChildNodes())
          {
            current = current.getFirstChild();
            continue;
          }
        
        // lateral?
        temp = current.getNextSibling();
        if (temp != null)
          {
            current = temp;
            continue;
          }
        
        // back up ... 
        do
          {
            temp = current.getParentNode();
            if (temp == null)
              {
                return null;
              }
            current = temp;
            temp = current.getNextSibling();
          }
        while (temp == null);
        current = temp;
      }
    return null;
  }

  private void checkNewChild(Node newChild)
  {
    if (newChild.getNodeType() == ELEMENT_NODE
        && getDocumentElement() != null)
      {
        throw new DomEx (DomEx.HIERARCHY_REQUEST_ERR,
                         "document element already present: " +
                         getDocumentElement(), newChild, 0);
      }
    if (newChild.getNodeType() == DOCUMENT_TYPE_NODE
        && getDoctype() != null)
      {
        throw new DomEx (DomEx.HIERARCHY_REQUEST_ERR,
                         "document type already present: " +
                         getDoctype(), newChild, 0);
      }
  }

  /**
   * <b>DOM L1</b>
   * Appends the specified node to this node's list of children,
   * enforcing the constraints that there be only one root element
   * and one document type child.
   */
  public Node appendChild(Node newChild)
  {
    if (checkingWellformedness)
      {
        checkNewChild(newChild);
      }
    return super.appendChild(newChild);
  }

  /**
   * <b>DOM L1</b>
   * Inserts the specified node in this node's list of children,
   * enforcing the constraints that there be only one root element
   * and one document type child.
   */
  public Node insertBefore(Node newChild, Node refChild)
  {
    if (checkingWellformedness)
      {
        checkNewChild(newChild);
      }
    return super.insertBefore(newChild, refChild);
  }

  /**
   * <b>DOM L1</b>
   * Replaces the specified node in this node's list of children,
   * enforcing the constraints that there be only one root element
   * and one document type child.
   */
  public Node replaceChild(Node newChild, Node refChild)
  {
    if (checkingWellformedness &&
        ((newChild.getNodeType() == ELEMENT_NODE &&
          refChild.getNodeType() != ELEMENT_NODE) ||
         (newChild.getNodeType() == DOCUMENT_TYPE_NODE &&
          refChild.getNodeType() != DOCUMENT_TYPE_NODE)))
      {
        checkNewChild(newChild);
      }
    return super.replaceChild(newChild, refChild);
  }
 
  // NOTE:  DOM can't really tell when the name of an entity,
  // notation, or PI must follow the namespace rules (excluding
  // colons) instead of the XML rules (which allow them without
  // much restriction).  That's an API issue.  verifyXmlName
  // aims to enforce the XML rules, not the namespace rules.
  
  /**
   * Throws a DOM exception if the specified name is not a legal XML 1.0
   * Name.
   * @deprecated This method is deprecated and may be removed in future
   * versions of GNU JAXP
   */
  public static void verifyXmlName(String name)
  {
    // XXX why is this public?
    checkName(name, false);
  }

  static void checkName(String name, boolean xml11)
  {
    if (name == null)
      {
        throw new DomEx (DomEx.NAMESPACE_ERR, name, null, 0);
      }
    int len = name.length();
    if (len == 0)
      {
        throw new DomEx (DomEx.NAMESPACE_ERR, name, null, 0);
      }

    // dog: rewritten to use the rules for XML 1.0 and 1.1
    
    // Name start character
    char c = name.charAt(0);
    if (xml11)
      {
        // XML 1.1
        if ((c < 0x0041 || c > 0x005a) &&
            (c < 0x0061 || c > 0x007a) &&
            c != ':' && c != '_' &&
            (c < 0x00c0 || c > 0x00d6) &&
            (c < 0x00d8 || c > 0x00f6) &&
            (c < 0x00f8 || c > 0x02ff) &&
            (c < 0x0370 || c > 0x037d) &&
            (c < 0x037f || c > 0x1fff) &&
            (c < 0x200c || c > 0x200d) &&
            (c < 0x2070 || c > 0x218f) &&
            (c < 0x2c00 || c > 0x2fef) &&
            (c < 0x3001 || c > 0xd7ff) &&
            (c < 0xf900 || c > 0xfdcf) &&
            (c < 0xfdf0 || c > 0xfffd) &&
            (c < 0x10000 || c > 0xeffff))
          {
            throw new DomEx(DomEx.INVALID_CHARACTER_ERR, name, null, c);
          }
      }
    else
      {
        // XML 1.0
        int type = Character.getType(c);
        switch (type)
          {
          case Character.LOWERCASE_LETTER: // Ll
          case Character.UPPERCASE_LETTER: // Lu
          case Character.OTHER_LETTER: // Lo
          case Character.TITLECASE_LETTER: // Lt
          case Character.LETTER_NUMBER: // Nl
            if ((c > 0xf900 && c < 0xfffe) ||
                (c >= 0x20dd && c <= 0x20e0))
              {
                // Compatibility area and Unicode 2.0 exclusions
                throw new DomEx(DomEx.INVALID_CHARACTER_ERR, name, null, c);
              }
            break;
          default:
            if (c != ':' && c != '_' && (c < 0x02bb || c > 0x02c1) &&
                c != 0x0559 && c != 0x06e5 && c != 0x06e6)
              {
                throw new DomEx(DomEx.INVALID_CHARACTER_ERR, name, null, c);
              }
          }
      }

    // Subsequent characters
    for (int i = 1; i < len; i++)
      {
        c = name.charAt(i);
        if (xml11)
          {
            // XML 1.1
            if ((c < 0x0041 || c > 0x005a) &&
                (c < 0x0061 || c > 0x007a) &&
                (c < 0x0030 || c > 0x0039) &&
                c != ':' && c != '_' && c != '-' && c != '.' &&
                (c < 0x00c0 || c > 0x00d6) &&
                (c < 0x00d8 || c > 0x00f6) &&
                (c < 0x00f8 || c > 0x02ff) &&
                (c < 0x0370 || c > 0x037d) &&
                (c < 0x037f || c > 0x1fff) &&
                (c < 0x200c || c > 0x200d) &&
                (c < 0x2070 || c > 0x218f) &&
                (c < 0x2c00 || c > 0x2fef) &&
                (c < 0x3001 || c > 0xd7ff) &&
                (c < 0xf900 || c > 0xfdcf) &&
                (c < 0xfdf0 || c > 0xfffd) &&
                (c < 0x10000 || c > 0xeffff) &&
                c != 0x00b7 &&
                (c < 0x0300 || c > 0x036f) &&
                (c < 0x203f || c > 0x2040))
              {
                throw new DomEx(DomEx.INVALID_CHARACTER_ERR, name, null, c);
              }
          }
        else
          {
            // XML 1.0
            int type = Character.getType(c);
            switch (type)
              {
              case Character.LOWERCASE_LETTER: // Ll
              case Character.UPPERCASE_LETTER: // Lu
              case Character.DECIMAL_DIGIT_NUMBER: // Nd
              case Character.OTHER_LETTER: // Lo
              case Character.TITLECASE_LETTER: // Lt
              case Character.LETTER_NUMBER: // Nl
              case Character.COMBINING_SPACING_MARK: // Mc
              case Character.ENCLOSING_MARK: // Me
              case Character.NON_SPACING_MARK: // Mn
              case Character.MODIFIER_LETTER: // Lm
                if ((c > 0xf900 && c < 0xfffe) ||
                    (c >= 0x20dd && c <= 0x20e0))
                  {
                    // Compatibility area and Unicode 2.0 exclusions
                    throw new DomEx(DomEx.INVALID_CHARACTER_ERR, name, null, c);
                  }
                break;
              default:
                if (c != '-' && c != '.' && c != ':' && c != '_' &&
                    c != 0x0387 && (c < 0x02bb || c > 0x02c1) &&
                    c != 0x0559 && c != 0x06e5 && c != 0x06e6 && c != 0x00b7)
                  {
                    throw new DomEx(DomEx.INVALID_CHARACTER_ERR, name, null, c);
                  }
              }
          }
      }

    // FIXME characters with a font or compatibility decomposition (i.e.
    // those with a "compatibility formatting tag" in field 5 of the
    // database -- marked by field 5 beginning with a "<") are not allowed.
  }

  // package private
  static void checkNCName(String name, boolean xml11)
  {
    checkName(name, xml11);
    int len = name.length();
    int index = name.indexOf(':');
    if (index != -1)
      {
        if (index == 0 || index == (len - 1) ||
            name.lastIndexOf(':') != index)
          {
            throw new DomEx(DomEx.NAMESPACE_ERR, name, null, 0);
          }
      }
  }

  // package private
  static void checkChar(String value, boolean xml11)
  {
    char[] chars = value.toCharArray();
    checkChar(chars, 0, chars.length, xml11);
  }
  
  static void checkChar(char[] buf, int off, int len, boolean xml11)
  {
    for (int i = 0; i < len; i++)
      {
        char c = buf[i];
        
        // assume surrogate pairing checks out OK, for simplicity
        if ((c >= 0x0020 && c <= 0xd7ff) ||
            (c == 0x000a || c == 0x000d || c == 0x0009) ||
            (c >= 0xe000 && c <= 0xfffd) ||
            (c >= 0x10000 && c <= 0x10ffff))
          {
            continue;
          }
        if (xml11)
          {
            if ((c >= 0x0001 && c <= 0x001f) ||
                (c >= 0x007f && c <= 0x0084) ||
                (c >= 0x0086 && c <= 0x009f))
              {
                continue;
              }
          }
        throw new DomEx(DomEx.INVALID_CHARACTER_ERR,
                        new String(buf, off, len), null, c);
      }
  }

  /**
   * <b>DOM L1</b>
   * Returns a newly created element with the specified name.
   */
  public Element createElement(String name)
  {
    Element element;
    
    if (checkingCharacters)
      {
        checkName(name, "1.1".equals(version));
      }
    if (name.startsWith("xml:"))
      {
        element = createElementNS(null, name);
      }
    else
      {
        element = new DomElement(this, null, name);
      }
    defaultAttributes(element, name);
    return element;
  }

  /**
   * <b>DOM L2</b>
   * Returns a newly created element with the specified name
   * and namespace information.
   */
  public Element createElementNS(String namespaceURI, String name)
  {
    if (checkingCharacters)
      {
        checkNCName(name, "1.1".equals(version));
      }
    
    if ("".equals(namespaceURI))
      {
        namespaceURI = null;
      }
    if (name.startsWith("xml:"))
      {
        if (namespaceURI != null
            && !XMLConstants.XML_NS_URI.equals(namespaceURI))
          {
            throw new DomEx(DomEx.NAMESPACE_ERR,
                            "xml namespace is always " +
                            XMLConstants.XML_NS_URI, this, 0);
          }
        namespaceURI = XMLConstants.XML_NS_URI;
      }
    else if (XMLConstants.XMLNS_ATTRIBUTE.equals(name) ||
             name.startsWith("xmlns:"))
      {
        throw new DomEx(DomEx.NAMESPACE_ERR,
                        "xmlns is reserved", this, 0);
      }
    else if (namespaceURI == null && name.indexOf(':') != -1)
      {
        throw new DomEx(DomEx.NAMESPACE_ERR,
                        "prefixed name '" + name + "' needs a URI", this, 0);
      }
    
    Element  element = new DomElement(this, namespaceURI, name);
    defaultAttributes(element, name);
    return element;
  }
  
  private void defaultAttributes(Element element, String name)
  {
    DomDoctype doctype = (DomDoctype) getDoctype();
    if (doctype == null)
      {
        return;
      }

    // default any attributes that need it
    DTDElementTypeInfo info = doctype.getElementTypeInfo(name);
    if (info != null)
      {
        for (Iterator i = info.attributes(); i != null && i.hasNext(); )
          {
            DTDAttributeTypeInfo attr = (DTDAttributeTypeInfo) i.next();
            DomAttr node = (DomAttr) createAttribute(attr.name);
            
            String value = attr.value;
            if (value == null)
              {
                value = "";
              }
            node.setValue(value);
            node.setSpecified(false);
            element.setAttributeNode(node);
          }
      }
  }

  /**
   * <b>DOM L1</b>
   * Returns a newly created document fragment.
   */
  public DocumentFragment createDocumentFragment()
  {
    return new DomFragment(this);
  }

  /**
   * <b>DOM L1</b>
   * Returns a newly created text node with the specified value.
   */
  public Text createTextNode(String value)
  {
    if (checkingCharacters)
      {
        checkChar(value, "1.1".equals(version));
      }
    return new DomText(this, value);
  }

  /**
   * Returns a newly created text node with the specified value.
   */
  public Text createTextNode(char[] buf, int off, int len)
  {
    if (checkingCharacters)
      {
        checkChar(buf, off, len, "1.1".equals(version));
      }
    return new DomText(this, buf, off, len);
  }

  /**
   * <b>DOM L1</b>
   * Returns a newly created comment node with the specified value.
   */
  public Comment createComment(String value)
  {
    if (checkingCharacters)
      {
        checkChar(value, "1.1".equals(version));
      }
    return new DomComment(this, value);
  }

  /**
   * <b>DOM L1</b>
   * Returns a newly created CDATA section node with the specified value.
   */
  public CDATASection createCDATASection(String value)
  {
    if (checkingCharacters)
      {
        checkChar(value, "1.1".equals(version));
      }
    return new DomCDATA(this, value);
  }

  /**
   * Returns a newly created CDATA section node with the specified value.
   */
  public CDATASection createCDATASection(char[] buf, int off, int len)
  {
    if (checkingCharacters)
      {
        checkChar(buf, off, len, "1.1".equals(version));
      }
    return new DomCDATA(this, buf, off, len);
  }

  /**
   * <b>DOM L1</b>
   * Returns a newly created processing instruction.
   */
  public ProcessingInstruction createProcessingInstruction(String target,
                                                           String data)
  {
    if (checkingCharacters)
      {
        boolean xml11 = "1.1".equals(version);
        checkName(target, xml11);
        if ("xml".equalsIgnoreCase(target))
          {
            throw new DomEx(DomEx.SYNTAX_ERR,
                            "illegal PI target name", this, 0);
          }
        checkChar(data, xml11);
      }
    return new DomPI(this, target, data);
  }

  /**
   * <b>DOM L1</b>
   * Returns a newly created attribute with the specified name.
   */
  public Attr createAttribute(String name)
  {
    if (checkingCharacters)
      {
        checkName(name, "1.1".equals(version));
      }
    if (name.startsWith("xml:"))
      {
        return createAttributeNS(XMLConstants.XML_NS_URI, name);
      }
    else if (XMLConstants.XMLNS_ATTRIBUTE.equals(name) ||
             name.startsWith("xmlns:"))
      {
        return createAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, name);
      }
    else
      {
        return new DomAttr(this, null, name);
      }
  }

  /**
   * <b>DOM L2</b>
   * Returns a newly created attribute with the specified name
   * and namespace information.
   */
  public Attr createAttributeNS(String namespaceURI, String name)
  {
    if (checkingCharacters)
      {
        checkNCName(name, "1.1".equals(version));
      }
    
    if ("".equals(namespaceURI))
      {
        namespaceURI = null;
      }
    if (name.startsWith ("xml:"))
      {
        if (namespaceURI == null)
          {
            namespaceURI = XMLConstants.XML_NS_URI;
          }
        else if (!XMLConstants.XML_NS_URI.equals(namespaceURI))
          {
            throw new DomEx(DomEx.NAMESPACE_ERR,
                            "xml namespace is always " +
                            XMLConstants.XML_NS_URI, this, 0);
          }
      }
    else if (XMLConstants.XMLNS_ATTRIBUTE.equals(name) ||
             name.startsWith("xmlns:"))
      {
        if (namespaceURI == null)
          {
            namespaceURI = XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
          }
        else if (!XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI))
          {
            throw new DomEx(DomEx.NAMESPACE_ERR,
                            "xmlns namespace must be " +
                            XMLConstants.XMLNS_ATTRIBUTE_NS_URI, this, 0);
          }
      }
    else if (namespaceURI == null && name.indexOf(':') != -1)
      {
        throw new DomEx(DomEx.NAMESPACE_ERR,
                        "prefixed name needs a URI: " + name, this, 0);
      }
    return new DomAttr(this, namespaceURI, name);
  }
  
  /**
   * <b>DOM L1</b>
   * Returns a newly created reference to the specified entity.
   * The caller should populate this with the appropriate children
   * and then mark it as readonly.
   *
   * @see DomNode#makeReadonly
   */
  public EntityReference createEntityReference(String name)
  {
    DomEntityReference ret = new DomEntityReference(this, name);
    DocumentType doctype = getDoctype();
    if (doctype != null)
      {
        DomEntity ent = (DomEntity) doctype.getEntities().getNamedItem(name);
        if (ent != null)
          {
            for (DomNode ctx = ent.first; ctx != null; ctx = ctx.next)
              {
                ret.appendChild(ctx.cloneNode(true));
              }
          }
      }
    ret.makeReadonly();
    return ret;
  }

  /**
   * <b>DOM L2</b>
   * Makes a copy of the specified node, with all nodes "owned" by
   * this document and with children optionally copied.  This type
   * of standard utility has become, well, a standard utility.
   *
   * <p> Note that EntityReference nodes created through this method (either
   * directly, or recursively) never have children, and that there is no
   * portable way to associate them with such children.
   *
   * <p> Note also that there is no requirement that the specified node
   * be associated with a different document.  This differs from the
   * <em>cloneNode</em> operation in that the node itself is not given
   * an opportunity to participate, so that any information managed
   * by node subclasses will be lost.
   */
  public Node importNode(Node src, boolean deep)
  {
    Node dst = null;
    switch (src.getNodeType())
      {
      case TEXT_NODE:
        dst = createTextNode(src.getNodeValue());
        break;
      case CDATA_SECTION_NODE:
        dst = createCDATASection(src.getNodeValue());
        break;
      case COMMENT_NODE:
        dst = createComment(src.getNodeValue());
        break;
      case PROCESSING_INSTRUCTION_NODE:
        dst = createProcessingInstruction(src.getNodeName(),
                                          src.getNodeValue());
        break;
      case NOTATION_NODE:
        // NOTE:  There's no standard way to create
        // these, or add them to a doctype.  Useless.
        Notation notation = (Notation) src;
        dst = new DomNotation(this, notation.getNodeName(),
                              notation.getPublicId(),
                              notation.getSystemId());
        break;
      case ENTITY_NODE:
        // NOTE:  There's no standard way to create
        // these, or add them to a doctype.  Useless.
        Entity entity = (Entity) src;
        dst = new DomEntity(this, entity.getNodeName(),
                            entity.getPublicId(),
                            entity.getSystemId(),
                            entity.getNotationName());
        if (deep)
          {
            for (Node ctx = src.getFirstChild(); ctx != null;
                 ctx = ctx.getNextSibling())
              {
                dst.appendChild(importNode(ctx, deep));
              }
          }
        break;
      case ENTITY_REFERENCE_NODE:
        dst = createEntityReference(src.getNodeName());
        break;
      case DOCUMENT_FRAGMENT_NODE:
        dst = new DomFragment(this);
        if (deep)
          {
            for (Node ctx = src.getFirstChild(); ctx != null;
                 ctx = ctx.getNextSibling())
              {
                dst.appendChild(importNode(ctx, deep));
              }
          }
        break;
      case ATTRIBUTE_NODE:
        String attr_nsuri = src.getNamespaceURI();
        if (attr_nsuri != null)
          {
            dst = createAttributeNS(attr_nsuri, src.getNodeName());
          }
        else
          {
            dst = createAttribute(src.getNodeName());
          }
        // this is _always_ done regardless of "deep" setting
        for (Node ctx = src.getFirstChild(); ctx != null;
             ctx = ctx.getNextSibling())
          {
            dst.appendChild(importNode(ctx, false));
          }
        break;
      case ELEMENT_NODE:
        String elem_nsuri = src.getNamespaceURI();
        if (elem_nsuri != null)
          {
            dst = createElementNS(elem_nsuri, src.getNodeName());
          }
        else
          {
            dst = createElement(src.getNodeName());
          }
        NamedNodeMap srcAttrs = src.getAttributes();
        NamedNodeMap dstAttrs = dst.getAttributes();
        int len = srcAttrs.getLength();
        for (int i = 0; i < len; i++)
          {
            Attr a = (Attr) srcAttrs.item(i);
            Attr dflt;
            
            // maybe update defaulted attributes
            dflt = (Attr) dstAttrs.getNamedItem(a.getNodeName());
            if (dflt != null)
              {
                String newval = a.getNodeValue();
                if (!dflt.getNodeValue().equals(newval)
                    || a.getSpecified () == true)
                  {
                    dflt.setNodeValue (newval);
                  }
                continue;
              }
            
            dstAttrs.setNamedItem((Attr) importNode(a, false));
          }
        if (deep)
          {
            for (Node ctx = src.getFirstChild(); ctx != null;
                 ctx = ctx.getNextSibling())
              {
                dst.appendChild(importNode(ctx, true));
              }
          }
        break;
        // can't import document or doctype nodes
      case DOCUMENT_NODE:
      case DOCUMENT_TYPE_NODE:
        // FALLTHROUGH
        // can't import unrecognized or nonstandard nodes
      default:
        throw new DomEx(DomEx.NOT_SUPPORTED_ERR, null, src, 0);
      }
    
    // FIXME cleanup a bit -- for deep copies, copy those
    // children in one place, here (code sharing is healthy)

    if (src instanceof DomNode)
      {
        ((DomNode) src).notifyUserDataHandlers(UserDataHandler.NODE_IMPORTED,
                                               src, dst);
      }
    return dst;
  }

  /**
   * <b>DOM L2 (Traversal)</b>
   * Returns a newly created node iterator.  Don't forget to detach
   * this iterator when you're done using it!
   *
   * @see DomIterator
   */
  public NodeIterator createNodeIterator(Node root,
                                         int whatToShow,
                                         NodeFilter filter,
                                         boolean expandEntities)
  {
    return new DomNodeIterator(root, whatToShow, filter, expandEntities,
                               false);
  }

  public TreeWalker createTreeWalker(Node root,
                                     int whatToShow,
                                     NodeFilter filter,
                                     boolean expandEntities)
  {
    return new DomNodeIterator(root, whatToShow, filter, expandEntities,
                               true);
  }

  // DOM Level 3 methods
  
  /**
   * DOM L3
   */
  public String getInputEncoding()
  {
    return inputEncoding;
  }

  public void setInputEncoding(String inputEncoding)
  {
    this.inputEncoding = inputEncoding;
  }
  
  /**
   * DOM L3
   */
  public String getXmlEncoding()
  {
    return encoding;
  }
  
  public void setXmlEncoding(String encoding)
  {
    this.encoding = encoding;
  }
  
  public boolean getXmlStandalone()
  {
    return standalone;
  }

  public void setXmlStandalone(boolean xmlStandalone)
  {
    standalone = xmlStandalone;
  }

  public String getXmlVersion()
  {
    return version;
  }

  public void setXmlVersion(String xmlVersion)
  {
    if (xmlVersion == null)
      {
        xmlVersion = "1.0";
      }
    if ("1.0".equals(xmlVersion) ||
        "1.1".equals(xmlVersion))
      {
        version = xmlVersion;
      }
    else
      {
        throw new DomEx(DomEx.NOT_SUPPORTED_ERR);
      }
  }

  public boolean getStrictErrorChecking()
  {
    return checkingCharacters;
  }

  public void setStrictErrorChecking(boolean strictErrorChecking)
  {
    checkingCharacters = strictErrorChecking;
  }

  public String lookupPrefix(String namespaceURI)
  {
    Node root = getDocumentElement();
    return (root == null) ? null : root.lookupPrefix(namespaceURI);
  }

  public boolean isDefaultNamespace(String namespaceURI)
  {
    Node root = getDocumentElement();
    return (root == null) ? false : root.isDefaultNamespace(namespaceURI);
  }

  public String lookupNamespaceURI(String prefix)
  {
    Node root = getDocumentElement();
    return (root == null) ? null : root.lookupNamespaceURI(prefix);
  }

  public String getBaseURI()
  {
    return getDocumentURI();
    /*
    Node root = getDocumentElement();
    if (root != null)
      {
        NamedNodeMap attrs = root.getAttributes();
        Node xmlBase = attrs.getNamedItemNS(XMLConstants.XML_NS_URI, "base");
        if (xmlBase != null)
          {
            return xmlBase.getNodeValue();
          }
      }
    return systemId;
    */
  }
  
  public String getDocumentURI()
  {
    return systemId;
  }

  public void setDocumentURI(String documentURI)
  {
    systemId = documentURI;
  }

  public Node adoptNode(Node source)
  {
    switch (source.getNodeType())
      {
      case DOCUMENT_NODE:
      case DOCUMENT_TYPE_NODE:
        throw new DomEx(DomEx.NOT_SUPPORTED_ERR);
      case ENTITY_NODE:
      case NOTATION_NODE:
        throw new DomEx(DomEx.NO_MODIFICATION_ALLOWED_ERR);
      }
    if (source instanceof DomNode)
      {
        DomNode src = (DomNode) source;
        DomNode dst = src;
        if (dst.parent != null)
          {
            dst = (DomNode) dst.cloneNode(true);
          }
        dst.setOwner(this);
        src.notifyUserDataHandlers(UserDataHandler.NODE_ADOPTED, src, dst);
        return dst;
      }
    return null;
  }

  public DOMConfiguration getDomConfig()
  {
    if (config == null)
      {
        config = new DomDocumentConfiguration();
      }
    return config;
  }

  public void normalizeDocument()
  {
    boolean save = building;
    building = true;
    normalizeNode(this);
    building = save;
  }

  void normalizeNode(DomNode node)
  {
    node.normalize();
    if (config != null)
      {
        switch (node.nodeType)
          {
          case CDATA_SECTION_NODE:
            if (!config.cdataSections)
              {
                // replace CDATA section with text node
                Text text = createTextNode(node.getNodeValue());
                node.parent.insertBefore(text, node);
                node.parent.removeChild(node);
                // merge adjacent text nodes
                String data = text.getWholeText();
                node = (DomNode) text.replaceWholeText(data);
              }
            else if (config.splitCdataSections)
              {
                String value = node.getNodeValue();
                int i = value.indexOf("]]>");
                while (i != -1)
                  {
                    Node node2 = createCDATASection(value.substring(0, i));
                    node.parent.insertBefore(node2, node);
                    value = value.substring(i + 3);
                    node.setNodeValue(value);
                    i = value.indexOf("]]>");
                  }
              }
            break;
          case COMMENT_NODE:
            if (!config.comments)
              {
                node.parent.removeChild(node);
              }
            break;
          case TEXT_NODE:
            if (!config.elementContentWhitespace &&
                ((Text) node).isElementContentWhitespace())
              {
                node.parent.removeChild(node);
              }
            break;
          case ENTITY_REFERENCE_NODE:
            if (!config.entities)
              {
                for (DomNode ctx = node.first; ctx != null; )
                  {
                    DomNode ctxNext = ctx.next;
                    node.parent.insertBefore(ctx, node);
                    ctx = ctxNext;
                  }
                node.parent.removeChild(node);
              }
            break;
          case ELEMENT_NODE:
            if (!config.namespaceDeclarations)
              {
                DomNamedNodeMap attrs =
                  (DomNamedNodeMap) node.getAttributes();
                boolean aro = attrs.readonly;
                attrs.readonly = false; // Ensure we can delete if necessary
                int len = attrs.getLength();
                for (int i = 0; i < len; i++)
                  {
                    Node attr = attrs.item(i);
                    String namespace = attr.getNamespaceURI();
                    if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespace))
                      {
                        attrs.removeNamedItemNS(namespace,
                                                attr.getLocalName());
                        i--;
                        len--;
                      }
                  }
                attrs.readonly = aro;
              }
            break;
          }
      }
    for (DomNode ctx = node.first; ctx != null; )
      {
        DomNode ctxNext = ctx.next;
        normalizeNode(ctx);
        ctx = ctxNext;
      }
  }
  
  public Node renameNode(Node n, String namespaceURI, String qualifiedName)
    throws DOMException
  {
    if (n instanceof DomNsNode)
      {
        DomNsNode src = (DomNsNode) n;
        if (src == null)
          {
            throw new DomEx(DomEx.NOT_FOUND_ERR);
          }
        if (src.owner != this)
          {
            throw new DomEx(DomEx.WRONG_DOCUMENT_ERR, null, src, 0);
          }
        boolean xml11 = "1.1".equals(version);
        checkName(qualifiedName, xml11);
        int ci = qualifiedName.indexOf(':');
        if ("".equals(namespaceURI))
          {
            namespaceURI = null;
          }
        if (namespaceURI != null)
          {
            checkNCName(qualifiedName, xml11);
            String prefix = (ci == -1) ? "" :
              qualifiedName.substring(0, ci);
            if (XMLConstants.XML_NS_PREFIX.equals(prefix) &&
                !XMLConstants.XML_NS_URI.equals(namespaceURI))
              {
                throw new DomEx(DomEx.NAMESPACE_ERR,
                                "xml namespace must be " +
                                XMLConstants.XML_NS_URI, src, 0);
              }
            else if (src.nodeType == ATTRIBUTE_NODE &&
                     (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix) ||
                      XMLConstants.XMLNS_ATTRIBUTE.equals(qualifiedName)) &&
                     !XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI))
              {
                throw new DomEx(DomEx.NAMESPACE_ERR,
                                "xmlns namespace must be " +
                                XMLConstants.XMLNS_ATTRIBUTE_NS_URI, src, 0);
              }
            if (XMLConstants.XML_NS_URI.equals(namespaceURI) &&
                !XMLConstants.XML_NS_PREFIX.equals(prefix))
              {
                throw new DomEx(DomEx.NAMESPACE_ERR,
                                "xml namespace must be " +
                                XMLConstants.XML_NS_URI, src, 0);
              }
            else if (src.nodeType == ATTRIBUTE_NODE &&
                     XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI) &&
                     !(XMLConstants.XMLNS_ATTRIBUTE.equals(prefix) ||
                       XMLConstants.XMLNS_ATTRIBUTE.equals(qualifiedName)))
              {
                throw new DomEx(DomEx.NAMESPACE_ERR,
                                "xmlns namespace must be " +
                                XMLConstants.XMLNS_ATTRIBUTE_NS_URI, src, 0);
              }
                
          }
        src.setNodeName(qualifiedName);
        src.setNamespaceURI(namespaceURI);
        src.notifyUserDataHandlers(UserDataHandler.NODE_RENAMED, src, src);
        // TODO MutationNameEvents
        // DOMElementNameChanged or DOMAttributeNameChanged
        return src;
      }
    throw new DomEx(DomEx.NOT_SUPPORTED_ERR, null, n, 0);
  }

  // -- XPathEvaluator --
  
  public XPathExpression createExpression(String expression,
                                          XPathNSResolver resolver)
    throws XPathException, DOMException
  {
    return new DomXPathExpression(this, expression, resolver);
  }
  
  public XPathNSResolver createNSResolver(Node nodeResolver)
  {
    return new DomXPathNSResolver(nodeResolver);
  }
    
  public Object evaluate(String expression,
                         Node contextNode,
                         XPathNSResolver resolver,
                         short type,
                         Object result)
    throws XPathException, DOMException
  {
    XPathExpression xpe =
      new DomXPathExpression(this, expression, resolver);
    return xpe.evaluate(contextNode, type, result);
  }

}

