/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2010, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.osgi.service;

import org.jboss.as.jmx.MBeanServerService;
import org.jboss.as.naming.InitialContext;
import org.jboss.as.network.SocketBinding;
import org.jboss.as.osgi.parser.SubsystemState;
import org.jboss.as.osgi.parser.SubsystemState.Activation;
import org.jboss.as.server.ServerEnvironment;
import org.jboss.as.server.ServerEnvironmentService;
import org.jboss.modules.DependencySpec;
import org.jboss.modules.Module;
import org.jboss.modules.ModuleClassLoader;
import org.jboss.modules.ModuleIdentifier;
import org.jboss.modules.ModuleLoadException;
import org.jboss.modules.ModuleLoader;
import org.jboss.modules.ModuleSpec;
import org.jboss.modules.filter.PathFilter;
import org.jboss.modules.filter.PathFilters;
import org.jboss.msc.service.AbstractService;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.ServiceBuilder;
import org.jboss.msc.service.ServiceContainer;
import org.jboss.msc.service.ServiceController;
import org.jboss.msc.service.ServiceController.Mode;
import org.jboss.msc.service.ServiceListener;
import org.jboss.msc.service.ServiceName;
import org.jboss.msc.service.ServiceTarget;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.jboss.osgi.framework.FrameworkModuleProvider;
import org.jboss.osgi.framework.Services;
import org.jboss.osgi.framework.SystemPathsProvider;
import org.jboss.osgi.framework.SystemServicesProvider;
import org.jboss.osgi.framework.internal.FrameworkBuilder;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceReference;

import javax.management.MBeanServer;
import javax.naming.spi.ObjectFactory;
import java.io.File;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import static org.jboss.as.network.SocketBinding.JBOSS_BINDING_NAME;
import static org.jboss.as.osgi.OSGiLogger.ROOT_LOGGER;
import static org.jboss.as.osgi.OSGiMessages.MESSAGES;
import static org.jboss.as.osgi.parser.SubsystemState.PROP_JBOSS_OSGI_SYSTEM_MODULES;
import static org.jboss.as.osgi.parser.SubsystemState.PROP_JBOSS_OSGI_SYSTEM_MODULES_EXTRA;
import static org.jboss.as.osgi.parser.SubsystemState.PROP_JBOSS_OSGI_SYSTEM_PACKAGES;
import static org.jboss.osgi.framework.Constants.JBOSGI_PREFIX;

/**
 * Service responsible for creating and managing the life-cycle of the OSGi Framework.
 *
 * @author Thomas.Diesler@jboss.com
 * @author David Bosschaert
 * @since 11-Sep-2010
 */
public class FrameworkBootstrapService implements Service<Void> {

    public static final ServiceName SERVICE_BASE_NAME = ServiceName.JBOSS.append("osgi", "as");
    public static final ServiceName FRAMEWORK_BASE_NAME = SERVICE_BASE_NAME.append("framework");
    public static final ServiceName FRAMEWORK_BOOTSTRAP = FRAMEWORK_BASE_NAME.append("bootstrap");
    public static final String MAPPED_OSGI_SOCKET_BINDINGS = "org.jboss.as.osgi.socket.bindings";

    private final InjectedValue<ServerEnvironment> injectedEnvironment = new InjectedValue<ServerEnvironment>();
    private final InjectedValue<SubsystemState> injectedSubsystemState = new InjectedValue<SubsystemState>();
    private final InjectedValue<SocketBinding> httpServerPortBinding = new InjectedValue<SocketBinding>();

    public static ServiceController<?> addService(final ServiceTarget target, final ServiceListener<Object>... listeners) {
        FrameworkBootstrapService service = new FrameworkBootstrapService();
        ServiceBuilder<?> builder = target.addService(FRAMEWORK_BOOTSTRAP, service);
        builder.addDependency(ServerEnvironmentService.SERVICE_NAME, ServerEnvironment.class, service.injectedEnvironment);
        builder.addDependency(SubsystemState.SERVICE_NAME, SubsystemState.class, service.injectedSubsystemState);
        builder.addDependency(JBOSS_BINDING_NAME.append("osgi-http"), SocketBinding.class, service.httpServerPortBinding);
        builder.addListener(listeners);
        return builder.install();
    }

