#!/usr/bin/env python
# Copyright (c) 2015-2016, Juniper Networks, Inc.
# All rights reserved.
#
# Copyright (C) 2012 Martin Blech and individual contributors.
#
# See the LICENSE file for further information.
"""Module that provides the XMLNodeBase class.

   This is the base class from which all the other XML node classes
   inherit.
"""
from __future__ import absolute_import

from xml.sax.saxutils import XMLGenerator
from copy import copy
from . import _node_refs, OrderedDict, StringIO, _unicode
from . import _XMLCDATAPlaceholder, _XMLDictPlaceholder, _XMLListPlaceholder

__all__ = ['XMLNodeBase']

class _NoArg(object): # pylint: disable=too-few-public-methods
    """Internal Use Only"""
    def __init__(self):
        pass

_common_docstring = lambda x: """Initialize an %s object.

The optional first parameter can be the value to which the
object should be initialized. All other parameters must be
given as keywords.

Normally, the user can simply run this as::

    >>> node = %s(initializer)

In fact, the best way to use this is::

    >>> root = XMLDictNode({'root': {'branch': { 'leaf': 'a'}}})

That will set all the tags, keys, etc. correctly. However,
if you really want to customize a node, there are other
parameters available. Note that these parameters only
impact *this* node and descendants. They don't actually
add the node to a tree. Therefore, their use is
discouraged. Instead, you can probably use the :py:meth:`add_node`
method to build your tree correctly.

The one exception to this general rule is when adding a
hunk of a tree. For example, assume you currently have this XML
structure::

    <a>
      <b>
        <node1>a</node1>
      </b>
    </a>

And, assume you want to add another node ``b`` to create this
XML structure::

    <a>
      <b>
        <node1>a</node1>
      </b>
      <b>
        <node2>b</node1>
      </b>
    </a>

In that case, you might do something like this::

    >>> root.prettyprint()
    {u'a': {u'b': {u'node1': u'a'}}}
    >>> new_b = {'node2': 'b'}
    >>> new_b = XMLDictNode(new_b, tag="b")
    >>> _ = root['a'].add_node(tag="b", new_node=new_b)
    >>> root.prettyprint()
    {u'a': {u'b': [{u'node1': u'a'}, {'node2': u'b'}]}}

And, you can print the XML to prove it is formatted correctly::

    >>> print root.emit_xml()
    <?xml version="1.0" encoding="utf-8"?>
    <a>
        <b>
            <node1>a</node1>
        </b>
        <b>
            <node2>b</node2>
        </b>
    </a>

Args:
    initializer (as appropriate for node): The initial value for
        the node.
    tag (string): The XML tag for this node.
    key (string or tuple): The dictionary key used for this node.
    xml_attrs (`dict`): The XML attributes for the node.
    text (string): The node's initial CDATA value. (Note
        that this is ignored for :py:class:`XMLCDATANode` objects.)
    parent (Instance of a sub-class of :py:class:`XMLNodeBase`): A
        reference to the object's parent node in the data structure.
    convert (bool): If True, the :py:meth:`convert` method is run on
        the object's children during object initialization.
    deep (bool): If True (and the :py:obj:`convert` parameter is
        True), the :py:meth:`convert` method is run recursively
        on the object's children during object initialization.
""" % (x, x)

def _docstring_fixup(cls):
    """Fixup docstrings for a subclass of XMLNodeBase.

       This method will find any class method with an unset docstring
       and try to copy it from the corresponding method in
       XMLNodeBase.
    """
    if hasattr(cls, '__init__') and not cls.__init__.__doc__:
        docstring = """Initialize an %s object.

        See the class documentation for initializer arguments.
        """ % (cls.__name__)
        try:
            cls.__init__.__doc__ = docstring
        except AttributeError:
            cls.__init__.__func__.__doc__ = docstring
    for k in dir(cls):
        if hasattr(getattr(cls, k), '__call__') and not getattr(cls, k).__doc__:
            if hasattr(XMLNodeBase, k):
                docstring = getattr(XMLNodeBase, k).__doc__
                try:
                    getattr(cls, k).__doc__ = docstring
                except AttributeError:
                    try:
                        getattr(cls, k).__func__.__doc__ = docstring
                    except AttributeError:
                        # You just can't update some methods...
                        pass

XMLCDATANode = _XMLCDATAPlaceholder
XMLDictNode = _XMLDictPlaceholder
XMLListNode = _XMLListPlaceholder
def _resolve_references_once():
    """Internal function to resolve late references.

    There are circular references between the three node types. If we try
    to import all of the references into each module at parse time, the
    parser (rightly) complains about an infinite loop. This function "solves"
    that by doing a one-time load of the symbols the first time an
    instance of the class is changed. The function then replaces its own
    name in the module symbol table with a lambda function to turn this
    into a no-op.
    """
    # pylint: disable=global-statement
    # pylint: disable=invalid-name
    global XMLCDATANode
    global XMLDictNode
    global XMLListNode
    global _resolve_references
    XMLCDATANode = _node_refs['XMLCDATANode']
    XMLDictNode = _node_refs['XMLDictNode']
    XMLListNode = _node_refs['XMLListNode']
    _resolve_references = lambda: None

_resolve_references = _resolve_references_once

