/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2007-2012 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: JOnAS.java 22065 2012-01-31 13:14:39Z joaninh $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.launcher.jonas;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.URL;
import java.rmi.RMISecurityManager;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.naming.Context;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.ow2.jonas.launcher.jonas.util.IOUtils;
import org.ow2.jonas.launcher.jonas.util.JOnASUtils;


/**
 * JOnAS Running on OSGi framework.
 * @author Guillaume Sauthier
 */
public class JOnAS {

    /**
     * OSGi Framework.
     */
    private Framework framework;

    /**
     * Property for the security manager.
     */
    private static final String SECURITY_MANAGER = "jonas.security.manager";

    /**
     * Framework factory property.
     */
    private static final String FRAMEWORK_FACTORY_PROPERTY = "META-INF/services/org.osgi.framework.launch.FrameworkFactory";

    /**
     * Property for the TUI bundle name.
     */
    private static final String TUI_BUNDLE_NAME_PROPERTY = "jonas-tui";

    /**
     * Property for the GUI bundle name.
     */
    private static final String GUI_BUNDLE_NAME_PROPERTY = "jonas-gui";

    /**
     * Provider.
     */
    private IConfigurationProvider provider = null;

    /**
     * TUI bundle name.
     */
    private String tuiBundleName;

    /**
     * GUI bundle name.
     */
    private String guiBundleName;

    /**
     * Logger (JDK).
     */
    private static final Logger LOGGER = Logger.getLogger(JOnAS.class.getName());