    private FrameworkBootstrapService() {
    }

    public synchronized void start(StartContext context) throws StartException {
        ServiceController<?> controller = context.getController();
        ROOT_LOGGER.debugf("Starting: %s in mode %s", controller.getName(), controller.getMode());
        try {
            ServiceContainer serviceContainer = context.getController().getServiceContainer();

            // Setup the OSGi {@link Framework} properties
            SubsystemState subsystemState = injectedSubsystemState.getValue();
            Map<String, Object> props = new HashMap<String, Object>(subsystemState.getProperties());
            setupIntegrationProperties(context, props);

            // Register the URLStreamHandlerFactory
            Module coreFrameworkModule = ((ModuleClassLoader) FrameworkBuilder.class.getClassLoader()).getModule();
            Module.registerURLStreamHandlerFactoryModule(coreFrameworkModule);
            Module.registerContentHandlerFactoryModule(coreFrameworkModule);

            ServiceTarget target = context.getChildTarget();
            AutoInstallIntegration.addService(target);
            FrameworkModuleIntegration.addService(target, props);
            JAXPServiceProvider.addService(target);
            ModuleLoaderIntegration.addService(target);
            ModuleIdentityArtifactProvider.addService(target);
            RepositoryProvider.addService(target);
            SystemServicesIntegration.addService(target);

            // Configure the {@link Framework} builder
            FrameworkBuilder builder = new FrameworkBuilder(props);
            builder.setServiceContainer(serviceContainer);
            builder.setServiceTarget(target);
            builder.addProvidedService(Services.AUTOINSTALL_PROVIDER);
            builder.addProvidedService(Services.BUNDLE_INSTALL_PROVIDER);
            builder.addProvidedService(Services.FRAMEWORK_MODULE_PROVIDER);
            builder.addProvidedService(Services.MODULE_LOADER_PROVIDER);
            builder.addProvidedService(Services.SYSTEM_SERVICES_PROVIDER);

            // Create the {@link Framework} services
            Activation activation = subsystemState.getActivationPolicy();
            Mode initialMode = (activation == Activation.EAGER ? Mode.ACTIVE : Mode.ON_DEMAND);
            builder.createFrameworkServices(initialMode, true);

        } catch (Throwable t) {
            throw new StartException(MESSAGES.failedToCreateFrameworkServices(), t);
        }
    }

    public synchronized void stop(StopContext context) {
        ServiceController<?> controller = context.getController();
        ROOT_LOGGER.debugf("Stopping: %s in mode %s", controller.getName(), controller.getMode());
        ROOT_LOGGER.stoppingOsgiFramework();
    }

    @Override
    public Void getValue() throws IllegalStateException {
        return null;
    }