class XMLNodeBase(object):
    """This module provides methods common to the XML node classes.

    This modules is not intended for standalone use.
    """

    def __new__(cls, *args, **kwargs):
        # Resolve delayed references, if necessary
        _resolve_references()

        # Make a copy of the kwargs dictonary. (We don't want to modify
        # the original; we only want to modify the values we pass on
        # to super classes.)
        kwargs = dict(kwargs)

        # Determine if we were called with an initial value. If so,
        # make sure there was only *one* initial value.
        initializer = kwargs.pop("initializer", _NoArg())
        if not isinstance(initializer, _NoArg):
            if len(args) > 0:
                raise TypeError("got multiple values for keyword "
                                "argument 'initializer'")
            args = (initializer,)
        elif len(args) == 1:
            initializer = args[0]


        # Pop off arguments that have a special meaning at our layer.
        for k in ("tag", "key", "parent", "xml_attrs", "text",
                  "convert", "deep"):
            _ = kwargs.pop(k, None)

        # Create the object.
        return super(XMLNodeBase, cls).__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        # Make a copy of the kwargs dictonary. (We don't want to modify
        # the original; we only want to modify the values we pass on
        # to super classes.)
        kwargs = dict(kwargs)

        # Determine if we were called with an initial value. If so,
        # make sure there was only *one* initial value.
        initializer = kwargs.pop("initializer", _NoArg())
        if not isinstance(initializer, _NoArg):
            if len(args) > 0:
                raise TypeError("got multiple values for keyword "
                                "argument 'initializer'")
            args = (initializer,)
        elif len(args) == 1:
            initializer = args[0]


        # Process arguments

        # Do not inherit tag, key, or parent in copies.
        tag = kwargs.pop("tag", None)
        key = kwargs.pop("key", tag)
        parent_node = kwargs.pop("parent", None)

        # If the initial value was an XMLNodeBase child, copy
        # node_text and xml_attrs.

        # Set some overall defaults
        xml_attrs = OrderedDict()
        node_text = _unicode()

        # Modify the defaults if the initial value was an XMLNodeBase object
        if isinstance(initializer, XMLNodeBase):
            xml_attrs = getattr(initializer, "xml_attrs", xml_attrs)
            node_text = getattr(initializer, "text", node_text)

        # Process the arguments to override the defaults
        xml_attrs = kwargs.pop("xml_attrs", xml_attrs)
        node_text = kwargs.pop("text", node_text)

        # Process other arguments
        convert = kwargs.pop('convert', True)
        deep = kwargs.pop('deep', True)

        # Initialize the object.
        super(XMLNodeBase, self).__init__(*args, **kwargs)

        # Add attributes to the object.
        self.tag = tag
        self.key = copy(key)
        self.xml_attrs = OrderedDict()
        for k in xml_attrs:
            self.xml_attrs[k] = _unicode(xml_attrs[k])
        if not hasattr(self, "text"):
            self.text = node_text
        self.parent = parent_node
        self._replacement_node = None
        if convert:
            self.standardize(deep=deep)

    def has_xml_attrs(self):
        """Determine if the node has XML attributes.

        Returns:
            A bool that is True if the node has XML attributes, and
                False otherwise.
        """
        if len(self.xml_attrs) > 0:
            return True
        return False

    def _check_replacement(self):
        # Ensure the object is current.
        if self._replacement_node:
            raise AttributeError(
                "Attempt to modify an out-of-date node. " +
                "Use get_current_node() to update the reference."
                )

    def set_xml_attr(self, attr, val):
        """Set an XML attribute.

        This method sets the XML attribute to the given value.  If the
        XML attribute already existed, its value is overridden by the
        new value.  If the XML attribute did not already exist, it is
        created.

        Args:
            attr (string): The name of the XML attribute.
            val (string): The value of the XML attribute.

        Returns:
            None

        Raises:
            :py:exc:`AttributeError`: If the node is out of date.
                (See :py:meth:`get_current_node`.)
        """
        self._check_replacement()
        self.xml_attrs[attr] = _unicode(val)

    def get_xml_attr(self, attr, defval=_NoArg()):
        """Get an XML attribute.

        This method returns the value of an XML attribute. If the XML
        attribute does not exist, it will return a user-supplied default value.
        If the user did not supply a default value, it raises a KeyError.

        Args:
            attr (string): The name of the XML attribute.
            defval (string): The default value. (Default: Raise a KeyError.)

        Returns:
            The string value of the XML attribute, or :py:obj:`defval`.

        Raises:
            :py:exc:`KeyError`: If the :py:obj:`attr` is not found and
                :py:obj:`defval` is not supplied.
        """
        try:
            return self.xml_attrs[attr]
        except KeyError:
            if not isinstance(defval, _NoArg):
                return defval
            raise

    def get_xml_attrs(self):
        """Return the XML attribute dictionary.

        This method returns the value of the XML attribute
        dictonary.  Note that it returns the actual XML attribute
        dictionary, rather than a copy.  Please take caution in
        modifying it.

        Returns:
            :py:class:`OrderedDict`: The XML attribute dictionary.
        """
        return self.xml_attrs

    def delete_xml_attr(self, attr):
        """Delete an XML attribute.

        This method deletes an XML attribute from the node.  If the
        attribute does not exist, it raises a KeyError.

        Args:
            attr (string): The name of the XML attribute.

        Returns:
            None

        Raises:
            :py:exc:`KeyError`: If the :py:obj:`attr` is not found.
            :py:exc:`AttributeError`: If the node is out of date. (See
                :py:meth:`get_current_node`.)
        """
        self._check_replacement()
        del self.xml_attrs[attr]

    def set_cdata(self, cdata, return_node=False):
        """Set a node's CDATA.

        This method sets a node's CDATA.  Note that any node can
        contain CDATA in what is called "semi-structured"
        XML. However, nodes that only contain CDATA are represented as
        :py:class:`XMLCDATANode` objects. Regardless of the node, you can use
        this same method to set the CDATA.

        **Note**: When running this on an XMLCDATANode, the actual node
        will be replaced with a new node in the tree. (This is a
        result of Python's string immutability.)  The function will
        update the XML tree, if necessary; however, any local
        references you have saved for the node will become stale.  You
        can obtain the updated node by setting the return_node
        parameter to True or by running the :py:meth:`get_current_node` method
        on the old node. For this reason, if you plan to keep a local
        reference to XML node in question, it is a good idea to run
        the method like this::

            >>> node = root['a']['b'][0]
            >>> node = node.set_cdata("foo", True)

        Args:
            cdata (string): The text value that should be used for the
                node's CDATA.
            return_node (bool): Whether the method should return the
                updated node.

        Returns:
            None or the updated node object if :py:obj:`return_node` is True.

        Raises:
            :py:exc:`AttributeError`: If the node is out of date. (See
                :py:meth:`get_current_node`.)
        """
        self._check_replacement()
        self.text = _unicode(cdata)
        if return_node:
            return self

    def append_cdata(self, cdata, return_node=False):
        """Append text to a node's CDATA.

        This method appends text to a node's CDATA.  Note that any
        node can contain CDATA in what is called "semi-structured"
        XML. However, nodes that only contain CDATA are represented as
        :py:class:`XMLCDATANode` objects. Regardless of the node, you can use
        this same method to append CDATA.

        **Note**: When running this on an :py:class:`XMLCDATANode`, the actual
        node will be replaced with a new node in the tree. (This is a
        result of Python's string immutability.)  The function will
        update the XML tree, if necessary; however, any local
        references you have saved for the node will become stale.  You
        can obtain the updated node by setting the :py:obj:`return_node`
        parameter to True or by running the :py:meth:`get_current_node` method
        on the old node. For this reason, if you plan to keep a local
        reference to XML node in question, it is a good idea to run
        the method like this::

            >>> node = root['a']['b'][0]
            >>> node = node.append_cdata("foo", True)

        Args:
            cdata (string): The text value that should be used for the
                node's CDATA.
            return_node (bool): Whether the method should return the
                updated node.

        Returns:
            None, if :py:obj:`return_node` is False, otherwise, the updated
                node object.

        Raises:
            :py:exc:`AttributeError`: If the node is out of date.
                (See :py:meth:`get_current_node`.)
        """
        self._check_replacement()
        self.text = self.text + cdata
        if return_node:
            return self

    def get_cdata(self):
        """Get a node's CDATA.

        Returns:
            A string containing the node's CDATA.
        """
        return self.text

    def strip_cdata(self, chars=None, return_node=False):
        """Strip leading/trailing characters from a node's CDATA.

        This method runs the string class' :py:meth:`strip` method on a node's
        CDATA and updates the node's CDATA with the result. (This is
        the functional equivalent to
        ``node.set_cdata(node.get_cdata().strip())``.)

        Note that any node can contain CDATA in what is called
        "semi-structured" XML. However, nodes that only contain CDATA
        are represented as :py:class:`XMLCDATANode` objects. Regardless of the
        node, you can use this same method to append CDATA.

        **Note**: When running this on an :py:class:`XMLCDATANode`, the actual
        node will be replaced with a new node in the tree. (This is a
        result of Python's string immutability.)  The function will
        update the XML tree, if necessary; however, any local
        references you have saved for the node will become stale.  You
        can obtain the updated node by setting the return_node
        parameter to True or by running the :py:meth:`get_current_node` method
        on the old node. For this reason, if you plan to keep a local
        reference to XML node in question, it is a good idea to run
        the method like this::

            >>> node = root['a']['b'][0]
            >>> node = node.strip_cdata(return_node=True)

        Args:
            chars (string): Contains the characters to strip. This is passed to
                the string class' :py:meth:`strip` method.
            return_node (bool): Whether the method should return the
                updated node.

        Returns:
            None if :py:obj:`return_node` is False; otherwise, the updated
                node object.

        Raises:
            :py:class:`AttributeError`: If the node is out of date.
                (See :py:meth:`get_current_node`.)
        """
        self._check_replacement()
        newtext = _unicode.strip(self.get_cdata(), chars)
        return self.set_cdata(newtext, return_node)

    def add_node(self, tag, key=None, text=_unicode(), new_node=None,
                 update=True, **kwargs):
        """Add an XML node to an XML tree.

        This method adds a new XML node as a child of the current
        node. If the current node is an :py:class:`XMLCDATANode`, it will be
        converted to an :py:class:`XMLDictNode` so that it can hold children.
        If the current node is an :py:class:`XMLDictNode` and you attempt to
        add a node with a duplicate key, the code will create a list to hold
        the existing node and add the new node to the list.

        By default, all new nodes are created as :py:class:`XMLCDATANode`
        objects. You can include any keyword parameters that you could
        provide when creating an :py:class:`XMLCDATANode` object. If supplied,
        these additional keyword parameters are passed to the
        :py:func:`XMLCDATANode.__init__` function.

        Args:
            tag (string): The XML tag of the node.
            key (string or tuple): The dictionary key that the method should
                use for the node. If None (the default), the :py:obj:`tag` is
                is used as the key.
            text (string): The CDATA for the new node. The default value is
                an empty string.
            new_node (instance of a subclass of :py:class:`XMLNodeBase`): If
                supplied, this will be used for the new node instead of a
                new instance of the :py:class:`XMLCDATANode`. If supplied, the
                :py:obj:`text` parameter and additional keyword arguments
                are ignored.
            update (bool): If True (the default), update the reverse linkages
                in the new node to point to the parent. If False, only create
                the one-way linkages from the parent to the child. (**Note**:
                This should always be True unless you are creating a temporary
                tree for some reason. Setting this to False may create
                inconsistent data that causes problems later.)

        Raises:
            :py:exc:`AttributeError`: If the node is out of date
                (See :py:meth:`get_current_node`) or the method encounters
                irrecoverable data inconsistency while making changes to the
                XML tree.
            :py:exc:`TypeError`: If :py:obj:`new_node` is not None and not
                an instance of :py:class:`XMLNodeBase`
        """
        raise NotImplementedError()

    def list(self, in_place=False):
        """Return a node as a list.

        This method returns a node as a list. This is useful when you
        are not sure whether a node will contain a single entry or a
        list. If the node contains a list, the node itself is
        returned. If the node does not already contain a list, the
        method creates a list, adds the node to it, and returns the
        list.

        If the :py:obj:`in_place` parameter is True, then the change is
        made in the XML tree. Otherwise, the XML tree is left unchanged and
        the method creates and returns a temporary list.

        Args:
            in_place (bool): Whether the change should be made in the
                XML tree.

        Returns:
            list or :py:class:`XMLListNode`
            If the current node is a list, the current node;
            otherwise, a list containing the current node as its sole
            member.

        Raises:
            :py:exc:`AttributeError`: If the node is out of date and
                :py:obj:`in_place` is True. (See :py:meth:`get_current_node`.)
        """
        if not in_place:
            return [self]
        new_node = XMLListNode(
            [self], tag=self.tag, key=self.key, parent=self.parent,
            convert=False
        )
        self._replace_node(new_node)

        # We weren't "replaced" in the destructive sense, so clear
        # the replacement node indicator.
        self._replacement_node = None

        # self._replace_node() may have updated our key if it was out
        # of date, so grab it.
        self.key = copy(new_node.key)

        # Reparent ourselves.
        self.parent = new_node

        # Return the list.
        return new_node

    def get_current_node(self):
        """Return the current node.

        There are times that the current node must be replaced in the
        XML tree for some reason. For example, due to the immutability
        of Python strings, a new XMLCDATANode (which masquerades as a
        string) is required anytime its CDATA value changes.

        When this occurs, you can retrieve the latest node using the
        get_current_node() method. This will attempt to find the node
        that succeeded the node in question. If the node is still
        current, it simply returns itself.

        Therefore, it should always be safe to run::

            >>> node = node.get_current_node()

        Returns:
            Subclass of :py:class:`XMLNodeBase` containing the current
            successor to the node (if any). If the node is still "current",
            the method returns the node itself.
        """
        if self._replacement_node is not None:
            # Recurse as deep as we can looking for replacement nodes.
            # If we hit an error, return the last replacement node we found.
            try:
                while self._replacement_node._replacement_node is not None:
                    self._replacement_node = (
                        self._replacement_node._replacement_node
                    )
            except: # pylint: disable=bare-except
                pass
            return self._replacement_node
        else:
            return self

    def _replace_node(self, newnode):
        # Replace a node with a new node. If the replacement node is
        # None, then the node is deleted.

        self._check_replacement()
        # We need to replace ourselves with a new node.
        if self.parent is None:
            raise AttributeError("Attempt to modify root document")
        # Record the replacement for anyone who still holds
        # references to this node.
        if newnode is not None:
            self._replacement_node = newnode
        # Case 1: Parent is dictionary
        try:
            # Plan A: Do the lookup by expected key.
            if self.key in self.parent:
                if self.parent[self.key] == self:
                    if newnode is not None:
                        self.parent[self.key] = newnode
                    else:
                        del self.parent[self.key]
                    return
            # Plan B: Brute force check, in case of some sort of
            # mismatch. We do our best, within reason.
            myiter = None
            try:
                # Python 2
                myiter = self.parent.iteritems()
            except AttributeError:
                # Python 3
                myiter = self.parent.items()
            for key, val in myiter:
                if val == self:
                    if newnode is not None:
                        # Update the new node's key
                        newnode.key = copy(key)
                        # Replace us
                        self.parent[key] = newnode
                    else:
                        del self.parent[key]
                    return
                elif isinstance(val, XMLListNode):
                    # Check the list
                    for i in range(0, len(val)):
                        if val[i] == self:
                            if newnode is not None:
                                # Update the new node's key and parent
                                newnode.key = copy(key)
                                newnode.parent = val
                                # Replace us
                                val[i] = newnode
                            else:
                                del val[i]
                                # Make sure we don't need to delete the
                                # enclosing list, too.
                                if len(val) == 0:
                                    val._replace_node(None)
                            return
        except: # pylint: disable=bare-except
            pass
        # Case 2: Parent is list
        try:
            made_change = False
            for i in range(0, len(self.parent)):
                if self.parent[i] == self and self.parent[i].xml_attrs == self.xml_attrs:
                    if newnode is not None:
                        self.parent[i] = newnode
                        return
                    else:
                        del self.parent[i]
                        made_change = True
                        break
            if made_change:
                # Make sure we don't need to delete the enclosing list,
                # too.
                if len(self.parent) == 0:
                    self.parent._replace_node(None)
                return
        except: # pylint: disable=bare-except
            pass
        raise AttributeError("Unable to find existing node in parent")

    def dict(self, attrs=None, tags=None, func=None, in_place=False,
             promote=False):
        """Return a dictionary keyed as indicated by the parameters.

        This method lets you re-key your data with some
        flexibility. It takes the current node (whether a single node
        or a list) and turns it into a dictionary. If the current node
        is a list, all the list members are added to the
        dictionary. If the current node is not a list, just the
        current node is added to the dictonary.

        The key for each node is determined by the :py:obj:`attrs`,
        :py:obj:`tags`, and :py:obj:`func` parameters, in that order of
        precedence. For *each node*, the method looks for child nodes that
        have an XML attribute that exactly matches one of the attributes
        in the :py:obj:`attrs` argument. If it finds a match, it uses the
        *node's* (not the attribute's) CDATA as the key.

        If the method does not find a matching attribute, it looks for
        child nodes that have a tag that exactly matches one of the
        tags in the :py:obj:`tags` argument. If it finds a match, it uses
        the node's CDATA as the key.

        If the method does not find a matching tag, it passes the node
        to the user-suppled function (supplied by the :py:obj:`func` parameter)
        and uses the return value as the key.

        If the :py:obj:`func` is not provided or returns a value that
        evaluates to False (e.g. None or ""), the method uses the node's
        XML tag as the key.

        If there are multiple matches, the order of precedence is like
        this (again, this is applied for *each node* independent of
        the other nodes):

        1. The attributes in the attrs parameter, in the order they
           appear in the attrs parameter.
        2. The tags in the tags parameter, in the order they appear
           in the attrs parameter.
        3. The return value of the user-supplied function.
        4. The node's XML tag.

        If the :py:obj:`in_place` parameter is True, then the method will
        replace the current node in the hierarchy with the
        dictionary. Otherwise, it will create a new dictionary and
        return it.

        If both the :py:obj:`in_place` and :py:obj:`promote` parameters are
        True, then the method will make the changes as described above;
        however, it will add the nodes to the first dictionary it finds
        enclosing the curent node.

        Some examples should help with this. Here is an example of the
        simple functionality. Note how the original nodes are turned
        into a dictionary with the appropriate keys, but the original
        root is left untouched::

            >>> root.prettyprint()
            {'a': {'b': [{'name': u'foo', 'value': u'1'},
                         {'name': u'bar', 'value': u'2'}]}}
            >>> root['a']['b'].dict(tags=['name']).prettyprint()
            {u'bar': {'name': u'bar', 'value': u'2'},
             u'foo': {'name': u'foo', 'value': u'1'}}
            >>> root.prettyprint()
            {'a': {'b': [{'name': u'foo', 'value': u'1'},
                         {'name': u'bar', 'value': u'2'}]}}

        Here is an example of a dictionary changed in place. Note how
        the original nodes are turned into a dictionary with the
        appropriate keys and this dictionary replaces the current node
        in the hierarchy::

            >>> root.prettyprint()
            {'a': {'b': [{'name': u'foo', 'value': u'1'},
                         {'name': u'bar', 'value': u'2'}]}}
            >>> root['a']['b'].dict(tags=['name'], in_place=True).prettyprint()
            {u'bar': {'name': u'bar', 'value': u'2'},
             u'foo': {'name': u'foo', 'value': u'1'}}
            >>> root.prettyprint()
            {'a': {'b': {u'bar': {'name': u'bar', 'value': u'2'},
                         u'foo': {'name': u'foo', 'value': u'1'}}}}

        Here is an example of the "promotion" functionality. Note how
        the original nodes are added directly to the ``root['a']``
        enclosing dictionary::

            >>> root.prettyprint()
            {'a': {'b': [{'name': u'foo', 'value': u'1'},
                         {'name': u'bar', 'value': u'2'}]}}
            >>> root['a']['b'].dict(tags=['name'], in_place=True, promote=True).prettyprint()
            {u'bar': {'name': u'bar', 'value': u'2'},
             u'foo': {'name': u'foo', 'value': u'1'}}
            >>> root.prettyprint()
            {'a': {u'bar': {'name': u'bar', 'value': u'2'},
                   u'foo': {'name': u'foo', 'value': u'1'}}}

        Quirks:

        1. If the current node is the only member of a list in
           the XML tree, the operation will occur on that single-node
           list instead of the node itself.
        2. If the method encounters an exception while trying to
           modify the XML tree (``in_place == True``), it will attempt
           to undo its changes; however, this logic is not
           completely reliable.

        Args:
            attrs (`list`): The list of XML attributes that signal a node
                should be used as a key.
            tags (`list`): The list of XML tags that signal a node should be
                used as a key.
            func (function): A function that will accept a node as a parameter
                and return a key.
            in_place (bool): Whether the change should be made in the XML tree.
            promote (bool): Whether the new nodes should be added to a
                dictonary placed at the current node, or they should be
                "promoted" to the first enclosing dictionary.

        Returns:
            An :py:class:`XMLDictNode`. If :py:obj:`in_place` is False, the
            dictionary formulated from the current node. If :py:obj:`in_place`
            is True, the dictionary to which the nodes were added.
            (Note: If :py:obj:`promote` is True, this dictionary may contain
            additional entries that already existed in the enclosing
            dictionary.)

        Raises:
            :py:class:`AttributeError`: If the node is out of date and
                :py:obj:`in_place` is True. (See :py:meth:`get_current_node`.)
            :py:class:`AttributeError`: If :py:obj:`in_place`
                is True and the method encounters irrecoverable data
                inconsistency while making changes to the XML tree.
        """
        if attrs is None:
            attrs = []
        if tags is None:
            tags = []
        newlist = None
        if in_place:
            # If editing in place, check that we are current.
            self._check_replacement()

            # Save some information to help restore things, if needed.
            orig = dict(parent=self.parent, tag=self.tag, key=self.key)

            # If we were editing in place, we need to figure out how to
            # properly replace ourselves.
            #
            # If our parent is a single-member list, then we can just
            # convert it. If our parent is a dictionary, then we can
            # convert ourselves in place. If our parent is anything
            # else, we just convert ourselves in place and do our best
            # to replace ourselves with a dictionary -- even if that
            # doesn't make complete sense.
            parent = self.parent
            if (isinstance(parent, XMLListNode) and len(parent) == 1
                    and parent[0] == self):
                # Easy. Just convert our parent.
                newlist = parent
            else:
                # Turn us into a list.
                newlist = self.list(in_place=True)
        if newlist is None:
            # Make a list and add us to it.
            newlist = XMLListNode(
                [self], tag=self.tag, key=self.key, convert=False
            )

        # Now, let's turn the list node into a dict.
        try:
            # pylint: disable=no-member
            return newlist.dict(
                attrs=attrs, tags=tags, func=func, in_place=in_place,
                promote=promote
            )
        except:
            if in_place:
                # Try to restore us.
                self.parent = orig['parent']
                self.tag = orig['tag']
                self.key = orig['key']
                self._replacement_node = None
                newlist._replace_node(self) # pylint: disable=no-member
            raise

    def jdict(self, in_place=False, promote=False):
        """Return a dictionary keyed appropriately for Junos output.

        This method is a shortcut to call the :py:meth:`dict`
        method with these parameters::

            attrs=[('junos:key', 'junos:key', 'junos:key'),
                   ('junos:key', 'junos:key'), 'junos:key']
            tags=['name']

        This will attempt to produce the correct key for each
        node. Some nodes have a multi-field key. If that occurs, the
        dictionary key will be a tuple. In cases where there is a
        single key, the dictionary key will be a string. If there is
        no matching node, the key will simply be the XML tag name.

        Some Junos nodes use a different tag for the key. And, in some
        cases, the ``junos:key`` attribute is not available. In those
        circumstances, you should directly call the :py:meth:`dict`
        method with the correct attributes or tags.

        Please see the documentation for the :py:meth:`dict` method for
        further information.

        Args:
            in_place (bool): Whether the change should be made in the XML tree.
            promote (bool): Whether the new nodes should be added to a
                dictonary placed at the current node, or they should be
                "promoted" to the first enclosing dictionary.

        Returns:
            An :py:class:`XMLDictNode`. If :py:obj:`in_place` is False, the
            dictionary formulated from the current node. If :py:obj:`in_place`
            is True, the dictionary to which the nodes were added.
            (Note: If :py:obj:`promote` is True, this dictionary may contain
            additional entries that already existed in the enclosing
            dictionary.)

        Raises:
            :py:class:`AttributeError`: If the node is out of date and
                :py:obj:`in_place` is True. (See :py:meth:`get_current_node`.)
            :py:class:`AttributeError`: If :py:obj:`in_place`
                is True and the method encounters irrecoverable data
                inconsistency while making changes to the XML tree.
        """
        return self.dict(attrs=[('junos:key', 'junos:key', 'junos:key'),
                                ('junos:key', 'junos:key'), 'junos:key'],
                         tags=['name'], in_place=in_place, promote=promote)

    def standardize(self, deep=True):
        """Convert all child nodes to instances of an XMLNodeBase sub-class.

        This method is useful when you have added a child node
        directly to a dictionary or list and now want to convert it to
        the appropriate :py:class:`XMLNodeBase` sub-class.

        Args:
            deep (bool): If True (the default), recursively descend
                through all children, converting all nodes, as needed. If
                False, only convert direct children of the node.
        Returns:
            None
        """
        raise NotImplementedError()

    def emit_handler(self, content_handler, pretty=True, newl='\n',
                     indent='    ', full_document=None):
        """Pass the contents of the XML tree to a ContentHandler object.

        This method will pass the contents of the XML tree to a
        :py:obj:`ContentHandler` object.

        Args:
            content_handler (:py:obj:`ContentHandler`): The
                :py:obj:`ContentHandler` object to which the XML tree wll
                be passed.
            pretty (bool): If True, this method will call the
                :py:meth:`content_handler.ignorableWhitespace` method to add
            whitespace to the output document.
            newl (string): The string which the method should use for new
                lines when adding white space (see the :py:obj:`pretty`
                parameter).
            indent (text): The string which the method should use for each
                level of indentation when adding white space (see the
                :py:obj:`pretty` parameter).
            full_document (bool): If True, the method will call the
                :py:meth:`content_handler.startDocument` and
                :py:meth:`content_handler.endDocument` methods at the start
                and end of the document, respectively. If False, it will not
                call these methods. If the parameter is not set, the method
                will attempt to determine whether the current node is the root
                of an XML tree with a single root tag. If so, it will set
                the full_document parameter to True; otherwise, it will
                set it to False.

        Returns:
            None
        """
        full_document_ok = None
        curnode = self
        # See if it is OK to be a full document. If we will have
        # multiple root nodes, it is not OK. Otherwise, we assume
        # it is.
        while full_document_ok is None:
            if isinstance(curnode, XMLCDATANode):
                # Always OK
                full_document_ok = True
            elif not isinstance(curnode, XMLNodeBase):
                # Never OK -- will probably produce an error later
                full_document_ok = False
            elif isinstance(curnode, XMLListNode) and len(curnode) > 1:
                # We have multiple tags. We cannot be a full document.
                full_document_ok = False
            elif len(curnode) == 0:
                # Empty nodes are always OK.
                full_document_ok = True
            elif ((curnode.tag is None and isinstance(curnode, XMLDictNode)) or
                  isinstance(curnode, XMLListNode)):
                # If curnode.tag is None (or this is a list), this
                # amounts to a request to output the child(ren) of
                # this node. If there are multiple children, we know
                # that it is not OK to make this a full document (as
                # there will be multiple top-level tags). If there is
                # just one child, we need to look at the child to
                # determine whether it is OK to treat it as a full
                # document.
                if len(curnode) > 1:
                    full_document_ok = False
                else:
                    if isinstance(curnode, XMLListNode):
                        curnode = curnode[0]
                    elif isinstance(curnode, XMLDictNode):
                        curnode = curnode[[i for i in curnode][0]]
            elif isinstance(curnode.tag, (_unicode, str)):
                # We have a tag. That will produce a top-level tag.
                full_document_ok = True
            else:
                # We generally shouldn't get here, but let the user
                # do what they want.
                full_document_ok = True

        if full_document is not None:
            # We were given a full_document argument. Make sure it is
            # OK to treat this as a "full document".
            if full_document and not full_document_ok:
                raise ValueError("Document will have more than one root node. "
                                 "The full_document argument must be False.")
        else:
            # Default to full_document=True, if it is OK to treat this
            # as a full document and it looks like we are at the root.
            full_document = full_document_ok
            if full_document:
                # Guess that we are at the root if:
                # - self.tag is None, or
                # - self.tag is not None, but we have no parent, or
                # - self.tag is not None, but our parent looks like a
                #   single-member, tagless root
                if (self.tag is not None and self.parent is not None and
                        (self.parent.tag is not None or len(self.parent) > 1)):
                    full_document = False

        if full_document:
            content_handler.startDocument()
        curnode._emit_handler(content_handler, depth=0, pretty=pretty, newl=newl,
                              indent=indent)
        if full_document:
            content_handler.endDocument()

    def emit_xml(self, output=None, encoding='utf-8', handler=XMLGenerator,
                 **kwargs):
        """Return the contents of the XML tree as an XML document.

        This method will create a :py:obj:`ContentHandler` by calling the
        method provided by the handler parameter.  It will call
        :py:meth:`emit_handler` with this :py:obj:`ContentHandler` object.
        In addition, this method will accept any parameter that the
        :py:meth:`emit_handler` method accepts (except the
        :py:obj:`content_handler` parameter).  It will pass
        them to the :py:meth:`emit_handler` method when it calls it.

        Args:
            output (A file-like IO object, or None): The file-like IO object
                in which output should be placed. If None, the method will
                return the XML output as a string.
            encoding (string): The encoding that should be used for the output.
            handler (function): A method that will return a
                :py:obj:`ContentHandler` object. This method will be called
                with two positional parameters: the output parameter
                (or, if None, a file-like IO object) and the encoding parameter.
        Returns:
            If :py:obj:`output` was None, the method will return the XML
            output as a string. Otherwise, None.
        """
        if output is None:
            output = StringIO()
            return_text = True
        else:
            return_text = False

        content_handler = handler(output, encoding)

        self.emit_handler(content_handler, **kwargs)

        if return_text:
            value = output.getvalue()
            try:  # pragma no cover
                value = value.decode(encoding)
            except AttributeError:  # pragma no cover
                pass
            return value

    def _emit_handler(self, content_handler, depth, pretty, newl, indent):
        raise NotImplementedError()

    def prettyprint(self, *args, **kwargs):
        """Print a "pretty" representation of the data structure.

        This uses the :py:meth:`pprint` method from the :py:mod:`pprint`
        module to print a "pretty" representation of the data structure.
        The parameters are passed unchanged to the :py:meth:`pprint` method.

        The output from this method shows only the main data and not the meta
        data (such as XML attributes).

        When using :py:meth:`pprint`, it is necessary to use this method to
        get a reasonable representation of the data; otherwise,
        :py:meth:`pprint` will not know how to represent the object in a
        "pretty" way.
        """
        raise NotImplementedError()

    def find_nodes_with_tag(self, tag, recursive=True):
        """Iterates over nodes that have a matching tag.

        This method searches the current node and its children for
        nodes that have a matching tag. The :py:obj:`tag` parameter
        accepts either a string value or a tuple, allowing you to
        search for one or more tags with a single
        operation. Optionally (by providing a False value to the
        :py:obj:`recursive` parameter), you can limit the search to
        the current node and direct children of the current node.

        The method will return a generator, which you can use to
        iterate over the matching nodes.

        For example, this will print all "name" nodes from the XML
        snippet that is shown::

            >>> root = jxmlease.parse(\"\"\"\
            ... <?xml version="1.0" encoding="utf-8"?>
            ... <root>
            ...     <a>
            ...         <name>name #1</name>
            ...         <b>
            ...             <name>name #2</name>
            ...         </b>
            ...         <b>
            ...             <c>
            ...                 <name>name #3</name>
            ...             </c>
            ...         </b>
            ...     </a>
            ... </root>\"\"\")
            >>> print root
            {u'root': {u'a': {u'b': [{u'name': u'name #2'},
                                     {u'c': {u'name': u'name #3'}}],
                              u'name': u'name #1'}}}
            >>> for node in root.find_nodes_with_tag('name'):
            ...   print node
            ...
            name #1
            name #2
            name #3

        However, if we turn off recursion, you will see that this
        returns only the direct children (if any) of the node we
        select::

            >>> for node in root.find_nodes_with_tag('name', recursive=False):
            ...   print node
            ...
            >>> for node in root['root']['a'].find_nodes_with_tag('name', recursive=False):
            ...   print node
            ...
            name #1

        If you run this against an :py:class:`XMLDictNode` without a
        tag (for example, the tagless root node), then the command is
        run on each member of the dictionary. The impact of this is
        that a non-recursive search will search for tags in the
        grandchildren of the tagless :py:class:`XMLDictNode`, rather
        than searching the children of the tagless
        :py:class:`XMLDictNode`::

            >>> root = jxmlease.parse(\"\"\"\
            ... <a>
            ...   <name>second-level tag</name>
            ...   <b>
            ...     <name>third-level tag</name>
            ...   </b>
            ... </a>\"\"\")
            >>> for i in root.find_nodes_with_tag('name', recursive=False):
            ...   print i
            ...
            second-level tag
            >>> for i in root['a'].find_nodes_with_tag('name', recursive=False):
            ...   print i
            ...
            second-level tag

        This method never returns a list. Instead, lists pass the
        command through to their child nodes, which may be
        returned. This ensures you get back each node you requested.

        For example, here is a root node with two top-level "name"
        elements. Searching non-recursively for the "name" tag returns
        the two "name" elements, even though they are enclosed within
        a dictionary and list::

            >>> root = XMLDictNode()
            >>> _ = root.add_node(tag='name', text='tag #1')
            >>> _ = root.add_node(tag='name', text='tag #2')
            >>> print root
            {'name': [u'tag #1', u'tag #2']}
            >>> for i in root.find_nodes_with_tag('name', recursive=False):
            ...   print i
            ...
            tag #1
            tag #2
            >>>

        Even though our examples up to this point have
        demonstrated text, it is worth noting that this method returns
        the actual node, whatever that may be::

            >>> root = jxmlease.parse(\"\"\"\
            ... <a>
            ...   <b>
            ...     <c>
            ...       <foo>bar</foo>
            ...       <status>ok</status>
            ...     </c>
            ...   </b>
            ... </a>\"\"\")
            >>> for i in root.find_nodes_with_tag('b'):
            ...   print i
            ...
            {u'c': {u'foo': u'bar', u'status': u'ok'}}

        If the :py:obj:`recursive` parameter is False, the code will
        check the current node. If the current node does not match,
        the code will check the current node's direct
        children. However, if the current node has a matching tag and
        the :py:obj:`recursive` parameter is False, the code will stop
        its search and not check the children of the current node.

        If the :py:obj:`recursive` parameter is True (the default),
        the code will search the current node and all of its children,
        even the children of other matching nodes. Therefore, the
        method may even return children of other matches, if you
        specify a :py:obj:`recursive` search::

            >>> root = jxmlease.parse(\"\"\"
            ... <a>
            ...   <a>
            ...     <a>foo</a>
            ...     <a>bar</a>
            ...   </a>
            ... </a>\"\"\")
            >>> count = 0
            >>> for i in root.find_nodes_with_tag("a"):
            ...     count += 1
            ...     print("%d: %s" % (count, i))
            ...
            1: {u'a': {u'a': [u'foo', u'bar']}}
            2: {u'a': [u'foo', u'bar']}
            3: foo
            4: bar
            >>> count = 0
            >>> for i in root.find_nodes_with_tag("a", recursive=False):
            ...     count += 1
            ...     print("%d: %s" % (count, i))
            ...
            1: {u'a': {u'a': [u'foo', u'bar']}}

        You can also use a tuple as the tag parameter, in which case
        the method will return nodes with a tag that matches any of
        the given tag values.

        You can use this function to create somewhat complicated logic
        that mimics the functionality from XPath "//tag" matches. For
        example, here we check for <xnm:warning> and <xnm:error> nodes
        and return their value::

            >>> root = jxmlease.parse(\"\"\"\
            ... <foo>
            ...   <xnm:warning>
            ...     <message>This is bad.</message>
            ...   </xnm:warning>
            ...   <bar>
            ...     <xnm:error>
            ...       <message>This is very bad.</message>
            ...     </xnm:error>
            ...   </bar>
            ... </foot>\"\"\")
            >>> if root.has_node_with_tag(('xnm:warning', 'xnm:error')):
            ...   print "Something bad happened."
            ...
            Something bad happened.
            >>> for node in root.find_nodes_with_tag(('xnm:warning', 'xnm:error')):
            ...   if node.tag == 'xnm:error':
            ...     level = "Error:"
            ...   elif node.tag == 'xnm:warning':
            ...     level = "Warning:"
            ...   else:
            ...     level = "Unknown:"
            ...   print(level + " " + node.get("message", "(unknown)"))
            ...
            Warning: This is bad.
            Error: This is very bad.

        Args:
            tag (string or tuple): The XML tag (or tags) for which to search.
            recursive (bool): If True (the default), search recursively through
                all children. If False, only search direct children.

        Returns:
            A generator which iterates over all matching nodes.

        """
        if isinstance(tag, str):
            tag = (tag,)
        return self._find_nodes_with_tag(tuple(tag), recursive=recursive,
                                         top_level=True)

    def _find_nodes_with_tag(self, tag, recursive, top_level):
        raise NotImplementedError()

    def has_node_with_tag(self, tag, recursive=True):
        """Determine whether a node with a matching tag exists.

        This method uses the :py:meth:`find_nodes_with_tag` method to
        search the current node and its children for a node that has a
        matching tag.  The method returns a boolean value to indicate
        whether at least one matching node is found.

        Because this function uses the :py:meth:`find_nodes_with_tag` method,
        the parameters and algorithm are the same as the
        :py:meth:`find_nodes_with_tag` method.

        Args:
            tag (string or tuple): The XML tag (or tags) for which to search.
            recursive (bool): If True (the default), search recursively through
                all children. If False, only search direct children.

        Returns:
            True if at least one matching node is found; otherwise, False.

        """
        for _ in self.find_nodes_with_tag(tag, recursive=recursive):
            return True
        return False

    def __repr__(self):
        return "%s(xml_attrs=%r, value=%s)" % (
            getattr(self, "__const_class_name__", self.__class__.__name__),
            self.xml_attrs, super(XMLNodeBase, self).__repr__()
        )

    def __str__(self):
        io_obj = StringIO()
        self.prettyprint(io_obj)
        return io_obj.getvalue().strip()
