/*
 * Copyright 2001-2004 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.
 */

package org.jboss.axis.utils;

import org.jboss.axis.AxisEngine;
import org.jboss.axis.Constants;
import org.jboss.axis.InternalException;
import org.jboss.axis.MessageContext;
import org.jboss.axis.components.encoding.XMLEncoder;
import org.jboss.axis.components.encoding.XMLEncoderFactory;
import org.jboss.logging.Logger;
import org.w3c.dom.Attr;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import java.util.List;


public class XMLUtils
{
   private static Logger log = Logger.getLogger(XMLUtils.class.getName());

   public static final String httpAuthCharEncoding = "ISO-8859-1";
   private static final String saxParserFactoryProperty =
           "javax.xml.parsers.SAXParserFactory";

   private static DocumentBuilderFactory dbf = getDOMFactory();
   private static SAXParserFactory saxFactory;

   private static String EMPTY = "";
   private static ByteArrayInputStream bais = new ByteArrayInputStream(EMPTY.getBytes());

   static
   {
      // Initialize SAX Parser factory defaults
      initSAXFactory(null, true, false);
   }

   /**
    * Encode a string appropriately for XML.
    *
    * @param orig the String to encode
    * @return a String in which XML special chars are repalced by entities
    */
   public static String xmlEncodeString(String orig)
   {
      XMLEncoder encoder = getXMLEncoder();
      return encoder.encode(orig);
   }

   /**
    * Get the current XMLEncoder
    *
    * @return XMLEncoder
    */
   private static XMLEncoder getXMLEncoder()
   {
      MessageContext msgContext = MessageContext.getCurrentContext();
      XMLEncoder encoder = null;
      if (msgContext == null)
      {
         encoder = XMLEncoderFactory.getDefaultEncoder();
      }
      else
      {
         String encoding = (String)msgContext.getAxisEngine().getOption(AxisEngine.PROP_XML_ENCODING);
         try
         {
            if (encoding != null)
            {
               encoder = XMLEncoderFactory.getEncoder(encoding);
            }
            else
            {
               encoder = XMLEncoderFactory.getDefaultEncoder();
            }
         }
         catch (Exception e)
         {
            log.error(Messages.getMessage("exception00"), e);
            encoder = XMLEncoderFactory.getDefaultEncoder();
         }
      }
      return encoder;
   }

   /**
    * Get the current encoding in effect
    *
    * @return string
    */
   public static String getEncoding()
   {
      XMLEncoder encoder = getXMLEncoder();
      return encoder.getEncoding();
   }

   /**
    * Initialize the SAX parser factory.
    *
    * @param factoryClassName The (optional) class name of the desired
    *                         SAXParserFactory implementation. Will be
    *                         assigned to the system property
    *                         <b>javax.xml.parsers.SAXParserFactory</b>
    *                         unless this property is already set.
    *                         If <code>null</code>, leaves current setting
    *                         alone.
    * @param namespaceAware   true if we want a namespace-aware parser
    * @param validating       true if we want a validating parser
    */
   public static void initSAXFactory(String factoryClassName,
                                     boolean namespaceAware,
                                     boolean validating)
   {
      if (factoryClassName != null)
      {
         try
         {
            saxFactory = (SAXParserFactory)Class.forName(factoryClassName).newInstance();
            /*
             * Set the system property only if it is not already set to
             * avoid corrupting environments in which Axis is embedded.
             */
            if (System.getProperty(saxParserFactoryProperty) == null)
            {
               System.setProperty(saxParserFactoryProperty, factoryClassName);
            }
         }
         catch (Exception e)
         {
            log.error(Messages.getMessage("exception00"), e);
            saxFactory = null;
         }
      }
      else
      {
         saxFactory = SAXParserFactory.newInstance();
      }
      saxFactory.setNamespaceAware(namespaceAware);
      saxFactory.setValidating(validating);
   }

