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

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.felix.main.AutoProcessor;
import org.osgi.framework.Constants;
import org.ow2.jonas.launcher.felix.util.IOUtils;
import org.ow2.jonas.launcher.felix.util.JOnASUtils;
import org.ow2.jonas.launcher.felix.util.Maven2Utils;
import org.ow2.util.substitution.ISubstitutionEngine;
import org.ow2.util.substitution.engine.DefaultSubstitutionEngine;
import org.ow2.util.substitution.resolver.ChainedResolver;
import org.ow2.util.substitution.resolver.PropertiesResolver;
import org.ow2.util.substitution.resolver.RecursiveResolver;

/**
 * Default {@link IConfigurationProvider} for Apache Felix.
 * @author Guillaume Sauthier
 */
public class DefaultConfigurationProvider implements IConfigurationProvider {

    /**
     * Property for the Felix bundles file.
     */
    private static final String FELIX_BUNDLES_CONFIG_FILE_PROP = "jonas.felix.bundles.configuration.file";

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

    /**
     * Property substitution engine.
     */
    private ISubstitutionEngine substitutionEngine;

    /**
     * The list of resolvers to be used.
     */
    private ChainedResolver resolver;

    /**
     * Default constructor.
     */
    public DefaultConfigurationProvider() {
        resolver = new ChainedResolver();
        this.substitutionEngine = createSubstitutionEngine();
    }

    /**
     * Can be overridden if required.
     * @return the substituion engine that will be used for variable value resolution.
     */
    protected ISubstitutionEngine createSubstitutionEngine() {
        DefaultSubstitutionEngine engine = new DefaultSubstitutionEngine();
        engine.setMarkerChar('$');
        engine.setOpeningChar('{');
        engine.setEndingChar('}');
        engine.setResolver(new RecursiveResolver(engine, resolver));
        return engine;
    }

    /**
     * @return a Felix default configuration.
     * @throws IOException configuration not found
     */
    public Map<?, ?> getConfiguration() throws IOException {

        // 1. Load javase-profiles.properties
        // User shouldn't override theses properties, so they're only retrieved as class resources
        Properties javaProfiles = IOUtils.getPropertiesFromClass("javase-profiles.properties", JOnAS.class);

        resolver.getResolvers().add(new PropertiesResolver(System.getProperties()));
        resolver.getResolvers().add(new PropertiesResolver(javaProfiles));

        resolveProperties(javaProfiles);

        // 2. Load defaults.properties
        Properties defaultsProperties;
        File defaultsFile = IOUtils.getSystemFile(JOnASUtils.getJOnASBase(), "conf/osgi/defaults.properties");
        if (defaultsFile.exists()) {
            defaultsProperties = IOUtils.getPropertiesFromFile(defaultsFile);
        } else {
            defaultsProperties = IOUtils.getPropertiesFromClass("defaults.properties", JOnAS.class);
        }

        // Add a special property 'javase.version'
        // Detect the JVM version (1.5 / 1.6), fallback on 1.5
        defaultsProperties.setProperty("javase.version", javaSeSpecificationVersion());

        // Add a new resolver
        resolver.getResolvers().add(new PropertiesResolver(defaultsProperties));

        // Resolve the variables
        resolveProperties(defaultsProperties);

        // 3. gateway.properties
        Properties gatewayProperties;
        File gatewayFile = IOUtils.getSystemFile(JOnASUtils.getJOnASBase(), "conf/osgi/gateway.properties");
        if (gatewayFile.exists()) {
            gatewayProperties = IOUtils.getPropertiesFromFile(gatewayFile);
        } else {
            gatewayProperties = IOUtils.getPropertiesFromClass("gateway.properties", JOnAS.class);
        }

        // Add a new resolver
        resolver.getResolvers().add(new PropertiesResolver(gatewayProperties));

        // Resolve the variables
        resolveProperties(gatewayProperties);

        // Augment that basic configuration with bundles to be started during gateway start-up
        // TODO Use the same system as above to easily update the initial set of bundle to be started
        findAutoDeployedBundles(gatewayProperties);

        // Set the Felix cache root directory in working directory (folder named felix-cache)
        File cacheDirectory = IOUtils.getSystemFile(JOnASUtils.getWorkDirectory(), "felix-cache");
        gatewayProperties.put(Constants.FRAMEWORK_STORAGE, cacheDirectory.getAbsolutePath());

        return gatewayProperties;
    }

    /**
     * Resolve this set of properties with the given resolver.
     * @param properties properties to be resolved
     */
    @SuppressWarnings("unchecked")
    private void resolveProperties(final Properties properties) {

        Enumeration<String> names = (Enumeration<String>) properties.propertyNames();
        while (names.hasMoreElements()) {
            String name = names.nextElement();

            String value = properties.getProperty(name);
            String systemValue = System.getProperty(name);

            // Override with System properties if possible
            if (systemValue != null) {
                value = systemValue;
            }

            // Resolve the value
            String resolved = substitutionEngine.substitute(value);
            properties.setProperty(name, resolved);
        }
    }

