/**
 * 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 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: DefaultConfigurationProvider.java 22065 2012-01-31 13:14:39Z joaninh $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.launcher.jonas;

import static org.ow2.jonas.launcher.jonas.util.IOUtils.getPropertiesFromClass;
import static org.ow2.jonas.launcher.jonas.util.IOUtils.getPropertiesFromFile;
import static org.ow2.jonas.launcher.jonas.util.IOUtils.props2Map;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
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.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.osgi.framework.Constants;
import org.ow2.jonas.launcher.jonas.util.IOUtils;
import org.ow2.jonas.launcher.jonas.util.JOnASUtils;
import org.ow2.jonas.launcher.jonas.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 bundles to start at startup.
     */
    private static final String JONAS_BUNDLES_CONFIG_FILE_PROP = "jonas.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;

    /**
     * List of auto install bundle locations (per level).
     */
    private SortedMap<Integer, List<String>> autoInstallLevelBundlesLocationMap = null;

    /**
     * List of auto start bundle locations (per level).
     */
    private SortedMap<Integer, List<String>> autoStartLevelBundlesLocationMap = null;

    /**
     * Default constructor.
     */
    public DefaultConfigurationProvider() {
        resolver = new ChainedResolver();
        this.substitutionEngine = createSubstitutionEngine();
        this.autoInstallLevelBundlesLocationMap = new TreeMap<Integer, List<String>>();
        this.autoStartLevelBundlesLocationMap = new TreeMap<Integer, List<String>>();
    }

    /**
     * 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 java.io.IOException configuration not found
     */
    public Map<String, String> 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 = getPropertiesFromFile(gatewayFile);
        } else {
            gatewayProperties = 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
        //
        Properties autoDeployProperties = null;
        File autoDeployFile = IOUtils.getSystemFile(JOnASUtils.getJOnASBase(), "conf/osgi/auto-deploy.properties");
        if (autoDeployFile.exists()) {
            autoDeployProperties = IOUtils.getPropertiesFromFile(autoDeployFile);
        } else {
            autoDeployProperties = IOUtils.getPropertiesFromClass("auto-deploy.properties", JOnAS.class);
        }
        initLevelBundles(autoDeployProperties);

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

        return props2Map(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();
    }


    /**
     *
     */
    protected void initLevelBundles(final Properties autoDeployProperties) throws IOException {
        // Iterates over the properties
        for (Iterator<Object> it = autoDeployProperties.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());

                // Add list of bundles locations for the given level
                autoInstallLevelBundlesLocationMap.put(Integer.valueOf(level), getBundleLocations(autoDeployProperties, key));

            }

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

                // Add list of bundles locations for the given level
                autoStartLevelBundlesLocationMap.put(Integer.valueOf(level), getBundleLocations(autoDeployProperties, key));
            }
        }


        // Create a list with bundles that needs to be started before all the other bundles of start level 1
        List<String> extraBundleListLocationLevelOne = new ArrayList<String>();


        // Adds the bootstrap bundles that the user may specify.
        File jonasBootStrapBundlesDir = IOUtils.getSystemFile(JOnASUtils.getJOnASRoot(), "lib/bootstrap/bundles");
        List<String> jonasBootStrapBundlesLocations = getBundleLocationsFromDirectory(jonasBootStrapBundlesDir);
        // Concatenate the locations with the new one
        if (jonasBootStrapBundlesLocations != null && !jonasBootStrapBundlesLocations.isEmpty()) {
            extraBundleListLocationLevelOne.addAll(jonasBootStrapBundlesLocations);
        }

        // 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.2") || jpaProviders.contains("eclipselink2.3") || jpaProviders.contains("hibernate3.5")  || jpaProviders.contains("hibernate3.6")  || jpaProviders.contains("hibernate4.0") ||  jpaProviders.contains("openjpa2")) {
            File jpa2BundlesDir = IOUtils.getSystemFile(JOnASUtils.getJOnASRoot(), "lib/bootstrap/bundles-jpa2.0");
            List<String> bundleLocations = getBundleLocationsFromDirectory(jpa2BundlesDir);
            // Concatenate this list with the existing list
            if (bundleLocations != null && !bundleLocations.isEmpty()) {
                extraBundleListLocationLevelOne.addAll(bundleLocations);
            }
        }

        // 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") || webServiceClass.contains("8")) {
            File servlets3BundlesDir = IOUtils.getSystemFile(JOnASUtils.getJOnASRoot(), "lib/bootstrap/bundles-servlets-3.0");
            List<String> bundleLocations = getBundleLocationsFromDirectory(servlets3BundlesDir);
            // Concatenate this list with the existing list
            if (bundleLocations != null && !bundleLocations.isEmpty()) {
                extraBundleListLocationLevelOne.addAll(bundleLocations);
            }
        }

        // JSF 2.0
        String jsfService = "";
        try {
            jsfService = JOnASUtils.getServerProperty("jonas.service.jsf.class", "");
        } catch (Exception e) {
            throw new IllegalStateException("Cannot get JSF service", e);
        }
        if (jsfService.contains("Mojarra20") || jsfService.contains("MyFaces20")) {
            File jsf2BundlesDir = IOUtils.getSystemFile(JOnASUtils.getJOnASRoot(), "lib/bootstrap/bundles-jsf-2.0");
            List<String> bundleLocations = getBundleLocationsFromDirectory(jsf2BundlesDir);
            // Concatenate this list with the existing list
            if (bundleLocations != null && !bundleLocations.isEmpty()) {
                extraBundleListLocationLevelOne.addAll(bundleLocations);
            }
        }

        // 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");
            List<String> bundleLocations = getBundleLocationsFromDirectory(cdiBundlesDir);
            // Concatenate this list with the existing list
            if (bundleLocations != null && !bundleLocations.isEmpty()) {
                extraBundleListLocationLevelOne.addAll(bundleLocations);
            }
        }

        // Bundles with start level of 1
        List<String> previousBundleListLocationLevelOne = autoStartLevelBundlesLocationMap.get(1);
        if (previousBundleListLocationLevelOne != null) {
            // add the existing bundles to the extra list
             extraBundleListLocationLevelOne.addAll(previousBundleListLocationLevelOne);
        }

        // Define new list as the extra list + previous list
        autoStartLevelBundlesLocationMap.put(1, extraBundleListLocationLevelOne);
    }



    /**
     * @param directory Directory to look for bundles.
     * @return A list of URLs corresponding to the
     *         files in the given directory. An empty String if the directory
     *         does not exists.
     * @throws java.io.IOException when a bundle File cannot be turned into an URL
     */
    protected List<String> getBundleLocationsFromDirectory(final File directory) throws IOException {
        List<String> locations = new ArrayList<String>();
        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) {
                locations.add("reference:".concat(file.toURI().toURL().toExternalForm()));
            }
        }
        return locations;
    }


    /**
     * 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 java.io.IOException If the list cannot be built.
     */
    private List<String> getBundleLocations(final Properties levels, final String key) throws IOException {
        // init list
        List<String> locations = new ArrayList<String>();

        String value = levels.getProperty(key);

        // 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)) {

                String location = "";

                // 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
                location = location.concat("reference:");

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

                // add to the list
                locations.add(location);
            }
        }

        return locations;
    }

    /**
     * @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 list of the bundles to install and add it to the start level
     */
    public SortedMap<Integer, List<String>> getAutoInstallBundles() {
        return autoInstallLevelBundlesLocationMap;
    }

    /**
     * @return map of the bundles to "automatically start for a given level"
     */
    public SortedMap<Integer, List<String>> getAutoStartBundles() {
        return autoStartLevelBundlesLocationMap;
    }


    public ClassLoader getFrameworkClassLoader(final ClassLoader parentClassLoader) {
        File root = JOnASUtils.getJOnASRoot();
        File frameworkDir = IOUtils.getSystemFile(root, "lib/bootstrap/framework");

        if (!frameworkDir.exists()) {
            throw new IllegalStateException("No jar found in the '" + frameworkDir + "' directory.");
        }

        // list all files
        FilenameFilter filter = new FilenameFilter() {
            public boolean accept(final File dir, final String name) {
                return name.endsWith(".jar");
            }
        };

        List<URL> urlList = new ArrayList<URL>();
        File[] frameworkFiles = frameworkDir.listFiles(filter);
        // Add each URL of the bundle
        for (File file : frameworkFiles) {
            try {
                urlList.add(file.toURI().toURL());
            } catch (MalformedURLException e) {
                throw new IllegalStateException("Unable to get the URL from the file '" + file + "'.");
            }
        }
        return new URLClassLoader(urlList.toArray(new URL[urlList.size()]), parentClassLoader);

    }

}
