/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2012 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.addon.deploy.impl.xml;

import org.ow2.jonas.addon.deploy.jonasaddon.v1.generated.JonasAddonType;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipOutputStream;

/**
 * Load the Xml jonas-addon
 * @author Mohammed Boukada
 */
public class JonasAddonXmlLoader {

    Object jonasAddon;

    /**
     * Default constructor
     */
    public JonasAddonXmlLoader() {}

    /**
     * Constructor. Loads the jonas-addon
     *
     * @param cloudApplicationURL path to the xml jonas-addon file
     * @param jonasAddonVersion {@link JonasAddonVersion}
     * @param xsdURLs URLs of XSD schemas
     * @throws Exception
     */
    public JonasAddonXmlLoader(final URL cloudApplicationURL, final JonasAddonVersion jonasAddonVersion,
                                     final List<URL> xsdURLs)
            throws Exception {
        this.jonasAddon = loadSchemaAndFile(xsdURLs,
                JonasAddonPropertiesManager.getCloudApplicationXMLNS(jonasAddonVersion),
                JonasAddonPropertiesManager.getCloudApplicationSchemaLocation(jonasAddonVersion), "jonas-addon",
                getRootClass(jonasAddonVersion), cloudApplicationURL);
    }

    /**
     * Constructor. Loads the jonas-addon
     *
     * @param xml XML to load.
     * @param jonasAddonVersion {@link JonasAddonVersion}
     * @param xsdURLs URLs of XSD schemas
     * @throws Exception
     */
    public JonasAddonXmlLoader(final String xml, final JonasAddonVersion jonasAddonVersion,
                                     final List<URL> xsdURLs)
            throws Exception {
        this.jonasAddon = loadSchemaAndFile(xsdURLs,
                JonasAddonPropertiesManager.getCloudApplicationXMLNS(jonasAddonVersion),
                JonasAddonPropertiesManager.getCloudApplicationSchemaLocation(jonasAddonVersion), "jonas-addon",
                getRootClass(jonasAddonVersion), xml);
    }

    /**
     * @param jonasAddonVersion {@link JonasAddonVersion}
     * @return the correct root class according to the jonas-addon version
     */
    private Class getRootClass(final JonasAddonVersion jonasAddonVersion) {
        if (JonasAddonVersion.JONAS_ADDON_1.equals(jonasAddonVersion)) {
            return JonasAddonType.class;
        }
        return null;
    }

    /**
     * Return the loaded jonas-addon
     *
     * @return the jonas-addon
     */
    public Object getJonasAddon() {
        return jonasAddon;
    }

    /**
     * Verifies an XML file against an XSD and instantiates it using a given
     * class.
     *
     * @param xsdPaths XSDs files path.
     * @param xmlns XML namespace, will be added if non found in the XML
     * @param schemaLocation XML schema location, will be added if non found in
     *        the XML
     * @param xmlRoot Root element of the XML (for completing the XML if XSD
     *        xmlns is missing)
     * @param rootClass Root class used for instantiating JAXB.
     * @param urlXML XML to load.
     * @return XML loaded using JAXB and the rootClass.
     * @throws Exception
     */
    public <T> T loadSchemaAndFile(final List<URL> xsdPaths, final String xmlns, final String schemaLocation,
                                   final String xmlRoot, final Class<T> rootClass, final URL urlXML)
            throws Exception {
        String xml = readURL(urlXML);
        return loadSchemaAndFile(xsdPaths, xmlns, schemaLocation, xmlRoot, rootClass, xml);
    }

