Package nokogiri

Source Code of nokogiri.XmlNode

/**
* (The MIT License)
*
* Copyright (c) 2008 - 2014:
*
* * {Aaron Patterson}[http://tenderlovemaking.com]
* * {Mike Dalessio}[http://mike.daless.io]
* * {Charles Nutter}[http://blog.headius.com]
* * {Sergio Arbeo}[http://www.serabe.com]
* * {Patrick Mahoney}[http://polycrystal.org]
* * {Yoko Harada}[http://yokolet.blogspot.com]
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* 'Software'), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package nokogiri;

import static java.lang.Math.max;
import static nokogiri.internals.NokogiriHelpers.getCachedNodeOrCreate;
import static nokogiri.internals.NokogiriHelpers.clearCachedNode;
import static nokogiri.internals.NokogiriHelpers.clearXpathContext;
import static nokogiri.internals.NokogiriHelpers.getNokogiriClass;
import static nokogiri.internals.NokogiriHelpers.nodeArrayToRubyArray;
import static nokogiri.internals.NokogiriHelpers.nonEmptyStringOrNil;
import static nokogiri.internals.NokogiriHelpers.rubyStringToString;
import static nokogiri.internals.NokogiriHelpers.stringOrNil;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import nokogiri.internals.HtmlDomParserContext;
import nokogiri.internals.NokogiriHelpers;
import nokogiri.internals.NokogiriNamespaceCache;
import nokogiri.internals.SaveContextVisitor;
import nokogiri.internals.XmlDomParserContext;

import org.apache.xerces.dom.CoreDocumentImpl;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.javasupport.util.RuntimeHelpers;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

/**
* Class for Nokogiri::XML::Node
*
* @author sergio
* @author Patrick Mahoney <pat@polycrystal.org>
* @author Yoko Harada <yokolet@gmail.com>
* @author John Shahid <jvshahid@gmail.com>
*/
@JRubyClass(name="Nokogiri::XML::Node")
public class XmlNode extends RubyObject {
    protected static final String TEXT_WRAPPER_NAME = "nokogiri_text_wrapper";

    /** The underlying Node object. */
    protected Node node;

    /* Cached objects */
    protected IRubyObject content = null;
    protected IRubyObject doc = null;
    protected IRubyObject name = null;

    /*
     * Taken from http://ejohn.org/blog/comparing-document-position/
     * Used for compareDocumentPosition.
     * <ironic>Thanks to both java api and w3 doc for its helpful documentation</ironic>
     */

    protected static final int IDENTICAL_ELEMENTS = 0;
    protected static final int IN_DIFFERENT_DOCUMENTS = 1;
    protected static final int SECOND_PRECEDES_FIRST = 2;
    protected static final int FIRST_PRECEDES_SECOND = 4;
    protected static final int SECOND_CONTAINS_FIRST = 8;
    protected static final int FIRST_CONTAINS_SECOND = 16;

    /**
     * Cast <code>node</code> to an XmlNode or raise a type error
     * in <code>context</code>.
     */
    protected static XmlNode asXmlNode(ThreadContext context, IRubyObject node) {
        if (node == null || !(node instanceof XmlNode)) {
            Ruby ruby = context.getRuntime();
            throw ruby.newTypeError(node, getNokogiriClass(ruby, "Nokogiri::XML::Node"));
        } else {
            return (XmlNode) node;
        }
    }

    /**
     * Cast <code>node</code> to an XmlNode, or null if RubyNil, or
     * raise a type error in <code>context</code>.
     */
    protected static XmlNode asXmlNodeOrNull(ThreadContext context, IRubyObject node) {
        if (node == null || node.isNil()) {
            return null;
        } else {
            return asXmlNode(context, node);
        }
    }

    /**
     * Coalesce to adjacent TextNodes.
     * @param context
     * @param prev Previous node to cur.
     * @param cur Next node to prev.
     */
    public static void coalesceTextNodes(ThreadContext context, IRubyObject prev, IRubyObject cur) {
        XmlNode p = asXmlNode(context, prev);
        XmlNode c = asXmlNode(context, cur);

        Node pNode = p.node;
        Node cNode = c.node;

        pNode.setNodeValue(pNode.getNodeValue()+cNode.getNodeValue());
        p.content = null;       // clear cached content

        c.assimilateXmlNode(context, p);
    }

    /**
     * Coalesce text nodes around <code>anchorNode</code>.  If
     * <code>anchorNode</code> has siblings (previous or next) that
     * are text nodes, the content will be merged into
     * <code>anchorNode</code> and the redundant nodes will be removed
     * from the DOM.
     *
     * To match libxml behavior (?) the final content of
     * <code>anchorNode</code> and any removed nodes will be
     * identical.
     *
     * @param context
     * @param anchorNode
     */
    protected static void coalesceTextNodes(ThreadContext context,
                                            IRubyObject anchorNode,
                                            AdoptScheme scheme) {
        XmlNode xa = asXmlNode(context, anchorNode);

        XmlNode xp = asXmlNodeOrNull(context, xa.previous_sibling(context));
        XmlNode xn = asXmlNodeOrNull(context, xa.next_sibling(context));

        Node p = xp == null ? null : xp.node;
        Node a = xa.node;
        Node n = xn == null ? null : xn.node;

        Node parent = a.getParentNode();

        boolean shouldMergeP = scheme == AdoptScheme.NEXT_SIBLING || scheme == AdoptScheme.CHILD || scheme == AdoptScheme.REPLACEMENT;
        boolean shouldMergeN = scheme == AdoptScheme.PREV_SIBLING || scheme == AdoptScheme.REPLACEMENT;

        // apply the merge right to left
        if (shouldMergeN && n != null && n.getNodeType() == Node.TEXT_NODE) {
            xa.setContent(a.getNodeValue() + n.getNodeValue());
            parent.removeChild(n);
            xn.assimilateXmlNode(context, xa);
        }
        if (shouldMergeP && p != null && p.getNodeType() == Node.TEXT_NODE) {
            xp.setContent(p.getNodeValue() + a.getNodeValue());
            parent.removeChild(a);
            xa.assimilateXmlNode(context, xp);
        }
    }

    /**
     * This is the allocator for XmlNode class.  It should only be
     * called from Ruby code.
     */
    public XmlNode(Ruby ruby, RubyClass cls) {
        super(ruby, cls);
    }

    /**
     * This is a constructor to create an XmlNode from an already
     * existing node.  It may be called by Java code.
     */
    public XmlNode(Ruby ruby, RubyClass cls, Node node) {
        super(ruby, cls);
        setNode(ruby.getCurrentContext(), node);
    }

    protected void decorate(Ruby ruby) {
        if (node != null) {
            resetCache();

            if (node.getNodeType() != Node.DOCUMENT_NODE) {
                doc = document(ruby.getCurrentContext());

                if (doc != null && doc.isTrue()) {
                    RuntimeHelpers.invoke(ruby.getCurrentContext(), doc, "decorate", this);
                }
            }
        }
    }