    /**
     * Creates a new configured framework instance.
     * @param forceCleanUp true if cache directory must be deleted.
     * @throws Exception cannot create the internal gateway.
     */
    public JOnAS(final boolean forceCleanUp) throws Exception {

        System.out.print("\nWelcome to OW2 JOnAS");

        // RMI Security Manager
        boolean useSecurityManager = new Boolean(JOnASUtils.getServerProperty(SECURITY_MANAGER, "true").trim()).booleanValue();

        if (useSecurityManager) {
            if (System.getSecurityManager() == null) {
                System.setSecurityManager(new RMISecurityManager());
            }
        }

        // Init required System properties
        initializeSystemProperties();

        // Get a new provider
        provider = getConfigurationProvider();

        // Retrieve the configuration
        Map<String, String> configProps = provider.getConfiguration();

        // Retrieve the bundle names for the shell
        tuiBundleName = configProps.get(TUI_BUNDLE_NAME_PROPERTY);
        guiBundleName = configProps.get(GUI_BUNDLE_NAME_PROPERTY);
        if (guiBundleName == null) {
            throw new Exception("Property not defined: " + GUI_BUNDLE_NAME_PROPERTY);
        }

        // Delete the cache directory if requested
        if (forceCleanUp) {
            IOUtils.deleteDir(new File(configProps.get(Constants.FRAMEWORK_STORAGE)));
        }

        framework = getFrameworkFactory().newFramework(configProps);

        framework.init();

        // Use of the system bundle to get the version (For example Equinox framework.getVersion() will answer 0.0.0)
        String version = framework.getBundleContext().getBundle(0).getVersion().toString();

        System.out.println(" (Running on " + framework.getClass().getSimpleName() + " v" + version + ").");
        System.out.println("-----------------------------------------------\n");


        // process the auto-install/auto-start bundles
        processBundles();

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                try {
                    // Only stop if the system is still running
                    if (framework.getState() == Bundle.ACTIVE) {
                        framework.stop();
                    }
                } catch (Exception e) {
                    e.printStackTrace(System.err);
                }
            }
        });
    }

    /**
     * Start JOnAS on the gateway.
     * @throws Exception Thrown if the start fails
     */
    public void start() throws Exception {
        // Start it !
        framework.start();

        startTransientBundles();

        // Wait for framework to stop to exit the VM.
        framework.waitForStop(0);
        System.exit(0);
    }

    /**
     * Stop JOnAS.
     * @throws Exception Thrown if the stop fails
     */
    public void stop() throws Exception {
        // Only stop if the system is still running
        if (framework.getState() == Bundle.ACTIVE) {
            framework.stop();
        }
    }

    /**
     * @return a new {@link IConfigurationProvider}.
     */
    protected IConfigurationProvider getConfigurationProvider() {
        // can be sub-classed
        return new DefaultConfigurationProvider();
    }

    /**
     * Start a new JOnAS.
     * @param args not used
     * @throws Exception if something failed
     */
    public static void main(final String[] args) throws Exception {
        JOnAS jonas = new JOnAS(Boolean.getBoolean("jonas.cache.clean"));
        jonas.start();
    }

    /**
     * Initialize required System properties (using reasonable default values
     * when needed).
     */
    protected void initializeSystemProperties() {
        // Set the current date to calculate the server startup time
        System.setProperty("jonas.start.date", Long.toString(System.currentTimeMillis()));

        // Sets the Wrappers that monolog will remove in order to print the
        // right caller
        System.setProperty("monolog.wrappers", "mx4j.log.CommonsLogger,mx4j.log.Logger,java.util.logging.Logger,"
                + "org.apache.commons.logging.impl.Jdk14Logger,"
                + "org.apache.juli.logging.impl.Jdk14Logger,"
                + "org.ow2.util.log.JDKLogger,org.apache.juli.logging.DirectJDKLog,"
                + "org.jgroups.logging.JDKLogImpl,"
                + "org.ow2.carol.util.configuration.TraceCarol,org.slf4j.impl.JCLLoggerAdapter");

        // Corba/JacORB Properties
        System.setProperty("org.omg.CORBA.ORBClass", "org.jacorb.orb.ORB");
        System.setProperty("org.omg.CORBA.ORBSingletonClass", "org.jacorb.orb.ORBSingleton");
        System.setProperty("org.omg.PortableInterceptor.ORBInitializerClass.standard_init",
                "org.jacorb.orb.standardInterceptors.IORInterceptorInitializer");
        // Corba/Carol Properties
        System.setProperty("javax.rmi.CORBA.PortableRemoteObjectClass", "org.ow2.carol.rmi.multi.MultiPRODelegate");
        System.setProperty("javax.rmi.CORBA.UtilClass", "org.ow2.carol.util.delegate.UtilDelegateImpl");

        // Naming properties
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.ow2.carol.jndi.intercept.spi.InterceptorInitialContextFactory");

        // Security Properties
        setDefaultPropertyIfNotSet("java.security.policy", IOUtils.getSystemPath(JOnASUtils.getJOnASBase(), "conf/java.policy"));
        setDefaultPropertyIfNotSet("java.security.auth.login.config", IOUtils.getSystemPath(JOnASUtils.getJOnASBase(),
                "conf/jaas.config"));

    }

    /**
     * @return a framework factory used to instantiate the framework
     * @throws Exception if the factory can't be retrieved
     */
    private FrameworkFactory getFrameworkFactory() throws Exception {
        // Add jar of the framework in a new classloader
        ClassLoader frameworkClassLoader = provider.getFrameworkClassLoader(getClass().getClassLoader());


        URL url = frameworkClassLoader.getResource(FRAMEWORK_FACTORY_PROPERTY);
        if (url != null) {
            BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
            try {
                for (String s = br.readLine(); s != null; s = br.readLine()) {
                    s = s.trim();
                    // Try to load first non-empty, non-commented line.
                    if ((s.length() > 0) && (s.charAt(0) != '#')) {
                        return (FrameworkFactory) Class.forName(s, true, frameworkClassLoader).newInstance();
                    }
                }
            } finally {
                if (br != null) {
                    br.close();
                }
            }
        }
        throw new Exception("Could not find any OSGi framework factory (no '" + FRAMEWORK_FACTORY_PROPERTY + "' property found).");
    }

    /**
     * If the property was already set, do not change its value, otherwise, use
     * the default.
     * @param key property name
     * @param def default property value
     */
    protected void setDefaultPropertyIfNotSet(final String key, final String def) {
        System.setProperty(key, System.getProperty(key, def));
    }

    /**
     * The bundle start operation is transient for TUI and GUI bundles.
     * @throws BundleException If bundles startup fails
     */
    protected void startTransientBundles() throws BundleException {
        Bundle[] bundles = framework.getBundleContext().getBundles();
        for (Bundle bundle : bundles) {
            // OSGi R3 bundle may not have the Bundle-SymbolicName manifest header
            String symbolicName = bundle.getSymbolicName();
            if (symbolicName != null) {

                if (symbolicName.equals(tuiBundleName)) {
                    // Use Text UI ?
                    if (Boolean.getBoolean("jonas.felix.tui.enabled")) {
                        startTransient(bundle);
                    }
                }

                if (symbolicName.startsWith(guiBundleName)) {
                    // Use GUI ?
                    if (Boolean.getBoolean("jonas.felix.gui.enabled")) {
                        startTransient(bundle);
                    }
                }
            }
        }
    }

    /**
     * Auto install and auto start the bundles.
     */
    protected void processBundles() {

        // Bundle context
        BundleContext bundleContext = framework.getBundleContext();

        // Auto install and auto start bundles
        SortedMap<Integer, List<String>> autoInstallBundles = provider.getAutoInstallBundles();
        SortedMap<Integer, List<String>> autoStartBundles = provider.getAutoStartBundles();

        // Merge keys (levels)
        Set<Integer> levels = new TreeSet<Integer>();
        levels.addAll(autoInstallBundles.keySet());
        levels.addAll(autoStartBundles.keySet());

        Iterator<Integer> levelIterator = levels.iterator();

        List<Bundle> bundlesToStart = new ArrayList<Bundle>();

        // For each level, install the given bundles and specify the start level for these bundles
        while (levelIterator.hasNext()) {
            Integer level = levelIterator.next();

            // Auto install or auto start bundles ?
            List<String> autoInstallBundleLocations = autoInstallBundles.get(level);
            List<String> autoStartBundleLocations = autoStartBundles.get(level);

            // for each location, install the bundle and sets the start level
            if (autoInstallBundleLocations != null && !autoInstallBundleLocations.isEmpty()) {
                for (String bundleLocation : autoInstallBundleLocations) {

                    //install the bundle only if it's not already installed in the felix cache
                    Bundle bundle = isDeploy(bundleLocation);
                    if (bundle == null) {
                        try {
                            bundle = bundleContext.installBundle(bundleLocation);
                            bundle.adapt(BundleStartLevel.class).setStartLevel(level);
                        } catch (BundleException e) {
                            LOGGER.log(Level.SEVERE, "Unable to install the bundle with location '" + bundleLocation
                                    + "' and the startlevel '" + level + "'.", e);
                        }
                    }
                }
            }

            // for auto start, install them and add it to the list of bundles to be started
            if (autoStartBundleLocations != null && !autoStartBundleLocations.isEmpty()) {
                for (String bundleLocation : autoStartBundleLocations) {

                    //install the bundle only if it's not already installed in the felix cache
                    Bundle bundle = isDeploy(bundleLocation);
                    if (bundle == null) {
                        try {
                            bundle = bundleContext.installBundle(bundleLocation);
                            bundle.adapt(BundleStartLevel.class).setStartLevel(level);
                        } catch (BundleException e) {
                            LOGGER.log(Level.SEVERE, "Unable to install the bundle with location '" + bundleLocation
                                    + "' and the startlevel '" + level + "'.", e);
                        }
                    }
                    if (bundle != null && (bundle.getState() == Bundle.INSTALLED || bundle.getState() == Bundle.RESOLVED)) {
                        //Bundle needs to be started
                        bundlesToStart.add(bundle);
                    }
                }
            }

        }

        // Start the bundles in the specified order
        for (Bundle bundle : bundlesToStart) {
            try {
                bundle.start();
            } catch (BundleException e) {
                LOGGER.log(Level.SEVERE, "Unable to start the bundle with name '" + bundle.getSymbolicName() + "'", e);
            }
        }
    }

    /**
     * @param bundleLocation A bundle location
     * @return the Bundle if it's already installed in the felix cache. Null if the bundle is not in the felix cache
     */
    private Bundle isDeploy(final String bundleLocation) {
        BundleContext bundleContext = this.framework.getBundleContext();
        for (Bundle bundle: bundleContext.getBundles()) {
            if (bundle.getLocation().equals(bundleLocation)) {
                return bundle;
            }
        }
        return null;
    }

    /**
     * The bundle start operation is transient for the given bundle.
     * @param bundle the given bundle.
     * @throws BundleException If bundle startup fails
     */
    protected void startTransient(final Bundle bundle) throws BundleException {
        bundle.start(Bundle.START_TRANSIENT);
    }

    /**
     * @return the Framework instance used by this launcher.
     */
    public Framework getFramework() {
        return framework;
    }

}
