/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2007-2008 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 org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.service.packageadmin.PackageAdmin;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.ow2.util.url.URLUtils;

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

/**
 * 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;

    /**
     * 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 jonasBaseWork = new File(jonasBase, "work");

        File rootLibExt = new File(new File(jonasRoot, "lib"), "ext");
        File baseLibExt = new File(new File(jonasBase, "lib"), "ext");
        final File bundleWork = new File(jonasBaseWork, "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
                bnd.getTarget().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 prefered way to extends 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");
            }
        }
    }

    private void applyBndWrap(File source, 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;
    }
}

