/**
 * JOnAS: Java(TM) Open 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
 *
 * Initial developer(s): Florent BENOIT & Ludovic BERT
 * --------------------------------------------------------------------------
 * $Id: EarClassPathManager.java 18550 2009-09-09 09:05:27Z fornacif $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.cpmanager;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.lib.util.Log;
import org.ow2.util.url.URLUtils;

/**
 * JOnAS Ear class path manager class. This class provides a way for managing
 * the class-path dependency libraries.
 * @author Florent Benoit
 * @author Ludovic Bert
 * @author Nicolas Van Caneghem <nicolas.vancaneghem@openpricer.com>Allow the
 *         deployment of an exploded ear
 */
public class EarClassPathManager {

    /**
     * Logger for this service.
     */
    private Logger logger = null;

    /**
     * Urls of the class path manager.
     */
    private URL[] urls = null;

    /**
     * List of jars that must be parsed.
     */
    private JarList toParse = null;

    /**
     * List of jars that have been parsed.
     */
    private JarList parsed = null;

    /**
     * List of jars which are libraries.
     */
    private JarList libraries = null;

    /**
     * List of the ejb jars.
     */
    private JarList ejbs = null;

    /**
     * List of the war files.
     */
    private JarList wars = null;

    /**
     * List of the client jars.
     */
    private JarList clients = null;

    /**
     * Directory of the ear.
     */
    private URL directory = null;

    /**
     * Construct an instance of a EarClassPathManager.
     * @param ejbs JarList of ejb-jar
     * @param wars JarList of war
     * @param directory URL of the directory
     * @throws EarClassPathManagerException if we can't create the manager
     */
    public EarClassPathManager(final JarList ejbs, final JarList wars, final URL directory) throws EarClassPathManagerException {

        //we init the logger
        logger = Log.getLogger(Log.JONAS_EAR_PREFIX);

        if ((ejbs == null) || (wars == null) || (directory == null)) {
            throw new EarClassPathManagerException("The constructor EarClassPathManager can't accept null parameters");
        }

        //check protocol
        if (!directory.getProtocol().equalsIgnoreCase("file")) {
            throw new EarClassPathManagerException("Only the file:/ URL can be used");
        }
        this.ejbs = ejbs;
        this.wars = wars;
        this.clients = new JarList();
        this.directory = directory;
    }

    /**
     * Construct an instance of a EarClassPathManager.
     * @param clients JarList of the clients
     * @param directory URL of the directory
     * @throws EarClassPathManagerException if we can't create the manager
     */
    public EarClassPathManager(final JarList clients, final URL directory) throws EarClassPathManagerException {

        if ((clients == null) || (directory == null)) {
            throw new EarClassPathManagerException("The constructor EarClassPathManager can't accept null parameters");
        }

        //check protocol
        if (!directory.getProtocol().equalsIgnoreCase("file")) {
            throw new EarClassPathManagerException("Only the file:/ URL can be used");
        }
        this.ejbs = new JarList();
        this.wars = new JarList();
        this.clients = clients;
        this.directory = directory;
    }

    /**
     * Get the class-path from the MANIFEST.MF file of the specified archive.
     * @param url the URL of the JAR file which contains the MANIFEST file.
     * @return the class-path from the MANIFEST.MF file of the specified
     *         archive.
     * @throws IOException if creation of a file based upon the url failed
     * @throws EarClassPathManagerException if it failed
     */
    private JarList getManifestClassPath(final URL url) throws EarClassPathManagerException, IOException {

        if (url == null) {
            throw new EarClassPathManagerException("JarList.getManifestClassPath : The url parameter can't be null");
        }

        Manifest manifest = null;

        if (new File(url.getFile()).isDirectory()) {
            File manifestFile = new File(url.getFile() + File.separator + JarFile.MANIFEST_NAME);
            if (manifestFile.exists()) {
                InputStream is = null;
                try {
                    is = new FileInputStream(manifestFile);
                    manifest = new Manifest(is);
                } finally {
                    if (is != null) {
                        is.close();
                    }
                }
            }

        } else {
            //Construct a JarFile in order to access to the manifest
            // IOException it if failed
            JarFile jarFile = null;

            try {
                jarFile = new JarFile(URLUtils.urlToFile(url));
                //get manifest from the jarFile
                manifest = jarFile.getManifest();
            } finally {
                if (jarFile != null) {
                    jarFile.close();
                }
            }

        }

        //classpath
        String classPath = null;

        //Only if a manifest is found
        if (manifest != null) {
            //get attributes (classpath)
            Attributes attributes = manifest.getMainAttributes();
            classPath = attributes.getValue(Attributes.Name.CLASS_PATH);
        }

        //New JarList
        JarList jarList = null;

        //The jarList will be Empty if classpath is null or populate with
        // classpath entries
        if (classPath != null) {
            jarList = new JarList(new StringTokenizer(classPath));
        } else {
            jarList = new JarList();
        }

        //Return the list
        return jarList;
    }

