/**
 * JOnAS : Java(TM) OpenSource Application Server
 * Copyright (C) 1999-2007 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.generators.genbase.archive;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.jar.Attributes;

import javax.xml.parsers.ParserConfigurationException;

import org.objectweb.util.monolog.api.BasicLevel;
import org.ow2.jonas.deployment.common.DeploymentDescException;
import org.ow2.jonas.deployment.ear.EarDeploymentDesc;
import org.ow2.jonas.deployment.ear.lib.EarDeploymentDescManager;
import org.ow2.jonas.deployment.ear.xml.Web;
import org.ow2.jonas.generators.genbase.GenBaseException;
import org.ow2.jonas.generators.genbase.utils.XMLUtils;
import org.ow2.jonas.lib.loader.EjbJarClassLoader;
import org.ow2.util.ee.deploy.api.deployable.EARDeployable;
import org.ow2.util.ee.deploy.api.deployable.IDeployable;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Application is a wrapper around an ear (packaged as a jar or as an unpacked
 * jar).
 *
 * @author Guillaume Sauthier
 */
public class Application extends J2EEArchive {

    /** list of embded clients archive. */
    private List clients;

    /** list of embded webapps archive. */
    private List webapps;

    /** list of embded ejbjars archive. */
    private List ejbjars;

    /** Application DeploymentDesc */
    private EarDeploymentDesc earDD = null;

    /** name to document map */
    private Map<String, Document> descriptors = new Hashtable<String, Document>();

    /** application Descriptor */
    private Document app;

    /** EJB Jar ClassLoader */
    private URLClassLoader ejbCL = null;

    /** Common Libs ClassLoader */
    private URLClassLoader commonCL = null;

    /** libs path File */
    private List pathFiles;

    /** application archive filename */
    private String appFilename;

    private IDeployable deployable = null;

    /**
     * Creates a new Application archive.
     *
     * @param archive the file containing the application archive.
     *
     * @throws GenBaseException When Init fails
     */
    public Application(final Archive archive, final IDeployable deployable) throws GenBaseException {
        super(archive);
        this.appFilename = archive.getRootFile().getName();
        this.deployable  = deployable;
        if (getLogger().isLoggable(BasicLevel.DEBUG)) {
            getLogger().log(BasicLevel.DEBUG, "Wrapping '" + archive.getName() + "' in Application");
        }
        init();
    }

    /**
     * Initialize the Application. Creates modules lists, unpack if not
     * unpacked.
     *
     * @throws GenBaseException When application unpack fails or when Decriptors
     *         cannot be parsed or found.
     */
    protected void init() throws GenBaseException {

        pathFiles = new Vector();

        ejbjars = new Vector();
        webapps = new Vector();
        clients = new Vector();

        // load META-INF/application.xml
        try {
            earDD = EarDeploymentDescManager.getDeploymentDesc((EARDeployable) deployable, Thread.currentThread().getContextClassLoader());
        } catch (DeploymentDescException dde) {
            String err = getI18n().getMessage("Application.init.earDDExc", getArchive().getRootFile());
            throw new GenBaseException(err, dde);
        }

        // add EjbJars
        String[] ejbs = earDD.getEjbTags();

        for (int i = 0; i < ejbs.length; i++) {
            File ejbFile = new File(getRootFile(), ejbs[i]);
            Archive ejbArch = null;

            if (ejbFile.isDirectory()) {
                // Unpacked Jar
                ejbArch = new FileArchive(ejbFile);
            } else {
                // Packed Jar
                ejbArch = new JarArchive(ejbFile);
            }
            ejbjars.add(new EjbJar(ejbArch, this));

            // add entries for Class-Path
            addClassPathEntry(ejbArch);
        }

        // add WebApps
        Web[] webs = earDD.getWebTags();

        for (int i = 0; i < webs.length; i++) {
            File webFile = new File(getRootFile(), webs[i].getWebUri());
            Archive webArch = null;

            if (webFile.isDirectory()) {
                // Unpacked Jar
                webArch = new FileArchive(webFile);
            } else {
                // Packed Jar
                webArch = new JarArchive(webFile);
            }
            webapps.add(new WebApp(webArch, this));
        }

        // add Clients
        String[] clts = earDD.getClientTags();

        for (int i = 0; i < clts.length; i++) {
            File clientFile = new File(getRootFile(), clts[i]);
            Archive clientArch = null;

            if (clientFile.isDirectory()) {
                // Unpacked Jar
                clientArch = new FileArchive(clientFile);
            } else {
                // Packed Jar
                clientArch = new JarArchive(clientFile);
            }
            clients.add(new Client(clientArch, this));
        }

        // Create ear ClassLoader (from MANIFEST/Class-Path general entry)
        setModuleClassloader(createEARClassLoader());

        // Create EJB ClassLoader
        ejbCL = createEJBClassLoader();

        loadDescriptors();
    }

