/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc., Oracle Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *   Oracle Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.context.support;

import java.beans.PropertyEditor;
import java.io.IOException;
import java.security.AccessControlContext;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Dictionary;
import java.util.Map;

import org.eclipse.gemini.blueprint.context.BundleContextAware;
import org.eclipse.gemini.blueprint.context.ConfigurableOsgiBundleApplicationContext;
import org.eclipse.gemini.blueprint.context.support.internal.classloader.ClassLoaderFactory;
import org.eclipse.gemini.blueprint.context.support.internal.scope.OsgiBundleScope;
import org.eclipse.gemini.blueprint.io.OsgiBundleResource;
import org.eclipse.gemini.blueprint.io.OsgiBundleResourcePatternResolver;
import org.eclipse.gemini.blueprint.util.OsgiBundleUtils;
import org.eclipse.gemini.blueprint.util.OsgiServiceUtils;
import org.eclipse.gemini.blueprint.util.OsgiStringUtils;
import org.eclipse.gemini.blueprint.util.internal.MapBasedDictionary;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.AbstractBeanFactory;
import org.springframework.beans.factory.support.SecurityContextProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.support.AbstractRefreshableApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * 
 * <code>AbstractRefreshableApplicationContext</code> subclass that implements the
 * {@link ConfigurableOsgiBundleApplicationContext} interface for OSGi environments. Pre-implements a
 * <code>configLocation</code> property, to be populated through the <code>ConfigurableOsgiApplicationContext</code>
 * interface after OSGi bundle startup.
 * 
 * <p> This class is as easy to subclass as <code>AbstractRefreshableApplicationContext</code>(see the javadoc for
 * details): all you need to implement is the <code>loadBeanDefinitions</code> method Note that implementations are
 * supposed to load bean definitions from the files specified by the locations returned by
 * <code>getConfigLocations</code> method.
 * 
 * <p> In addition to the special beans detected by <code>AbstractApplicationContext</code>, this class registers the
 * <code>BundleContextAwareProcessor</code> for processing beans that implement the <code>BundleContextAware</code>
 * interface. Also it interprets resource paths as OSGi bundle resources (either from the bundle class space, bundle
 * space or jar space).
 * 
 * <p> This application context implementation offers the OSGi-specific, <em>bundle</em> scope.
 * 
 * <p> <strong>Note:</strong> <code>OsgiApplicationContext</code> implementations are generally supposed to configure
 * themselves based on the configuration received through the <code>ConfigurableOsgiBundleApplicationContext</code>
 * interface. In contrast, a stand-alone application context might allow for configuration in custom startup code (for
 * example, <code>GenericApplicationContext</code>).
 * 
 * @author Costin Leau
 * @author Adrian Colyer
 * @author Hal Hildebrand
 * 
 */
