/*
 * Copyright (c) 2001-2011 Convertigo SA.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 *
 * $URL$
 * $Author$
 * $Revision$
 * $Date$
 */

package com.twinsoft.convertigo.engine.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

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.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.log4j.Logger;
import org.apache.xml.resolver.Catalog;
import org.apache.xml.resolver.tools.CatalogResolver;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.DOMException;
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.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.twinsoft.convertigo.beans.common.XMLizable;
import com.twinsoft.convertigo.engine.Engine;
import com.twinsoft.convertigo.engine.EngineException;
import com.twinsoft.convertigo.engine.EnginePropertiesManager;
import com.twinsoft.convertigo.engine.EnginePropertiesManager.PropertyName;
import com.twinsoft.convertigo.engine.enums.JsonOutput.JsonRoot;
import com.twinsoft.util.StringEx;

public class XMLUtils {
	private static ThreadLocal<DocumentBuilderFactory> defaultDocumentBuilderFactory = new ThreadLocal<DocumentBuilderFactory>() {
		@Override
		protected DocumentBuilderFactory initialValue() {
			DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			try {
				String s = EnginePropertiesManager.getProperty(PropertyName.DOCUMENT_NAMESPACE_AWARE);
				if (s.equalsIgnoreCase("true"))
					documentBuilderFactory.setNamespaceAware(true);
			}
			catch (IllegalStateException e) {
				documentBuilderFactory.setNamespaceAware(true);
			}
			return documentBuilderFactory;
		}
	};

	private static ThreadLocal<DocumentBuilder> defaultDocumentBuilder = new ThreadLocal<DocumentBuilder>() {
		@Override
		protected DocumentBuilder initialValue() {
			try {
				return defaultDocumentBuilderFactory.get().newDocumentBuilder();
			} catch (ParserConfigurationException e) {
				e.printStackTrace();
				return null;
			}
		}
	};

	public static DocumentBuilder getDefaultDocumentBuilder() {
		return defaultDocumentBuilder.get();
	}
	
	private static ThreadLocal<TransformerFactory> defaultTransformerFactory = new ThreadLocal<TransformerFactory>() {
		@Override
		protected TransformerFactory initialValue() {
			return TransformerFactory.newInstance();
		}
	};

	public static Transformer getNewTransformer() throws TransformerConfigurationException {
		return defaultTransformerFactory.get().newTransformer();
	}
	
	public static Transformer getNewTransformer(Source source) throws TransformerConfigurationException {
		return defaultTransformerFactory.get().newTransformer(source);
	}
	
	private static ThreadLocal<SAXParser> defaultSAXParser = new ThreadLocal<SAXParser>() {
		@Override
		protected SAXParser initialValue() {
			try {
				return SAXParserFactory.newInstance().newSAXParser();
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		}
	};
	
	public static void saxParse(File file, DefaultHandler defaultHandler) throws SAXException, IOException {
		defaultSAXParser.get().parse(file, defaultHandler);
	}
	
	public static void saxParse(InputStream inputStream, DefaultHandler defaultHandler) throws SAXException, IOException {
		defaultSAXParser.get().parse(inputStream, defaultHandler);
	}
	
	public static String simplePrettyPrintDOM(String sDocument) {
		StringEx sxDocument = new StringEx(sDocument);
		sxDocument.replaceAll(">", ">\n");
		return sxDocument.toString();
	}

	public static String prettyPrintDOM(String sDocument) throws ParserConfigurationException, SAXException,
			IOException {
		Document document = getDefaultDocumentBuilder().parse(new InputSource(new StringReader(sDocument)));
		return XMLUtils.prettyPrintDOM(document);
	}

	public static String prettyPrintDOM(String sDocument, String relativeUriResolver)
			throws ParserConfigurationException, SAXException, IOException {
		InputSource inputSource = new InputSource(new StringReader(sDocument));
		inputSource.setSystemId(relativeUriResolver);
		Document document = getDefaultDocumentBuilder().parse(inputSource);
		return XMLUtils.prettyPrintDOM(document);
	}

	public static String prettyPrintDOM(Node node) {
		return XMLUtils.prettyPrintDOM(node.getOwnerDocument());
	}

	public static String prettyPrintDOMWithEncoding(Document doc) {
		return prettyPrintDOMWithEncoding(doc, "ISO-8859-1");
	}

	public static String prettyPrintDOMWithEncoding(Document doc, String defaultEncoding) {
		StringWriter writer = new StringWriter();
		prettyPrintDOMWithEncoding(doc, defaultEncoding, writer);
		return writer.getBuffer().toString();
	}

	public static String prettyPrintDOMWithEncoding(String sDocument, String encoding)
			throws ParserConfigurationException, SAXException, IOException {
		Document document = getDefaultDocumentBuilder().parse(new InputSource(new StringReader(sDocument)));
		return XMLUtils.prettyPrintDOMWithEncoding(document, encoding);
	}

	public static void prettyPrintDOM(Document doc, Writer writer) {
		prettyPrintDOMWithEncoding(doc, "UTF-8", writer);
	}

	public static void prettyPrintDOM(Document doc, String defaultEncoding, Writer writer) {
		prettyPrintDOMWithEncoding(doc, defaultEncoding, writer);
	}
	
	public static void prettyPrintDOMWithEncoding(Document doc, String defaultEncoding, Writer writer) {
		prettyPrintDOMWithEncoding(doc, defaultEncoding, new StreamResult(writer));
	}
	
	public static void prettyPrintDOMWithEncoding(Document doc, String defaultEncoding, OutputStream outputStream) {
		prettyPrintDOMWithEncoding(doc, defaultEncoding, new StreamResult(outputStream));
	}
	
	public static void prettyPrintDOMWithEncoding(Document doc, String defaultEncoding, Result result) {
		Node firstChild = doc.getFirstChild();
		boolean omitXMLDeclaration = false;
		String encoding = defaultEncoding; // default Encoding char set if non
											// is found in the PI

		if ((firstChild.getNodeType() == Document.PROCESSING_INSTRUCTION_NODE)
				&& (firstChild.getNodeName().equals("xml"))) {
			omitXMLDeclaration = true;
			String piValue = firstChild.getNodeValue();
			// extract from PI the encoding Char Set
			int encodingOffset = piValue.indexOf("encoding=\"");
			if (encodingOffset != -1) {
				encoding = piValue.substring(encodingOffset + 10);
				// remove the last "
				encoding = encoding.substring(0, encoding.length() - 1);
			}
		}

		try {
			Transformer t = getNewTransformer();
			t.setOutputProperty(OutputKeys.ENCODING, encoding);
			t.setOutputProperty(OutputKeys.INDENT, "yes");
			t.setOutputProperty(OutputKeys.METHOD, "xml"); // xml, html, text
			t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
			t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, omitXMLDeclaration ? "yes" : "no");
			t.transform(new DOMSource(doc), result);
		} catch (Exception e) {
			Engine.logEngine.error("Unexpected exception while pretty print DOM", e);
		}
	}