   private static DocumentBuilderFactory getDOMFactory()
   {
      DocumentBuilderFactory dbf;
      try
      {
         dbf = DocumentBuilderFactory.newInstance();
         dbf.setNamespaceAware(true);
      }
      catch (Exception e)
      {
         log.error(Messages.getMessage("exception00"), e);
         dbf = null;
      }
      return (dbf);
   }

   /**
    * Get a SAX parser instance from the JAXP factory.
    *
    * @return a SAXParser instance.
    */
   public static synchronized SAXParser getSAXParser()
   {
      try
      {
         SAXParser parser = saxFactory.newSAXParser();
         XMLReader reader = parser.getXMLReader();
         // parser.getParser().setEntityResolver(new DefaultEntityResolver());
         // The above commented line and the following line are added
         // for preventing XXE (bug #14105).
         // We may need to uncomment the deprecated setting
         // in case that it is considered necessary.
         reader.setEntityResolver(new DefaultEntityResolver());
         reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
         return parser;
      }
      catch (ParserConfigurationException e)
      {
         log.error(Messages.getMessage("parserConfigurationException00"), e);
         return null;
      }
      catch (SAXException se)
      {
         log.error(Messages.getMessage("SAXException00"), se);
         return null;
      }
   }


   /**
    * Get an empty new Document
    *
    * @return Document
    * @throws ParserConfigurationException if construction problems occur
    */
   public static Document newDocument()
           throws ParserConfigurationException
   {
      synchronized (dbf)
      {
         return dbf.newDocumentBuilder().newDocument();
      }
   }

   /**
    * Get a new Document read from the input source
    *
    * @return Document
    * @throws ParserConfigurationException if construction problems occur
    * @throws SAXException                 if the document has xml sax problems
    * @throws IOException                  if i/o exceptions occur
    */
   public static Document newDocument(InputSource inp)
           throws ParserConfigurationException, SAXException, IOException
   {
      DocumentBuilder db;
      synchronized (dbf)
      {
         db = dbf.newDocumentBuilder();
      }
      db.setEntityResolver(new DefaultEntityResolver());
      db.setErrorHandler(new ParserErrorHandler());
      return (db.parse(inp));
   }

   /**
    * Get a new Document read from the input stream
    *
    * @return Document
    * @throws ParserConfigurationException if construction problems occur
    * @throws SAXException                 if the document has xml sax problems
    * @throws IOException                  if i/o exceptions occur
    */
   public static Document newDocument(InputStream inp)
           throws ParserConfigurationException, SAXException, IOException
   {
      return XMLUtils.newDocument(new InputSource(inp));
   }

   /**
    * Get a new Document read from the indicated uri
    *
    * @return Document
    * @throws ParserConfigurationException if construction problems occur
    * @throws SAXException                 if the document has xml sax problems
    * @throws IOException                  if i/o exceptions occur
    */
   public static Document newDocument(String uri)
           throws ParserConfigurationException, SAXException, IOException
   {
      // call the authenticated version as there might be
      // username/password info embeded in the uri.
      return XMLUtils.newDocument(uri, null, null);
   }

   /**
    * Create a new document from the given URI, use the username and password
    * if the URI requires authentication.
    *
    * @param uri      the resource to get
    * @param username basic auth username
    * @param password basic auth password
    * @throws ParserConfigurationException if construction problems occur
    * @throws SAXException                 if the document has xml sax problems
    * @throws IOException                  if i/o exceptions occur
    */
   public static Document newDocument(String uri, String username, String password)
           throws ParserConfigurationException, SAXException, IOException
   {
      InputSource ins = XMLUtils.getInputSourceFromURI(uri, username, password);
      Document doc = XMLUtils.newDocument(ins);
      // Close the Stream
      if (ins.getByteStream() != null)
      {
         ins.getByteStream().close();
      }
      else if (ins.getCharacterStream() != null)
      {
         ins.getCharacterStream().close();
      }
      return doc;
   }

