/*
 * utils - XML.java - Copyright © 2006-2009 David Roden
 *
 * 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 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/>.
 */

package net.pterodactylus.util.xml;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
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 net.pterodactylus.util.io.Closer;
import net.pterodactylus.util.logging.Logging;

import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Contains method to transform DOM XML trees to byte arrays and vice versa.
 *
 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
 */
public class XML {

	/** The logger. */
	private static final Logger logger = Logging.getLogger(XML.class.getName());

	/** Cached document builder factory. */
	private static DocumentBuilderFactory documentBuilderFactory = null;

	/** Cached document builder. */
	private static DocumentBuilder documentBuilder = null;

	/** Cached transformer factory. */
	private static TransformerFactory transformerFactory = null;

	/**
	 * Returns a document builder factory. If possible the cached instance will
	 * be returned.
	 *
	 * @return A document builder factory
	 */
	private static DocumentBuilderFactory getDocumentBuilderFactory() {
		if (documentBuilderFactory != null) {
			return documentBuilderFactory;
		}
		documentBuilderFactory = DocumentBuilderFactory.newInstance();
		documentBuilderFactory.setXIncludeAware(true);
		documentBuilderFactory.setNamespaceAware(true);
		return documentBuilderFactory;
	}

	/**
	 * Returns a document builder. If possible the cached instance will be
	 * returned.
	 *
	 * @return A document builder
	 */
	private static DocumentBuilder getDocumentBuilder() {
		if (documentBuilder != null) {
			return documentBuilder;
		}
		try {
			documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
		} catch (ParserConfigurationException pce1) {
			logger.log(Level.WARNING, "Could not create DocumentBuilder.", pce1);
		}
		return documentBuilder;
	}

	/**
	 * Returns a transformer factory. If possible the cached instance will be
	 * returned.
	 *
	 * @return A transformer factory
	 */
	private static TransformerFactory getTransformerFactory() {
		if (transformerFactory != null) {
			return transformerFactory;
		}
		transformerFactory = TransformerFactory.newInstance();
		return transformerFactory;
	}

	/**
	 * Creates a new XML document.
	 *
	 * @return A new XML document
	 */
	public static Document createDocument() {
		return getDocumentBuilder().newDocument();
	}

	/**
	 * Transforms the DOM XML document into a byte array.
	 *
	 * @param document
	 *            The document to transform
	 * @return The byte array containing the XML representation
	 */
	public static byte[] transformToByteArray(Document document) {
		ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
		OutputStreamWriter converter = new OutputStreamWriter(byteOutput, Charset.forName("UTF-8"));
		writeToOutputStream(document, converter);
		try {
			converter.flush();
			byteOutput.flush();
			byte[] result = byteOutput.toByteArray();
			return result;
		} catch (IOException ioe1) {
			return null;
		} finally {
			Closer.close(converter);
			Closer.close(byteOutput);
		}
	}

	/**
	 * Writes the given document to the given writer.
	 *
	 * @param document
	 *            The document to write
	 * @param writer
	 *            The writer to write the document to
	 */
	public static void writeToOutputStream(Document document, Writer writer) {
		writeToOutputStream(document, writer, true);
	}

	/**
	 * Writes the given document to the given writer.
	 *
	 * @param document
	 *            The document to write
	 * @param writer
	 *            The writer to write the document to
	 * @param preamble
	 *            <code>true</code> to include the XML header,
	 *            <code>false</code> to not include it
	 */
	public static void writeToOutputStream(Document document, Writer writer, boolean preamble) {
		Result transformResult = new StreamResult(writer);
		Source documentSource = new DOMSource(document);
		try {
			Transformer transformer = getTransformerFactory().newTransformer();
			transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, preamble ? "no" : "yes");
			transformer.transform(documentSource, transformResult);
		} catch (TransformerConfigurationException tce1) {
			logger.log(Level.WARNING, "Could create Transformer.", tce1);
		} catch (TransformerException te1) {
			logger.log(Level.WARNING, "Could not transform Document.", te1);
		}
	}

	/**
	 * Transforms the byte array into a DOM XML document.
	 *
	 * @param data
	 *            The byte array to parse
	 * @return The DOM XML document
	 */
	public static Document transformToDocument(byte[] data) {
		return transformToDocument(new ByteArrayInputStream(data));
	}

	/**
	 * Transforms the input stream into a DOM XML document.
	 *
	 * @param inputStream
	 *            The input stream to parse
	 * @return The DOM XML document
	 */
	public static Document transformToDocument(InputStream inputStream) {
		return transformToDocument(new InputSource(inputStream));
	}

	/**
	 * Transforms the reader into a DOM XML document.
	 *
	 * @param inputReader
	 *            The reader to read the XML from
	 * @return The DOM XML document
	 */
	public static Document transformToDocument(Reader inputReader) {
		return transformToDocument(new InputSource(inputReader));
	}

	/**
	 * Transforms the inout source into a DOM XML document.
	 *
	 * @param inputSource
	 *            The source to read the XML from
	 * @return The DOM XML document
	 */
	public static Document transformToDocument(InputSource inputSource) {
		try {
			DocumentBuilder documentBuilder = getDocumentBuilder();
			return documentBuilder.parse(inputSource);
		} catch (SAXException saxe1) {
			logger.log(Level.WARNING, "Could not parse InputSource.", saxe1);
		} catch (IOException ioe1) {
			logger.log(Level.WARNING, "Could not read InputSource.", ioe1);
		}
		return null;
	}

}