/*
 * reserved comment block
 * DO NOT REMOVE OR ALTER!
 */
/*
 * Copyright 2005 The Apache Software Foundation.
 *
 *  Licensed 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.
 *
 */
/*
 * Copyright (c) 2005, 2008, Oracle and/or its affiliates. All rights reserved.
 */
/*
 * $Id: DOMUtils.java,v 1.2 2008/07/24 15:20:32 mullan Exp $
 */
package org.jcp.xml.dsig.internal.dom;

import java.util.*;
import java.security.spec.AlgorithmParameterSpec;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.crypto.*;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.spec.*;

/**
 * Useful static DOM utility methods.
 *
 * @author Sean Mullan
 */
public class DOMUtils {

    // class cannot be instantiated
    private DOMUtils() {}

    /**
     * Returns the owner document of the specified node.
     *
     * @param node the node
     * @return the owner document
     */
    public static Document getOwnerDocument(Node node) {
        if (node.getNodeType() == Node.DOCUMENT_NODE) {
            return (Document) node;
        } else {
            return node.getOwnerDocument();
        }
    }

    /**
     * Creates an element in the specified namespace, with the specified tag
     * and namespace prefix.
     *
     * @param doc the owner document
     * @param tag the tag
     * @param nsURI the namespace URI
     * @param prefix the namespace prefix
     * @return the newly created element
     */
    public static Element createElement(Document doc, String tag, String nsURI,
        String prefix) {
        String qName = (prefix == null || prefix.length() == 0)
                       ? tag : prefix + ":" + tag;
        return doc.createElementNS(nsURI, qName);
    }

    /**
     * Sets an element's attribute (using DOM level 2) with the
     * specified value and namespace prefix.
     *
     * @param elem the element to set the attribute on
     * @param name the name of the attribute
     * @param value the attribute value. If null, no attribute is set.
     */
    public static void setAttribute(Element elem, String name, String value) {
        if (value == null) return;
        elem.setAttributeNS(null, name, value);
    }

    /**
     * Sets an element's attribute (using DOM level 2) with the
     * specified value and namespace prefix AND registers the ID value with
     * the specified element. This is for resolving same-document
     * ID references.
     *
     * @param elem the element to set the attribute on
     * @param name the name of the attribute
     * @param value the attribute value. If null, no attribute is set.
     */
    public static void setAttributeID(Element elem, String name, String value) {
        if (value == null) return;
        elem.setAttributeNS(null, name, value);
        elem.setIdAttributeNS(null, name, true);
    }