   public static String ElementToString(Node element, boolean omitXMLDecl)
   {
      return DOM2Writer.nodeToString(element, omitXMLDecl);
   }

   /**
    * turn an element into an XML fragment
    *
    * @param element
    * @return stringified element
    */
   public static String ElementToString(Node element)
   {
      return ElementToString(element, true);
   }

   /**
    * Turn a whole DOM document into XML
    *
    * @param doc DOM document
    * @return string representation of the document, including XML declaration
    */
   public static String DocumentToString(Document doc)
   {
      return ElementToString(doc.getDocumentElement(), false);
   }

   public static String PrettyDocumentToString(Document doc)
   {
      StringWriter sw = new StringWriter();
      PrettyElementToWriter(doc.getDocumentElement(), sw);
      return sw.toString();
   }

   public static void ElementToWriter(Element element, Writer writer,
                                      boolean omitXMLDecl,
                                      boolean pretty)
   {
      DOM2Writer.serializeAsXML(element, writer, omitXMLDecl, pretty, 0);
   }

   public static void ElementToStream(Element element, OutputStream out)
   {
      Writer writer = getWriter(out);
      ElementToWriter(element, writer, true, false);
   }

   public static void PrettyElementToStream(Element element, OutputStream out)
   {
      Writer writer = getWriter(out);
      ElementToWriter(element, writer, true, true);
   }

   public static void ElementToWriter(Element element, Writer writer)
   {
      ElementToWriter(element, writer, true, false);
   }

   public static void PrettyElementToWriter(Element element, Writer writer)
   {
      ElementToWriter(element, writer, true, true);
   }

   public static void DocumentToStream(Document doc, OutputStream out)
   {
      Writer writer = getWriter(out);
      ElementToWriter(doc.getDocumentElement(), writer, false, false);
   }

   public static void PrettyDocumentToStream(Document doc, OutputStream out)
   {
      Writer writer = getWriter(out);
      ElementToWriter(doc.getDocumentElement(), writer, false, true);
   }

   private static Writer getWriter(OutputStream os)
   {
      Writer writer = null;
      try
      {
         writer = new OutputStreamWriter(os, "UTF-8");
      }
      catch (UnsupportedEncodingException uee)
      {
         log.error(Messages.getMessage("exception00"), uee);
         writer = new OutputStreamWriter(os);
      }
      return writer;
   }

   public static void DocumentToWriter(Document doc, Writer writer)
   {
      ElementToWriter(doc.getDocumentElement(), writer, false, false);
   }

   public static void PrettyDocumentToWriter(Document doc, Writer writer)
   {
      ElementToWriter(doc.getDocumentElement(), writer, false, true);
   }

   /**
    * Convert a simple string to an element with a text node
    *
    * @param namespace - element namespace
    * @param name      - element name
    * @param string    - value of the text node
    * @return element - an XML Element, null if no element was created
    */
   public static Element StringToElement(String namespace, String name, String string)
   {
      try
      {
         Document doc = XMLUtils.newDocument();
         Element element = doc.createElementNS(namespace, name);
         Text text = doc.createTextNode(string);
         element.appendChild(text);
         return element;
      }
      catch (ParserConfigurationException e)
      {
         // This should not occur
         throw new InternalException(e);
      }
   }

   /**
    * get the inner XML inside an element as a string. This is done by
    * converting the XML to its string representation, then extracting the
    * subset between beginning and end tags.
    *
    * @param element
    * @return textual body of the element, or null for no inner body
    */
   public static String getInnerXMLString(Element element)
   {
      String elementString = ElementToString(element);
      int start, end;
      start = elementString.indexOf(">") + 1;
      end = elementString.lastIndexOf("</");
      if (end > 0)
         return elementString.substring(start, end);
      else
         return null;
   }

