/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.bpm.console.server.util;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.*;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.util.*;

/**
 * DOM2 utilites
 *
 * @author Thomas.Diesler@jboss.org
 * @author alessio.soldano@jboss.com
 */
public final class DOMUtils
{
   private static Log log = LogFactory.getLog(DOMUtils.class);

   // All elements created by the same thread are created by the same builder and belong to the same doc
   private static ThreadLocal<Document> documentThreadLocal = new ThreadLocal<Document>();
   private static ThreadLocal<DocumentBuilder> builderThreadLocal = new ThreadLocal<DocumentBuilder>()
   {
      protected DocumentBuilder initialValue()
      {
         try
         {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setValidating(false);
            factory.setNamespaceAware(true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            setEntityResolver(builder);
            return builder;
         }
         catch (ParserConfigurationException e)
         {
            throw new RuntimeException("Failed to create DocumentBuilder", e);
         }
      }

      private void setEntityResolver(DocumentBuilder builder)
      {
         String[] resolvers = new String[] { "org.jboss.ws.core.utils.JBossWSEntityResolver", "org.jboss.util.xml.JBossEntityResolver" };

         EntityResolver entityResolver = null;
         ClassLoader loader = Thread.currentThread().getContextClassLoader();
         for (String resolver : resolvers)
         {
            try
            {
               Class<?> resolverClass = loader.loadClass(resolver);
               entityResolver = (EntityResolver)resolverClass.newInstance();
            }
            catch (Exception ex)
            {
               log.debug("Cannot load: " + resolver);
            }
         }

         if (entityResolver != null)
            builder.setEntityResolver(entityResolver);
      }
   };

   public static void clearThreadLocals()
   {
      documentThreadLocal.remove();
      builderThreadLocal.remove();
   }

   // Hide the constructor
   private DOMUtils()
   {
   }

   /** Initialize the DocumentBuilder
    */
   public static DocumentBuilder getDocumentBuilder()
   {
      return builderThreadLocal.get();
   }

   /** Parse the given XML string and return the root Element
    */
   public static Element parse(String xmlString) throws IOException
   {
      try
      {
         return parse(new ByteArrayInputStream(xmlString.getBytes("UTF-8")));
      }
      catch (IOException e)
      {
         log.error("Cannot parse: " + xmlString);
         throw e;
      }
   }

   /** Parse the given XML stream and return the root Element
    */
   public static Element parse(InputStream xmlStream) throws IOException
   {
      try
      {
         return getDocumentBuilder().parse(xmlStream).getDocumentElement();
      }
      catch (SAXException se)
      {
         throw new IOException(se.toString());
      }
      finally
      {
         xmlStream.close();
      }
   }

   /** Parse the given input source and return the root Element
    */
   public static Element parse(InputSource source) throws IOException
   {
      try
      {
         return getDocumentBuilder().parse(source).getDocumentElement();
      }
      catch (SAXException se)
      {
         throw new IOException(se.toString());
      }
      finally
      {
         InputStream is = source.getByteStream();
         if (is != null)
         {
            is.close();
         }
         Reader r = source.getCharacterStream();
         if (r != null)
         {
            r.close();
         }
      }
   }

   /** Create an Element for a given name
    */
   public static Element createElement(String localPart)
   {
      Document doc = getOwnerDocument();
      log.trace("createElement {}" + localPart);
      return doc.createElement(localPart);
   }

   /** Create an Element for a given name and prefix
    */
   public static Element createElement(String localPart, String prefix)
   {
      Document doc = getOwnerDocument();
      log.trace("createElement {}" + prefix + ":" + localPart);
      return doc.createElement(prefix + ":" + localPart);
   }

   /** Create an Element for a given name, prefix and uri
    */
   public static Element createElement(String localPart, String prefix, String uri)
   {
      Document doc = getOwnerDocument();
      if (prefix == null || prefix.length() == 0)
      {
         log.trace("createElement {" + uri + "}" + localPart);
         return doc.createElementNS(uri, localPart);
      }
      else
      {
         log.trace("createElement {" + uri + "}" + prefix + ":" + localPart);
         return doc.createElementNS(uri, prefix + ":" + localPart);
      }
   }

   /** Create an Element for a given QName
    */
   public static Element createElement(QName qname)
   {
      return createElement(qname.getLocalPart(), qname.getPrefix(), qname.getNamespaceURI());
   }

   /** Create a org.w3c.dom.Text node
    */
   public static Text createTextNode(String value)
   {
      Document doc = getOwnerDocument();
      return doc.createTextNode(value);
   }

   /** Get the qname of the given node.
    */
   public static QName getElementQName(Element el)
   {
      String qualifiedName = el.getNodeName();
      return resolveQName(el, qualifiedName);
   }

   /** Transform the given qualified name into a QName
    */
   public static QName resolveQName(Element el, String qualifiedName)
   {
      QName qname;
      String prefix = "";
      String namespaceURI = "";
      String localPart = qualifiedName;

      int colIndex = qualifiedName.indexOf(":");
      if (colIndex > 0)
      {
         prefix = qualifiedName.substring(0, colIndex);
         localPart = qualifiedName.substring(colIndex + 1);

         if ("xmlns".equals(prefix))
         {
            namespaceURI = "URI:XML_PREDEFINED_NAMESPACE";
         }
         else
         {
            Element nsElement = el;
            while (namespaceURI.equals("") && nsElement != null)
            {
               namespaceURI = nsElement.getAttribute("xmlns:" + prefix);
               if (namespaceURI.equals(""))
                  nsElement = getParentElement(nsElement);
            }
         }

         if (namespaceURI.equals(""))
            throw new IllegalArgumentException("Cannot find namespace uri for: " + qualifiedName);
      }
      else
      {
         Element nsElement = el;
         while (namespaceURI.equals("") && nsElement != null)
         {
            namespaceURI = nsElement.getAttribute("xmlns");
            if (namespaceURI.equals(""))
               nsElement = getParentElement(nsElement);
         }
      }

      qname = new QName(namespaceURI, localPart, prefix);
      return qname;
   }

   /** Get the value from the given attribute
    *
    * @return null if the attribute value is empty or the attribute is not present
    */
   public static String getAttributeValue(Element el, String attrName)
   {
      return getAttributeValue(el, new QName(attrName));
   }

   /** Get the value from the given attribute
    *
    * @return null if the attribute value is empty or the attribute is not present
    */
   public static String getAttributeValue(Element el, QName attrName)
   {
      String attr = null;
      if ("".equals(attrName.getNamespaceURI()))
         attr = el.getAttribute(attrName.getLocalPart());
      else attr = el.getAttributeNS(attrName.getNamespaceURI(), attrName.getLocalPart());

      if ("".equals(attr))
         attr = null;

      return attr;
   }

   /** Get the qname value from the given attribute
    */
   public static QName getAttributeValueAsQName(Element el, String attrName)
   {
      return getAttributeValueAsQName(el, new QName(attrName));

   }

   /** Get the qname value from the given attribute
    */
   public static QName getAttributeValueAsQName(Element el, QName attrName)
   {
      QName qname = null;

      String qualifiedName = getAttributeValue(el, attrName);
      if (qualifiedName != null)
      {
         qname = resolveQName(el, qualifiedName);
      }

      return qname;
   }

   /** Get the boolean value from the given attribute
    */
   public static boolean getAttributeValueAsBoolean(Element el, String attrName)
   {
      return getAttributeValueAsBoolean(el, new QName(attrName));
   }

   /** Get the boolean value from the given attribute
    */
   public static boolean getAttributeValueAsBoolean(Element el, QName attrName)
   {
      String attrVal = getAttributeValue(el, attrName);
      boolean ret = "true".equalsIgnoreCase(attrVal) || "1".equalsIgnoreCase(attrVal);
      return ret;
   }

   /** Get the integer value from the given attribute
    */
   public static Integer getAttributeValueAsInteger(Element el, String attrName)
   {
      return getAttributeValueAsInteger(el, new QName(attrName));
   }

   /** Get the integer value from the given attribute
    */
   public static Integer getAttributeValueAsInteger(Element el, QName attrName)
   {
      String attrVal = getAttributeValue(el, attrName);
      return (attrVal != null ? new Integer(attrVal) : null);
   }

   /** Get the attributes as Map<QName, String>
    */
   public static Map getAttributes(Element el)
   {
      Map attmap = new HashMap();
      NamedNodeMap attribs = el.getAttributes();
      for (int i = 0; i < attribs.getLength(); i++)
      {
         Attr attr = (Attr)attribs.item(i);
         String name = attr.getName();
         QName qname = resolveQName(el, name);
         String value = attr.getNodeValue();
         attmap.put(qname, value);
      }
      return attmap;
   }

   /** Copy attributes between elements
    */
   public static void copyAttributes(Element destElement, Element srcElement)
   {
      NamedNodeMap attribs = srcElement.getAttributes();
      for (int i = 0; i < attribs.getLength(); i++)
      {
         Attr attr = (Attr)attribs.item(i);
         String uri = attr.getNamespaceURI();
         String qname = attr.getName();
         String value = attr.getNodeValue();

         // Prevent DOMException: NAMESPACE_ERR: An attempt is made to create or
         // change an object in a way which is incorrect with regard to namespaces.
         if (uri == null && qname.startsWith("xmlns"))
         {
            log.trace("Ignore attribute: [uri=" + uri + ",qname=" + qname + ",value=" + value + "]");
         }
         else
         {
            destElement.setAttributeNS(uri, qname, value);
         }
      }
   }

   /** True if the node has text child elements only
    */
   public static boolean hasTextChildNodesOnly(Node node)
   {
      NodeList nodeList = node.getChildNodes();
      if (nodeList.getLength() == 0)
         return false;

      for (int i = 0; i < nodeList.getLength(); i++)
      {
         Node acksToChildNode = nodeList.item(i);
         if (acksToChildNode.getNodeType() != Node.TEXT_NODE)
            return false;
      }

      return true;
   }

   /** True if the node has child elements
    */
   public static boolean hasChildElements(Node node)
   {
      NodeList nlist = node.getChildNodes();
      for (int i = 0; i < nlist.getLength(); i++)
      {
         Node child = nlist.item(i);
         if (child.getNodeType() == Node.ELEMENT_NODE)
            return true;
      }
      return false;
   }

   /** Gets child elements
    */
   public static Iterator<Element> getChildElements(Node node)
   {
      List<Element> list = new LinkedList<Element>();
      NodeList nlist = node.getChildNodes();
      for (int i = 0; i < nlist.getLength(); i++)
      {
         Node child = nlist.item(i);
         if (child.getNodeType() == Node.ELEMENT_NODE)
            list.add((Element)child);
      }
      return list.iterator();
   }

   /** Get the concatenated text content, or null.
    */
   public static String getTextContent(Node node)
   {
      boolean hasTextContent = false;
      StringBuffer buffer = new StringBuffer();
      NodeList nlist = node.getChildNodes();
      for (int i = 0; i < nlist.getLength(); i++)
      {
         Node child = nlist.item(i);
         if (child.getNodeType() == Node.TEXT_NODE)
         {
            buffer.append(child.getNodeValue());
            hasTextContent = true;
         }
      }
      return (hasTextContent ? buffer.toString() : null);
   }

   /** Gets the first child element
    */
   public static Element getFirstChildElement(Node node)
   {
      return getFirstChildElement(node, false);
   }

   /** Gets the first child element
    */
   public static Element getFirstChildElement(Node node, boolean recursive)
   {
      return getFirstChildElementIntern(node, null, recursive);
   }

   /** Gets the first child element for a given local name without namespace
    */
   public static Element getFirstChildElement(Node node, String nodeName)
   {
      return getFirstChildElement(node, nodeName, false);
   }

   /** Gets the first child element for a given local name without namespace
    */
   public static Element getFirstChildElement(Node node, String nodeName, boolean recursive)
   {
      return getFirstChildElementIntern(node, new QName(nodeName), recursive);
   }

   /** Gets the first child element for a given qname
    */
   public static Element getFirstChildElement(Node node, QName nodeName)
   {
      return getFirstChildElement(node, nodeName, false);
   }

   /** Gets the first child element for a given qname
    */
   public static Element getFirstChildElement(Node node, QName nodeName, boolean recursive)
   {
      return getFirstChildElementIntern(node, nodeName, recursive);
   }

   private static Element getFirstChildElementIntern(Node node, QName nodeName, boolean recursive)
   {
      Element childElement = null;
      Iterator it = getChildElementsIntern(node, nodeName, recursive);
      if (it.hasNext())
      {
         childElement = (Element)it.next();
      }
      return childElement;
   }

   /** Gets the child elements for a given local name without namespace
    */
   public static Iterator getChildElements(Node node, String nodeName)
   {
      return getChildElements(node, nodeName, false);
   }

   /** Gets the child elements for a given local name without namespace
    */
   public static Iterator getChildElements(Node node, String nodeName, boolean recursive)
   {
      return getChildElementsIntern(node, new QName(nodeName), recursive);
   }

   /** Gets the child element for a given qname
    */
   public static Iterator getChildElements(Node node, QName nodeName)
   {
      return getChildElements(node, nodeName, false);
   }

   /** Gets the child element for a given qname
    */
   public static Iterator getChildElements(Node node, QName nodeName, boolean recursive)
   {
      return getChildElementsIntern(node, nodeName, recursive);
   }

   public static List<Element> getChildElementsAsList(Node node, String nodeName)
   {
      return getChildElementsAsList(node, nodeName, false);
   }

   public static List<Element> getChildElementsAsList(Node node, String nodeName, boolean recursive)
   {
      return getChildElementsAsListIntern(node, new QName(nodeName), recursive);
   }

   public static List<Element> getChildElementsAsList(Node node, QName nodeName)
   {
      return getChildElementsAsList(node, nodeName, false);
   }

   public static List<Element> getChildElementsAsList(Node node, QName nodeName, boolean recursive)
   {
      return getChildElementsAsListIntern(node, nodeName, recursive);
   }

   private static List<Element> getChildElementsAsListIntern(Node node, QName nodeName, boolean recursive)
   {
      List<Element> list = new LinkedList<Element>();
      NodeList nlist = node.getChildNodes();
      for (int i = 0; i < nlist.getLength(); i++)
      {
         Node child = nlist.item(i);
         if (child.getNodeType() == Node.ELEMENT_NODE)
         {
            search(list, (Element)child, nodeName, recursive);
         }
      }
      return list;
   }

   private static void search(List<Element> list, Element baseElement, QName nodeName, boolean recursive)
   {
      if (nodeName == null)
      {
         list.add(baseElement);
      }
      else
      {
         QName qname;
         if (nodeName.getNamespaceURI().length() > 0)
         {
            qname = new QName(baseElement.getNamespaceURI(), baseElement.getLocalName());
         }
         else
         {
            qname = new QName(baseElement.getLocalName());
         }
         if (qname.equals(nodeName))
         {
            list.add(baseElement);
         }
      }
      if (recursive)
      {
         NodeList nlist = baseElement.getChildNodes();
         for (int i = 0; i < nlist.getLength(); i++)
         {
            Node child = nlist.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE)
            {
               search(list, (Element)child, nodeName, recursive);
            }
         }
      }
   }

