/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2004-2009 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 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */
package org.ow2.jonas.ws.publish.internal.file;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.lib.util.Log;
import org.ow2.jonas.lib.util.XMLSerializer;
import org.w3c.dom.Document;

import javax.wsdl.Definition;
import javax.wsdl.Import;
import javax.wsdl.Types;
import javax.wsdl.WSDLException;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.UnknownExtensibilityElement;
import javax.wsdl.extensions.schema.Schema;
import javax.wsdl.extensions.schema.SchemaImport;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

/**
 * Wrote the given Definition and all imported WSDL to the given base directory.
 * @author Guillaume Sauthier
 */
public class JDefinitionWriter {

    /** WSDL Directory */
    private File base;

    /** charset */
    private Charset cs;

    /** Definition to write */
    private Definition definition;

    /** base WSDL filename */
    private String filename;

    /** logger */
    private static Logger logger = Log.getLogger("org.ow2.jonas.ws");

    /**
     * Constructs a new JDefinitionWriter that will wrote the given Definition
     * and all imported WSDL to the given base directory.
     * @param def base Definition
     * @param context Context where file should be wrote (MUST exists)
     * @param cs Charset to use
     * @param filename base Definition filename
     */
    public JDefinitionWriter(Definition def, File context, Charset cs, String filename) {
        if (!context.exists()) {
            throw new IllegalArgumentException("Context MUST exists : " + context);
        }
        this.definition = def;
        if (context.isDirectory()) {
            this.base = context;
        } else {
            this.base = context.getParentFile();
        }
        this.cs = cs;
        this.filename = filename;
    }

    /**
     * Write the given Definition into the base directory
     * @throws IOException if a file or directory cannot be created
     * @throws WSDLException if WSDLWriter is not able to write its Definition
     */
    public void write() throws IOException, WSDLException {

        // Write the main definition as-is
        writeDefinition(definition, base, filename);

        // Then, handle imports of this definition
        writeWsdlImports();

        // Finally, write the imported types of this definition
        writeXsdImports();
    }

    /**
     * Write to the FS the imported schema documents (imported using
     * the <code>xsd:import</code> element).
     * @throws IOException if writing failed
     */
    private void writeXsdImports() throws IOException {

        Types types = definition.getTypes();
        if (types != null) {
            List extList = types.getExtensibilityElements();
            for (Iterator extIt = extList.iterator(); extIt.hasNext();) {
                ExtensibilityElement ext = (ExtensibilityElement) extIt.next();

                if (ext instanceof Schema) {
                    // schema handling
                    Schema schema = (Schema) ext;
                    Map<String, Vector<SchemaImport>> imports = schema.getImports();
                    if (imports != null) {
                        for (Vector<SchemaImport> list : imports.values()) {

                            for(SchemaImport imported : list) {
                                String uri = imported.getSchemaLocationURI();

                                // Exclude absolute imports
                                if ((uri != null) && !uri.startsWith("http://")) {
                                    Document doc = imported.getReferencedSchema().getElement().getOwnerDocument();
                                    File dir = new File(base, filename).getCanonicalFile().getParentFile();
                                    writeDocument(doc, dir, uri);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Write to the FileSystem all the documents imported (using the
     * <code>wsdl:import</code> element)
     * @throws IOException If writing fails
     * @throws WSDLException if WSDLWriter creation failed
     */
    private void writeWsdlImports() throws IOException, WSDLException {
        Map imports = definition.getImports();
        for (Iterator ns = imports.keySet().iterator(); ns.hasNext();) {
            // for each nsURI, we have a List of Import
            String uri = (String) ns.next();
            List importeds = (List) imports.get(uri);
            // for each import related to the given ns
            for (Iterator imp = importeds.iterator(); imp.hasNext();) {
                Import imported = (Import) imp.next();

                // create a resolved URL to know if the import is relative or
                // not to the publish
                String locURI = imported.getLocationURI();
                if (!locURI.startsWith("http://")) {
                    // locResolvedURL starts with the publish URI, so it's a
                    // relative import
                    // and we should wrote it.
                    Definition impDef = imported.getDefinition();
                    if (locURI.toLowerCase().endsWith("wsdl")) {
                        // if we have another WSDL
                        JDefinitionWriter jdw = new JDefinitionWriter(impDef, new File(base, filename).getCanonicalFile(), cs, locURI);
                        jdw.write();
                    } else {
                        // Assume this is a types import
                        Types types = impDef.getTypes();
                        if (types != null) {
                            List extList = types.getExtensibilityElements();
                            for (Iterator extIt = extList.iterator(); extIt.hasNext();) {
                                ExtensibilityElement ext = (ExtensibilityElement) extIt.next();
                                Document doc = null;

                                if (ext instanceof Schema) {
                                    // schema handling
                                    Schema schema = (Schema) ext;
                                    doc = schema.getElement().getOwnerDocument();
                                } else if (ext instanceof UnknownExtensibilityElement) {
                                    // other XML
                                    UnknownExtensibilityElement unknownExtElem = (UnknownExtensibilityElement) ext;
                                    doc = unknownExtElem.getElement().getOwnerDocument();
                                }

                                if (doc != null) {
                                    // we have something to serialize
                                    File dir = new File(base, filename).getCanonicalFile().getParentFile();
                                    writeDocument(doc, dir, locURI);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * @param doc XML Document to write
     * @param base storing directory
     * @param locURI Document filename (may include directories)
     * @throws IOException if OutputStream cannot be created
     */
    private void writeDocument(Document doc, File base, String locURI) throws IOException {
        XMLSerializer ser = new XMLSerializer(doc);
        File file = new File(base, locURI).getCanonicalFile();
        logger.log(BasicLevel.DEBUG, "Writing XML Document in " + file);
        createParentIfNeeded(file);
        Writer writer = new OutputStreamWriter(new FileOutputStream(file), cs);
        ser.serialize(writer);
        writer.close();
    }

    /**
     * @param def Definition to write (that does not write imported files)
     * @param base storing directory
     * @param filename Definition filename
     * @throws WSDLException if WSDLWriter failed to wrote the Definition
     * @throws IOException if OutputStream cannot be created
     */
    private void writeDefinition(Definition def, File base, String filename) throws WSDLException, IOException {
        WSDLFactory factory = WSDLFactory.newInstance();
        WSDLWriter writer = factory.newWSDLWriter();
        File wsdl = new File(base, filename).getCanonicalFile();
        logger.log(BasicLevel.DEBUG, "Writing WSDL Definition in " + wsdl);
        createParentIfNeeded(wsdl);
        Writer w = new OutputStreamWriter(new FileOutputStream(wsdl), cs);
        writer.writeWSDL(def, w);
        w.close();
    }

    /**
     * Creates the parent of the given file as a directory
     * @param file with a parent that must be a directory
     * @throws IOException if directory cannot be created
     */
    private void createParentIfNeeded(File file) throws IOException {
        File parent = file.getParentFile();
        if (!parent.exists()) {
            if (!parent.mkdirs()) {
                // cannot create directory
                throw new IOException("Cannot create directory " + parent.getCanonicalPath());
            }
        } else if (!parent.isDirectory()) {
            // parent exists but is not a directory
            throw new IOException("Parent " + parent.getCanonicalPath() + " already exists but is not a directory.");
        }
    }
}