    private void setupIntegrationProperties(StartContext context, Map<String, Object> props) {

        // Configure the OSGi HttpService port
        // [TODO] This will go away once the HTTP subsystem from AS implements the OSGi HttpService.
        props.put("org.osgi.service.http.port", "" + httpServerPortBinding.getValue().getSocketAddress().getPort());

        // Setup the Framework's storage area. Always clean the framework storage on first init.
        // [TODO] Differentiate beetween user data and persisted bundles. Persist bundle state in the domain model.
        props.put(Constants.FRAMEWORK_STORAGE_CLEAN, Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
        String storage = (String) props.get(Constants.FRAMEWORK_STORAGE);
        if (storage == null) {
            ServerEnvironment environment = injectedEnvironment.getValue();
            File dataDir = environment.getServerDataDir();
            storage = dataDir.getAbsolutePath() + File.separator + "osgi-store";
            props.put(Constants.FRAMEWORK_STORAGE, storage);
        }

        // Setup default system modules
        String sysmodules = (String) props.get(PROP_JBOSS_OSGI_SYSTEM_MODULES);
        if (sysmodules == null) {
            StringBuffer buffer = new StringBuffer();
            buffer.append("javax.api,");
            buffer.append("javax.inject.api,");
            buffer.append("org.apache.xerces,");
            buffer.append("org.jboss.as.configadmin,");
            buffer.append("org.jboss.as.osgi,");
            buffer.append("org.jboss.logging,");
            buffer.append("org.jboss.modules,");
            buffer.append("org.jboss.msc,");
            buffer.append("org.jboss.osgi.framework,");
            buffer.append("org.jboss.osgi.repository,");
            buffer.append("org.slf4j");
            props.put(PROP_JBOSS_OSGI_SYSTEM_MODULES, buffer.toString());
        }

        // Setup default system packages
        String syspackages = (String) props.get(PROP_JBOSS_OSGI_SYSTEM_PACKAGES);
        if (syspackages == null) {
            Set<String> sysPackages = new LinkedHashSet<String>();
            sysPackages.addAll(Arrays.asList(SystemPathsProvider.DEFAULT_SYSTEM_PACKAGES));
            sysPackages.addAll(Arrays.asList(SystemPathsProvider.DEFAULT_FRAMEWORK_PACKAGES));
            sysPackages.add("javax.inject");
            sysPackages.add("org.apache.xerces.jaxp");
            sysPackages.add("org.jboss.as.configadmin.service");
            sysPackages.add("org.jboss.as.osgi.service");
            sysPackages.add("org.jboss.logging;version=3.1.0");
            sysPackages.add("org.jboss.osgi.repository;version=1.0");
            sysPackages.add("org.jboss.osgi.resolver.v2;version=2.0");
            sysPackages.add("org.osgi.service.repository;version=1.0");
            sysPackages.add("org.osgi.framework.resource;version=1.0");
            sysPackages.add("org.osgi.framework.wiring;version=1.1");
            sysPackages.add("org.slf4j;version=1.6.1");
            syspackages = sysPackages.toString();
            syspackages = syspackages.substring(1, syspackages.length() - 1);
            props.put(PROP_JBOSS_OSGI_SYSTEM_PACKAGES, syspackages);
        }

        String extrapackages = (String) props.get(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA);
        if (extrapackages != null) {
            syspackages += "," + extrapackages;
        }
        props.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, syspackages);
    }

    private static final class SystemServicesIntegration implements Service<SystemServicesProvider>, SystemServicesProvider {

        private final InjectedValue<MBeanServer> injectedMBeanServer = new InjectedValue<MBeanServer>();
        private final InjectedValue<BundleContext> injectedBundleContext = new InjectedValue<BundleContext>();
        private ServiceContainer serviceContainer;
        private JNDIServiceListener jndiServiceListener;

        public static ServiceController<?> addService(final ServiceTarget target) {
            SystemServicesIntegration service = new SystemServicesIntegration();
            ServiceBuilder<SystemServicesProvider> builder = target.addService(Services.SYSTEM_SERVICES_PROVIDER, service);
            builder.addDependency(MBeanServerService.SERVICE_NAME, MBeanServer.class, service.injectedMBeanServer);
            builder.addDependency(Services.SYSTEM_CONTEXT, BundleContext.class, service.injectedBundleContext);
            builder.addDependency(Services.FRAMEWORK_CREATE);
            builder.setInitialMode(Mode.ON_DEMAND);
            return builder.install();
        }

        private SystemServicesIntegration() {
        }

