/***************************************************************************
    Copyright          : (C) 2002 by Neoworks Limited. All rights reserved
    URL                : http://www.neoworks.com
 ***************************************************************************/
/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

package com.neoworks.util;

import org.apache.xerces.parsers.DOMParser;
import org.xml.sax.InputSource;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.io.StringReader;
import org.apache.log4j.Category;
import org.apache.xml.serialize.XMLSerializer;
import org.apache.xml.serialize.OutputFormat;
import java.lang.Object;
import java.lang.String;
import org.w3c.dom.Document;
import java.lang.Exception;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Element;
import org.w3c.dom.DOMException;
import org.w3c.dom.DocumentFragment;
import java.lang.Class;
import java.lang.Throwable;
import java.io.Reader;
import java.io.InputStream;
import java.lang.Comparable;
import java.io.Writer;
import java.io.OutputStream;
import org.apache.xml.serialize.BaseMarkupSerializer;
import org.w3c.dom.Attr;
import java.lang.RuntimeException;
import java.io.IOException;
import java.lang.Error;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotSupportedException;
import java.lang.ClassNotFoundException;
import java.lang.NoClassDefFoundError;
import java.lang.LinkageError;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;

/**
 * DOM Utility functions
 *
 * @author $Author: nigel $
 * @version $Revision: 1.2 $
 */
public class DOMUtils
{
	private static final Category log = Category.getInstance(DOMUtils.class.getName());
	private static final boolean logDebugEnabled = log.isDebugEnabled();
	private static final boolean logInfoEnabled = log.isInfoEnabled();

	/**
	 * Private Constructor
	 */
	private DOMUtils()
	{
	}
	
	/**
	 * Converts the passed <code>String</code> object into a DOM <code>Document</code>
	 * 
	 * @param xmldoc The string to convert to a DOM
	 * @throws Exception If the String could not be parsed into a DOM
	 * @return A DOM document
	 */
	public static Document string2DOM(String xmldoc) throws Exception
	{
		if (logDebugEnabled) log.debug("Converting string to DOM ["+xmldoc+"]");
		
		//InputSource input = new InputSource(new StringReader(xmldoc));		// Ready the String for DOM parsing
		
		Document parsedDoc = null;
		
		try
		{
			// The next block is Xerces 1 API work.  Replaced with the new 'standard' API
			/*
			DOMParser parser = new DOMParser();		// Make a new parser
			
			parser.setIncludeIgnorableWhitespace(false);				// This seems to have been removed in Xerces 2
			parser.setFeature("http://xml.org/sax/features/validation",false);						// Turn validation off for speedy bonus.
			parser.setFeature("http://apache.org/xml/features/dom/defer-node-expansion",false);		// Don't wait to be asked to parse.
			parser.parse( input );			// Try and parse the URI from the filename
			
			parsedDoc = parser.getDocument();			// Get the parsed document if you've been that lucky
			*/
			
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setValidating( false );
			factory.setIgnoringElementContentWhitespace( false );
			DocumentBuilder builder = factory.newDocumentBuilder();
			parsedDoc = builder.parse( xmldoc );
		}
		catch (Exception e)
		{
			if (logDebugEnabled) log.debug("Could not parse XML string ["+xmldoc+"] due to error ["+e.getMessage()+"]");
			throw new Exception("Could not parse XML string due to error ["+e.getMessage()+"]");
		}
		
		return parsedDoc;
	}
	
	/**
	 * Converts the passed DOM <code>Node</code> into a <code>String</code>
	 * object.  This function currently supports <code>Element</code>, 
	 * <code>Node</code>, <code>DocumentFragment</code>, <code>Document</code> 
	 * and <code>Attribute</code> types.
	 * Throws a <code>RuntimeException</code> if the Node type passed is not supported.
	 *
	 * @param node An XML Node to be converted into a string
	 * @return A String representation of the passed XML DOM
	 */
	public static String DOM2String(Node node) 
	{ 
		return DOM2String(node, false); 
	}

	/**
	 * Converts the passed DOM <code>NodeList</code> into a <code>String</code>
	 * object. This function currently supports <code>Element</code>, 
	 * <code>Node</code>, <code>DocumentFragment</code>, <code>Document</code> 
	 * and <code>Attribute</code> types.
	 * Throws a <code>RuntimeException</code> if the Node type passed is not supported.
	 *
	 * @param nodelist An XML NodeList to be converted into a string
	 * @return A String representation of the passed XML DOM
	 */
	public static String DOM2String(NodeList nodelist)
	{
		StringBuffer retval = new StringBuffer();
		retval.append("[");
		for (int x=0 ; x<nodelist.getLength() ; x++)
		{
			retval.append( DOM2String( nodelist.item(x) , true ) ).append(" , ");
		}
		retval.append("]");
		return retval.toString();
	}
	