    /**
     * Load Deployment Descriptor of an Application.
     *
     * @throws GenBaseException When parsing of application.xml fails
     */
    protected void loadDescriptors() throws GenBaseException {

        InputStream is = null;

        try {
          is = getApplicationInputStream();
        } catch (IOException e) {
            throw new GenBaseException("Cannot get inputstream", e);
        }

        if (is == null) {
            this.app = XMLUtils.newApplication();
            return;
        }

        try {
            app = XMLUtils.newDocument(is, "META-INF/application.xml", isDTDsAllowed());
        } catch (SAXException saxe) {
            String err = getI18n().getMessage("Application.loadDescriptors.parseError");
            throw new GenBaseException(err, saxe);
        } catch (ParserConfigurationException pce) {
            String err = getI18n().getMessage("Application.loadDescriptors.prepare");
            throw new GenBaseException(err, pce);
        } catch (IOException ioe) {
            String err = getI18n().getMessage("Application.loadDescriptors.parseError");
            throw new GenBaseException(err, ioe);
        }

        descriptors.put("META-INF/application.xml", app);
    }

    /**
     * Initialize the Archive.
     * @throws GenBaseException When initialization fails.
     */
    @Override
    public void initialize() throws GenBaseException {

        // init ejbjars
        for (Iterator i = ejbjars.iterator(); i.hasNext();) {
            EjbJar ejb = (EjbJar) i.next();
            ejb.initialize();
        }

        // init webapps
        for (Iterator i = webapps.iterator(); i.hasNext();) {
            WebApp web = (WebApp) i.next();
            web.initialize();
        }

        // init clients
        for (Iterator i = clients.iterator(); i.hasNext();) {
            Client client = (Client) i.next();
            client.initialize();
        }

    }

    /**
     * Returns the name of the Archive. Overrides J2EEArchive.getName();
     *
     * @see org.ow2.jonas.generators.genbase.archive.J2EEArchive#getName()
     *
     * @return the name of the Archive.
     */
    @Override
    public String getName() {
        return appFilename;
    }

    /**
     * Construct a ClassLoader for EJBs inside an application.
     *
     * @return the Ejb-Jar ClassLoader
     *
     * @throws GenBaseException When URLClassLoader cannot be created.
     */
    private URLClassLoader createEJBClassLoader() throws GenBaseException {

        URL[] urls = new URL[pathFiles.size() + ejbjars.size()];
        int index = 0;
        for (Iterator i = pathFiles.iterator(); i.hasNext(); index++) {
            File f = (File) i.next();
            try {
                urls[index] = f.toURL();
            } catch (IOException ioe) {
                String err = "Cannot convert " + f + " to URL.";
                throw new GenBaseException(err, ioe);
            }
        }

        for (Iterator i = ejbjars.iterator(); i.hasNext(); index++) {
            try {
                urls[index] = ((EjbJar) i.next()).getRootFile().toURL();
            } catch (IOException ioe) {
                String err = "Cannot transform as a URL : " + ioe.getMessage();
                throw new GenBaseException(err, ioe);
            }
        }

        try {
            return new EjbJarClassLoader(urls, getModuleClassloader());
        } catch (IOException ioe) {
            String err = "Cannot create EjbJarClassLoader";
            throw new GenBaseException(err, ioe);
        }

    }

    /**
     * Construct a ClassLoader for the application.
     *
     * @return the Application ClassLoader
     *
     * @throws GenBaseException When URLClassLoader cannot be created.
     */
    private URLClassLoader createEARClassLoader() throws GenBaseException {

        // find parent ClassLoader
        ClassLoader parent = Thread.currentThread().getContextClassLoader();

        // get Manifest Attributes if any
        String classpath = getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
        URL[] urls = new URL[0];
        if (classpath != null) {
            // Lookup specified files.
            String[] paths = classpath.split(" ");
            urls = new URL[paths.length];
            for (int i = 0; i < paths.length; i++) {
                try {
                    URL path = new File(getRootFile(), paths[i]).toURL();
                    urls[i] = path;
                } catch (IOException ioe) {
                    String err = "Cannot transform '" + paths[i] + "' as a URL";
                    throw new GenBaseException(err, ioe);
                }
            }
        }

        return new URLClassLoader(urls, parent);
    }

    /**
     * Search the given Archive for ClassPath Manifest entry and add the entries
     * in the EAR classpath.
     *
     * @param a the Archive to explore
     *
     * @throws GenBaseException When a path cannot be added in the EAR classpath.
     */
    private void addClassPathEntry(final Archive a) throws GenBaseException {
        // get Manifest Attributes if any
        String classpath = a.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH);