    /**
     * Get the list of the URLs.
     * @return the list of the URLs.
     * @throws EarClassPathManagerException if we can't resolve the path
     */
    public URL[] getResolvedClassPath() throws EarClassPathManagerException {

        //If we don't have already compute
        if (urls == null) {
            resolveClassPath();
        }

        //return the cache
        return urls;
    }

    /**
     * Resolve the class-path dependencies of WAR and JAR files.
     * @throws EarClassPathManagerException if it failed
     */
    private void resolveClassPath() throws EarClassPathManagerException {

        //Set the list to parsed
        toParse = new JarList();

        //Set the list already parsed
        parsed = new JarList();

        //Set the list of libraries
        libraries = new JarList();

        //add the ejbs, wars and clients to this list
        toParse.merge(ejbs);
        toParse.merge(wars);
        toParse.merge(clients);

        //dependencies list
        JarList lstOfFilesDep = new JarList();

        //Url of the current filename
        URL depUrl = null;

        //While there are elements to analyse
        while (toParse.size() > 0) {

            //File to look for Manifest
            String fileName = (String) toParse.firstElement();

            if (fileName.endsWith("/")) {
                throw new EarClassPathManagerException("In j2ee application, Class-Path with directory is forbidden. '"
                        + fileName + "' is not authorized.");
            }
            try {
                //Get dependency entries
                depUrl = new URL(directory.toExternalForm() + "/" + fileName);
                lstOfFilesDep = getManifestClassPath(depUrl);
            } catch (MalformedURLException mue) {
                lstOfFilesDep.removeAllElements();
                logger.log(BasicLevel.ERROR, "Error while trying to get the url for "
                        + directory.toExternalForm() + File.separator + fileName + " : ", mue);
            } catch (IOException ioe) {
                lstOfFilesDep.removeAllElements();
                logger.log(BasicLevel.ERROR, "Error while reading manifest file from the file " + fileName
                        + " : ", ioe);
            }

            String parentDir = new File(fileName).getParent();
            //subDirectory (the parent dir or "")
            String subDir = null;
            if (parentDir != null) {
                subDir = parentDir;
            } else {
                subDir = "";
            }

            //Set the relative path of EAR / file
            lstOfFilesDep.setRelativePath(subDir);

            //Merge the list
            toParse.merge(lstOfFilesDep);

            //Add the parsed file
            parsed.add(fileName);

            //Add to the libraries if it's not an EJB or a WEB application
            if (isALibrary(fileName)) {
                libraries.add(fileName);
            }

            //Remove all the parsed files
            toParse.remove(parsed);

        }

        //We've got the list of files, its the JarList : parsed
        try {
            urls = libraries.getURLs(directory.toExternalForm());
        } catch (JarListException e) {
            throw new EarClassPathManagerException(
                    "Error while geting the URLs of the jars files which must be loaded at the EAR level");

        }

    }

    /**
     * Check if the file is a library , ie : - It's not an EJB Jar. - It's not a
     * War.
     * @param fileName name of the file to check
     * @return true if it's not either an ejbjar either a war file. (a library).
     */
    private boolean isALibrary(final String fileName) {
        return (!ejbs.contains(fileName) && !wars.contains(fileName));
    }

}