	/**
	 * Converts the passed DOM <code>Node</code> into a <code>String</code>
	 * object.  This function currently supports <code>Element</code>, 
	 * <code>Node</code>, <code>DocumentFragment</code>, <code>Document</code> 
	 * and <code>Attribute</code> types.
	 * Throws a RuntimeException if the Node type passed is not supported
	 *
	 * @param node An XML Node to be converted into a string
	 * @param quiet If <code>true</code> suppress all XML headers and disable line wrapping
	 * @return A String representation of the passed XML DOM
	 */
	public static String DOM2String(Node node, boolean quiet)
	{
		String outEncoding = "UTF-8";
		OutputFormat outputFormat = null;
		
		// Create the appropriate OutputFormat object for the Node type
		if ( node.getNodeType() == Node.ELEMENT_NODE )
		{
			outputFormat = new OutputFormat("xml", outEncoding, true);
		}
		else if ( node.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE )
		{
			outputFormat = new OutputFormat(((DocumentFragment) node).getOwnerDocument(), outEncoding, true);
		}
		else if ( node.getNodeType() == Node.DOCUMENT_NODE )
		{
			outputFormat = new OutputFormat(((Document) node), outEncoding, true);
		}
		
		// Stop the output of XFragment being escaped so that it can be used in CDATA style unescaped sections
		//outputFormat.setNonEscapingElements( new String[] { "XFragment" } );
		
		// Set the parameters if no headers are required.
		if (quiet)
		{
			outputFormat.setPreserveSpace(true);
			outputFormat.setLineWidth(0);			// Disable linewrapping
			outputFormat.setOmitDocumentType(true);
			outputFormat.setOmitXMLDeclaration(true);
		}
		
		return DOM2String(node, outputFormat);
	}	
	
	/**
	 * Converts the passed DOM <code>Node</code> into a <code>String</code>
	 * object.  This function currently supports <code>Element</code>, 
	 * <code>Node</code>, <code>DocumentFragment</code>, <code>Document</code> 
	 * and <code>Attribute</code> types.
	 * Throws a <code>RuntimeException</code> if the Node type passed is not supported
	 *
	 * @param node An XML Node to be converted into a string
	 * @param outputFormat The OutputFormat to use to convert the <code>Node</code> to a <code>String</code>.
	 * @return A String representation of the passed XML DOM
	 */
	public static String DOM2String(Node node, OutputFormat outputFormat)
	{
		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);
		
		XMLSerializer out = new XMLSerializer(pw, outputFormat);
		try
		{
			if ( node.getNodeType() == Node.ELEMENT_NODE )
			{
				out.serialize( (Element) node );
			}
			else if ( node.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE )
			{
				out.serialize( (DocumentFragment) node );
			}
			else if ( node.getNodeType() == Node.DOCUMENT_NODE )
			{
				out.serialize( (Document) node );
			}
			else if ( node.getNodeType() == Node.ATTRIBUTE_NODE )
			{
				/* If it's an attribute we shouldn't really be handling it, but 
				   will do for debug purposes.  Write it out as valid XML to 
				   prevent things going down in a ball of flames if the output
				   is reparsed */
				return new String("<AttributeOutput>"+((Attr)node).getName()+"="+((Attr)node).getValue()+"</AttributeOutput>");
			}
			else
			{
				throw new RuntimeException("DOM2String does not support casting of NodeType");
			}
		}
		catch (java.io.IOException ioe)
		{
			log.warn("Caught exception ["+ioe.getMessage()+"] converting DOM Element to String");
		}
		
		pw.flush();
		return sw.toString();
	}
	
	/**
	 * Replace an element in the DOM with a different specified element
	 * @param oldElement The element to replace
	 * @param newElement the element to put in its place
	 * @throws DOMException on error
	 */
	public static void ReplaceElement(Element oldElement, Element newElement) throws DOMException
	{
		// Get a copy of the node owned by the target document
		Node correctNewElement = oldElement.getOwnerDocument().importNode(newElement, true);
		
		// Replace the old node with the new node
		oldElement.getParentNode().replaceChild(correctNewElement, oldElement);
	}
	
	/**
	 * Replace an element in the DOM with a list of elements
	 * @param oldElement The element to replace
	 * @param newElements The element to put in its place
	 * @throws DOMException on error
	 */
	public static void ReplaceElement(Element oldElement, NodeList newElements) throws DOMException
	{
		Node parentNode = oldElement.getParentNode();
		Document document = oldElement.getOwnerDocument();
		
		for(int i=0; i<newElements.getLength(); i++)
		{
			Node node = newElements.item(i);
			
			if (node instanceof Element)
			{
				// Import the node into the correct document
				Node correctNewElement = document.importNode(node, true);
				
				// Insert the node into the DOM
				parentNode.insertBefore(correctNewElement, oldElement);
			}
			else
			{
				log.warn("ReplaceElement passes a node that is not an element! "+node);
			}
		}
		
		parentNode.removeChild(oldElement);
	}
	
	/**
	 * Moves the nodes from a NodeList into a new DocumentFragment
	 * @param factory the factory to use to create the DocumentFragment
	 * @param nodeList The Nodes to add to the DocumentFragment
	 * @throws Exception on error
	 * @return the DocumentFactory containg the Nodes passed
	 */
	public static DocumentFragment moveNodes2DocumentFragment(Document factory, NodeList nodeList) throws Exception
	{
		DocumentFragment frag = factory.createDocumentFragment();
		
		int length = nodeList.getLength();
		
		if (length > 0)
		{
			// Move first node
			frag.appendChild(nodeList.item(0));
			
			if (nodeList.getLength() == length)
			{
				// NodeList is not 'live' iterate through the remaining elements
				for (int i=1; i<length; i++)
				{
					frag.appendChild(nodeList.item(i));
				}
			}
			else if (nodeList.getLength() == (length-1))
			{
				// NodeList is 'live', move head element untill no more elements
		
				while(nodeList.getLength() > 0)
				{
					frag.appendChild(nodeList.item(0));
				}
			}
			else
			{
				log.warn("Badly behaved NodeList. I moved only one item and the size changed from: "+length+" to: "+nodeList.getLength());
				throw new Exception("Badly behaved NodeList. I moved only one item and the size changed from: "+length+" to: "+nodeList.getLength());
			}
		}
		return frag;
	}
}