    /**
     * Create and return a copy of this object.
     *
     * @return a clone of this object
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    protected void resetCache() {
        node.setUserData(NokogiriHelpers.CACHED_NODE, this, null);
    }

    /**
     * Allocate a new object, perform initialization, call that
     * object's initialize method, and call any block passing the
     * object as the only argument.  If <code>cls</code> is
     * Nokogiri::XML::Node, creates a new Nokogiri::XML::Element
     * instead.
     *
     * This static method seems to be inherited, strangely enough.
     * E.g. creating a new XmlAttr from Ruby code calls this method if
     * XmlAttr does not define its own 'new' method.
     *
     * Since there is some Java bookkeeping that always needs to
     * happen, we don't define the 'initialize' method in Java because
     * we'd have to count on subclasses calling 'super'.
     *
     * The main consequence of this is that every subclass needs to
     * define its own 'new' method.
     *
     * As a convenience, this method does the following:
     *
     * <ul>
     *
     * <li>allocates a new object using the allocator assigned to
     * <code>cls</code></li>
     *
     * <li>calls the Java method init(); subclasses can override this,
     * otherwise they should implement a specific 'new' method</li>
     *
     * <li>invokes the Ruby initializer</li>
     *
     * <li>if a block is given, calls the block with the new node as
     * the argument</li>
     *
     * </ul>
     *
     * -pmahoney
     */
    @JRubyMethod(name = "new", meta = true, rest = true)
    public static IRubyObject rbNew(ThreadContext context, IRubyObject cls,
                                    IRubyObject[] args, Block block) {
        Ruby ruby = context.getRuntime();
        RubyClass klazz = (RubyClass) cls;

        if (cls.equals(getNokogiriClass(ruby, "Nokogiri::XML::Node"))) {
            klazz = getNokogiriClass(ruby, "Nokogiri::XML::Element");
        }

        XmlNode xmlNode = (XmlNode) klazz.allocate();
        xmlNode.init(context, args);
        xmlNode.callInit(args, block);
        if (xmlNode.node == null) context.getRuntime().newRuntimeError("NODE IS NULL");
        if (block.isGiven()) block.call(context, xmlNode);
        return xmlNode;
    }

    /**
     * Initialize the object from Ruby arguments.  Should be
     * overridden by subclasses.  Should check for a minimum number of
     * args but not for an exact number.  Any extra args will then be
     * passed to 'initialize'.  The way 'new' and this 'init' function
     * interact means that subclasses cannot arbitrarily change the
     * require aruments by defining an 'initialize' method.  This is
     * how the C libxml wrapper works also.
     *
     * As written it performs initialization for a new Element with
     * the given <code>name</code> within the document
     * <code>doc</code>.  So XmlElement need not override this.  This
     * implementation cannot be moved to XmlElement however, because
     * subclassing XmlNode must result in something that behaves much
     * like XmlElement.
     */
    protected void init(ThreadContext context, IRubyObject[] args) {
        if (args.length < 2)
            throw context.getRuntime().newArgumentError(args.length, 2);

        IRubyObject name = args[0];
        IRubyObject doc = args[1];

        Document document = asXmlNode(context, doc).getOwnerDocument();
        if (document == null) {
            throw getRuntime().newArgumentError("node must have owner document");
        }

        Element element = null;
        String node_name = rubyStringToString(name);
        String prefix = NokogiriHelpers.getPrefix(node_name);
        if (prefix == null) {
            element = document.createElement(node_name);
        } else {
            String namespace_uri = null;
            if (document.getDocumentElement() != null) {
                namespace_uri = document.getDocumentElement().lookupNamespaceURI(prefix);
            }
            element = document.createElementNS(namespace_uri, node_name);
        }
        setNode(context, element);
    }

    /**
     * Set the underlying node of this node to the underlying node of
     * <code>otherNode</code>.
     *
     * FIXME: also update the cached node?
     */
    protected void assimilateXmlNode(ThreadContext context, IRubyObject otherNode) {
        XmlNode toAssimilate = asXmlNode(context, otherNode);

        this.node = toAssimilate.node;
        content = null;         // clear cache
    }

    /**
     * See org.w3.dom.Node#normalize.
     */
    public void normalize() {
        node.normalize();
    }

    public Node getNode() {
        return node;
    }

    public static Node getNodeFromXmlNode(ThreadContext context, IRubyObject xmlNode) {
        return asXmlNode(context, xmlNode).node;
    }

    protected String indentString(IRubyObject indentStringObject, String xml) {
        String[] lines = xml.split("\n");

        if(lines.length <= 1) return xml;

        String[] resultLines  = new String[lines.length];

        String curLine;
        boolean closingTag = false;
        String indentString = rubyStringToString(indentStringObject);
        int lengthInd = indentString.length();
        StringBuffer curInd = new StringBuffer();

        resultLines[0] = lines[0];

        for(int i = 1; i < lines.length; i++) {

            curLine = lines[i].trim();

            if(curLine.length() == 0) continue;

            if(curLine.startsWith("</")) {
                closingTag = true;
                curInd.setLength(max(0,curInd.length() - lengthInd));
            }

            resultLines[i] = curInd.toString() + curLine;

            if(!curLine.endsWith("/>") && !closingTag) {
                curInd.append(indentString);
            }

            closingTag = false;
        }

        StringBuffer result = new StringBuffer();
        for(int i = 0; i < resultLines.length; i++) {
            result.append(resultLines[i]);
            result.append("\n");
        }

        return result.toString();
    }

    public boolean isComment() { return false; }

    public boolean isElement() {
        if (node instanceof Element) return true; // in case of subclassing
        else return false;
    }

    public boolean isProcessingInstruction() { return false; }

    /**
     * Return the string value of the attribute <code>key</code> or
     * nil.
     *
     * Only applies where the underlying Node is an Element node, but
     * implemented here in XmlNode because not all nodes with
     * underlying Element nodes subclass XmlElement, such as the DTD
     * declarations like XmlElementDecl.
     */
    protected IRubyObject getAttribute(ThreadContext context, String key) {
        return getAttribute(context.getRuntime(), key);
    }

    protected IRubyObject getAttribute(Ruby runtime, String key) {
        String value = getAttribute(key);
        return nonEmptyStringOrNil(runtime, value);
    }

    protected String getAttribute(String key) {
        if (node.getNodeType() != Node.ELEMENT_NODE) return null;

        String value = ((Element)node).getAttribute(key);
        return value.length() == 0 ? null : value;
    }

    public void post_add_child(ThreadContext context, XmlNode current, XmlNode child) {
    }

    public void relink_namespace(ThreadContext context) {
        if (node instanceof Element) {
            clearCachedNode(node);
            Element e = (Element) node;
            String prefix = e.getPrefix();
            String currentNS = e.getNamespaceURI();
            if (prefix == null && currentNS == null) {
                prefix = NokogiriHelpers.getPrefix(e.getNodeName());
            } else if (currentNS != null) {
                prefix = e.lookupPrefix(currentNS);
            }
            e.getOwnerDocument().setStrictErrorChecking(false);
            String nsURI = e.lookupNamespaceURI(prefix);
            this.node = NokogiriHelpers.renameNode(e, nsURI, e.getNodeName());

            if (e.hasAttributes()) {
                NamedNodeMap attrs = e.getAttributes();

                for (int i = 0; i < attrs.getLength(); i++) {
                    Attr attr = (Attr) attrs.item(i);
                    String nsUri = "";
                    String attrPrefix = attr.getPrefix();
                    if (attrPrefix == null) {
                        attrPrefix = NokogiriHelpers.getPrefix(attr.getNodeName());
                    }
                    String nodeName = attr.getNodeName();
                    if ("xml".equals(attrPrefix)) {
                        nsUri = "http://www.w3.org/XML/1998/namespace";
                    } else if ("xmlns".equals(attrPrefix) || nodeName.equals("xmlns")) {
                        nsUri = "http://www.w3.org/2000/xmlns/";
                    } else {
                        nsUri = attr.lookupNamespaceURI(attrPrefix);
                    }

                    if (nsUri == e.getNamespaceURI()) {
                        nsUri = null;
                    }

                    if (!(nsUri == null || "".equals(nsUri) || "http://www.w3.org/XML/1998/namespace".equals(nsUri))) {
                        XmlNamespace.createFromAttr(context.getRuntime(), attr);
                    }
                    clearCachedNode(attr);
                    NokogiriHelpers.renameNode(attr, nsUri, nodeName);
                }
            }

            if (this.node.hasChildNodes()) {
                XmlNodeSet nodeSet = (XmlNodeSet)(children(context));
                nodeSet.relink_namespace(context);
            }
        }
    }

