package org.ow2.jonas.migration.jboss;

import java.util.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;

/**
 * The Transformer class contains methods for querying one or more
 * input documents and building a new output document. Transformation
 * tasks such as these can also be programmed in XSL and performed by
 * an XSLT engine, however, for certain kinds of transformations it
 * may be useful to perform them in Java by extending the Transformer
 * class.
 *
 * @author Rafael H. Schloming &lt;rhs@mit.edu&gt;
 **/

public class Transformer {

    /**
     * Interface used to filter nodes when querying an input document.
     */

    protected interface NodeFilter {

        /**
         * Tests the given node and returns true iff it is accepted by
         * this NodeFilter.
         *
         * @param node the node to test
         * @return true iff <var>node</var> is accepted by this NodeFilter
         */

        boolean accept(Node node);
    }

    /**
     * A predefined {@link NodeFilter} that accepts all nodes.
     */

    protected static final NodeFilter ALL = new NodeFilter() {
        public boolean accept(Node node) { return true; }
    };

    /**
     * A predefined {@link String} containing the line seperator for
     * this system.
     */

    protected static final String LINE =
        System.getProperty("line.separator", "\n\r");

    /**
     * A stack of nodes containing the enclosing elements for the
     * current node being output. This stack is maintained by the
     * {@link #open(String)} and {@link #close()} methods.
     */
    private Stack m_stack = new Stack();
    /**
     * The document being output.
     */
    private Document m_doc;
    /**
     * The current node being output.
     */
    private Node m_node;

    /**
     * Constructs a new Transformer ready to output to an empty document.
     */

    protected Transformer() {
        try {
            DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            m_doc = db.newDocument();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
        m_node = m_doc;
    }

    /**
     * Returns the output document.
     *
     * @return the output document
     */

    protected Document getDocument() {
        return m_doc;
    }

    /**
     * Return a string containing the correct number of spaces to
     * indent for the given level of nesting. Uses a two space indent.
     *
     * @param level the number of levels to indent
     * @return a string consisting of <code>2*level</code> spaces
     */

    private String indent(int level) {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < level; i++) {
            buf.append("  ");
        }
        return buf.toString();
    }

    /**
     * Conditionally indents if <var>indent</var> is true and it is
     * necessry to indent. The amount to indent is determined by the
     * current stack size adjusted by the amount specified by
     * <var>offset</var>.
     *
     * @param indent a flag indicating whether to indent at all
     * @param offset adjusts the amount to indent
     */

    private void indent(boolean indent, int offset) {
        int level = m_stack.size() + offset;
        if (indent && level > 0) {
            text(LINE + indent(level));
        }
    }

    /**
     * Creates a new child element of the current node with the given
     * name and makes the new element the current node. A text node is
     * added with an appropriate amount of indenting whitespace before
     * the new element is added.
     *
     * @param name the name of the new element
     *
     * @see #open(String, boolean)
     */

    protected void open(String name) {
        open(name, true);
    }

    /**
     * Creates a new child element of the current node with the given
     * name and makes the new element the current node. If indent is
     * true then a text node with the appropriate amount of whitespace
     * is added before the new element is created.
     *
     * @param name the name of the new node
     * @param indent a flag indicating whether or not to indent
     */

    protected void open(String name, boolean indent) {
        indent(indent, 0);
        Element el = m_doc.createElement(name);
        m_node.appendChild(el);
        m_stack.push(m_node);
        m_node = el;
    }

    /**
     * Adds a text node with the appropriate amount of whitespace and
     * restores the previous current node.
     *
     * @see #close(boolean)
     */

    protected void close() {
        close(true);
    }

    /**
     * Restores the previous current node. If <var>indent</var> is
     * true an appropriate amount of indenting whitespace is added
     * before restoring the previous current node.
     *
     * @param indent a flag indicating whether or not to indent
     */

    protected void close(boolean indent) {
        indent(indent, -1);
        m_node = (Node) m_stack.pop();
    }

    /**
     * Sets an attribute on the current element.
     *
     * @param name the attribute name
     * @param value the attribute value
     * @throws IllegalStateException if the current node is not an
     * element
     */

    protected void set(String name, String value) {
        if (m_node.getNodeType() != Node.ELEMENT_NODE) {
            throw new IllegalStateException
                ("not currently in an element");
        }
        Element el = (Element) m_node;
        el.setAttribute(name, value);
    }

    /**
     * Adds the given text to the current node.
     *
     * @param text the text to add
     */

    protected void text(String text) {
        m_node.appendChild(m_doc.createTextNode(text));
    }

    /**
     * Adds a comment to the current node.
     *
     * @param comment the text of the comment
     */

    protected void comment(String comment) {
        m_node.appendChild(m_doc.createComment(comment));
    }

    /**
     * Creates an element with the given name that contains a single
     * text node with the given value and adds it as a child to the
     * current node.
     *
     * @param name the name of the element
     * @param value the value of the text node
     */

