/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package hu.astrid.xmltools.beantoxml;

import hu.astrid.xmltools.beans.XmlConvertable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.net.URL;
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.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
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;

/**
 * Universal class to convertBeanToXml XmlConvertable objects (? extends XmlConvertable) to xml documents
 * @author István Zoltán Nagy
 */
public class BeanToXmlConverter extends XmlConverter {

    /**
     * The location of the XML Schema Definition (.xsd document)
     */
    private String schemaLocation;
    /**
     * The target namespace of the Schema document
     */
    private String nameSpace;
    /**
     * The prefix of the namespace
     */
    private String nameSpacePrefix;
    /**
     * Indicator to use Schema of not (default: false)
     */
    private boolean useSchema = false;
    /**
     * The name of the root element without prefix  (default: root)
     */
    private String root = "root";
    /**
     * The output location, where the result should go
     */
    private OutputStream output;

    /**
     * Returns the value of the nameSpacePrefix
     * @return nameSpacePrefix
     */
    public String getNameSpacePrefix() {
        return nameSpacePrefix;
    }

    /**
     * Sets the value of the nameSpacePrefix attribute
     * @param nameSpacePrefix The prefix of the namespace.
     */
    public void setNameSpacePrefix(String nameSpacePrefix) {
        this.nameSpacePrefix = nameSpacePrefix;
    }

    /**
     * Returns the output stream
     * @return output
     */
    public OutputStream getOutput() {
        return output;
    }

    /**
     * Sets the output stream
     * @param output The new stream
     */
    public void setOutput(OutputStream output) {
        this.output = output;
    }

    /**
     * Returns the name of the namespace
     * @return nameSpace
     */
    public String getNameSpace() {
        return nameSpace;
    }

    /**
     * Sets the value of the namespace attribute
     * @param nameSpace The name of the namespace.
     */
    public void setNameSpace(String nameSpace) {
        this.nameSpace = nameSpace;
    }

    /**
     * Returns the location of the xsd document
     * @return schemaLocation
     */
    public String getSchemaLocation() {
        return schemaLocation;
    }

    /**
     * Sets the location of the schema document
     * @param schemaLocation The new location
     */
    public void setSchemaLocation(String schemaLocation) {
        this.schemaLocation = schemaLocation;
    }

    /**
     * Returns the name of the root element
     * @return root
     */
    public String getRoot() {
        return root;
    }

    /**
     * Sets the name of the root element
     * @param root The new name of the root element
     */
    public void setRoot(String root) {
        this.root = root;
    }

    /**
     * Returns the value of the use schema indicator
     * @return useSchema
     */
    public boolean isUseSchema() {
        return useSchema;
    }

    /**
     * Sets the value of the useSchema indicator
     * @param useSchema The new value of useSchema
     */
    public void setUseSchema(boolean useSchema) {
        this.useSchema = useSchema;
    }

    /**
     * Constructor, which didn't initializes anything.
     */
    public BeanToXmlConverter() {
    }

    /**
     * Constructor, to set everything automatically from the Schema.
     * @param schema The location of the XML Schema Definition document
     * @param output The URL where the result will be written
     * @throws Exception When the schema is unaccessible, or there is a problem with the URL
     */
    public BeanToXmlConverter(URL schema, URL output) throws Exception {
        this(schema, output.openConnection().getOutputStream());
    }

    /**
     * Constructor, to set everything automatically from the Schema.
     * @param schema The location of the XML Schema Definition document
     * @param output The file where the result will be written
     * @throws Exception When the schema is unaccessible, or there is a problem with the File
     */
    public BeanToXmlConverter(URL schema, File output) throws Exception {
        this(schema, new FileOutputStream(output));
    }

    /**
     * Constructor, to set everything automatically from the Schema.
     * @param schema The location of the XML Schema Definition document
     * @param output The stream where the result will be sent
     * @throws Exception When the schema is unaccessible, or there is a problem with the Stream
     */
    public BeanToXmlConverter(URL schema, OutputStream output) throws Exception {
        try {
            this.output = output;
            this.schemaLocation = schema.toString();
            boolean old = this.validate;
            this.validate = false;

            //create a new factory with the validating feature
            DocumentBuilderFactory builderFactory = createDocumentBuilderFactory();

            //create a new builder from the factory with default error-handler
            DocumentBuilder builder = createDocumentBuilder(builderFactory);

            //parse the document
            Document doc = builder.parse(schema.openStream());
            Element documentElement = doc.getDocumentElement();
            NodeList elements = documentElement.getElementsByTagName(documentElement.getPrefix() + ":element");
            Node rootNode = elements.item(0);
            NamedNodeMap attributes = documentElement.getAttributes();
            this.setNameSpace(documentElement.getAttribute("targetNamespace"));
            for (int i = 0; i < attributes.getLength(); i++) {
                Node item = attributes.item(i);
                if (item.getNodeName().startsWith("xmlns:") && item.getNodeValue().equals(nameSpace)) {
                    this.setNameSpacePrefix(item.getNodeName().substring(6));
                }
            }
            this.setRoot(rootNode.getAttributes().item(0).getNodeValue());
            this.setUseSchema(true);
            this.setVersion("1.0");
            this.validate = old;
        } catch (Exception ex) {
            throw ex;
        }
    }

    /**
     * Creates a new XML document from the given object and sends to the specified output stream, when it is possible.
     * @param object The XmlConvertable object
     * @throws Exception When the conversion is not possible
     */
    public void convertToXml(XmlConvertable object) throws Exception {
        try {
            Document doc = this.createDocument();
            doc.setXmlVersion(this.version);
            writeToStream(convertBeanToXml(doc, object));
        } catch (TransformerConfigurationException ex) {
            //Logger.getLogger(BeanToXmlConverter.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        } catch (ParserConfigurationException ex) {
            //Logger.getLogger(BeanToXmlConverter.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        } catch (TransformerException ex) {
            //Logger.getLogger(BeanToXmlConverter.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
    }

    /**
     * Inserts the data into the document.
     * @param doc The document
     * @return root The root element
     * @throws DOMException When the conversion isn't posible.
     * @throws Exception When the conversion isn't posible.
     */
    private Document convertBeanToXml(Document doc, XmlConvertable object) throws DOMException, Exception {
        Element rootElement = null;
        rootElement = (Element) object.toXml(doc, getNameSpacePrefix());
        if (this.useSchema) {
            rootElement.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
            rootElement.setAttribute("xmlns:" + this.nameSpacePrefix, this.nameSpace);
            rootElement.setAttribute("xsi:schemaLocation", this.nameSpace + " " + this.schemaLocation);
        }
        doc.appendChild(rootElement);
        return doc;
    }

    /**
     * Writes the given document to the stream
     * @param doc The document to save
     * @throws TransformerFactoryConfigurationError When the saving process isn't possible.
     * @throws TransformerException When the saving process isn't possible.
     * @throws TransformerConfigurationException When the saving process isn't possible.
     */
    private void writeToStream(Document doc) throws TransformerFactoryConfigurationError, TransformerException, TransformerConfigurationException {
        Source source = new DOMSource(doc);
        Result result = new StreamResult(this.output);
        Transformer xformer = TransformerFactory.newInstance().newTransformer();
        xformer.setOutputProperty(OutputKeys.INDENT, "yes");
        xformer.transform(source, result);
    }
}