	public static String prettyPrintDOM(Document doc) {
		return prettyPrintDOMWithEncoding(doc, "ISO-8859-1");
	}
	
	public static String prettyPrintElement(Element elt) {
		return prettyPrintElement(elt, true, true);
	}
	
	public static String prettyPrintElement(Element elt, boolean omitXMLDeclaration, boolean bIndent) {
		Node firstChild = elt;
		String encoding = "ISO-8859-1"; // default Encoding char set if non is
										// found in the PI

		if (omitXMLDeclaration && (firstChild.getNodeType() == Document.PROCESSING_INSTRUCTION_NODE)
				&& (firstChild.getNodeName().equals("xml"))) {
			String piValue = firstChild.getNodeValue();
			// extract from PI the encoding Char Set
			int encodingOffset = piValue.indexOf("encoding=\"");
			if (encodingOffset != -1) {
				encoding = piValue.substring(encodingOffset + 10);
				// remove the last "
				encoding = encoding.substring(0, encoding.length() - 1);
			}
		}
		StringWriter strWtr = new StringWriter();
		try {
			Transformer t = getNewTransformer();
			t.setOutputProperty(OutputKeys.ENCODING, encoding);
			t.setOutputProperty(OutputKeys.INDENT, bIndent ? "yes" : "no");
			t.setOutputProperty(OutputKeys.METHOD, "xml"); // xml, html, text
			t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
			t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, omitXMLDeclaration ? "yes" : "no");
			t.transform(new DOMSource(elt), new StreamResult(strWtr));
			return strWtr.getBuffer().toString();
		} catch (Exception e) {
			System.err.println("XML.toString(Document): " + e);
			Engine.logEngine.error("Unexpected exception", e);
			return e.getMessage();
		}
	}

	/*
	 * public static String prettyPrintElement(Element elt) { Node firstChild =
	 * elt; boolean omitXMLDeclaration = false; if ( (firstChild.getNodeType()
	 * == Document.PROCESSING_INSTRUCTION_NODE) &&
	 * (firstChild.getNodeName().equals("xml"))) omitXMLDeclaration = true;
	 * 
	 * OutputFormat format = new OutputFormat("XML", "ISO-8859-1", true);
	 * format.setLineWidth(0); format.setIndent(4);
	 * 
	 * format.setNonEscapingElements(new String[] {"SCRIPT", "script"});
	 * 
	 * format.setOmitDocumentType(true);
	 * format.setOmitXMLDeclaration(omitXMLDeclaration); StringWriter sw = new
	 * StringWriter(); XMLSerializer serializer = new XMLSerializer(sw, format);
	 * 
	 * try { serializer.serialize(elt); return sw.toString(); }
	 * catch(IOException e) { Engine.logEngine.error("Unexpected exception", e);
	 * return e.getMessage(); } }
	 * 
	 * public static String prettyPrintElement(Element elt, boolean
	 * omitXMLDeclaration, int indent) { OutputFormat format = new
	 * OutputFormat("XML", "ISO-8859-1", true); format.setLineWidth(0);
	 * format.setIndent(indent);
	 * 
	 * format.setNonEscapingElements(new String[] {"SCRIPT", "script"});
	 * 
	 * format.setOmitDocumentType(true);
	 * format.setOmitXMLDeclaration(omitXMLDeclaration); StringWriter sw = new
	 * StringWriter(); XMLSerializer serializer = new XMLSerializer(sw, format);
	 * 
	 * try { serializer.serialize(elt); return sw.toString(); }
	 * catch(IOException e) { Engine.logEngine.error("Unexpected exception", e);
	 * return e.getMessage(); } }
	 */
	public static Node writeObjectToXml(Document document, Object object) throws Exception {
		return writeObjectToXml(document, object, null);
	}

	public static Node writeObjectToXml(Document document, Object object, Object compiledValue)
			throws Exception {
		if (object == null)
			return null;
		
		if (object instanceof Enum) {
			object = ((Enum<?>) object).name();
		}
		
		// Simple objects
		if ((object instanceof Boolean) || (object instanceof Integer) || (object instanceof Double)
				|| (object instanceof Float) || (object instanceof Character) || (object instanceof Long)
				|| (object instanceof Short) || (object instanceof Byte)) {
			Element element = document.createElement(object.getClass().getName());
			element.setAttribute("value", object.toString());
			if (compiledValue != null)
				element.setAttribute("compiledValue", compiledValue.toString());
			return element;
		}
		// Strings
		else if (object instanceof String) {
			Element element = document.createElement(object.getClass().getName());
			element.setAttribute("value", object.toString());
			if (compiledValue != null) {
				element.setAttribute("compiledValueClass", compiledValue.getClass().getCanonicalName());
				element.setAttribute("compiledValue", compiledValue.toString());
			}
			return element;
		}
		// Arrays
		else if (object.getClass().getName().startsWith("[")) {
			String arrayClassName = object.getClass().getName();
			int i = arrayClassName.lastIndexOf('[');

			String itemClassName = arrayClassName.substring(i + 1);
			char c = itemClassName.charAt(itemClassName.length() - 1);
			switch (c) {
			case ';':
				itemClassName = itemClassName.substring(1, itemClassName.length() - 1);
				break;
			case 'B':
				itemClassName = "byte";
				break;
			case 'C':
				itemClassName = "char";
				break;
			case 'D':
				itemClassName = "double";
				break;
			case 'F':
				itemClassName = "float";
				break;
			case 'I':
				itemClassName = "int";
				break;
			case 'J':
				itemClassName = "long";
				break;
			case 'S':
				itemClassName = "short";
				break;
			case 'Z':
				itemClassName = "boolean";
				break;
			}

			Element element = document.createElement("array");
			element.setAttribute("classname", itemClassName);

			int len = Array.getLength(object);
			element.setAttribute("length", Integer.toString(len));

			Node subNode;
			for (int k = 0; k < len; k++) {
				subNode = writeObjectToXml(document, Array.get(object, k));
				if (subNode != null) {
					element.appendChild(subNode);
				}
			}

			return element;
		}
		// XMLization
		else if (object instanceof XMLizable) {
			Element element = document.createElement("xmlizable");
			element.setAttribute("classname", object.getClass().getName());
			element.appendChild(((XMLizable) object).writeXml(document));
			return element;
		}
		// Default serialization
		else {
			Element element = document.createElement("serializable");
			element.setAttribute("classname", object.getClass().getName());

			String objectBytesEncoded = null;
			byte[] objectBytes = null;

			// We write the object to a bytes array
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
			ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
			objectOutputStream.writeObject(object);
			objectOutputStream.flush();
			outputStream.close();

			// Now, we retrieve the object bytes
			objectBytes = outputStream.toByteArray();
			objectBytesEncoded = org.apache.commons.codec.binary.Base64.encodeBase64String(objectBytes);

			CDATASection cDATASection = document.createCDATASection(new String(objectBytesEncoded));
			element.appendChild(cDATASection);

			return element;
		}
	}

	public static Object readObjectFromXml(Element node) throws Exception {
		String nodeName = node.getNodeName();
		String nodeValue = ((Element) node).getAttribute("value");

		if (nodeName.equals("java.lang.Boolean")) {
			return new Boolean(nodeValue);
		} else if (nodeName.equals("java.lang.Byte")) {
			return new Byte(nodeValue);
		} else if (nodeName.equals("java.lang.Character")) {
			return new Character(nodeValue.charAt(0));
		} else if (nodeName.equals("java.lang.Integer")) {
			return new Integer(nodeValue);
		} else if (nodeName.equals("java.lang.Double")) {
			return new Double(nodeValue);
		} else if (nodeName.equals("java.lang.Float")) {
			return new Float(nodeValue);
		} else if (nodeName.equals("java.lang.Long")) {
			return new Long(nodeValue);
		} else if (nodeName.equals("java.lang.Short")) {
			return new Short(nodeValue);
		} else if (nodeName.equals("java.lang.String")) {
			return nodeValue;
		} else if (nodeName.equals("array")) {
			String className = node.getAttribute("classname");
			String length = node.getAttribute("length");
			int len = (new Integer(length)).intValue();

			Object array;
			if (className.equals("byte")) {
				array = new byte[len];
			} else if (className.equals("boolean")) {
				array = new boolean[len];
			} else if (className.equals("char")) {
				array = new char[len];
			} else if (className.equals("double")) {
				array = new double[len];
			} else if (className.equals("float")) {
				array = new float[len];
			} else if (className.equals("int")) {
				array = new int[len];
			} else if (className.equals("long")) {
				array = new long[len];
			} else if (className.equals("short")) {
				array = new short[len];
			} else {
				array = Array.newInstance(Class.forName(className), len);
			}

			Node xmlNode = null;
			NodeList nl = node.getChildNodes();
			int len_nl = nl.getLength();
			int i = 0;
			for (int j = 0; j < len_nl; j++) {
				xmlNode = nl.item(j);
				if (xmlNode.getNodeType() == Node.ELEMENT_NODE) {
					Object o = XMLUtils.readObjectFromXml((Element) xmlNode);
					Array.set(array, i, o);
					i++;
				}
			}

			return array;
		}
		// XMLization
		else if (nodeName.equals("xmlizable")) {
			String className = node.getAttribute("classname");

			Node xmlNode = findChildNode(node, Node.ELEMENT_NODE);
			Object xmlizable = Class.forName(className).newInstance();
			((XMLizable) xmlizable).readXml(xmlNode);

			return xmlizable;
		}
		// Serialization
		else if (nodeName.equals("serializable")) {
			Node cdata = findChildNode(node, Node.CDATA_SECTION_NODE);
			String objectSerializationData = cdata.getNodeValue();
			Engine.logEngine.debug("Object serialization data:\n" + objectSerializationData);
			byte[] objectBytes = org.apache.commons.codec.binary.Base64.decodeBase64(objectSerializationData);

			// We read the object to a bytes array
			ByteArrayInputStream inputStream = new ByteArrayInputStream(objectBytes);
			ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
			Object object = objectInputStream.readObject();
			inputStream.close();

			return object;
		}

		return null;
	}

	public static Node findChildNode(Node parentNode, int type) {
		Node foundNode = null;
		NodeList nl = parentNode.getChildNodes();
		int len_nl = nl.getLength();
		for (int j = 0; j < len_nl; j++) {
			foundNode = nl.item(j);
			if (foundNode.getNodeType() == type)
				return foundNode;
		}

		return null;
	}

	public static Element findNodeByAttributeValue(NodeList nodeList, String attributeName, String attributeValue) {
		int len = nodeList.getLength();
		String tmp;
		Element property;
		for (int i = 0; i < len; i++) {
			property = (Element) nodeList.item(i);
			tmp = property.getAttribute(attributeName);
			if (attributeValue.equals(tmp)) {
				return property;
			}
		}
		return null;
	}

	public static Object findPropertyValue(Element databaseObjectNode, String propertyName) throws Exception {
		NodeList properties = databaseObjectNode.getElementsByTagName("property");

		Element projectNameElement = XMLUtils.findNodeByAttributeValue(properties, "name", propertyName);

		Node xmlNode = null;
		NodeList nl = projectNameElement.getChildNodes();
		int len_nl = nl.getLength();
		for (int j = 0; j < len_nl; j++) {
			xmlNode = nl.item(j);
			if (xmlNode.getNodeType() == Node.ELEMENT_NODE) {
				return XMLUtils.readObjectFromXml((Element) xmlNode);
			}
		}

		throw new EngineException("No such property ('" + propertyName + "')");
	}

	public static String findXslHref(Node node) {
		int type = node.getNodeType();

		if (type == Node.PROCESSING_INSTRUCTION_NODE) {
			String nodeName = node.getNodeName();
			if (nodeName.equalsIgnoreCase("xml-stylesheet")) {
				String nodeValue = node.getNodeValue();
				try {
					int i = nodeValue.indexOf("href=\"");
					int j = nodeValue.indexOf("\"", i + 6);
					String result = nodeValue.substring(i + 6, j);
					return result;
				} catch (StringIndexOutOfBoundsException e) {
					return null;
				}
			}
			return null;
		} else {
			NodeList children = node.getChildNodes();
			int len = children.getLength();
			for (int i = 0; i < len; i++) {
				String result = findXslHref(children.item(i));
				if (result != null)
					return result;
			}
		}
		return null;
	}

	public static void copyDocument(Document from, Document to) {
		Node node = to.importNode(from.getDocumentElement(), true);
		to.getDocumentElement().appendChild(node);
	}

	public static Element findSingleElement(Element parent, String fullXPath) {
		Element elt = null;
		if (parent != null) {
			if (parent.hasChildNodes()) {
				if (fullXPath.startsWith("/"))
					fullXPath = fullXPath.substring(1);
				int index = fullXPath.indexOf("/");
				String childName = ((index != -1) ? fullXPath.substring(0, index) : fullXPath);

				NodeList list = parent.getChildNodes();
				for (int i = 0; i < list.getLength(); i++) {
					Node child = list.item(i);
					if (child.getNodeType() == Node.ELEMENT_NODE) {
						if (child.getNodeName().equalsIgnoreCase(childName)) {
							if (index == -1) {
								elt = (Element) child;
								break;
							} else {
								fullXPath = fullXPath.substring(index + 1);
								elt = findSingleElement((Element) child, fullXPath);
							}
						}
					}
				}
			}
		}
		return elt;
	}

	public static NodeList findElements(Element parent, String fullXPath) {
		NodeList list = null;
		Element elt = findSingleElement(parent, fullXPath);
		if (elt != null)
			list = ((Element) elt.getParentNode()).getElementsByTagName(elt.getNodeName());
		return list;
	}

	public static String getXPath(Element elt) {
		String xpath = "";
		if (elt != null) {
			Document doc = elt.getOwnerDocument();
			Element root = doc.getDocumentElement();
			Element parent = elt;
			while (parent != root) {
				xpath = "/" + parent.getNodeName() + xpath;
				parent = (Element) parent.getParentNode();
			}
		}
		return xpath;
	}

	public static String calcXpath(Node node) {
		return calcXpath(node, null);
	}

	/**
	 * Compute the xpath of a node relative to an anchor.
	 * 
	 * @param node
	 *            node to find the xpath from.
	 * @param anchor
	 *            the relative point to fid from.
	 * @return the computed xpath.
	 */
	public static String calcXpath(Node node, Node anchor) {
		String xpath = "";
		Node current = null;

		if (node == null || node.equals(anchor))
			return "";

		// add attribute to xpath
		if (node instanceof Attr) {
			Attr attr = (Attr) node;
			node = attr.getOwnerElement();
			xpath = '@' + attr.getName() + '/';
		}

		while ((current = node.getParentNode()) != anchor) {
			Engine.logEngine.trace("Calc Xpath : current node : " + current.getNodeName());
			NodeList childs = current.getChildNodes();
			int index = 0;
			for (int i = 0; i < childs.getLength(); i++) {
				if (childs.item(i).getNodeType() != Node.ELEMENT_NODE
						&& !childs.item(i).getNodeName().equalsIgnoreCase("#text"))
					continue;

				Engine.logEngine.trace("Calc Xpath : ==== > Child node : " + childs.item(i).getNodeName());

				// Bump the index if we have the same tag names..
				if (childs.item(i).getNodeName().equalsIgnoreCase(node.getNodeName())) {
					// tag names are equal ==> bump the index.
					index++;
					// is our node the one that is listed ?
					if (childs.item(i).equals(node))
						// We found our node in the parent node list
						break;
				}
			}
			// count the number of elements having the same tag
			int nbElements = 0;
			for (int i = 0; i < childs.getLength(); i++) {
				if (childs.item(i).getNodeName().equalsIgnoreCase(node.getNodeName())) {
					nbElements++;
				}
			}

			String name = node.getNodeName();
			if (name.equalsIgnoreCase("#text"))
				name = "text()";
			name = xpathEscapeColon(name);

			if (nbElements > 1) {
				xpath = name + "[" + index + "]/" + xpath;
			} else {
				// only one element had the same tag ==> do not compute the [xx]
				// syntax..
				xpath = name + "/" + xpath;
			}
			node = current;
		}
		if (xpath.length() > 0)
			// remove the trailing '/'
			xpath = xpath.substring(0, xpath.length() - 1);

		return xpath;
	}

	public static String xpathEscapeColon(String nameToEscape) {
		if (nameToEscape.contains(":"))
			return "*[name()=\"" + nameToEscape + "\"]";
		return nameToEscape;
	}
	
	public static String xpathGenerateConcat(String queryString) {
		String returnString = "";
		String searchString = queryString;
	    
	    int quotePosition = (searchString.indexOf("'") != -1)?searchString.indexOf("'"):searchString.indexOf("\"");
	    if (quotePosition == -1) {
	    	returnString = "'" + searchString + "'";
	    } else {
	        returnString = "concat(";
	        while (quotePosition != -1)
	        {
	            String subString = searchString.substring(0, quotePosition);
	            returnString += "'" + subString + "', ";
	            if (("'").equals(searchString.substring(quotePosition, quotePosition + 1)))
	            {
	                returnString += "\"'\", ";
	            }
	            else
	            {
	                //must be a double quote
	                returnString += "'\"', ";
	            }
	            searchString = searchString.substring(quotePosition + 1,searchString.length());
	            quotePosition = (searchString.indexOf("'") != -1)?searchString.indexOf("'"):searchString.indexOf("\"");
	        }
	        returnString += "'" + searchString + "')";
	    }
	    return returnString;
	}

	public static Document loadXml(File file) throws ParserConfigurationException, SAXException, IOException {
		Document document = getDefaultDocumentBuilder().parse(file);
		return document;
	}
	
	public static Document loadXml(String filePath) throws ParserConfigurationException, SAXException, IOException {
		return loadXml(new File(filePath));
	}

	public static Document createDom() throws ParserConfigurationException {
		Document document = getDefaultDocumentBuilder().newDocument();
		return document;
	}

	public static Document createDom(String xmlEngine) throws ParserConfigurationException {
		return createDom();
	}

	public static Document parseDOM(String xmlEngine, String xml) throws ParserConfigurationException,
			SAXException, IOException {
		Document document = getDefaultDocumentBuilder().parse(new InputSource(new StringReader(xml)));
		return document;
	}

	public static void saveXml(Document dom, String filePath) throws IOException {
		saveXml(dom, filePath, false);
	}
	
	public static void saveXml(Document dom, String filePath, boolean omitXmlDeclaration) throws IOException {
		saveXml(dom, new File(filePath), omitXmlDeclaration);
	}

	public static void saveXml(Document dom, File file) throws IOException {
		saveXml(dom, file, false);
	}
	
	public static void saveXml(Document dom, File file, boolean omitXmlDeclaration) throws IOException {
		try {
			Transformer transformer = getNewTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			transformer.setOutputProperty(OutputKeys.METHOD, "xml");
			transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
			transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, omitXmlDeclaration ? "yes" : "no");
			transformer.transform(new DOMSource(dom.getDocumentElement()), new StreamResult(file.toURI().getPath()));
		} catch (Exception e) {
			throw new IOException("saveXml failed because : " + e.getMessage());
		}
	}

	public static String getNormalizedText(Node node) {
		String res = "";
		if (node.hasChildNodes()) {
			NodeList nl = node.getChildNodes();
			for (int i = 0; i < nl.getLength(); i++) {
				if (nl.item(i).getNodeType() == Node.TEXT_NODE) {
					res += nl.item(i).getNodeValue();
				} else {
					// ignore <SCRIPT> nodes ...
					if (!nl.item(i).getNodeName().equalsIgnoreCase("script"))
						res += getNormalizedText(nl.item(i));
				}
			}
		}
		return res;
	}

	public static void removeChilds(Node node) {
		NodeList nl = node.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++)
			node.removeChild(nl.item(i));
	}

	public static void removeNode(Node node) {
		node.getParentNode().removeChild(node);
	}

	public static void removeNodeListContent(NodeList nodeList) {
		while (nodeList.getLength() > 0) {
			removeNode(nodeList.item(0));
		}
	}

	static public Document parseDOM(InputStream is) throws SAXException, IOException, EngineException {
		Document doc = getDefaultDocumentBuilder().parse(is);
		doc.normalizeDocument();
		return doc;

	}

	static public Document parseDOM(File file) throws SAXException, IOException, EngineException {
		InputStream is = null;
		try {
			return parseDOM(is = new FileInputStream(file));
		} finally {
			try {
				is.close();
			} catch (Exception e) {}
		}
	}

	static public Document parseDOM(String filename) throws SAXException, IOException, EngineException {
		return parseDOM(new File(filename));
	}

	static public Document parseDOMFromString(String sDom) throws SAXException, IOException {
		Document dom = getDefaultDocumentBuilder().parse(new InputSource(new StringReader(sDom)));
		return dom;
	}

	public static EntityResolver getEntityResolver() {
		return new EntityResolver() {
			public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
				if ("-//W3C//ENTITIES Latin 1 for XHTML//EN".equals(publicId))
					return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml-lat1.ent"));
				if ("-//W3C//ENTITIES Special for XHTML//EN".equals(publicId))
					return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml-special.ent"));
				if ("-//W3C//ENTITIES Symbols for XHTML//EN".equals(publicId))
					return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml-symbol.ent"));
				if ("-//W3C//DTD XHTML 1.0 Strict//EN".equals(publicId))
					return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml1-strict.dtd"));
				if ("-//W3C//DTD XHTML 1.0 Transitional//EN".equals(publicId))
					return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml1-transitional.dtd"));
				return new InputSource(new FileInputStream(systemId));
			}
		};
	}
	
	public static CatalogResolver getCatalogResolver() {
		return new CatalogResolver() {

			@Override
			public Catalog getCatalog() {
				return super.getCatalog();
			}

			@Override
			public String getResolvedEntity(String publicId, String systemId) {
				return super.getResolvedEntity(publicId, systemId);
			}

			@Override
			public Source resolve(String href, String base) throws TransformerException {
				return super.resolve(href, base);
			}

			@Override
			public InputSource resolveEntity(String publicId, String systemId) {
				try {
					if ("-//W3C//ENTITIES Latin 1 for XHTML//EN".equals(publicId))
						return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml-lat1.ent"));
					if ("-//W3C//ENTITIES Special for XHTML//EN".equals(publicId))
						return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml-special.ent"));
					if ("-//W3C//ENTITIES Symbols for XHTML//EN".equals(publicId))
						return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml-symbol.ent"));
					if ("-//W3C//DTD XHTML 1.0 Strict//EN".equals(publicId))
						return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml1-strict.dtd"));
					if ("-//W3C//DTD XHTML 1.0 Transitional//EN".equals(publicId))
						return new InputSource(new FileInputStream(Engine.DTD_PATH + "/xhtml1-transitional.dtd"));
				}
				catch (FileNotFoundException e) {}
				return super.resolveEntity(publicId, systemId);
			}
        };
	}
	
	public static String getCDataText(String s) {
		String cdataText = "";
		try {
			if (!s.equals("")) {
				Document dom = createDom("java");
				Element root = dom.createElement("root");
				CDATASection cDATASection = dom.createCDATASection(s);
				root.appendChild(cDATASection);
				dom.appendChild(root);

				cdataText = prettyPrintElement(root, true, true);
				cdataText = cdataText.replaceAll("<root>", "");
				cdataText = cdataText.replaceAll("</root>", "");

				String cdataStart = "<![CDATA[";
				if (cdataText.startsWith(cdataStart)) {
					int i = cdataText.substring(cdataStart.length()).indexOf(cdataStart);
					if (i < 0) {
						cdataText = cdataText.replaceAll("<!\\[CDATA\\[", "");
						cdataText = cdataText.replaceAll("\\]\\]>", "");
					}
				}
			}
		} catch (ParserConfigurationException e) {
		}
		return cdataText;
	}

	public static String getCDataXml(String s) {
		return StringEscapeUtils.escapeXml(getCDataText(s));
	}

	public static int MAX_XML_SIZE_FOR_LOG_INFO = 5;

	public static void logXml(Document document, Logger log, String message) {
		if (document != null && log.isInfoEnabled()) {
			String xml = XMLUtils.prettyPrintDOM(document);

			if (xml.length() > XMLUtils.MAX_XML_SIZE_FOR_LOG_INFO * 1000) {
				if (!log.isDebugEnabled()) {
					log.info(message
							+ "\n[XML size is > " + XMLUtils.MAX_XML_SIZE_FOR_LOG_INFO
							+ "KB, enable DEBUG log level for this logger to see it completly!]\n"
							+ "[Extract limited to the first " + XMLUtils.MAX_XML_SIZE_FOR_LOG_INFO + "KB]\n"
							+ xml.substring(0, XMLUtils.MAX_XML_SIZE_FOR_LOG_INFO * 1000)
							+ "... (see the complete message in DEBUG log level)");
				}
				log.debug(message + ":\n" + xml);
			} else {
				log.info(message + ":\n" + xml);
			}
		}
	}
	
	public static Node[] toNodeArray(NodeList nl) {
		Node[] res = new Node[nl.getLength()];
		for (int i = 0 ; i < res.length ; i++) {
			res[i] = nl.item(i);
		}
		return res;
	}
	
	public static List<Node> toArrayList(NodeList nl) {
		List<Node> res = new ArrayList<Node>();
		for (int i = 0 ; i < nl.getLength() ; i++) {
			res.add(nl.item(i));
		}
		return res;
	}
	
	public static NodeList toNodeList(List<Node> nl) {	
		Document doc = getDefaultDocumentBuilder().newDocument();
		Element root = doc.createElement("root");
		for (Node n : nl) {
			root.appendChild(doc.adoptNode(n));
		}
		return root.getChildNodes();
	}
		
	public static void spreadNamespaces(Node node, String tns, boolean overwrite) {
		Document doc = node instanceof Document ? (Document) node : node.getOwnerDocument();
		boolean isParent = false;
		while (node != null) {
			Node next = null;
			if (!isParent && node.getNodeType() == Node.ELEMENT_NODE) {
				if (node.getNamespaceURI() == null) {
					node = doc.renameNode(node, tns, node.getNodeName());
				} else {
					if (overwrite) {
						tns = node.getNamespaceURI();
					}
				}
				NamedNodeMap nodeMap = node.getAttributes();
				int nodeMapLengthl = nodeMap.getLength();
				for (int i = 0; i < nodeMapLengthl; i++) {
					Node attr = nodeMap.item(i);
					if (attr.getNamespaceURI() == null) {
						doc.renameNode(attr, tns, attr.getNodeName());
					}
				}
			}
			isParent = (isParent || (next = node.getFirstChild()) == null) && (next = node.getNextSibling()) == null;
			node = isParent ? node.getParentNode() : next;
			if (isParent && node != null) {
				if (overwrite) {
					tns = node.getNamespaceURI();
				}
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	public static <N extends Node> N setNamespace(N node, String tns) {
		return (N) node.getOwnerDocument().renameNode(node, tns, node.getNodeName());
	}
	
	public static NodeList asNodeList(String...strings) {
		Document doc = getDefaultDocumentBuilder().newDocument();
		Element root = doc.createElement("root");
		for (String string : strings) {
			root.appendChild(doc.createTextNode(string));
		}
		return root.getChildNodes();
	}

	/**
	 * Check if a name is XML-compliant.
	 * @param xmlName the XML name
	 * @return true if the provided name is XML compliant or false otherwise
	 */
	public static boolean checkName(String xmlName) {
		try {
			createDom("java").createElement(xmlName);
			return true;
		} catch (DOMException e) {
			return false;
		} catch (ParserConfigurationException e) {
			// Should never occur
			return false;
		}
	}

	public static Document copyDocumentWithoutNamespace(Document document) throws ParserConfigurationException {
		Document newDocument = XMLUtils.createDom("java");
		copyNodeWithoutNamespace(newDocument, newDocument, document);
		return newDocument;
	}
	
	private static void copyNodeWithoutNamespace(Document document, Node parentNode, Node sourceNode) {
		Node destinationNode;
		if (sourceNode instanceof Document) {
			destinationNode = parentNode;
		} else {
			if (sourceNode instanceof Element) {
				String localName = XMLUtils.getLocalName(sourceNode);
				destinationNode = document.createElement(localName);
				
				// Copy attributes
				NamedNodeMap attributes = sourceNode.getAttributes();
				for (int i = 0; i < attributes.getLength(); i++) {
					Node sourceAttribute = attributes.item(i);
					
					String prefix = XMLUtils.getPrefix(sourceAttribute);
					
					if (!prefix.equalsIgnoreCase("xmlns")) {
						((Element) destinationNode).setAttribute(XMLUtils.getLocalName(sourceAttribute),
								sourceAttribute.getNodeValue());
					}
				}
			} else {
				destinationNode = document.importNode(sourceNode, false);
			}
			
			parentNode.appendChild(destinationNode);
		}
		
		NodeList childNodes = sourceNode.getChildNodes();
		int len = childNodes.getLength();
		for (int i = 0; i < len; i++) {
			XMLUtils.copyNodeWithoutNamespace(document, destinationNode, childNodes.item(i));
		}
	}
	
	public static String getPrefix(Node node) {
		String prefix = node.getPrefix();
		if (prefix == null) {
			prefix = node.getNodeName();
							
			// If the document is not namespace aware, we must split the attribute name
			// with ':' character.
			int i = prefix.indexOf(':');
			if (i != -1) {
				prefix = prefix.substring(0, i);
			}
		}
		
		return prefix;
	}
	
	public static String getLocalName(Node node) {
		String localName = node.getLocalName();
		if (localName == null) {
			localName = node.getNodeName();
							
			// If the document is not namespace aware, we must split the tag name
			// with ':' character.
			int i = localName.indexOf(':');
			if (i != -1) {
				localName = localName.substring(i+1);
			}
		}
		
		return localName;
	}
	
	private static Object getValue(Element elt, boolean ignoreStepIds, boolean useType) throws JSONException {
		Object value = null;

		try {
			if (elt.hasAttribute("type")) {
				String type = elt.getAttribute("type");

				if (type.equals("object")) {
					JSONObject jsonObject = new JSONObject();

					NodeList nl = elt.getChildNodes();
					for (int i = 0; i < nl.getLength(); i++) {
						Node node = nl.item(i);

						if (node instanceof Element) {
							Element child = (Element) node;
							String childName = child.hasAttribute("originalKeyName") ? child.getAttribute("originalKeyName") : child.getTagName();
							Object childValue = getValue(child, ignoreStepIds, useType);
							
							if (childValue != null) {
								jsonObject.put(childName, childValue);			
							} else {
								handleElement(child, jsonObject, ignoreStepIds, useType);
							}
						}
					}
					value = jsonObject;
				} else if (type.equals("array")) {
					JSONArray array = new JSONArray();

					NodeList nl = elt.getChildNodes();
					for (int i = 0; i < nl.getLength(); i++) {
						Node node = nl.item(i);

						if (node instanceof Element) {
							Element child = (Element) node;
							Object childValue = getValue(child, ignoreStepIds, useType);
							
							if (childValue != null) {
								array.put(childValue);							
							} else {
								JSONObject obj = new JSONObject();
								array.put(obj);
								handleElement(child, obj, ignoreStepIds, useType);
							}
						}
					}

					value = array;
				} else if (type.equals("string")) {
					value = elt.getTextContent();
				} else if (type.equals("boolean")) {
					value = Boolean.parseBoolean(elt.getTextContent());
				} else if (type.equals("null")) {
					value = JSONObject.NULL;
				} else if (type.equals("integer")) {
					value = Integer.parseInt(elt.getTextContent());
				} else if (type.equals("long")) {
					value = Long.parseLong(elt.getTextContent());
				} else if (type.equals("double")) {
					value = Double.parseDouble(elt.getTextContent());
				} else if (type.equals("float")) {
					value = Float.parseFloat(elt.getTextContent());
				}

				if (value != null) {
					elt.removeAttribute(type);
				}
			}
		} catch (Throwable t) {
			Engine.logEngine.debug("failed to convert the element " + elt.getTagName(), t);
		}
		
		return value;
	}
	
	public static  void handleElement(Element elt, JSONObject obj, boolean ignoreStepIds, boolean useType) throws JSONException {
		String key = elt.getTagName();
		JSONObject value = new JSONObject();
		NodeList nl = elt.getChildNodes();
		
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
			
			if (node instanceof Element) {
				Element child = (Element) node;
				Object childValue = useType ? getValue(child, ignoreStepIds, useType) : null;
				
				if (childValue != null) {
					value.accumulate(child.getTagName(), childValue);
				} else {
					handleElement(child, value, ignoreStepIds, useType);					
				}
			}
		}

		JSONObject attr = new JSONObject();
		NamedNodeMap nnm = elt.getAttributes();
		
		for (int i = 0; i < nnm.getLength(); i++) {
			Node node = nnm.item(i);
			if (ignoreStepIds && (node.getNodeName() != "step_id")) {
				attr.accumulate(node.getNodeName(), node.getNodeValue());
			}
		}

		if (value.length() == 0) {
			String content = elt.getTextContent();
			if (attr.length() == 0) {
				obj.accumulate(key, content);
			} else {
				value.accumulate("text", content);
			}
		}

		if (attr.length() != 0) {
			value.accumulate("attr", attr);
		}

		if (value.length() != 0) {
			obj.accumulate(key, value);
		}
	}
	
	public static String XmlToJson(Element elt, boolean ignoreStepIds) throws JSONException {
		return(XmlToJson(elt, ignoreStepIds, false));
	}

	public static String XmlToJson(Element elt, boolean ignoreStepIds, boolean useType) throws JSONException {
		return (XmlToJson(elt, ignoreStepIds, useType, null));
	}
	
	public static String XmlToJson(Element elt, boolean ignoreStepIds, boolean useType, JsonRoot jsonRoot) throws JSONException {
		JSONObject json = new JSONObject();
		handleElement(elt, json, ignoreStepIds, useType);
		String jsonString =   json.toString(1);
		if (jsonRoot != null && !jsonRoot.equals(JsonRoot.docNode)) {
			JSONObject jso = new JSONObject(jsonString).getJSONObject(elt.getTagName());
			if (jsonRoot.equals(JsonRoot.docChildNodes)) {
				jso.remove("attr");
			}
			jsonString = jso.toString(1);
		}
		return jsonString;
	}
	
	public static void jsonToXml(Object object, Element element) throws JSONException {
		jsonToXml(object, null, element, true, true, false, "item");
	}
	
	public static void jsonToXml(Object object, String objectKey, Element parentElement, boolean includeDataType, boolean compactArray, String arrayChildrenTag) throws JSONException {
		jsonToXml(object, objectKey, parentElement, false, includeDataType, compactArray, arrayChildrenTag);
	}
	
	private static void jsonToXml(Object object, String objectKey, Element parentElement, boolean modifyElement, boolean includeDataType, boolean compactArray, String arrayChildrenTag) throws JSONException {
		Engine.logBeans.trace("Converting JSON to XML: object=" + object + "; objectKey=\"" + objectKey + "\"");
		
		Document doc = parentElement.getOwnerDocument();

		if ("_attachments".equals(parentElement.getNodeName()) && "item".equals(arrayChildrenTag) && object instanceof JSONObject) {
			// special case when retrieving attachments with Couch : attachment name is the object key
			((JSONObject) object).put("name", objectKey);
			objectKey = "attachment";
		}
		
		// Normalize object key
		String originalObjectKey = objectKey;
		if (objectKey != null) {
			objectKey = StringUtils.normalize(objectKey);
		}

		// JSON object value case
		if (object instanceof JSONObject) {
			JSONObject json = (JSONObject) object;

			Element element = doc.createElement(objectKey == null ? "object" : objectKey);
			if (objectKey != null && !objectKey.equals(originalObjectKey)) {
				element.setAttribute("originalKeyName", originalObjectKey);
			}

			if (compactArray || modifyElement) {
				if (objectKey == null) {
					element = parentElement;
				} else {
					parentElement.appendChild(element);
				}
			} else {
				parentElement.appendChild(element);
			}

			if (includeDataType) {
				element.setAttribute("type", "object");
			}

			String[] keys = new String[json.length()];
			
			int index = 0;
			for (Iterator<String> i = GenericUtils.cast(json.keys()); i.hasNext();) {
				keys[index++] = i.next();
			}
			
			Arrays.sort(keys);
			
			for (String key: keys) {
				jsonToXml(json.get(key), key, element, false, includeDataType, compactArray, arrayChildrenTag);
			}
		}
		// Array value case
		else if (object instanceof JSONArray) {
			JSONArray array = (JSONArray) object;
			int len = array.length();

			Element arrayElement = parentElement;
			String arrayItemObjectKey = arrayChildrenTag;
			if (!(compactArray || modifyElement)) {
				arrayElement = doc.createElement(objectKey == null ? "array" : objectKey);
				if (objectKey != null && !objectKey.equals(originalObjectKey)) {
					arrayElement.setAttribute("originalKeyName", originalObjectKey);
				}
				parentElement.appendChild(arrayElement);

				if (includeDataType) {
					arrayElement.setAttribute("type", "array");
					arrayElement.setAttribute("length", "" + len);
				}
			} else if (objectKey != null) {
				arrayItemObjectKey = objectKey;
			}

			for (int i = 0; i < len; i++) {
				Object itemArray = array.get(i);
				jsonToXml(itemArray, arrayItemObjectKey, arrayElement, false, includeDataType, compactArray, arrayChildrenTag);
			}
		}
		else {
			Element element = doc.createElement(objectKey == null ? "value" : objectKey);
			if (objectKey != null && !objectKey.equals(originalObjectKey)) {
				element.setAttribute("originalKeyName", originalObjectKey);
			}

			parentElement.appendChild(element);

			if (JSONObject.NULL.equals(object)) {
				object = null;
			}
			
			if (object != null) {
				Text text = doc.createTextNode(object.toString());
				element.appendChild(text);
			}
			
			if (includeDataType) {
				String objectType = object == null ? "null" : object.getClass().getCanonicalName();
				if (objectType.startsWith("java.lang.")) {
					objectType = objectType.substring(10);
				}
				element.setAttribute("type", objectType.toLowerCase());
			}
		}
		
	}
	
	public static Charset getEncoding(File file) {
		return getEncoding(file, null);
	}
	
	public static Charset getEncoding(File file, Charset charset) {
		InputStream is = null;
		try {
			byte[] buffer = new byte[128];
			int nb = (is = new FileInputStream(file)).read(buffer);
			String encoding = new String(buffer, 0, nb, "ASCII").replaceFirst("[\\d\\D]*encoding=\"(.*?)\"[\\d\\D]*", "$1");
			charset = Charset.forName(encoding);
		} catch (Exception e) {
			Engine.logEngine.debug("failed to detect xml encoding", e);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {}
			}
		}
		
		return charset == null ? Charset.defaultCharset() : charset;
	}
	
	public static Charset getEncoding(byte[] bytes, Charset charset) {
		try {
			String encoding = new String(bytes, 0, Math.min(bytes.length, 128), "ASCII").replaceFirst("[\\d\\D]*encoding=\"(.*?)\"[\\d\\D]*", "$1");
			charset = Charset.forName(encoding);
		} catch (Exception e) { }
		
		return charset == null ? Charset.defaultCharset() : charset;
	}
	
	public static String stripNonValidXMLCharacters(String text) {
		if (text == null || ("".equals(text))) {
			return "";
		}
		
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < text.length(); i++) {
		    int codePoint = text.codePointAt(i);
		    if (codePoint > 0xFFFF) {
		        i++;
		    }
		    if ((codePoint == 0x9) || (codePoint == 0xA) || (codePoint == 0xD)
		            || ((codePoint >= 0x20) && (codePoint <= 0xD7FF))
		            || ((codePoint >= 0xE000) && (codePoint <= 0xFFFD))
		            || ((codePoint >= 0x10000) && (codePoint <= 0x10FFFF))) {
		        sb.appendCodePoint(codePoint);
		    }
		}
		return sb.toString();
	}
}