   public static String getPrefix(String uri, Node e)
   {
      while (e != null && (e.getNodeType() == Element.ELEMENT_NODE))
      {
         NamedNodeMap attrs = e.getAttributes();
         for (int n = 0; n < attrs.getLength(); n++)
         {
            Attr a = (Attr)attrs.item(n);
            String name;
            if ((name = a.getName()).startsWith("xmlns:") &&
                    a.getNodeValue().equals(uri))
            {
               return name.substring(6);
            }
         }
         e = e.getParentNode();
      }
      return null;
   }

   /**
    * Searches for the namespace URI of the given prefix in the given DOM range.
    * <p/>
    * The namespace is not searched in parent of the "stopNode". This is
    * usefull to get all the needed namespaces when you need to ouput only a
    * subtree of a DOM document.
    *
    * @param prefix   the prefix to find
    * @param e        the starting node
    * @param stopNode null to search in all the document or a parent node where the search must stop.
    * @return null if no namespace is found, or the namespace URI.
    */
   public static String getNamespace(String prefix, Node e, Node stopNode)
   {
      while (e != null && (e.getNodeType() == Node.ELEMENT_NODE))
      {
         Attr attr = null;
         if (prefix == null)
         {
            attr = ((Element)e).getAttributeNode("xmlns");
         }
         else
         {
            attr = ((Element)e).getAttributeNodeNS(Constants.NS_URI_XMLNS,
                    prefix);
         }
         if (attr != null) return attr.getValue();
         if (e == stopNode)
            return null;
         e = e.getParentNode();
      }
      return null;
   }

   public static String getNamespace(String prefix, Node e)
   {
      return getNamespace(prefix, e, null);
   }

   /**
    * Return a QName when passed a string like "foo:bar" by mapping
    * the "foo" prefix to a namespace in the context of the given Node.
    *
    * @return a QName generated from the given string representation
    */
   public static QName getQNameFromString(String str, Node e)
   {
      return getQNameFromString(str, e, false);
   }

   /**
    * Return a QName when passed a string like "foo:bar" by mapping
    * the "foo" prefix to a namespace in the context of the given Node.
    * If default namespace is found it is returned as part of the QName.
    *
    * @return a QName generated from the given string representation
    */
   public static QName getFullQNameFromString(String str, Node e)
   {
      return getQNameFromString(str, e, true);
   }

   private static QName getQNameFromString(String str, Node e, boolean defaultNS)
   {
      if (str == null || e == null)
         return null;

      int idx = str.indexOf(':');
      if (idx > -1)
      {
         String prefix = str.substring(0, idx);
         String ns = getNamespace(prefix, e);
         if (ns == null)
         {
            log.warn("Cannot obtain namespaceURI for prefix: " + prefix);
            return null;
         }
         return new QName(ns, str.substring(idx + 1));
      }
      else
      {
         if (defaultNS)
         {
            String ns = getNamespace(null, e);
            if (ns != null)
               return new QName(ns, str);
         }
         return new QName("", str);
      }
   }

   /**
    * Return a string for a particular QName, mapping a new prefix
    * if necessary.
    */
   public static String getStringForQName(QName qname, Element e)
   {
      String uri = qname.getNamespaceURI();
      String prefix = getPrefix(uri, e);
      if (prefix == null)
      {
         int i = 1;
         prefix = "ns" + i;
         while (getNamespace(prefix, e) != null)
         {
            i++;
            prefix = "ns" + i;
         }
         e.setAttributeNS(Constants.NS_URI_XMLNS,
                 "xmlns:" + prefix, uri);
      }
      return prefix + ":" + qname.getLocalPart();
   }

   /**
    * Concat all the text and cdata node children of this elem and return
    * the resulting text.
    * (by Matt Duftler)
    *
    * @param parentEl the element whose cdata/text node values are to
    *                 be combined.
    * @return the concatanated string.
    */
   public static String getChildCharacterData(Element parentEl)
   {
      if (parentEl == null)
      {
         return null;
      }
      Node tempNode = parentEl.getFirstChild();
      StringBuffer strBuf = new StringBuffer();
      CharacterData charData;

      while (tempNode != null)
      {
         switch (tempNode.getNodeType())
         {
            case Node.TEXT_NODE:
            case Node.CDATA_SECTION_NODE:
               charData = (CharacterData)tempNode;
               strBuf.append(charData.getData());
               break;
         }
         tempNode = tempNode.getNextSibling();
      }
      return strBuf.toString();
   }