    /**
     * Gather a Java specification version (1.5 / 1.6) from system package, system
     * properties or use a default value.
     * @return the Java specification version supported by this JVM
     */
    private static String javaSeSpecificationVersion() {

        VersionNumber version;

        // Try package version
        String value = System.class.getPackage().getSpecificationVersion();
        if (value != null) {
            version = new VersionNumber(value);
        } else {
            // Try system property (with default to 1.5)
            value = System.getProperty("java.specification.version", "1.5");
            version = new VersionNumber(value);
        }

        return version.getMajorMinor();
    }

    /**
     * Update the given configuration with start level options.
     * @param config configuration to enhance.
     * @throws IOException if default property file is not found.
     */
    private void findAutoDeployedBundles(final Properties config) throws IOException {

        // Felix bundles configuration properties
        Properties levels = null;

        // Get the system property "jonas.felix.configuration.file"
        String felixBundlesConfigFilename = System.getProperty(FELIX_BUNDLES_CONFIG_FILE_PROP);

        // Check if it exists
        if (felixBundlesConfigFilename != null && IOUtils.exists(felixBundlesConfigFilename)) {
            // Get the configuration file
            levels = IOUtils.getPropertiesFromFile(new File(felixBundlesConfigFilename));
        } else {
            // Get the default bundles configuration file
            levels = getAutoDeployedBundles();
        }

        // Iterates over the properties
        for (Iterator<Object> it = levels.keySet().iterator(); it.hasNext();) {

            // Process level properties
            String key = (String) it.next();

            if (key.startsWith("install.level.")) {
                // Get the associated level
                String level = key.substring("install.level".length());

                // Configure the bundle list to install
                config.put(AutoProcessor.AUTO_INSTALL_PROP.concat(level), getBundleList(levels, key));
            }

            if (key.startsWith("start.level.")) {
                // Get the associated level
                String level = key.substring("start.level".length());

                // Configure the bundle list to start
                config.put(AutoProcessor.AUTO_START_PROP.concat(level), getBundleList(levels, key));
            }
        }

        // Bundles with start level of 1
        String newList = (String) config.get(AutoProcessor.AUTO_START_PROP.concat(".1"));
        if (newList == null) {
           newList = "";
        }

        // Adds the bootstrap bundles that the user may specify.
        File jonasBootStrapBundlesDir = IOUtils.getSystemFile(JOnASUtils.getJOnASRoot(), "lib/bootstrap/bundles");
        String jonasBootStrapBundlesList = getBundles(jonasBootStrapBundlesDir);
        // Concatenate this list with the existing list
        if (!"".equals(jonasBootStrapBundlesList)) {
            newList = jonasBootStrapBundlesList.concat(" ").concat(newList);
        }

        // JPA 2.0
        String jpaProviders = "";
        try {
            jpaProviders = JOnASUtils.getServerProperty("jonas.service.ejb3.jpa.provider", "");
        } catch (Exception e) {
            throw new IllegalStateException("Cannot get ejb3 persistence providers property", e);
        }
        if (jpaProviders.contains("eclipselink2") || jpaProviders.contains("hibernate3.5") || jpaProviders.contains("openjpa2")) {
            File jpa2BundlesDir = IOUtils.getSystemFile(JOnASUtils.getJOnASRoot(), "lib/bootstrap/bundles-jpa2.0");
            String bundleList = getBundles(jpa2BundlesDir);
            // Concatenate this list with the existing list
            if (!"".equals(bundleList)) {
                newList = bundleList.concat(" ").concat(newList);
            }
        }

        // Adds JSP 2.2 / Servlet 3.0 / EL 2.2 if there is servlet 3 container
        String webServiceClass = "";
        try {
            webServiceClass = JOnASUtils.getServerProperty("jonas.service.web.class", "");
        } catch (Exception e) {
            throw new IllegalStateException("Cannot get jonas.service.web.class property", e);
        }
        if (webServiceClass.contains("7")) {
            File servlets3BundlesDir = IOUtils.getSystemFile(JOnASUtils.getJOnASRoot(), "lib/bootstrap/bundles-servlets-3.0");
            String bundleList = getBundles(servlets3BundlesDir);
            // Concatenate this list with the existing list
            if (!"".equals(bundleList)) {
                newList = bundleList.concat(" ").concat(newList);
            }
        }


        // Add interceptor (v1.1) API bundle for CDI
        // This is required because if we ship that API bundle in the 'cdi' deployment plan, it comes too
        // late and it causes class spaces inconsistencies (with JSF for examples that uses the 'old'
        // javax.interceptor package) causing cascading errors when deploying cdi enabled web applications.
        String services = "";
        try {
            services = JOnASUtils.getServerProperty("jonas.services", "");
        } catch (Exception e) {
            throw new IllegalStateException("Cannot get 'jonas.services' property in conf/jonas.properties", e);
        }
        List<String> sections = Arrays.asList(services.split(","));
        if (sections.contains("cdi")) {

            File cdiBundlesDir = IOUtils.getSystemFile(JOnASUtils.getJOnASRoot(), "lib/bootstrap/bundles-cdi");
            String bundleList = getBundles(cdiBundlesDir);
            // Concatenate this list with the existing list
            if (!"".equals(bundleList)) {
                newList = bundleList.concat(" ").concat(newList);
            }
        }

        // Define new list
        config.put(AutoProcessor.AUTO_START_PROP.concat(".1"), newList);

    }