    /**
     * Returns the first child element of the specified node, or null if there
     * is no such element.
     *
     * @param node the node
     * @return the first child element of the specified node, or null if there
     *    is no such element
     * @throws NullPointerException if <code>node == null</code>
     */
    public static Element getFirstChildElement(Node node) {
        Node child = node.getFirstChild();
        while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
            child = child.getNextSibling();
        }
        return (Element) child;
    }

    /**
     * Returns the last child element of the specified node, or null if there
     * is no such element.
     *
     * @param node the node
     * @return the last child element of the specified node, or null if there
     *    is no such element
     * @throws NullPointerException if <code>node == null</code>
     */
    public static Element getLastChildElement(Node node) {
        Node child = node.getLastChild();
        while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
            child = child.getPreviousSibling();
        }
        return (Element) child;
    }

    /**
     * Returns the next sibling element of the specified node, or null if there
     * is no such element.
     *
     * @param node the node
     * @return the next sibling element of the specified node, or null if there
     *    is no such element
     * @throws NullPointerException if <code>node == null</code>
     */
    public static Element getNextSiblingElement(Node node) {
        Node sibling = node.getNextSibling();
        while (sibling != null && sibling.getNodeType() != Node.ELEMENT_NODE) {
            sibling = sibling.getNextSibling();
        }
        return (Element) sibling;
    }

    /**
     * Returns the attribute value for the attribute with the specified name.
     * Returns null if there is no such attribute, or
     * the empty string if the attribute value is empty.
     *
     * <p>This works around a limitation of the DOM
     * <code>Element.getAttributeNode</code> method, which does not distinguish
     * between an unspecified attribute and an attribute with a value of
     * "" (it returns "" for both cases).
     *
     * @param elem the element containing the attribute
     * @param name the name of the attribute
     * @return the attribute value (may be null if unspecified)
     */
    public static String getAttributeValue(Element elem, String name) {
        Attr attr = elem.getAttributeNodeNS(null, name);
        return (attr == null) ? null : attr.getValue();
    }

    /**
     * Returns a Set of <code>Node</code>s, backed by the specified
     * <code>NodeList</code>.
     *
     * @param nl the NodeList
     * @return a Set of Nodes
     */
    public static Set nodeSet(NodeList nl) {
        return new NodeSet(nl);
    }

    static class NodeSet extends AbstractSet {
        private NodeList nl;
        public NodeSet(NodeList nl) {
            this.nl = nl;
        }

        public int size() { return nl.getLength(); }
        public Iterator iterator() {
            return new Iterator() {
                int index = 0;

                public void remove() {
                    throw new UnsupportedOperationException();
                }
                public Object next() {
                    if (!hasNext()) {
                        throw new NoSuchElementException();
                    }
                    return nl.item(index++);
                }
                public boolean hasNext() {
                    return index < nl.getLength() ? true : false;
                }
            };
        }
    }

    /**
     * Returns the prefix associated with the specified namespace URI
     *
     * @param context contains the namespace map
     * @param nsURI the namespace URI
     * @return the prefix associated with the specified namespace URI, or
     *    null if not set
     */
    public static String getNSPrefix(XMLCryptoContext context, String nsURI) {
        if (context != null) {
            return context.getNamespacePrefix
                (nsURI, context.getDefaultNamespacePrefix());
        } else {
            return null;
        }
    }

    /**
     * Returns the prefix associated with the XML Signature namespace URI
     *
     * @param context contains the namespace map
     * @return the prefix associated with the specified namespace URI, or
     *    null if not set
     */
    public static String getSignaturePrefix(XMLCryptoContext context) {
        return getNSPrefix(context, XMLSignature.XMLNS);
    }

    /**
     * Removes all children nodes from the specified node.
     *
     * @param node the parent node whose children are to be removed
     */
    public static void removeAllChildren(Node node) {
        NodeList children = node.getChildNodes();
        for (int i = 0, length = children.getLength(); i < length; i++) {
            node.removeChild(children.item(i));
        }
    }

    /**
     * Compares 2 nodes for equality. Implementation is not complete.
     */
    public static boolean nodesEqual(Node thisNode, Node otherNode) {
        if (thisNode == otherNode) {
            return true;
        }
        if (thisNode.getNodeType() != otherNode.getNodeType()) {
            return false;
        }
        // FIXME - test content, etc
        return true;
    }

    /**
     * Checks if child element has same owner document before
     * appending to the parent, and imports it to the parent's document
     * if necessary.
     */
    public static void appendChild(Node parent, Node child) {
        Document ownerDoc = getOwnerDocument(parent);
        if (child.getOwnerDocument() != ownerDoc) {
            parent.appendChild(ownerDoc.importNode(child, true));
        } else {
            parent.appendChild(child);
        }
    }

    public static boolean paramsEqual(AlgorithmParameterSpec spec1,
        AlgorithmParameterSpec spec2) {
        if (spec1 == spec2) {
            return true;
        }
        if (spec1 instanceof XPathFilter2ParameterSpec &&
            spec2 instanceof XPathFilter2ParameterSpec) {
            return paramsEqual((XPathFilter2ParameterSpec) spec1,
                (XPathFilter2ParameterSpec) spec2);
        }
        if (spec1 instanceof ExcC14NParameterSpec &&
            spec2 instanceof ExcC14NParameterSpec) {
            return paramsEqual((ExcC14NParameterSpec) spec1,
                (ExcC14NParameterSpec) spec2);
        }
        if (spec1 instanceof XPathFilterParameterSpec &&
            spec2 instanceof XPathFilterParameterSpec) {
            return paramsEqual((XPathFilterParameterSpec) spec1,
                (XPathFilterParameterSpec) spec2);
        }
        if (spec1 instanceof XSLTTransformParameterSpec &&
            spec2 instanceof XSLTTransformParameterSpec) {
            return paramsEqual((XSLTTransformParameterSpec) spec1,
                (XSLTTransformParameterSpec) spec2);
        }
        return false;
    }

    private static boolean paramsEqual(XPathFilter2ParameterSpec spec1,
        XPathFilter2ParameterSpec spec2) {

        List types = spec1.getXPathList();
        List otypes = spec2.getXPathList();
        int size = types.size();
        if (size != otypes.size()) {
            return false;
        }
        for (int i = 0; i < size; i++) {
            XPathType type = (XPathType) types.get(i);
            XPathType otype = (XPathType) otypes.get(i);
            if (!type.getExpression().equals(otype.getExpression()) ||
                !type.getNamespaceMap().equals(otype.getNamespaceMap()) ||
                type.getFilter() != otype.getFilter()) {
                return false;
            }
        }
        return true;
    }

    private static boolean paramsEqual(ExcC14NParameterSpec spec1,
        ExcC14NParameterSpec spec2) {
        return spec1.getPrefixList().equals(spec2.getPrefixList());
    }

    private static boolean paramsEqual(XPathFilterParameterSpec spec1,
        XPathFilterParameterSpec spec2) {
        return (spec1.getXPath().equals(spec2.getXPath()) &&
            spec1.getNamespaceMap().equals(spec2.getNamespaceMap()));
    }

    private static boolean paramsEqual(XSLTTransformParameterSpec spec1,
        XSLTTransformParameterSpec spec2) {

        XMLStructure ostylesheet = spec2.getStylesheet();
        if (!(ostylesheet instanceof javax.xml.crypto.dom.DOMStructure)) {
            return false;
        }
        Node ostylesheetElem =
            ((javax.xml.crypto.dom.DOMStructure) ostylesheet).getNode();
        XMLStructure stylesheet = spec1.getStylesheet();
        Node stylesheetElem =
            ((javax.xml.crypto.dom.DOMStructure) stylesheet).getNode();
        return nodesEqual(stylesheetElem, ostylesheetElem);
    }
}
