/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2007-2011 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.extension.loader;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.service.packageadmin.PackageAdmin;
import org.ow2.jonas.properties.ServerProperties;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.ow2.util.url.URLUtils;

import aQute.lib.osgi.Analyzer;

/**
 * This service will load all the jar file from lib/ext and convert them into bundles.
 * If the transformation has already been done for a given jar, it's not re-done.
 * @author Guillaume Sauthier
 */
public class ExtensionLoaderComponent {

    /**
     * Logger.
     */
    private static final Log logger = LogFactory.getLog(ExtensionLoaderComponent.class);

    /**
     * The bundle's context.
     */
    private BundleContext context;

    /**
     * Server Properties.
     */
    private ServerProperties serverProps;

    /**
     * Installed bundles.
     */
    private List<Bundle> bundles;

    /**
     * All the bundlizables.
     */
    private Map<String, Bundlizable> bundlizables;

    /**
     * Used to pre-resolve installed bundles.
     */
    private PackageAdmin packageAdmin;

    /**
     * Construct a new Extension Loader.
     * @param context this module's Context
     */
    public ExtensionLoaderComponent(final BundleContext context) {
        this.context = context;
        bundles = new ArrayList<Bundle>();
        bundlizables = new HashMap<String, Bundlizable>();
    }

    /**
     * At startup, convert all jar files from lib/ext into bundles.
     */
    public void start() {

        bundles.clear();
        bundlizables.clear();

        // Find resources in lib/ext
        File jonasRoot = new File(System.getProperty("jonas.root"));
        File jonasBase = new File(System.getProperty("jonas.base"));


        File rootLibExt = new File(new File(jonasRoot, "lib"), "ext");
        File baseLibExt = new File(new File(jonasBase, "lib"), "ext");
        final File bundleWork = new File(serverProps.getWorkDirectory(), "ext-bundles");

        if (!bundleWork.exists()) {
            bundleWork.mkdirs();
        }

        FileFilter selectJar = new FileFilter() {
            public boolean accept(final File child) {
                // Do not manage non .jar files
                return (child.getName().endsWith(".jar") && child.isFile());
            }
        };

        // Iterates on the 'sources' directory take #1
        if (rootLibExt.isDirectory()) {
            for (File child : rootLibExt.listFiles(selectJar)) {
                String name = child.getName();
                Bundlizable bnd = bundlizables.get(name);
                if (bnd == null) {
                    bnd = new Bundlizable();
                    bnd.setSource(child);
                    bundlizables.put(child.getName(), bnd);
                }
            }
        }
        if (baseLibExt.isDirectory() && !rootLibExt.equals(baseLibExt)) {
            // ... and on the 'sources' directory take #2
            for (File child : baseLibExt.listFiles(selectJar)) {
                String name = child.getName();
                Bundlizable bnd = bundlizables.get(name);
                if (bnd == null) {
                    bnd = new Bundlizable();
                    bnd.setSource(child);
                    bundlizables.put(child.getName(), bnd);
                }
            }
        }

        // Now, list the 'target' directory
        for (File child : bundleWork.listFiles(selectJar)) {
            String name = child.getName();
            Bundlizable bnd = bundlizables.get(name);
            if (bnd == null) {
                bnd = new Bundlizable();
                bnd.setTarget(child);
                bundlizables.put(child.getName(), bnd);
            }
        }


        for (Iterator<Map.Entry<String,Bundlizable>> i = bundlizables.entrySet().iterator(); i.hasNext();) {
            Map.Entry<String,Bundlizable> entry = i.next();
            Bundlizable bnd = entry.getValue();

            if (bnd.isSourceSuppressed()) {
                // Now, we will remove the bundles that have no more a source file:
                // i.e it was suppressed from lib/ext, seems logical to not install the bundles anymore
                // We also need to uninstall the bundle if it was previously installed
                File target = bnd.getTarget();
                uninstall(target);
                target.delete();
                i.remove();
            } else if (bnd.isFirstUsage()) {
                // Initial bundle creation
                File target = new File(bundleWork, bnd.getSource().getName());
                bnd.setTarget(target);
                applyBndWrap(bnd.getSource(), target);
            } else if (bnd.isTargetOutdated()) {
                // Needs to update the bundle
                bnd.getTarget().delete();
                applyBndWrap(bnd.getSource(), bnd.getTarget());
            } // else nothing to do
        }


        if (bundlizables.size() != 0) {
            StringBuilder sb = new StringBuilder();
            sb.append("Some jars have been found in [");
            sb.append(rootLibExt);
            if (!rootLibExt.equals(baseLibExt)) {
                sb.append(", ");
                sb.append(baseLibExt);
            }
            sb.append("].");
            sb.append(" They have been transformed into bundles (see ");
            sb.append(bundleWork);
            sb.append(" directory).");

            sb.append("This is NOT the preferred way to extend JOnAS libraries, ");
            sb.append("prefer to use carefully created and tested bundles, ");
            sb.append("and place them in your deploy/ directory.");

            logger.info(sb.toString());
        }
        for (Iterator<Map.Entry<String,Bundlizable>> i = bundlizables.entrySet().iterator(); i.hasNext();) {
            Map.Entry<String,Bundlizable> entry = i.next();
            Bundlizable bnd = entry.getValue();

            // Try to install the bundle
            String url = URLUtils.fileToURL(bnd.getTarget()).toExternalForm();
            try {
                Bundle installed = context.installBundle(url);
                logger.info("{0} installed as a bundle.", url);
                bundles.add(installed);
            } catch (BundleException e) {
                logger.warn("Unable to install '{0}': {1}", url, e.getMessage());
            }
        }

        // Resolve installed bundles
        Bundle[] resolvables = bundles.toArray(new Bundle[bundles.size()]);
        if (resolvables != null) {
            boolean resolved = packageAdmin.resolveBundles(resolvables);
            if (!resolved) {
                logger.debug("Some lib/ext bundles could not be resolved");
            }
        }
    }