    /**
     * @param directory Directory to look for bundles.
     * @return A space (' ') separated list of URLs corresponding to the
     *         files in the given directory. An empty String if the directory
     *         does not exists.
     * @throws IOException when a bundle File cannot be turned into an URL
     */
    protected String getBundles(final File directory) throws IOException {
        String bundleList = "";
        if (directory.exists()) {
            // list all files
            FilenameFilter filter = new FilenameFilter() {
                public boolean accept(final File dir, final String name) {
                    return name.endsWith(".jar");
                }
            };
            File[] bundles = directory.listFiles(filter);
            LOGGER.log(Level.FINE, "Adding bundles from the directory '" + directory + "' :" + Arrays.asList(bundles));
            // Add each URL of the bundle
            for (File file : bundles) {
                if (!bundleList.equals("")) {
                    bundleList = bundleList.concat(" ");
                }
                bundleList = bundleList.concat("reference:").concat(file.toURI().toURL().toExternalForm());
            }
        }
        return bundleList;
    }


    /**
     * Get the bundle list for a given property.
     * @param levels The property list
     * @param key The searched key
     * @return The bundle list for a given property.
     * @throws IOException If the list cannot be built.
     */
    private String getBundleList(final Properties levels, final String key) throws IOException {
        String value = levels.getProperty(key);
        StringBuilder sb = new StringBuilder();

        // The bundle list separator is ','
        String[] bundles = value.split(",");
        for (int i = 0; i < bundles.length; i++) {
            String bundle = bundles[i];

            // Only process non-empty parts
            if (!"".equals(bundle)) {

                // The bundle is specified using the following format:
                // <groupId>:<artifactId>[:<version>]
                String[] artifact = bundle.split(":");

                String groupId = artifact[0].trim();
                String artifactId = artifact[1].trim();
                String version = null;
                String classifier = null;

                if (artifact.length == 3) {
                    // Is the third element a version or a
                    // classifier ?
                    classifier = getClassifier(artifact[2]);
                    if (classifier == null) {
                        // this is NOT a classifier
                        version = artifact[2].trim();
                    }
                } else if (artifact.length == 4) {
                    // We have both version + classifier
                    // first is the version
                    version = artifact[2].trim();
                    // then, the classifier
                    classifier = getClassifier(artifact[3]);
                    if (classifier == null) {
                        // in this case, this is an error
                        throw new IOException("Incorrect classifier in bundle: " + bundle);
                    }
                } else if (artifact.length > 4) {
                    // More elements, invalid
                    throw new IOException("Incorrect number of parts in bundle: " + bundle);
                }

                // no version specified, get the default one
                if (version == null) {
                    version = JOnASUtils.getVersion();
                }

                // Use the reference attribute to load the bundle
                // from the local file without copying it into the
                // cache of Felix
                sb.append("reference:");

                File repository = null;
                if (JOnASUtils.isDeveloperMode()) {
                    // Use m2 repository
                    repository = Maven2Utils.getMaven2Repository();
                } else {
                    // Use repositories/internal
                    repository = Maven2Utils.getMaven2InternalRepository();
                }
                sb.append(Maven2Utils.getBundleMaven2Location(repository.getPath(), groupId, artifactId, version, classifier)
                        .concat(" "));
            }
        }

        return sb.toString();
    }

    /**
     * @param value value from which the classifier will be extracted
     * @return the classifier value if this is a valid classifier, null otherwise
     */
    private static String getClassifier(final String value) {
        String classifier = null;
        String trimmed = value.trim();
        if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
            // classifier
            classifier = trimmed.substring(1, trimmed.length() - 1);
        }
        return classifier;
    }

    /**
     * @return the config file for auto deployed bundles.
     * @throws IOException file not found
     */
    private static Properties getAutoDeployedBundles() throws IOException {
        return IOUtils.getPropertiesFromClass("jonas-autodeploy-bundles.properties", JOnAS.class);
    }

}