        @Override
        public void start(StartContext context) throws StartException {
            ServiceController<?> controller = context.getController();
            ROOT_LOGGER.debugf("Starting: %s in mode %s", controller.getName(), controller.getMode());
            serviceContainer = context.getController().getServiceContainer();
            final BundleContext syscontext = injectedBundleContext.getValue();

            // Register the JNDI service listener
            jndiServiceListener = new JNDIServiceListener(syscontext);
            try {
                String filter = "(" + Constants.OBJECTCLASS + "=" + ObjectFactory.class.getName() + ")";
                syscontext.addServiceListener(jndiServiceListener, filter);
            } catch (InvalidSyntaxException e) {
                // ignore
            }

            // Register the socket-binding services
            String bindingNames = syscontext.getProperty(MAPPED_OSGI_SOCKET_BINDINGS);
            if (bindingNames != null) {
                final Set<ServiceName> socketBindingNames = new HashSet<ServiceName>();
                for (String sufix : bindingNames.split(",")) {
                    socketBindingNames.add(JBOSS_BINDING_NAME.append(sufix));
                }
                ServiceTarget serviceTarget = context.getChildTarget();
                ServiceName serviceName = Services.SYSTEM_SERVICES_PROVIDER.append("BINDINGS");
                ServiceBuilder<Void> builder = serviceTarget.addService(serviceName, new AbstractService<Void>() {
                    public void start(StartContext context) throws StartException {
                        for (ServiceName serviceName : socketBindingNames) {
                            SocketBinding binding = (SocketBinding) serviceContainer.getRequiredService(serviceName).getValue();
                            Dictionary<String, String> props = new Hashtable<String, String>();
                            props.put("socketBinding", serviceName.getSimpleName());
                            InetSocketAddress value = binding.getSocketAddress();
                            syscontext.registerService(InetSocketAddress.class.getName(), value, props);
                        }
                    }
                });
                ServiceName[] serviceNameArray = socketBindingNames.toArray(new ServiceName[socketBindingNames.size()]);
                builder.addDependencies(serviceNameArray);
                builder.install();
            }
        }

        @Override
        public void stop(StopContext context) {
            ServiceController<?> controller = context.getController();
            ROOT_LOGGER.debugf("Stopping: %s in mode %s", controller.getName(), controller.getMode());
            injectedBundleContext.getValue().removeServiceListener(jndiServiceListener);
        }

        @Override
        public SystemServicesProvider getValue() {
            return this;
        }

        @Override
        public void registerSystemServices(final BundleContext context) {

            // Register the {@link MBeanServer} as OSGi service
            MBeanServer mbeanServer = injectedMBeanServer.getValue();
            context.registerService(MBeanServer.class.getName(), mbeanServer, null);

            // Register the {@link ServiceContainer} as OSGi service
            context.registerService(ServiceContainer.class.getName(), serviceContainer, null);
        }
    }

    private static final class FrameworkModuleIntegration implements FrameworkModuleProvider {

        private final Map<String, Object> props;
        private Module frameworkModule;

        private static ServiceController<?> addService(final ServiceTarget target, Map<String, Object> props) {
            FrameworkModuleIntegration service = new FrameworkModuleIntegration(props);
            ServiceBuilder<?> builder = target.addService(Services.FRAMEWORK_MODULE_PROVIDER, service);
            builder.setInitialMode(Mode.ON_DEMAND);
            return builder.install();
        }

        private FrameworkModuleIntegration(Map<String, Object> props) {
            this.props = props;
        }

        @Override
        public void start(StartContext context) throws StartException {
            ServiceController<?> controller = context.getController();
            ROOT_LOGGER.debugf("Starting: %s in mode %s", controller.getName(), controller.getMode());
        }

        @Override
        public void stop(StopContext context) {
            ServiceController<?> controller = context.getController();
            ROOT_LOGGER.debugf("Stopping: %s in mode %s", controller.getName(), controller.getMode());
            frameworkModule = null;
        }

        @Override
        public FrameworkModuleProvider getValue() throws IllegalStateException {
            return this;
        }

        @Override
        public Module getFrameworkModule(Bundle systemBundle) {
            if (frameworkModule == null) {
                frameworkModule = createFrameworkModule(systemBundle);
            }
            return frameworkModule;
        }