        if (classpath != null) {
            // Lookup specified files.
            String[] paths = classpath.split(" ");
            for (int i = 0; i < paths.length; i++) {
                try {
                    File path = new File(a.getRootFile().getParentFile(), paths[i]).getCanonicalFile();
                    if (path.exists() && !pathFiles.contains(path)) {
                        pathFiles.add(path);
                    }
                } catch (IOException ioe) {
                    String err = "Cannot add in EAR classpath :" + paths[i];
                    throw new GenBaseException(err, ioe);
                }
            }

        }

    }

    /**
     * Returns the Document of the application.xml file.
     *
     * @return the Document of the application.xml file.
     */
    public Document getApplicationDoc() {
        return app;
    }

    /**
     * Returns the InputStream of the application.xml file.
     *
     * @return the InputStream of the application.xml file.
     *
     * @throws IOException When InputStream of application.xml cannot be
     *         returned
     */
    public InputStream getApplicationInputStream() throws IOException {
        InputStream is = null;

        if (isPacked()) {
            is = getInputStream("META-INF/application.xml");
        } else {
            is = getInputStream("META-INF" + File.separator + "application.xml");
        }

        return is;
    }

    /**
     * Add a new EjbJar in the Application.
     *
     * @param ejbjar the added EjbJar
     */
    public void addEjbJar(final EjbJar ejbjar) {
        ejbjars.add(ejbjar);
        // add module in application.xml
        XMLUtils.addEjb(app, ejbjar);
    }

    /**
     * Add a new Client in the Application.
     *
     * @param client the added Client
     */
    public void addClient(final Client client) {
        clients.add(client);
        // add module in application.xml
        XMLUtils.addClient(app, client);
    }

    /**
     * Add a new WebApp in the Application.
     *
     * @param webapp the added webapp
     * @param context context of the webapp
     */
    public void addWebApp(final WebApp webapp, final String context) {
        if (!XMLUtils.isWebModuleAlreadyDeclared(app, webapp.getName())) {
            // the web module is not already declared
            webapps.add(webapp);
            // add module in application.xml
            XMLUtils.addWebApp(app, webapp, context);
        }
        // use latest file
        addFile(webapp.getRootFile(), webapp.getName());
    }

    /**
     * Returns the Iterator of EjbJar contained in this Application.
     *
     * @return the Iterator of EjbJar contained in this Application.
     */
    public Iterator getEjbJars() {
        return ejbjars.iterator();
    }

    /**
     * Returns the Iterator of WebApp contained in this Application.
     *
     * @return the Iterator of WebApp contained in this Application.
     */
    public Iterator getWebApps() {
        return webapps.iterator();
    }

    /**
     * Returns the Iterator of WebApp contained in this Application.
     *
     * @return the Iterator of WebApp contained in this Application.
     */
    public Iterator getClients() {
        return clients.iterator();
    }

    /**
     * Returns the ClassLoader of this ear archive.
     *
     * @return the ClassLoader of this ear archive.
     */
    public URLClassLoader getEARClassLoader() {
        return commonCL;
    }

    /**
     * Returns the ClassLoader of the ejbs within this archive.
     *
     * @return the ClassLoader of the ejbs within this archive.
     */
    public URLClassLoader getEJBClassLoader() {
        return ejbCL;
    }

    /**
     * Returns a Map of name to Document for each modified Descriptor of the
     * archive.
     *
     * @return a Map of name to Document
     */
    @Override
    public Map getDescriptors() {
        return descriptors;
    }

    /**
     * Returns true if filename must be omitted in the archive.
     *
     * @param name filename to be tested
     *
     * @return true if filename must be omitted.
     */
    @Override
    public boolean omit(final String name) {
        return (name.equals("META-INF/application.xml") || name.equals("META-INF\\application.xml"));
    }

    /**
     * @param clients The clients to set.
     */
    public void setClients(final List clients) {
        this.clients = clients;
    }

    /**
     * @param ejbjars The ejbjars to set.
     */
    public void setEjbjars(final List ejbjars) {
        this.ejbjars = ejbjars;
    }

    /**
     * @param webapps The webapps to set.
     */
    public void setWebapps(final List webapps) {
        this.webapps = webapps;
    }

    /**
     * @return Returns the app.
     */
    public Document getApp() {
        return app;
    }

    /**
     * Close this archive
     */
    @Override
    public void close() {
        super.close();
        // Remove ear deployment desc
        earDD = null;
        // reset fields
        descriptors = null;
        app = null;
        ejbCL = null;
        commonCL = null;
        pathFiles = null;

        // reset
        for (Iterator i = getEjbJars(); i.hasNext();) {
            EjbJar ejbjar = (EjbJar) i.next();
            ejbjar.close();
        }

        // fill webapp list
        for (Iterator i = getWebApps(); i.hasNext();) {
            WebApp webapp = (WebApp) i.next();
            webapp.close();
        }

        // fill client list
        for (Iterator i = getClients(); i.hasNext();) {
            Client client = (Client) i.next();
            client.close();
        }

        // reset internal lists
        clients = null;
        webapps = null;
        ejbjars = null;
    }
}