   private static Iterator getChildElementsIntern(Node node, QName nodeName, boolean recursive)
   {
      return getChildElementsAsListIntern(node, nodeName, recursive).iterator();
   }

   /** Gets parent element or null if there is none
    */
   public static Element getParentElement(Node node)
   {
      Node parent = node.getParentNode();
      return (parent instanceof Element ? (Element)parent : null);
   }

   /** Get the owner document that is associated with the current thread */
   public static Document getOwnerDocument()
   {
      Document doc = documentThreadLocal.get();
      if (doc == null)
      {
         doc = getDocumentBuilder().newDocument();
         documentThreadLocal.set(doc);
      }
      return doc;
   }

   public static Element sourceToElement(Source source) throws IOException
   {
      Element retElement = null;

      try
      {
         if (source instanceof StreamSource)
         {
            StreamSource streamSource = (StreamSource)source;

            InputStream ins = streamSource.getInputStream();
            if (ins != null)
            {
               retElement = DOMUtils.parse(ins);
            }
            else
            {
               Reader reader = streamSource.getReader();
               retElement = DOMUtils.parse(new InputSource(reader));
            }
         }
         else if (source instanceof DOMSource)
         {
            DOMSource domSource = (DOMSource)source;
            Node node = domSource.getNode();
            if (node instanceof Element)
            {
               retElement = (Element)node;
            }
            else if (node instanceof Document)
            {
               retElement = ((Document)node).getDocumentElement();
            }
            else
            {
               throw new RuntimeException("Unsupported Node type: " + node.getClass().getName());
            }
         }
         else if (source instanceof SAXSource)
         {
            // The fact that JAXBSource derives from SAXSource is an implementation detail.
            // Thus in general applications are strongly discouraged from accessing methods defined on SAXSource.
            // The XMLReader object obtained by the getXMLReader method shall be used only for parsing the InputSource object returned by the getInputSource method.

            TransformerFactory tf = TransformerFactory.newInstance();
            ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
            Transformer transformer = tf.newTransformer();
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.transform(source, new StreamResult(baos));
            retElement = DOMUtils.parse(new ByteArrayInputStream(baos.toByteArray()));
         }
         else
         {
            throw new RuntimeException("Source type not implemented: " + source.getClass().getName());
         }

      }
      catch (TransformerException ex)
      {
         IOException ioex = new IOException();
         ioex.initCause(ex);
         throw ioex;
      }

      return retElement;
   }
}