    // Users might extend XmlNode. This method works for such a case.
    public void accept(ThreadContext context, SaveContextVisitor visitor) {
        visitor.enter(node);
        XmlNodeSet xmlNodeSet = (XmlNodeSet) children(context);
        if (xmlNodeSet.length() > 0) {
            RubyArray array = (RubyArray) xmlNodeSet.to_a(context);
            for(int i = 0; i < array.getLength(); i++) {
                Object item = array.get(i);
                if (item instanceof XmlNode) {
                  XmlNode cur = (XmlNode) item;
                  cur.accept(context, visitor);
                } else if (item instanceof XmlNamespace) {
                    XmlNamespace cur = (XmlNamespace)item;
                    cur.accept(context, visitor);
                }
            }
        }
        visitor.leave(node);
    }

    public void setName(IRubyObject name) {
        this.name = name;
    }

    public void setDocument(ThreadContext context, IRubyObject doc) {
        this.doc = doc;
        setInstanceVariable("@document", doc);
        if (doc != null) {
            RuntimeHelpers.invoke(context, doc, "decorate", this);
        }
    }

    public void setNode(ThreadContext context, Node node) {
        this.node = node;

        decorate(context.getRuntime());

        if (this instanceof XmlAttr) {
            ((XmlAttr)this).setNamespaceIfNecessary(context.getRuntime());
        }
    }

    public void updateNodeNamespaceIfNecessary(ThreadContext context, XmlNamespace ns) {
        String oldPrefix = this.node.getPrefix();
        String uri = rubyStringToString(ns.href(context));

        /*
         * Update if both prefixes are null or equal
         */
        boolean update = (oldPrefix == null && ns.prefix(context).isNil()) ||
                            (oldPrefix != null && !ns.prefix(context).isNil()
                && oldPrefix.equals(rubyStringToString(ns.prefix(context))));

        if(update) {
            this.node = NokogiriHelpers.renameNode(this.node, uri, this.node.getNodeName());
        }
    }

    protected IRubyObject getNodeName(ThreadContext context) {
        if (name != null) return name;
        String str = null;

        if (this.name == null && node != null) {
            str = node.getNodeName();
            str = NokogiriHelpers.getLocalPart(str);
        }
        if (str == null) str = "";
        if (str.startsWith("#")) str = str.substring(1)// eliminates '#'
        name = NokogiriHelpers.stringOrBlank(context.getRuntime(), str);

        return name;
    }

    /**
     * Add a namespace definition to this node.  To the underlying
     * node, add an attribute of the form
     * <code>xmlns:prefix="uri"</code>.
     */
    @JRubyMethod(name = {"add_namespace_definition", "add_namespace"})
    public IRubyObject add_namespace_definition(ThreadContext context,
                                                IRubyObject prefix,
                                                IRubyObject href) {
        String prefixString = rubyStringToString(prefix);
        String hrefString = rubyStringToString(href);

        NokogiriNamespaceCache nsCache = NokogiriHelpers.getNamespaceCacheFormNode(node);
        XmlNamespace cachedNamespace = nsCache.get(prefixString, hrefString);
        if (cachedNamespace != null) return cachedNamespace;

        Node namespaceOwner;
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            namespaceOwner = node;
            Element element = (Element) node;
            // adds namespace as node's attribute
            final String uri = "http://www.w3.org/2000/xmlns/";
            String qName =
                prefix.isNil() ? "xmlns" : "xmlns:" + prefixString;
            element.setAttributeNS(uri, qName, hrefString);
        }
        else if (node.getNodeType() == Node.ATTRIBUTE_NODE) namespaceOwner = ((Attr)node).getOwnerElement();
        else namespaceOwner = node.getParentNode();
        XmlNamespace ns = XmlNamespace.createFromPrefixAndHref(namespaceOwner, prefix, href);
        if (node != namespaceOwner) {
            this.node = NokogiriHelpers.renameNode(node, ns.getHref(), ns.getPrefix() + ":" + node.getLocalName());
        }
        updateNodeNamespaceIfNecessary(context, ns);