   public static class ParserErrorHandler implements ErrorHandler
   {
      private static Logger log = Logger.getLogger(ParserErrorHandler.class.getName());

      /**
       * Returns a string describing parse exception details
       */
      private String getParseExceptionInfo(SAXParseException spe)
      {
         String systemId = spe.getSystemId();
         if (systemId == null)
         {
            systemId = "null";
         }
         String info = "URI=" + systemId +
                 " Line=" + spe.getLineNumber() +
                 ": " + spe.getMessage();
         return info;
      }

      // The following methods are standard SAX ErrorHandler methods.
      // See SAX documentation for more info.

      public void warning(SAXParseException spe) throws SAXException
      {
         if (log.isDebugEnabled())
            log.debug(Messages.getMessage("warning00", getParseExceptionInfo(spe)));
      }

      public void error(SAXParseException spe) throws SAXException
      {
         String message = "Error: " + getParseExceptionInfo(spe);
         throw new SAXException(message);
      }

      public void fatalError(SAXParseException spe) throws SAXException
      {
         String message = "Fatal Error: " + getParseExceptionInfo(spe);
         throw new SAXException(message);
      }
   }


   /**
    * Utility to get the bytes uri.
    * Does NOT handle authenticated URLs,
    * use getInputSourceFromURI(uri, username, password)
    *
    * @param uri the resource to get
    * @see #getInputSourceFromURI(String uri, String username, String password)
    */
   public static InputSource getInputSourceFromURI(String uri)
   {
      return new InputSource(uri);
   }

   /**
    * Utility to get the bytes uri
    *
    * @param source the resource to get
    */
   public static InputSource sourceToInputSource(Source source)
   {
      if (source instanceof SAXSource)
      {
         return ((SAXSource)source).getInputSource();
      }
      else if (source instanceof DOMSource)
      {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         Node node = ((DOMSource)source).getNode();
         if (node instanceof Document)
         {
            node = ((Document)node).getDocumentElement();
         }
         Element domElement = (Element)node;
         ElementToStream(domElement, baos);
         InputSource isource = new InputSource(source.getSystemId());
         isource.setByteStream(new ByteArrayInputStream(baos.toByteArray()));
         return isource;
      }
      else if (source instanceof StreamSource)
      {
         StreamSource ss = (StreamSource)source;
         InputSource isource = new InputSource(ss.getSystemId());
         isource.setByteStream(ss.getInputStream());
         isource.setCharacterStream(ss.getReader());
         isource.setPublicId(ss.getPublicId());
         return isource;
      }
      else
      {
         return getInputSourceFromURI(source.getSystemId());
      }
   }