    /**
     * Uninstall this bundle from the gateway.
     * @param target the given file to uninstall
     */
    protected void uninstall(final File target) {
        // Do nothing if file is not here
        if (target == null || !target.exists()) {
            return;
        }

        // Load symbolic-name of the target
        String symbolicName = null;
        JarFile jar = null;
        try {
            jar = new JarFile(target);
            symbolicName = jar.getManifest().getMainAttributes().getValue(Analyzer.BUNDLE_SYMBOLICNAME);
        } catch (IOException e) {
            logger.error("Unable to find symbolic name from the bundle ''{0}''", target);
            return;
        } finally {
            if (jar != null) {
                try {
                    jar.close();
                } catch (IOException e) {
                    logger.debug("Unable to close the bundle file ''{0}''", target);
                }
            }
        }

        // Unable to find it, skip
        if (symbolicName == null) {
            logger.error("Unable to find symbolic name from the bundle ''{0}''", target);
            return;
        }


        // Now, search the bundle
        boolean bundleFound = false;
        int i = 0;
        Bundle[] bundles = context.getBundles();
        Bundle bundle = null;
        if (bundles != null) {
            while (!bundleFound  && i < bundles.length) {
                bundle = bundles[i];
                if (symbolicName.equals(bundle.getSymbolicName())) {
                    // found
                    bundleFound = true;
                    break;
                }
                i++;
            }
        }
        // Uninstall the bundle if it has been found
        if (bundleFound) {
            try {
                logger.info("Uninstalling the bundle ''{0}'' as the origin file has been removed from lib/ext", target);
                bundle.uninstall();
            } catch (BundleException e) {
                logger.warn("Unable to uninstall ''{0}''", target, e);
            }
        }

    }


    private void applyBndWrap(final File source, final File output) {
        Bnd myBnd = new Bnd();
        boolean errors = false;
        try {
            // default wrapping (Export all content)
            errors = myBnd.wrap(source, output);
        } catch (Exception e) {
            logger.warn("Unable to bundlize '{0}'", source, e);
        }

        // Only prints errors in debug mode
        if (errors && logger.isDebugEnabled()) {
            List<String> messages = myBnd.getErrors();
            if (messages != null) {
                for (String message : messages) {
                    logger.debug("[Bnd] {0}: {1}", source.getName(), message);
                }
            }
        }
    }

    /**
     * After stop, all installed bundles must be uninstalled.
     */
    public void stop() {
        for (Bundle bundle : bundles) {
            try {
                bundle.uninstall();
            } catch (BundleException e) {
                logger.debug("Unable to uninstall bundle '{0}[{1}]': {2}",
                             bundle.getSymbolicName(),
                             bundle.getBundleId(),
                             e.getMessage());
            }
        }
    }

    /**
     * Inject PackageAdmin
     * @param packageAdmin admin interface.
     */
    public void setPackageAdmin(final PackageAdmin packageAdmin) {
        this.packageAdmin = packageAdmin;
    }

    /**
     * Set the Server Properties.
     * @param props {@link ServerProperties} instance
     */
    public void setServerProperties(final ServerProperties props) {
        this.serverProps = props;
    }
}