        return ns;
    }

    @JRubyMethod(name = {"attribute", "attr"})
    public IRubyObject attribute(ThreadContext context, IRubyObject name){
        NamedNodeMap attrs = this.node.getAttributes();
        Node attr = attrs.getNamedItem(rubyStringToString(name));
        if(attr == null) {
            return  context.getRuntime().getNil();
        }
        return getCachedNodeOrCreate(context.getRuntime(), attr);
    }

    @JRubyMethod
    public IRubyObject attribute_nodes(ThreadContext context) {
        NamedNodeMap nodeMap = this.node.getAttributes();

        Ruby ruby = context.getRuntime();
        if(nodeMap == null){
            return ruby.newEmptyArray();
        }

        RubyArray attr = ruby.newArray();

        for(int i = 0; i < nodeMap.getLength(); i++) {
            if ((doc instanceof HtmlDocument) || !NokogiriHelpers.isNamespace(nodeMap.item(i))) {
                attr.append(getCachedNodeOrCreate(context.getRuntime(), nodeMap.item(i)));
            }
        }

        return attr;
    }

    @JRubyMethod
    public IRubyObject attribute_with_ns(ThreadContext context, IRubyObject name, IRubyObject namespace) {
        String namej = rubyStringToString(name);
        String nsj = (namespace.isNil()) ? null : rubyStringToString(namespace);

        Node el = this.node.getAttributes().getNamedItemNS(nsj, namej);

        if(el == null) {
            return context.getRuntime().getNil();
        }
        return NokogiriHelpers.getCachedNodeOrCreate(context.getRuntime(), el);
    }

    @JRubyMethod(name = "blank?")
    public IRubyObject blank_p(ThreadContext context) {
        // according to libxml doc,
        // a node is blank if if it is a Text or CDATA node consisting of whitespace only
        if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE) {
            String data = node.getTextContent();
            if (data == null) return context.getRuntime().getTrue();
            if ("".equals(data.trim())) return context.getRuntime().getTrue();
        }
        return context.getRuntime().getFalse();
    }

    @JRubyMethod
    public IRubyObject child(ThreadContext context) {
        return getCachedNodeOrCreate(context.getRuntime(), node.getFirstChild());
    }

    @JRubyMethod
    public IRubyObject children(ThreadContext context) {
        XmlNodeSet xmlNodeSet = (XmlNodeSet) NokogiriService.XML_NODESET_ALLOCATOR.allocate(context.getRuntime(), getNokogiriClass(context.getRuntime(), "Nokogiri::XML::NodeSet"));
        xmlNodeSet.setNodeList(node.getChildNodes());
        return xmlNodeSet;
    }

    @JRubyMethod
    public IRubyObject first_element_child(ThreadContext context) {
        List<Node> elementNodes = new ArrayList<Node>();
        addElements(node, elementNodes, true);
        if (elementNodes.size() == 0) return context.getRuntime().getNil();
        return getCachedNodeOrCreate(context.getRuntime(), elementNodes.get(0));
    }

    @JRubyMethod
    public IRubyObject last_element_child(ThreadContext context) {
        List<Node> elementNodes = new ArrayList<Node>();
        addElements(node, elementNodes, false);
        if (elementNodes.size() == 0) return context.getRuntime().getNil();
        return getCachedNodeOrCreate(context.getRuntime(), elementNodes.get(elementNodes.size()-1));
    }

    @JRubyMethod(name = {"element_children", "elements"})
    public IRubyObject element_children(ThreadContext context) {
        List<Node> elementNodes = new ArrayList<Node>();
        addElements(node, elementNodes, false);
        if (elementNodes.size() == 0) return XmlNodeSet.newEmptyNodeSet(context);
        RubyArray array = NokogiriHelpers.nodeArrayToRubyArray(context.getRuntime(), elementNodes.toArray(new Node[0]));
        XmlNodeSet xmlNodeSet = XmlNodeSet.newXmlNodeSet(context, array);
        return xmlNodeSet;
    }

    private void addElements(Node n, List<Node> nodes, boolean isFirstOnly) {
        NodeList children = n.getChildNodes();
        if (children.getLength() == 0) return;
        for (int i=0; i< children.getLength(); i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                nodes.add(child);
                if (isFirstOnly) return;
            }
        }
    }

    /**
     * call-seq:
     *  compare(other)
     *
     * Compare this Node to +other+ with respect to their Document
     */
    @JRubyMethod(visibility=Visibility.PRIVATE)
    public IRubyObject compare(ThreadContext context, IRubyObject other) {
        if (!(other instanceof XmlNode)) {
            return context.getRuntime().newFixnum(-2);
        }

        Node otherNode = asXmlNode(context, other).node;

        // Do not touch this if, if it's not for a good reason.
        if (node.getNodeType() == Node.DOCUMENT_NODE ||
           otherNode.getNodeType() == Node.DOCUMENT_NODE) {
            return context.getRuntime().newFixnum(-1);
        }

        try{
            int res = node.compareDocumentPosition(otherNode);
            if ((res & FIRST_PRECEDES_SECOND) == FIRST_PRECEDES_SECOND) {
                return context.getRuntime().newFixnum(-1);
            } else if ((res & SECOND_PRECEDES_FIRST) == SECOND_PRECEDES_FIRST) {
                return context.getRuntime().newFixnum(1);
            } else if (res == IDENTICAL_ELEMENTS) {
                return context.getRuntime().newFixnum(0);
            }

            return context.getRuntime().newFixnum(-2);
        } catch (Exception ex) {
            return context.getRuntime().newFixnum(-2);
        }
    }

    /**
     * TODO: this is a stub implementation.  It's not clear what
     * 'in_context' is supposed to do.  Also should take
     * <code>options</code> into account.
     */
    @JRubyMethod(required = 2, visibility = Visibility.PRIVATE)
    public IRubyObject in_context(ThreadContext context,
                                  IRubyObject str,
                                  IRubyObject options) {
        RubyModule klass;
        XmlDomParserContext ctx;
        InputStream istream;
        XmlDocument document;

        IRubyObject d = document(context);
        Ruby runtime = context.getRuntime();
        if (d != null && d instanceof XmlDocument) {
            document = (XmlDocument)d;
        } else {
            return runtime.getNil();
        }

        if (document instanceof HtmlDocument) {
            klass = getNokogiriClass(runtime, "Nokogiri::HTML::Document");
            ctx = new HtmlDomParserContext(runtime, options);
            ((HtmlDomParserContext)ctx).enableDocumentFragment();
            istream = new ByteArrayInputStream((rubyStringToString(str)).getBytes());
        } else if (document instanceof XmlDocument) {
            klass = getNokogiriClass(runtime, "Nokogiri::XML::Document");
            ctx = new XmlDomParserContext(runtime, options);
            String input = rubyStringToString(str);
            istream = new ByteArrayInputStream(input.getBytes());
        } else {
            return runtime.getNil();
        }

        ctx.setInputSource(istream);
        XmlDocument doc = ctx.parse(context, klass, runtime.getNil());

        RubyArray documentErrors = getErrorArray(document);
        RubyArray docErrors = getErrorArray(doc);
        if (isErrorIncreased(documentErrors, docErrors)) {
            for (int i = 0; i < docErrors.getLength(); i++) {
                documentErrors.add(docErrors.get(i));
            }
            document.setInstanceVariable("@errors", documentErrors);
            XmlNodeSet xmlNodeSet = XmlNodeSet.newXmlNodeSet(context, RubyArray.newArray(runtime));
            return xmlNodeSet;
        }

        // The first child might be document type node (dtd declaration).
        // XmlNodeSet to be return should not have dtd decl in its list.
        Node first;
        if (doc.node.getFirstChild().getNodeType() == Node.DOCUMENT_TYPE_NODE) {
            first = doc.node.getFirstChild().getNextSibling();
        } else {
            first = doc.node.getFirstChild();
        }
        RubyArray nodeArray = RubyArray.newArray(runtime);
        nodeArray.add(NokogiriHelpers.getCachedNodeOrCreate(runtime, first));

        XmlNodeSet xmlNodeSet = XmlNodeSet.newXmlNodeSet(context, nodeArray);
        return xmlNodeSet;
    }

    private RubyArray getErrorArray(XmlDocument document) {
        IRubyObject obj = document.getInstanceVariable("@errors");
        if (obj != null && obj instanceof RubyArray) {
            return (RubyArray)obj;
        }
        return RubyArray.newArray(document.getRuntime());
    }

    private boolean isErrorIncreased(RubyArray baseErrors, RubyArray createdErrors) {
        RubyFixnum length = ((RubyArray)createdErrors.op_diff(baseErrors)).length();
        int diff_in_length = (Integer)length.toJava(Integer.class);
        return diff_in_length > 0;
    }

    @JRubyMethod(name = {"content", "text", "inner_text"})
    public IRubyObject content(ThreadContext context) {
        if (!node.hasChildNodes() && node.getNodeValue() == null &&
            (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE))
          return context.nil;
        String textContent;
        if (this instanceof XmlDocument) {
            Node node = ((Document)this.node).getDocumentElement();
            if (node == null) {
                textContent = "";
            } else {
                Node documentElement = ((Document)this.node).getDocumentElement();
                StringBuffer buffer = new StringBuffer();
                getTextContentRecursively(context, buffer, documentElement);
                textContent = buffer.toString();
            }
        } else {
            StringBuffer buffer = new StringBuffer();
            getTextContentRecursively(context, buffer, node);
            textContent = buffer.toString();
        }
        NokogiriHelpers.convertEncodingByNKFIfNecessary(context.getRuntime(), (XmlDocument)document(context), textContent);
        return stringOrNil(context.getRuntime(), textContent);
    }

    private void getTextContentRecursively(ThreadContext context, StringBuffer buffer, Node currentNode) {
      String textContent = currentNode.getNodeValue();
      if (textContent != null && NokogiriHelpers.shouldDecode(currentNode))
        textContent = NokogiriHelpers.decodeJavaString(textContent);
      if (textContent != null)
        buffer.append(textContent);
      NodeList children = currentNode.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        if (hasTextContent(child))
          getTextContentRecursively(context, buffer, child);
      }
    }

    private boolean hasTextContent(Node child) {
      return child.getNodeType() != Node.COMMENT_NODE &&
          child.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE;
    }

    @JRubyMethod
    public IRubyObject document(ThreadContext context) {
        if (doc == null) {
            doc = (XmlDocument) node.getOwnerDocument().getUserData(NokogiriHelpers.CACHED_NODE);
        }
        if (doc == null) {
            doc = getCachedNodeOrCreate(context.getRuntime(), node.getOwnerDocument());
            node.getOwnerDocument().setUserData(NokogiriHelpers.CACHED_NODE, doc, null);
        }
        return doc;
    }

    @JRubyMethod
    public IRubyObject dup(ThreadContext context) {
        return this.dup_implementation(context, true);
    }

    @JRubyMethod
    public IRubyObject dup(ThreadContext context, IRubyObject depth) {
        boolean deep = (Integer)depth.toJava(Integer.class) != 0;

        return this.dup_implementation(context, deep);
    }

    protected IRubyObject dup_implementation(ThreadContext context, boolean deep) {
        XmlNode clone;
        try {
            clone = (XmlNode) clone();
        } catch (CloneNotSupportedException e) {
            throw context.getRuntime().newRuntimeError(e.toString());
        }
        if (node == null) throw context.getRuntime().newRuntimeError("FFFFFFFFFUUUUUUU");
        Node newNode = node.cloneNode(deep);
        clone.node = newNode;
        return clone;
    }

    public static IRubyObject encode_special_chars(ThreadContext context,
                                                   IRubyObject string) {
        String s = rubyStringToString(string);
        String enc = NokogiriHelpers.encodeJavaString(s);
        return context.getRuntime().newString(enc);
    }

    /**
     * Instance method version of the above static method.
     */
    @JRubyMethod(name="encode_special_chars")
    public IRubyObject i_encode_special_chars(ThreadContext context,
                                              IRubyObject string) {
        return encode_special_chars(context, string);
    }

    /**
     * Get the attribute at the given key, <code>key</code>.
     * Assumes that this node has attributes (i.e. that key? returned
     * true).
     */
    @JRubyMethod(visibility = Visibility.PRIVATE)
    public IRubyObject get(ThreadContext context, IRubyObject rbkey) {
        if (node instanceof Element) {
            if (rbkey == null || rbkey.isNil()) context.getRuntime().getNil();
            String key = rubyStringToString(rbkey);
            Element element = (Element) node;
            if (!element.hasAttribute(key)) return context.getRuntime().getNil();
            String value = element.getAttribute(key);
            return stringOrNil(context.getRuntime(), value);
        }
        return context.getRuntime().getNil();
    }

    /**
     * Returns the owner document, checking if this node is the
     * document, or returns null if there is no owner.
     */
    protected Document getOwnerDocument() {
        if (node.getNodeType() == Node.DOCUMENT_NODE) {
            return (Document) node;
        } else {
            return node.getOwnerDocument();
        }
    }

    @JRubyMethod
    public IRubyObject internal_subset(ThreadContext context) {
        Document document = getOwnerDocument();

        if(document == null) {
            return context.getRuntime().getNil();
        }

        XmlDocument xdoc =
            (XmlDocument) getCachedNodeOrCreate(context.getRuntime(), document);
        IRubyObject xdtd = xdoc.getInternalSubset(context);
        return xdtd;
    }

    @JRubyMethod
    public IRubyObject create_internal_subset(ThreadContext context,
                                              IRubyObject name,
                                              IRubyObject external_id,
                                              IRubyObject system_id) {
        IRubyObject subset = internal_subset(context);
        if (!subset.isNil()) {
            throw context.getRuntime()
                .newRuntimeError("Document already has internal subset");
        }

        Document document = getOwnerDocument();
        if(document == null) {
            return context.getRuntime().getNil();
        }

        XmlDocument xdoc =
            (XmlDocument) getCachedNodeOrCreate(context.getRuntime(), document);
        IRubyObject xdtd = xdoc.createInternalSubset(context, name,
                                                     external_id, system_id);
        return xdtd;
    }

    @JRubyMethod
    public IRubyObject external_subset(ThreadContext context) {
        Document document = getOwnerDocument();

        if (document == null) {
            return context.getRuntime().getNil();
        }

        XmlDocument xdoc =
            (XmlDocument) getCachedNodeOrCreate(context.getRuntime(), document);
        IRubyObject xdtd = xdoc.getExternalSubset(context);
        return xdtd;
    }

    @JRubyMethod
    public IRubyObject create_external_subset(ThreadContext context,
                                              IRubyObject name,
                                              IRubyObject external_id,
                                              IRubyObject system_id) {
        IRubyObject subset = external_subset(context);
        if (!subset.isNil()) {
            throw context.getRuntime()
                .newRuntimeError("Document already has external subset");
        }

        Document document = getOwnerDocument();
        if(document == null) {
            return context.getRuntime().getNil();
        }
        XmlDocument xdoc = (XmlDocument) getCachedNodeOrCreate(context.getRuntime(), document);
        IRubyObject xdtd = xdoc.createExternalSubset(context, name, external_id, system_id);
        return xdtd;
    }

    /**
     * Test if this node has an attribute named <code>rbkey</code>.
     * Overridden in XmlElement.
     */
    @JRubyMethod(name = {"key?", "has_attribute?"})
    public IRubyObject key_p(ThreadContext context, IRubyObject rbkey) {
        if (node instanceof Element) {
            String key = rubyStringToString(rbkey);
            Element element = (Element) node;
            return context.getRuntime().newBoolean(element.hasAttribute(key));
        } else {
            return context.getRuntime().getNil();
        }
    }

    @JRubyMethod
    public IRubyObject namespace(ThreadContext context) {
        if (doc instanceof HtmlDocument) return context.getRuntime().getNil();
        NokogiriNamespaceCache nsCache = NokogiriHelpers.getNamespaceCacheFormNode(node);
        String prefix = node.getPrefix();
        XmlNamespace namespace = nsCache.get(prefix == null ? "" : prefix, node.getNamespaceURI());
        if (namespace == null || namespace.isEmpty()) {
            return context.getRuntime().getNil();
        }

        return namespace;
    }

    /**
     * Return an array of XmlNamespace nodes based on the attributes
     * of this node.
     */
    @JRubyMethod
    public IRubyObject namespace_definitions(ThreadContext context) {
        // don't use namespace_definitions cache anymore since
        // namespaces might be deleted. Reflecting the result of
        // namesapce removals is complicated, so the cache might not be
        // updated.
        Ruby ruby = context.getRuntime();
        RubyArray namespace_definitions = ruby.newArray();
        if (doc == null) return namespace_definitions;
        if (doc instanceof HtmlDocument) return namespace_definitions;
        List<XmlNamespace> namespaces = ((XmlDocument)doc).getNamespaceCache().get(node);
        for (XmlNamespace namespace : namespaces) {
            namespace_definitions.append(namespace);
        }

        return namespace_definitions;
    }

    /**
     * Return an array of XmlNamespace nodes defined on this node and
     * on any ancestor node.
     */
    @JRubyMethod
    public IRubyObject namespace_scopes(ThreadContext context) {
        RubyArray scoped_namespaces = context.getRuntime().newArray();
        if (doc == null) return scoped_namespaces;
        if (doc instanceof HtmlDocument) return scoped_namespaces;

        Node previousNode;
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            previousNode = node;
        } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
            previousNode = ((Attr)node).getOwnerElement();
        } else {
            previousNode = findPreviousElement(node);
        }
        if (previousNode == null) return scoped_namespaces;

        List<String> prefixes_in_scope = new ArrayList<String>();
        NokogiriNamespaceCache nsCache = NokogiriHelpers.getNamespaceCacheFormNode(previousNode);
        for (Node previous=previousNode; previous != null; ) {
            List<XmlNamespace> namespaces = nsCache.get(previous);
            for (XmlNamespace namespace : namespaces) {
                if (prefixes_in_scope.contains(namespace.getPrefix())) continue;
                scoped_namespaces.append(namespace);
                prefixes_in_scope.add(namespace.getPrefix());
            }
            previous = findPreviousElement(previous);
        }
        return scoped_namespaces;
    }

    private Node findPreviousElement(Node n) {
        Node previous = n.getPreviousSibling() == null ? n.getParentNode() : n.getPreviousSibling();
        if (previous == null || previous.getNodeType() == Node.DOCUMENT_NODE) return null;
        if (previous.getNodeType() == Node.ELEMENT_NODE) {
            return previous;
        } else {
            return findPreviousElement(previous);
        }
    }

    @JRubyMethod(name="namespaced_key?")
    public IRubyObject namespaced_key_p(ThreadContext context, IRubyObject elementLName, IRubyObject namespaceUri) {
        return this.attribute_with_ns(context, elementLName, namespaceUri).isNil() ?
            context.getRuntime().getFalse() : context.getRuntime().getTrue();
    }

    protected void setContent(IRubyObject content) {
        String javaContent = rubyStringToString(content);
        node.setTextContent(javaContent);
        if (javaContent.length() == 0) return;
        if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE) return;
        if (node.getFirstChild() != null) {
            node.getFirstChild().setUserData(NokogiriHelpers.ENCODED_STRING, true, null);
        }
    }

    private void setContent(String content) {
        node.setTextContent(content);
        this.content = null;    // clear cache
    }

    @JRubyMethod(name = "native_content=")
    public IRubyObject native_content_set(ThreadContext context, IRubyObject content) {
        setContent(content);
        return content;
    }

    /**
     * @param args {IRubyObject io,
     *              IRubyObject encoding,
     *              IRubyObject indentString,
     *              IRubyObject options}
     */
    @JRubyMethod(required=4, visibility=Visibility.PRIVATE)
    public IRubyObject native_write_to(ThreadContext context, IRubyObject[] args) {

        IRubyObject io = args[0];
        IRubyObject encoding = args[1];
        IRubyObject indentString = args[2];
        IRubyObject options = args[3];

        String encString = encoding.isNil() ? null : rubyStringToString(encoding);

        SaveContextVisitor visitor =
            new SaveContextVisitor((Integer) options.toJava(Integer.class), rubyStringToString(indentString), encString, isHtmlDoc(context), isFragment(), 0);
        accept(context, visitor);

        IRubyObject rubyString = null;
        if (NokogiriHelpers.isUTF8(encString)) {
            rubyString = stringOrNil(context.getRuntime(), visitor.toString());
        } else {
            try {
                byte[] bytes = NokogiriHelpers.convertEncoding(Charset.forName(encString), visitor.toString());
                rubyString = stringOrNil(context.getRuntime(), bytes);
            } catch (CharacterCodingException e) {
                throw context.getRuntime().newRuntimeError(e.getMessage());
            }
        }
        RuntimeHelpers.invoke(context, io, "write", rubyString);

        return io;
    }

    private boolean isHtmlDoc(ThreadContext context) {
        return document(context).getMetaClass().isKindOfModule(getNokogiriClass(context.getRuntime(), "Nokogiri::HTML::Document"));
    }

    private boolean isFragment() {
        if (node instanceof DocumentFragment) return true;
        if (node.getParentNode() != null && node.getParentNode() instanceof DocumentFragment) return true;
        return false;
    }

    @JRubyMethod(name = {"next_sibling", "next"})
    public IRubyObject next_sibling(ThreadContext context) {
        return getCachedNodeOrCreate(context.getRuntime(), node.getNextSibling());
    }

    @JRubyMethod(name = {"previous_sibling", "previous"})
    public IRubyObject previous_sibling(ThreadContext context) {
        return getCachedNodeOrCreate(context.getRuntime(), node.getPreviousSibling());
    }

    @JRubyMethod(meta = true, rest = true)
    public static IRubyObject new_from_str(ThreadContext context,
                                           IRubyObject cls,
                                           IRubyObject[] args) {
        XmlDocument doc = (XmlDocument) XmlDocument.read_memory(context, args);
        return doc.root(context);
    }

    @JRubyMethod(name = {"node_name", "name"})
    public IRubyObject node_name(ThreadContext context) {
        return getNodeName(context);
    }

    @JRubyMethod(name = {"node_name=", "name="})
    public IRubyObject node_name_set(ThreadContext context, IRubyObject nodeName) {
        String newName = rubyStringToString(nodeName);
        this.node = NokogiriHelpers.renameNode(node, null, newName);
        setName(nodeName);
        return this;
    }

    @JRubyMethod(visibility = Visibility.PRIVATE)
    public IRubyObject set(ThreadContext context, IRubyObject rbkey, IRubyObject rbval) {
        if (node instanceof Element) {
            String key = rubyStringToString(rbkey);
            String val = rubyStringToString(rbval);
            Element element = (Element) node;

            String uri = null;
            int colonIndex = key.indexOf(":");
            if (colonIndex > 0) {
              String prefix = key.substring(0, colonIndex);
              if (prefix.equals("xml")) {
                uri = "http://www.w3.org/XML/1998/namespace";
              } else if (prefix.equals("xmlns")) {
                uri = "http://www.w3.org/2000/xmlns/";
              } else {
                uri = findNamespaceHref(context, prefix);
              }
            }

            if (uri != null) {
              element.setAttributeNS(uri, key, val);
            } else {
              element.setAttribute(key, val);
            }
            clearXpathContext(node);
            return this;
        } else {
            return rbval;
        }
    }

    private String findNamespaceHref(ThreadContext context, String prefix) {
      XmlNode currentNode = this;
      while(currentNode != document(context)) {
        RubyArray namespaces = (RubyArray) currentNode.namespace_scopes(context);
        Iterator iterator = namespaces.iterator();
        while(iterator.hasNext()) {
          XmlNamespace namespace = (XmlNamespace) iterator.next();
          if (namespace.getPrefix().equals(prefix)) {
            return namespace.getHref();
          }
        }
        if (currentNode.parent(context).isNil()) {
            break;
        } else {
            currentNode = (XmlNode) currentNode.parent(context);
        }
      }
      return null;
    }

    @JRubyMethod
    public IRubyObject parent(ThreadContext context) {
        /*
         * Check if this node is the root node of the document.
         * If so, parent is the document.
         */
        if (node.getOwnerDocument() != null &&
            node.getOwnerDocument().getDocumentElement() == node) {
            return document(context);
        } else {
            return getCachedNodeOrCreate(context.getRuntime(), node.getParentNode());
        }
    }

    @JRubyMethod
    public IRubyObject path(ThreadContext context) {
        return RubyString.newString(context.getRuntime(), NokogiriHelpers.getNodeCompletePath(this.node));
    }

    @JRubyMethod
    public IRubyObject pointer_id(ThreadContext context) {
        return RubyFixnum.newFixnum(context.getRuntime(), this.node.hashCode());
    }

    @JRubyMethod(name = {"remove_attribute", "delete"})
    public IRubyObject remove_attribute(ThreadContext context, IRubyObject name) {
        if (node instanceof Element) {
            String key = name.convertToString().asJavaString();
            Element element = (Element) node;
            element.removeAttribute(key);
        }
        return this;
    }

    @JRubyMethod(visibility=Visibility.PRIVATE)
    public IRubyObject set_namespace(ThreadContext context, IRubyObject namespace) {
        if (namespace.isNil()) {
            if (doc != null) {
                Node n = node;
                String prefix = n.getPrefix();
                String href = n.getNamespaceURI();
                ((XmlDocument)doc).getNamespaceCache().remove(prefix == null ? "" : prefix, href);
                this.node = NokogiriHelpers.renameNode(n, null, NokogiriHelpers.getLocalPart(n.getNodeName()));
            }
        } else {
            XmlNamespace ns = (XmlNamespace) namespace;
            String prefix = rubyStringToString(ns.prefix(context));
            String href = rubyStringToString(ns.href(context));

            // Assigning node = ...renameNode() or not seems to make no
            // difference.  Why not? -pmahoney

            // It actually makes a great deal of difference. renameNode()
            // will operate in place if it can, but sometimes it can't.
            // The node you passed in *might* come back as you expect, but
            // it might not. It's much safer to throw away the original
            // and keep the return value. -mbklein
            String new_name = NokogiriHelpers.newQName(prefix, node);
            this.node = NokogiriHelpers.renameNode(node, href, new_name);
        }

        clearXpathContext(getNode());

        return this;
    }

    @JRubyMethod(name = {"unlink", "remove"})
    public IRubyObject unlink(ThreadContext context) {
        if(node.getParentNode() == null) {
            throw context.getRuntime().newRuntimeError("TYPE: " + node.getNodeType()+ " PARENT NULL");
        } else {
            clearXpathContext(node.getParentNode());
            node.getParentNode().removeChild(node);
        }

        return this;
    }

    /**
     * The C-library simply returns libxml2 magic numbers.  Here we
     * convert Java Xml nodes to the appropriate constant defined in
     * xml/node.rb.
     */
    @JRubyMethod(name = {"node_type", "type"})
    public IRubyObject node_type(ThreadContext context) {
        String type;
        switch (node.getNodeType()) {
        case Node.ELEMENT_NODE:
            if (this instanceof XmlElementDecl)
                type = "ELEMENT_DECL";
            else if (this instanceof XmlAttributeDecl)
                type = "ATTRIBUTE_DECL";
            else if (this instanceof XmlEntityDecl)
                type = "ENTITY_DECL";
            else
                type = "ELEMENT_NODE";
            break;
        case Node.ATTRIBUTE_NODE: type = "ATTRIBUTE_NODE"; break;
        case Node.TEXT_NODE: type = "TEXT_NODE"; break;
        case Node.CDATA_SECTION_NODE: type = "CDATA_SECTION_NODE"; break;
        case Node.ENTITY_REFERENCE_NODE: type = "ENTITY_REF_NODE"; break;
        case Node.ENTITY_NODE: type = "ENTITY_NODE"; break;
        case Node.PROCESSING_INSTRUCTION_NODE: type = "PI_NODE"; break;
        case Node.COMMENT_NODE: type = "COMMENT_NODE"; break;
        case Node.DOCUMENT_NODE:
            if (this instanceof HtmlDocument)
                type = "HTML_DOCUMENT_NODE";
            else
                type = "DOCUMENT_NODE";
            break;
        case Node.DOCUMENT_TYPE_NODE: type = "DOCUMENT_TYPE_NODE"; break;
        case Node.DOCUMENT_FRAGMENT_NODE: type = "DOCUMENT_FRAG_NODE"; break;
        case Node.NOTATION_NODE: type = "NOTATION_NODE"; break;
        default:
            return context.getRuntime().newFixnum(0);
        }

        return getNokogiriClass(context.getRuntime(), "Nokogiri::XML::Node").getConstant(type);
    }

    @JRubyMethod
    public IRubyObject line(ThreadContext context) {
        Node root = getOwnerDocument();
        int[] counter = new int[1];
        count(root, counter);
        return RubyFixnum.newFixnum(context.getRuntime(), counter[0]+1);
    }

    private boolean count(Node node, int[] counter) {
        if (node == this.node) {
            return true;
        }
        NodeList list = node.getChildNodes();
        for (int i=0; i<list.getLength(); i++) {
            Node n = list.item(i);
            if (n instanceof Text
                    && ((Text)n).getData().contains("\n")) {
                counter[0] += 1;
            }
            if (count(n, counter)) return true;
        }
        return false;
    }

    @JRubyMethod
    public IRubyObject next_element(ThreadContext context) {
        Node nextNode = node.getNextSibling();
        Ruby ruby = context.getRuntime();
        if (nextNode == null) return ruby.getNil();
        if (nextNode instanceof Element) {
            return getCachedNodeOrCreate(context.getRuntime(), nextNode);
        }
        Node deeper = nextNode.getNextSibling();
        if (deeper == null) return ruby.getNil();
        return getCachedNodeOrCreate(context.getRuntime(), deeper);
    }

    @JRubyMethod
    public IRubyObject previous_element(ThreadContext context) {
        Node prevNode = node.getPreviousSibling();
        Ruby ruby = context.getRuntime();
        if (prevNode == null) return ruby.getNil();
        if (prevNode instanceof Element) {
            return getCachedNodeOrCreate(context.getRuntime(), prevNode);
        }
        Node shallower = prevNode.getPreviousSibling();
        if (shallower == null) return ruby.getNil();
        return getCachedNodeOrCreate(context.getRuntime(), shallower);
    }

    protected enum AdoptScheme {
        CHILD, PREV_SIBLING, NEXT_SIBLING, REPLACEMENT;
    }

    /**
     * Adopt XmlNode <code>other</code> into the document of
     * <code>this</code> using the specified scheme.
     */
    protected IRubyObject adoptAs(ThreadContext context, AdoptScheme scheme,
                                  IRubyObject other_) {
        XmlNode other = asXmlNode(context, other_);
        // this.doc might be null since this node can be empty node.
        if (this.doc != null) {
            other.setDocument(context, this.doc);
        }
        IRubyObject nodeOrTags = other;
        Node thisNode = node;
        Node otherNode = other.node;

         try {
            Document prev = otherNode.getOwnerDocument();
            Document doc = thisNode.getOwnerDocument();
            clearXpathContext(prev);
            clearXpathContext(doc);
            if (doc != null && doc != otherNode.getOwnerDocument()) {
                Node ret = doc.adoptNode(otherNode);
                // FIXME: this is really a hack, see documentation of fixUserData() for more details.
                fixUserData(prev, ret);
                if (ret == null) {
                    throw context.getRuntime().newRuntimeError("Failed to take ownership of node");
                }
                otherNode = ret;
            }

            Node parent = thisNode.getParentNode();

            switch (scheme) {
            case CHILD:
                Node[] children = adoptAsChild(context, thisNode, otherNode);
                if (children.length == 1 && otherNode == children[0]) {
                    break;
                } else {
                    nodeOrTags = nodeArrayToRubyArray(context.getRuntime(), children);
                }
                break;
            case PREV_SIBLING:
                adoptAsPrevSibling(context, parent, thisNode, otherNode);
                break;
            case NEXT_SIBLING:
                adoptAsNextSibling(context, parent, thisNode, otherNode);
                break;
            case REPLACEMENT:
                adoptAsReplacement(context, parent, thisNode, otherNode);
                break;
            }
         } catch (Exception e) {
             throw context.getRuntime().newRuntimeError(e.toString());
         }

        if (otherNode.getNodeType() == Node.TEXT_NODE) {
            coalesceTextNodes(context, other, scheme);
        }

        relink_namespace(context);
        // post_add_child(context, this, other);

        return nodeOrTags;
    }

    /**
     * This is a hack to fix #839. We should submit a patch to Xerces.
     * It looks like CoreDocumentImpl.adoptNode() doesn't copy
     * the user data associated with child nodes (recursively).
     */
    private void fixUserData(Document previous, Node ret) {
      String key = NokogiriHelpers.ENCODED_STRING;
      for (Node child = ret.getFirstChild(); child != null; child = child.getNextSibling()) {
        CoreDocumentImpl previousDocument = (CoreDocumentImpl) previous;
        child.setUserData(key, previousDocument.getUserData(child, key), null);
        fixUserData(previous, child);
      }
    }

    protected Node[] adoptAsChild(ThreadContext context, Node parent,
                                Node otherNode) {
        /*
         * This is a bit of a hack.  C-Nokogiri allows adding a bare
         * text node as the root element.  Java (and XML spec?) does
         * not.  So we wrap the text node in an element.
         */
        if (parent.getNodeType() == Node.DOCUMENT_NODE && otherNode.getNodeType() == Node.TEXT_NODE) {
          Element e = (Element) parent.getFirstChild();
          if (e == null || !e.getNodeName().equals(TEXT_WRAPPER_NAME)) {
            e = ((Document)parent).createElement(TEXT_WRAPPER_NAME);
            adoptAsChild(context, parent, e);
          }
          e.appendChild(otherNode);
          otherNode = e;
        } else {
          addNamespaceURIIfNeeded(otherNode);
          parent.appendChild(otherNode);
        }
        Node[] nodes = new Node[1];
        nodes[0] = otherNode;
        return nodes;
    }

    private void addNamespaceURIIfNeeded(Node child) {
        if (this instanceof XmlDocumentFragment && ((XmlDocumentFragment)this).getFragmentContext() != null) {
            XmlElement fragmentContext = ((XmlDocumentFragment)this).getFragmentContext();
            String namespace_uri = fragmentContext.node.getNamespaceURI();
            if (namespace_uri != null && namespace_uri.length() > 0) {
                NokogiriHelpers.renameNode(child, namespace_uri, child.getNodeName());
            }
        }
    }

    protected void adoptAsPrevSibling(ThreadContext context,
                                      Node parent,
                                      Node thisNode, Node otherNode) {
        if (parent == null) {
            /* I'm not sure what do do here...  A node with no
             * parent can't exactly have a 'sibling', so we make
             * otherNode parentless also. */
            if (otherNode.getParentNode() != null)
                otherNode.getParentNode().removeChild(otherNode);
            return;
        }

        parent.insertBefore(otherNode, thisNode);
    }

    protected void adoptAsNextSibling(ThreadContext context,
                                      Node parent,
                                      Node thisNode, Node otherNode) {
        if (parent == null) {
            /* I'm not sure what do do here...  A node with no
             * parent can't exactly have a 'sibling', so we make
             * otherNode parentless also. */
            if (otherNode.getParentNode() != null)
                otherNode.getParentNode().removeChild(otherNode);

            return;
        }

        Node nextSib = thisNode.getNextSibling();

        if (nextSib != null &&
            nextSib.getNodeType() == Node.TEXT_NODE &&
            otherNode.getNodeType() == Node.TEXT_NODE) return;

        if (nextSib != null) {
            parent.insertBefore(otherNode, nextSib);
        } else {
            parent.appendChild(otherNode);
        }
    }

    protected void adoptAsReplacement(ThreadContext context,
                                      Node parentNode,
                                      Node thisNode, Node otherNode) {
        if (parentNode == null) {
            /* nothing to replace? */
            return;
        }

        try {
            parentNode.replaceChild(otherNode, thisNode);
            if (otherNode.getNodeType() != Node.TEXT_NODE) {
                NokogiriHelpers.renameNode(otherNode, thisNode.getNamespaceURI(), otherNode.getNodeName());
            }
        } catch (Exception e) {
            String prefix = "could not replace child: ";
            throw context.getRuntime().newRuntimeError(prefix + e.toString());
        }
    }

    /**
     * Add <code>other</code> as a child of <code>this</code>.
     */
    @JRubyMethod(visibility=Visibility.PRIVATE)
    public IRubyObject add_child_node(ThreadContext context, IRubyObject other) {
        return adoptAs(context, AdoptScheme.CHILD, other);
    }

    /**
     * Replace <code>this</code> with <code>other</code>.
     */
    @JRubyMethod(visibility=Visibility.PRIVATE)
    public IRubyObject replace_node(ThreadContext context, IRubyObject other) {
        return adoptAs(context, AdoptScheme.REPLACEMENT, other);
    }

    /**
     * Add <code>other</code> as a sibling before <code>this</code>.
     */
    @JRubyMethod(visibility=Visibility.PRIVATE)
    public IRubyObject add_previous_sibling_node(ThreadContext context, IRubyObject other) {
        return adoptAs(context, AdoptScheme.PREV_SIBLING, other);
    }

    /**
     * Add <code>other</code> as a sibling after <code>this</code>.
     */
    @JRubyMethod(visibility=Visibility.PRIVATE)
    public IRubyObject add_next_sibling_node(ThreadContext context, IRubyObject other) {
        return adoptAs(context, AdoptScheme.NEXT_SIBLING, other);
    }

    /**
     * call-seq:
     *   process_xincludes(options)
     *
     * Loads and substitutes all xinclude elements below the node. The
     * parser context will be initialized with +options+.
     *
     */
    @JRubyMethod(visibility=Visibility.PRIVATE)
    public IRubyObject process_xincludes(ThreadContext context, IRubyObject options) {
        XmlDocument xmlDocument = (XmlDocument)document(context);
        RubyArray errors = (RubyArray)xmlDocument.getInstanceVariable("@errors");
        while(errors.getLength() > 0) {
            XmlSyntaxError error = (XmlSyntaxError)errors.shift(context);
            if (error.toString().contains("Include operation failed")) {
                throw new RaiseException(error);
            }
        }
        return this;
    }

}
TOP

Related Classes of nokogiri.XmlNode

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.