   /**
    * Utility to get the bytes at a protected uri
    * <p/>
    * This will retrieve the URL if a username and password are provided.
    * The java.net.URL class does not do Basic Authentication, so we have to
    * do it manually in this routine.
    * <p/>
    * If no username is provided, we create an InputSource from the uri
    * and let the InputSource go fetch the contents.
    *
    * @param uri      the resource to get
    * @param username basic auth username
    * @param password basic auth password
    */
   private static InputSource getInputSourceFromURI(String uri,
                                                    String username,
                                                    String password)
           throws IOException, ProtocolException, UnsupportedEncodingException
   {
      URL wsdlurl = null;
      try
      {
         wsdlurl = new URL(uri);
      }
      catch (MalformedURLException e)
      {
         // we can't process it, it might be a 'simple' foo.wsdl
         // let InputSource deal with it
         return new InputSource(uri);
      }

      // if no authentication, just let InputSource deal with it
      if (username == null && wsdlurl.getUserInfo() == null)
      {
         return new InputSource(uri);
      }

      // if this is not an HTTP{S} url, let InputSource deal with it
      if (!wsdlurl.getProtocol().startsWith("http"))
      {
         return new InputSource(uri);
      }

      URLConnection connection = wsdlurl.openConnection();
      // Does this work for https???
      if (!(connection instanceof HttpURLConnection))
      {
         // can't do http with this URL, let InputSource deal with it
         return new InputSource(uri);
      }
      HttpURLConnection uconn = (HttpURLConnection)connection;
      String userinfo = wsdlurl.getUserInfo();
      uconn.setRequestMethod("GET");
      uconn.setAllowUserInteraction(false);
      uconn.setDefaultUseCaches(false);
      uconn.setDoInput(true);
      uconn.setDoOutput(false);
      uconn.setInstanceFollowRedirects(true);
      uconn.setUseCaches(false);

      // username/password info in the URL overrides passed in values
      String auth = null;
      if (userinfo != null)
      {
         auth = userinfo;
      }
      else if (username != null)
      {
         auth = (password == null) ? username : username + ":" + password;
      }

      if (auth != null)
      {
         uconn.setRequestProperty("Authorization",
                 "Basic " +
                 base64encode(auth.getBytes(httpAuthCharEncoding)));
      }

      uconn.connect();

      return new InputSource(uconn.getInputStream());
   }

   public static final String base64encode(byte[] bytes)
   {
      return new String(Base64.encode(bytes));
   }

   public static InputSource getEmptyInputSource()
   {
      return new InputSource(bais);
   }

   /**
    * Find a Node with a given QName
    *
    * @param node parent node
    * @param name QName of the child we need to find
    * @return child node
    */
   public static Node findNode(Node node, QName name)
   {
      if (name.getNamespaceURI().equals(node.getNamespaceURI()) &&
              name.getLocalPart().equals(node.getLocalName()))
         return node;
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++)
      {
         Node ret = findNode(children.item(i), name);
         if (ret != null)
            return ret;
      }
      return null;
   }

   /**
    * Trim all new lines from text nodes.
    *
    * @param node
    */
   public static void normalize(Node node)
   {
      if (node.getNodeType() == Node.TEXT_NODE)
      {
         String data = ((Text)node).getData();
         if (data.length() > 0)
         {
            char ch = data.charAt(data.length() - 1);
            if (ch == '\n' || ch == '\r' || ch == ' ')
            {
               String data2 = trim(data);
               ((Text)node).setData(data2);
            }
         }
      }
      for (Node currentChild = node.getFirstChild(); currentChild != null; currentChild = currentChild.getNextSibling())
      {
         normalize(currentChild);
      }
   }

   public static String trim(String str)
   {
      if (str.length() == 0)
      {
         return str;
      }

      if (str.length() == 1)
      {
         if ("\r".equals(str) || "\n".equals(str))
         {
            return "";
         }
         else
         {
            return str;
         }
      }

      int lastIdx = str.length() - 1;
      char last = str.charAt(lastIdx);
      while (lastIdx > 0)
      {
         if (last != '\n' && last != '\r' && last != ' ')
            break;
         lastIdx--;
         last = str.charAt(lastIdx);
      }
      if (lastIdx == 0)
         return "";
      return str.substring(0, lastIdx);
   }

   /**
    * Converts a List with org.w3c.dom.Element objects to an Array
    * with org.w3c.dom.Element objects.
    *
    * @param list List containing org.w3c.dom.Element objects
    * @return Element[] Array with org.w3c.dom.Element objects
    */
   public static Element[] asElementArray(List list)
   {

      Element[] elements = new Element[list.size()];

      int i = 0;
      Iterator detailIter = list.iterator();
      while (detailIter.hasNext())
      {
         elements[i++] = (Element)detailIter.next();
      }

      return elements;
   }
}