        private Module createFrameworkModule(final Bundle systemBundle) {
            // Setup the extended framework module spec
            ModuleSpec.Builder specBuilder = ModuleSpec.build(ModuleIdentifier.create(JBOSGI_PREFIX + ".framework"));

            // Add the framework module dependencies
            String sysmodules = (String) props.get(PROP_JBOSS_OSGI_SYSTEM_MODULES);
            if (sysmodules == null)
                sysmodules = "";

            String extramodules = (String) props.get(PROP_JBOSS_OSGI_SYSTEM_MODULES_EXTRA);
            if (extramodules != null)
                sysmodules += "," + extramodules;

            // Add a dependency on the default framework modules
            ModuleLoader bootLoader = Module.getBootModuleLoader();
            PathFilter acceptAll = PathFilters.acceptAll();
            for (String modid : sysmodules.split(",")) {
                modid = modid.trim();
                if (modid.length() > 0) {
                    ModuleIdentifier identifier = ModuleIdentifier.create(modid);
                    specBuilder.addDependency(DependencySpec.createModuleDependencySpec(acceptAll, acceptAll, bootLoader, identifier, false));
                }
            }

            specBuilder.setModuleClassLoaderFactory(new BundleReferenceClassLoader.Factory(systemBundle));

            try {
                final ModuleSpec moduleSpec = specBuilder.create();
                ModuleLoader moduleLoader = new ModuleLoader() {

                    @Override
                    protected ModuleSpec findModule(ModuleIdentifier identifier) throws ModuleLoadException {
                        return (moduleSpec.getModuleIdentifier().equals(identifier) ? moduleSpec : null);
                    }

                    @Override
                    public String toString() {
                        return "FrameworkModuleLoader";
                    }
                };
                return moduleLoader.loadModule(specBuilder.getIdentifier());
            } catch (ModuleLoadException ex) {
                throw new IllegalStateException(ex);
            }
        }
    }

    // This listener registers OSGi Services that are registered under javax.naming.spi.ObjectFactory with the AS7 naming system.
    private static class JNDIServiceListener implements org.osgi.framework.ServiceListener {
        private static final String OSGI_JNDI_URL_SCHEME = "osgi.jndi.url.scheme";
        private final BundleContext bundleContext;

        public JNDIServiceListener(BundleContext bundleContext) {
            this.bundleContext = bundleContext;

            try {
                // Register the pre-existing services
                ServiceReference[] refs = bundleContext.getServiceReferences(ObjectFactory.class.getName(), null);
                if (refs != null) {
                    for (ServiceReference ref : refs) {
                        handleJNDIRegistration(ref, true);
                    }
                }
            } catch (InvalidSyntaxException e) {
                throw new IllegalStateException(e);
            }
        }

        @Override
        public void serviceChanged(ServiceEvent event) {
            ServiceReference ref = event.getServiceReference();
            switch (event.getType()) {
                case ServiceEvent.REGISTERED:
                    handleJNDIRegistration(ref, true);
                    break;
                case ServiceEvent.UNREGISTERING:
                    handleJNDIRegistration(ref, false);
                    break;
            }
        }

        private void handleJNDIRegistration(ServiceReference ref, boolean register) {
            String[] objClasses = (String[]) ref.getProperty(Constants.OBJECTCLASS);
            for (String objClass : objClasses) {
                if (ObjectFactory.class.getName().equals(objClass)) {
                    for (String scheme : getStringPlusProperty(ref.getProperty(OSGI_JNDI_URL_SCHEME))) {
                        if (register)
                            InitialContext.addUrlContextFactory(scheme, (ObjectFactory) bundleContext.getService(ref));
                        else
                            InitialContext.removeUrlContextFactory(scheme, (ObjectFactory) bundleContext.getService(ref));
                    }
                }
            }
        }

        @SuppressWarnings("unchecked")
        private static Collection<String> getStringPlusProperty(Object property) {
            if (property instanceof Collection) {
                return (Collection<String>) property;
            } else if (property instanceof String[]) {
                return Arrays.asList((String[]) property);
            } else if (property instanceof String) {
                return Collections.singleton((String) property);
            }
            return Collections.emptyList();
        }
    }
}