    /**
     * Verifies an XML file against an XSD and instantiates it using a given
     * class.
     *
     * @param xsdPaths XSDs files path.
     * @param xmlns XML namespace, will be added if non found in the XML
     * @param schemaLocation XML schema location, will be added if non found in
     *        the XML
     * @param xmlRoot Root element of the XML (for completing the XML if XSD
     *        xmlns is missing)
     * @param rootClass Root class used for instantiating JAXB.
     * @param xml XML to load.
     * @return XML loaded using JAXB and the rootClass.
     * @throws Exception
     */
    public <T> T loadSchemaAndFile(final List<URL> xsdPaths, final String xmlns, final String schemaLocation,
                                   final String xmlRoot, final Class<T> rootClass, String xml)
            throws Exception {

        JAXBContext jc = JAXBContext.newInstance(rootClass);
        Unmarshaller unMarshaller = jc.createUnmarshaller();
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        List<Source> xsdSources = getSources(xsdPaths);
        Schema schema = schemaFactory.newSchema(xsdSources.toArray(new Source[xsdSources.size()]));
        unMarshaller.setSchema(schema);

        final int xmlRootStart = xml.indexOf("<" + xmlRoot);
        if (xmlRootStart != -1) {
            final int xmlRootEnd = xml.indexOf(">", xmlRootStart);

            if (xmlRootEnd != -1) {
                final int xmlnsIndex = xml.indexOf("xmlns", xmlRootStart);

                if (xmlnsIndex == -1) {
                    xml = xml.substring(0, xmlRootStart) + "<" + xmlRoot + " xmlns=\"" + xmlns
                            + "\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"" + xmlns + " "
                            + schemaLocation + "\"" + xml.substring(xmlRootStart + xmlRoot.length() + 1);
                }
            }
        }
        InputStream xmlInputStream = new ByteArrayInputStream(xml.getBytes());

        T value;
        JAXBElement<T> root = unMarshaller.unmarshal(new StreamSource(xmlInputStream), rootClass);
        value = root.getValue();

        return value;
    }

    /**
     * Generate xml content
     * @param jaxbElement root element
     * @param jonasAddonVersion jonas-addon version
     * @return xml content
     * @throws javax.xml.bind.JAXBException
     */
    public String toXml(JAXBElement<?> jaxbElement, final List<URL> xsdURLs, JonasAddonVersion jonasAddonVersion) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(getRootClass(jonasAddonVersion));
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        // Load schemas
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        List<Source> xsdSources = getSources(xsdURLs);
        Schema schema = schemaFactory.newSchema(xsdSources.toArray(new Source[xsdSources.size()]));
        marshaller.setSchema(schema);
        StringWriter sw = new StringWriter();
        marshaller.marshal(jaxbElement, sw);
        return sw.toString();
    }

    /**
     * Generate xml content
     * @param jaxbElement root element
     * @param jonasAddonVersion jonas-addon version
     * @param zipOutputStream
     * @throws javax.xml.bind.JAXBException
     */
    public void toXml(JAXBElement<?> jaxbElement, final List<URL> xsdURLs, JonasAddonVersion jonasAddonVersion,
                        ZipOutputStream zipOutputStream) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(getRootClass(jonasAddonVersion));
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        // Load schemas
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        List<Source> xsdSources = getSources(xsdURLs);
        Schema schema = schemaFactory.newSchema(xsdSources.toArray(new Source[xsdSources.size()]));
        marshaller.setSchema(schema);
        marshaller.marshal(jaxbElement, zipOutputStream);
    }

    /**
     * Read url content
     * @param url
     * @return content
     * @throws java.io.IOException
     */
    private static String readURL(final URL url) throws IOException {
        URLConnection connection = url.openConnection();
        BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));

        try {
            StringBuilder sb = new StringBuilder();
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                if (sb.length() > 0) {
                    sb.append('\n');
                }
                sb.append(inputLine);
            }

            return sb.toString();
        } finally {
            in.close();
        }
    }

    /**
     * Get sources list corresponding to URLs
     * @param urls List of URL
     * @return the list of {@link javax.xml.transform.Source} associated to the list of url
     */
    protected List<Source> getSources(final List<URL> urls) throws Exception {
        List<Source> sources = new ArrayList<Source>();
        for (URL url: urls) {
            Source source = getSource(url);
            if (source != null) {
                sources.add(source);
            }
        }
        return sources;
    }

    /**
     * Get the source corresponding to URL
     * @param url The {@link java.net.URL}
     * @return the {@link javax.xml.transform.Source} associated to the given URL
     * @throws Exception
     */
    protected Source getSource(final URL url) throws Exception {
        if (url != null) {
            InputStream inputStream = null;
            try {
                inputStream = url.openStream();
            } catch (IOException e) {
                throw new Exception("Cannot get the inpustream of the URL " + url.getPath() + "\n");
            }

            try {
                return new StreamSource(inputStream);
            } catch (Exception e) {
                throw new Exception("Cannot create a new StreamSource for the URL : " + url.getPath()
                        + "\n", e);
            }
        } else {
            return null;
        }
    }
}