public abstract class AbstractOsgiBundleApplicationContext extends AbstractRefreshableApplicationContext implements
		ConfigurableOsgiBundleApplicationContext {

	private static final String EXPORTER_IMPORTER_DEPENDENCY_MANAGER =
			"org.eclipse.gemini.blueprint.service.dependency.internal.MandatoryDependencyBeanPostProcessor";

	/** OSGi bundle - determined from the BundleContext */
	private Bundle bundle;

	/** OSGi bundle context */
	private BundleContext bundleContext;

	/** Path to configuration files */
	private String[] configLocations;

	/** Used for publishing the app context */
	private ServiceRegistration serviceRegistration;

	/** Should context be published as an OSGi service? */
	private boolean publishContextAsService = true;

	/** class loader used for loading the beans */
	private ClassLoader classLoader;

	/**
	 * Internal pattern resolver. The parent one can't be used since it is being instantiated inside the constructor
	 * when the bundle field is not initialized yet.
	 */
	private ResourcePatternResolver osgiPatternResolver;

	private volatile AccessControlContext acc;

	/**
	 * Creates a new <code>AbstractOsgiBundleApplicationContext</code> with no parent.
	 */
	public AbstractOsgiBundleApplicationContext() {
		super();
		setDisplayName("Root OsgiBundleApplicationContext");
	}

	/**
	 * Creates a new <code>AbstractOsgiBundleApplicationContext</code> with the given parent context.
	 * 
	 * @param parent the parent context
	 */
	public AbstractOsgiBundleApplicationContext(ApplicationContext parent) {
		super(parent);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * <p/> Will automatically determine the bundle, create a new <code>ResourceLoader</code> (and set its
	 * <code>ClassLoader</code> (if none is set already) to a custom implementation that will delegate the calls to the
	 * bundle).
	 */
	public void setBundleContext(BundleContext bundleContext) {
		this.bundleContext = bundleContext;
		this.bundle = bundleContext.getBundle();
		this.osgiPatternResolver = createResourcePatternResolver();

		if (getClassLoader() == null)
			this.setClassLoader(createBundleClassLoader(this.bundle));

		this.setDisplayName(ClassUtils.getShortName(getClass()) + "(bundle=" + getBundleSymbolicName() + ", config="
				+ StringUtils.arrayToCommaDelimitedString(getConfigLocations()) + ")");

		this.acc = AccessControlFactory.createContext(bundle);
	}

	public BundleContext getBundleContext() {
		return this.bundleContext;
	}

	public Bundle getBundle() {
		return this.bundle;
	}

	public void setConfigLocations(String[] configLocations) {
		this.configLocations = configLocations;
	}

	/**
	 * Returns this application context configuration locations. The default implementation will check whether there are
	 * any locations configured and, if not, will return the default locations.
	 * 
	 * @return application context configuration locations.
	 * @see #getDefaultConfigLocations()
	 */
	public String[] getConfigLocations() {
		return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations());
	}

	/**
	 * Unregister the ApplicationContext OSGi service (in case there is any).
	 */
	protected void doClose() {
		unpublishContextAsOsgiService();
		// call super class
		super.doClose();
	}

	/*
	 * Clean up any beans from the bundle scope.
	 */
	protected void destroyBeans() {
		super.destroyBeans();

		try {
			cleanOsgiBundleScope(getBeanFactory());
		} catch (Exception ex) {
			logger.warn("got exception when closing", ex);
		}
	}

	/**
	 * Returns the default configuration locations to use, for the case where no explicit configuration locations have
	 * been specified. <p> Default implementation returns <code>null</code>, requiring explicit configuration locations.
	 * 
	 * @return application context default configuration locations
	 * @see #setConfigLocations
	 */
	protected String[] getDefaultConfigLocations() {
		return null;
	}

	protected void prepareRefresh() {
		super.prepareRefresh();
		// unpublish the service (if there is any) during the refresh
		unpublishContextAsOsgiService();
	}

	protected void finishRefresh() {
		super.finishRefresh();
		// publish the context only after all the beans have been published
		publishContextAsOsgiServiceIfNecessary();
	}

	protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		super.postProcessBeanFactory(beanFactory);

		beanFactory.addBeanPostProcessor(new BundleContextAwareProcessor(this.bundleContext));
		beanFactory.ignoreDependencyInterface(BundleContextAware.class);

		if (beanFactory instanceof AbstractBeanFactory) {
			AbstractBeanFactory bf = (AbstractBeanFactory) beanFactory;
			bf.setSecurityContextProvider(new SecurityContextProvider() {

				public AccessControlContext getAccessControlContext() {
					return acc;
				}
			});
		}

		enforceExporterImporterDependency(beanFactory);

		// add predefined beans
		// bundleContext
		addPredefinedBean(beanFactory, BUNDLE_CONTEXT_BEAN_NAME, this.bundleContext);
		addPredefinedBean(beanFactory, BUNDLE_BEAN_NAME, this.bundle);

		// register property editors
		registerPropertyEditors(beanFactory);

		// register a 'bundle' scope
		beanFactory.registerScope(OsgiBundleScope.SCOPE_NAME, new OsgiBundleScope());
	}

	private void addPredefinedBean(ConfigurableListableBeanFactory beanFactory, String name, Object value) {
		// add bundleContext bean
		if (!beanFactory.containsLocalBean(name)) {
			logger.debug("Registering pre-defined bean named " + name);
			beanFactory.registerSingleton(name, value);
		} else {
			logger.warn("A bean named " + name + " already exists; aborting registration of the predefined value...");
		}
	}

	/**
	 * Takes care of enforcing the relationship between exporter and importers.
	 * 
	 * @param beanFactory
	 */
	private void enforceExporterImporterDependency(ConfigurableListableBeanFactory beanFactory) {
		Object instance = null;

		instance = AccessController.doPrivileged(new PrivilegedAction<Object>() {

			public Object run() {
				// create the service manager
				ClassLoader loader = AbstractOsgiBundleApplicationContext.class.getClassLoader();
				try {
					Class<?> managerClass = loader.loadClass(EXPORTER_IMPORTER_DEPENDENCY_MANAGER);
					return BeanUtils.instantiateClass(managerClass);
				} catch (ClassNotFoundException cnfe) {
					throw new ApplicationContextException("Cannot load class " + EXPORTER_IMPORTER_DEPENDENCY_MANAGER,
							cnfe);
				}
			}
		});

		// sanity check
		Assert.isInstanceOf(BeanFactoryAware.class, instance);
		Assert.isInstanceOf(BeanPostProcessor.class, instance);
		((BeanFactoryAware) instance).setBeanFactory(beanFactory);
		beanFactory.addBeanPostProcessor((BeanPostProcessor) instance);
	}

	/**
	 * Register OSGi-specific {@link PropertyEditor}s.
	 * 
	 * @param beanFactory beanFactory used for registration.
	 */
	private void registerPropertyEditors(ConfigurableListableBeanFactory beanFactory) {
		beanFactory.addPropertyEditorRegistrar(new OsgiPropertyEditorRegistrar(getClassLoader()));
	}

	private void cleanOsgiBundleScope(ConfigurableListableBeanFactory beanFactory) {
		Scope scope = beanFactory.getRegisteredScope(OsgiBundleScope.SCOPE_NAME);
		if (scope != null && scope instanceof OsgiBundleScope) {
			if (logger.isDebugEnabled())
				logger.debug("Destroying existing bundle scope beans...");
			((OsgiBundleScope) scope).destroy();
		}
	}

	/**
	 * Publishes the application context as an OSGi service. The method internally takes care of parsing the bundle
	 * headers and determined if actual publishing is required or not.
	 * 
	 */
	@SuppressWarnings("unchecked")
	private void publishContextAsOsgiServiceIfNecessary() {
		if (publishContextAsService && serviceRegistration == null) {
			final Dictionary<Object, Object> serviceProperties = new MapBasedDictionary<Object, Object>();

			customizeApplicationContextServiceProperties((Map<Object, Object>) serviceProperties);

			if (logger.isInfoEnabled()) {
				logger.info("Publishing application context as OSGi service with properties " + serviceProperties);
			}

			// export only interfaces
			Class<?>[] classes =
					org.eclipse.gemini.blueprint.util.internal.ClassUtils.getClassHierarchy(getClass(),
							org.eclipse.gemini.blueprint.util.internal.ClassUtils.ClassSet.INTERFACES);

			// filter classes based on visibility
			Class<?>[] filterClasses =
					org.eclipse.gemini.blueprint.util.internal.ClassUtils.getVisibleClasses(classes, this.getClass()
							.getClassLoader());

			final String[] serviceNames =
					org.eclipse.gemini.blueprint.util.internal.ClassUtils.toStringArray(filterClasses);

			if (logger.isDebugEnabled())
				logger.debug("Publishing service under classes " + ObjectUtils.nullSafeToString(serviceNames));

			// Publish under all the significant interfaces we see
			boolean hasSecurity = (System.getSecurityManager() != null);

			if (hasSecurity) {
				try {
					serviceRegistration = AccessController.doPrivileged(new PrivilegedAction<ServiceRegistration>() {
						public ServiceRegistration run() {
							return getBundleContext().registerService(serviceNames, AbstractOsgiBundleApplicationContext.this, serviceProperties);
						}
					}, acc);
				} catch (AccessControlException ex) {
					logger.error("Application context service publication aborted due to security issues "
							+ "- does the bundle has the rights to publish the service ? ", ex);
				}
			} else {
				serviceRegistration = getBundleContext().registerService(serviceNames, this, serviceProperties);
			}

		} else {
			if (logger.isInfoEnabled()) {
				logger.info("Not publishing application context OSGi service for bundle "
						+ OsgiStringUtils.nullSafeNameAndSymName(bundle));
			}
		}
	}

	/**
	 * Unpublishes the application context OSGi service.
	 */
	private void unpublishContextAsOsgiService() {
		if (OsgiServiceUtils.unregisterService(serviceRegistration)) {
			logger.info("Unpublishing application context OSGi service for bundle "
					+ OsgiStringUtils.nullSafeNameAndSymName(bundle));
			serviceRegistration = null;
		} else {
			if (publishContextAsService)
				logger.info("Application Context service already unpublished");
		}
	}

	/**
	 * Customizes the properties of the application context OSGi service. This method is called only if the application
	 * context will be published as an OSGi service.
	 * 
	 * <p/> The default implementation stores the bundle symbolic name under {@link Constants#BUNDLE_SYMBOLICNAME} and
	 * {@link ConfigurableOsgiBundleApplicationContext#APPLICATION_CONTEXT_SERVICE_PROPERTY_NAME} and the bundle version
	 * under {@link Constants#BUNDLE_VERSION} property.
	 * 
	 * Can be overridden by subclasses to add more properties if needed (for example for web applications where multiple
	 * application contexts are available inside the same bundle).
	 * 
	 * @param serviceProperties service properties map (can be casted to {@link Dictionary})
	 */
	protected void customizeApplicationContextServiceProperties(Map<Object, Object> serviceProperties) {
		serviceProperties.put(APPLICATION_CONTEXT_SERVICE_PROPERTY_NAME, getBundleSymbolicName());
		serviceProperties.put(SPRING_DM_APPLICATION_CONTEXT_SERVICE_PROPERTY_NAME, getBundleSymbolicName());
		serviceProperties.put(Constants.BUNDLE_SYMBOLICNAME, getBundleSymbolicName());
		serviceProperties.put(Constants.BUNDLE_VERSION, OsgiBundleUtils.getBundleVersion(bundle));
	}

	private String getBundleSymbolicName() {
		return OsgiStringUtils.nullSafeSymbolicName(getBundle());
	}

	/**
	 * Creates an OSGi specific resource pattern resolver.
	 * 
	 * @return returns an OSGi specific pattern resolver.
	 */
	protected ResourcePatternResolver createResourcePatternResolver() {
		return new OsgiBundleResourcePatternResolver(getBundle());
	}

	/**
	 * This implementation supports pattern matching inside the OSGi bundle.
	 * 
	 * @see OsgiBundleResourcePatternResolver
	 */
	protected ResourcePatternResolver getResourcePatternResolver() {
		return osgiPatternResolver;
	}

	// delegate methods to a proper osgi resource loader

	public ClassLoader getClassLoader() {
		return classLoader;
	}

	public Resource getResource(String location) {
		return (osgiPatternResolver != null ? osgiPatternResolver.getResource(location) : null);
	}

	public Resource[] getResources(String locationPattern) throws IOException {
		return (osgiPatternResolver != null ? osgiPatternResolver.getResources(locationPattern) : null);
	}

	public void setClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	protected Resource getResourceByPath(String path) {
		Assert.notNull(path, "Path is required");
		return new OsgiBundleResource(this.bundle, path);
	}

	public void setPublishContextAsService(boolean publishContextAsService) {
		this.publishContextAsService = publishContextAsService;
	}

	/**
	 * Create the class loader that delegates to the underlying OSGi bundle.
	 * 
	 * @param bundle
	 * @return
	 */
	private ClassLoader createBundleClassLoader(Bundle bundle) {
		return ClassLoaderFactory.getBundleClassLoaderFor(bundle);
	}
}