/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.util;

import java.io.PrintWriter;
import java.io.Writer;

import org.apache.tomcat.util.security.Escape;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * A DOM writer optimised for use by WebDAV.
 */
public class DOMWriter {

    private final PrintWriter out;


    /**
     * Construct a DOMWriter that writes to the given Writer.
     *
     * @param writer The Writer to write to
     */
    public DOMWriter(Writer writer) {
        out = new PrintWriter(writer);
    }


    /**
     * Prints the specified node, recursively.
     *
     * @param node The node to output
     */
    public void print(Node node) {

        // is there anything to do?
        if (node == null) {
            return;
        }

        int type = node.getNodeType();
        switch (type) {
            // print document
            case Node.DOCUMENT_NODE:
                print(((Document) node).getDocumentElement());
                out.flush();
                break;

            // print element with attributes
            case Node.ELEMENT_NODE:
                out.print('<');
                out.print(node.getLocalName());
                Attr[] attrs = sortAttributes(node.getAttributes());
                boolean xmlns = false;
                for (Attr attr : attrs) {
                    if ("xmlns".equals(attr.getPrefix())) {
                        // Skip namespace prefixes as they are removed
                        continue;
                    }
                    out.print(' ');
                    out.print(attr.getLocalName());
                    if ("xmlns".equals(attr.getLocalName())) {
                        xmlns = true;
                    }
                    out.print("=\"");
                    out.print(Escape.xml("", true, attr.getNodeValue()));
                    out.print('"');
                }
                if (!xmlns && node.getNamespaceURI() != null) {
                    out.print(" xmlns=\"");
                    out.print(Escape.xml(node.getNamespaceURI()));
                    out.print('"');
                }
                out.print('>');
                printChildren(node);
                break;

            // handle entity reference nodes
            case Node.ENTITY_REFERENCE_NODE:
                printChildren(node);
                break;

            // print cdata sections
            case Node.CDATA_SECTION_NODE:
                out.print(Escape.xml("", true, node.getNodeValue()));
                break;

            // print text
            case Node.TEXT_NODE:
                out.print(Escape.xml("", false, node.getNodeValue()));
                break;

            // print processing instruction
            case Node.PROCESSING_INSTRUCTION_NODE:
                out.print("<?");
                out.print(node.getLocalName());
                String piData = node.getNodeValue();
                if (piData != null && !piData.isEmpty()) {
                    out.print(' ');
                    // The only illegal sequence in PI data is ?> which would
                    // terminate the PI early. Break it with a space. PI data
                    // is opaque and must not have entity escaping applied.
                    int start = 0;
                    int end = piData.indexOf("?>");
                    while (end >= 0) {
                        out.print(piData.substring(start, end));
                        out.print("? >");
                        start = end + 2;
                        end = piData.indexOf("?>", start);
                    }
                    out.print(piData.substring(start));
                }
                out.print("?>");
                break;

            // print comment
            case Node.COMMENT_NODE:
                out.print("<!--");
                String commentValue = node.getNodeValue();
                if (commentValue != null) {
                    // The only illegal sequence in comment data is --> which
                    // would terminate the comment early. Break it with a space.
                    int start = 0;
                    int end = commentValue.indexOf("-->");
                    while (end >= 0) {
                        out.print(commentValue.substring(start, end));
                        out.print("- >");
                        start = end + 3;
                        end = commentValue.indexOf("-->", start);
                    }
                    out.print(commentValue.substring(start));
                }
                out.print("-->");
                break;

            // print document fragment (just its children)
            case Node.DOCUMENT_FRAGMENT_NODE:
                printChildren(node);
                break;

            default:
                // Unhandled node types (ENTITY_NODE, NOTATION_NODE,
                // ATTRIBUTE_NODE) are silently skipped.
                break;
        }

        if (type == Node.ELEMENT_NODE) {
            out.print("</");
            out.print(node.getLocalName());
            out.print('>');
        }

        out.flush();

    } // print(Node)


    private void printChildren(Node node) {
        NodeList children = node.getChildNodes();
        if (children != null) {
            int len = children.getLength();
            for (int i = 0; i < len; i++) {
                print(children.item(i));
            }
        }
    }


    /**
     * Returns a sorted list of attributes.
     *
     * @param attrs The map to sort
     *
     * @return a sorted attribute array
     */
    private Attr[] sortAttributes(NamedNodeMap attrs) {
        if (attrs == null) {
            return new Attr[0];
        }

        int len = attrs.getLength();
        Attr[] array = new Attr[len];
        for (int i = 0; i < len; i++) {
            array[i] = (Attr) attrs.item(i);
        }
        for (int i = 0; i < len - 1; i++) {
            String name = array[i].getLocalName();
            int index = i;
            for (int j = i + 1; j < len; j++) {
                String curName = array[j].getLocalName();
                if (curName.compareTo(name) < 0) {
                    name = curName;
                    index = j;
                }
            }
            if (index != i) {
                Attr temp = array[i];
                array[i] = array[index];
                array[index] = temp;
            }
        }

        return array;
    }
}