    protected void tag(String name, String value) {
        if (value == null) { return; }
        open(name);
        text(value);
        close(false);
    }

    /**
     * Queries a node for child elements with a given name and filters
     * the result based on a given {@link NodeFilter}.
     *
     * @param node the node to query
     * @param name the name of the child elements to get
     * @param result the collection to add the results to
     * @param filter the result filter
     */

    protected void get(Node node, String name, Collection result,
                     NodeFilter filter) {
        NodeList nodes = node.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node child = nodes.item(i);
            if (child.getNodeName().equals(name) && filter.accept(child)) {
                result.add(child);
            }
        }
    }

    /**
     * Queries the given node for all elements at a given path.
     *
     * @param node the node to query
     * @param path the path
     * @param result the collection to add results to
     */

    protected void query(Node node, String path, Collection result) {
        query(node, path, result, ALL);
    }

    /**
     * Queries the given node for all elements at a given path and
     * filters the result.
     *
     * @param node the node to query
     * @param the path
     * @param result the collection to add results to
     * @param filter the {@link NodeFilter} used to filter the results
     */

    protected void query(Node node, String path, Collection result,
                       NodeFilter filter) {
        int idx = path.indexOf('/');
        if (idx < 0) {
            get(node, path, result, filter);
            return;
        }

        String first = path.substring(0, idx);
        String rest = path.substring(idx+1);
        ArrayList l = new ArrayList();
        get(node, first, l, ALL);
        for (int i = 0; i < l.size(); i++) {
            Node n = (Node) l.get(i);
            query(n, rest, result, filter);
        }
    }

    /**
     * Queries a node for all descendent elements at a given path.
     *
     * @param node the node to query
     * @param path the path
     * @return the resulting list of nodes
     */

    protected List nodes(Node node, String path) {
        List result = new ArrayList();
        query(node, path, result);
        return result;
    }

    /**
     * Queries a node for a single descedent element at a given path.
     *
     * @param node the node to query
     * @param path the path
     * @return the node or null if none is found
     * @throws IllegalStateException if more than one node is found
     */

    protected Node node(Node node, String path) {
        ArrayList result = new ArrayList();
        query(node, path, result);
        return singleton(result);
    }

    /**
     * Queries a node for a string value at a given path.
     *
     * @param node the node to query
     * @param path the path
     * @return the value or null if there is none
     * @throws IllegalStateException if the node exists but has no
     * value
     */

    protected String value(Node node, String path) {
        if (node == null) { return null; }
        Node val = node(node, path + "/#text");
        if (val == null) {
            return null;
        } else {
            String result = val.getNodeValue();
            if (result == null) {
                throw new IllegalStateException
                    ("node has no value: " + val);
            } else {
                return result.trim();
            }
        }
    }

    protected List values(Node node, String path) {
        List result = new ArrayList();
        List nodes = nodes(node, path + "/#text");
        for (int i = 0; i < nodes.size(); i++) {
            Node nd = (Node) nodes.get(i);
            result.add(nd.getNodeValue());
        }
        return result;
    }

    protected Node singleton(Collection c) {
        if (c.isEmpty()) {
            return null;
        } else if (c.size() > 1) {
            throw new IllegalStateException
                ("too many results: " + c);
        } else {
            return (Node) c.iterator().next();
        }
    }

    private boolean isInlinable(NodeList nodes) {
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            switch (node.getNodeType()) {
            case Node.TEXT_NODE:
            case Node.ENTITY_REFERENCE_NODE:
                continue;
            default:
                return false;
            }
        }
        return true;
    }

    protected boolean isEmpty(String str) {
        return str == null || str.equals("");
    }

    protected void rename(Collection nodes, Map substitutions) {
        for (Iterator it = nodes.iterator(); it.hasNext(); ) {
            Node node = (Node) it.next();
            rename(node, substitutions);
        }
    }

    protected void rename(Node node, Map substitutions) {
        if (node == null) { return; }
        if (node.getNodeType() == Node.TEXT_NODE) {
            String value = node.getNodeValue().trim();
            if (!value.equals("")) {
                text(node.getNodeValue());
            }
            return;
        }
        String name = node.getNodeName();
        if (!substitutions.containsKey(name)) {
            return;
        }
        name = (String) substitutions.get(name);
        NodeList nodes = node.getChildNodes();
        boolean opened = false;
        if (!isEmpty(name)) {
            open(name);
            opened = true;
        }
        for (int i = 0; i < nodes.getLength(); i++) {
            rename(nodes.item(i), substitutions);
        }
        if (opened) {
            close(!isInlinable(nodes));
        }
    }

    protected static class Mapper extends LinkedHashMap {
        Mapper copy(String element) {
            return rename(element, element);
        }
        Mapper rename(String from, String to) {
            put(from, to);
            return this;
        }
        Mapper remove(String element) {
            return rename(element, "");
        }
    }

}
