/******************************************************************************
 * 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.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;

import org.eclipse.gemini.blueprint.io.OsgiBundleResource;
import org.eclipse.gemini.blueprint.util.OsgiStringUtils;
import org.eclipse.gemini.blueprint.util.internal.BundleUtils;
import org.osgi.framework.BundleContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
import org.springframework.beans.factory.xml.DelegatingEntityResolver;
import org.springframework.beans.factory.xml.NamespaceHandlerResolver;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;
import org.xml.sax.EntityResolver;

/**
 * Stand-alone XML application context, backed by an OSGi bundle.
 * 
 * <p> The configuration location defaults can be overridden via {@link #getDefaultConfigLocations()}. Note that
 * locations can either denote concrete files like <code>/myfiles/context.xml</code> or <em>Ant-style</em> patterns like
 * <code>/myfiles/*-context.xml</code> (see the {@link org.springframework.util.AntPathMatcher} javadoc for pattern
 * details). </p>
 * 
 * <p> <strong>Note:</strong> In case of multiple configuration locations, later bean definitions will override ones
 * defined in earlier loaded files. This can be leveraged to deliberately override certain bean definitions via an extra
 * XML file. </p>
 * 
 * <p/> <b>This is the main ApplicationContext class for OSGi environments.</b>
 * 
 * @author Adrian Colyer
 * @author Costin Leau
 * @author Andy Piper
 * @author Hal Hildebrand
 */
public class OsgiBundleXmlApplicationContext extends AbstractDelegatedExecutionApplicationContext {

	/** Default config location for the root context(s) */
	public static final String DEFAULT_CONFIG_LOCATION =
			OsgiBundleResource.BUNDLE_URL_PREFIX + "/META-INF/spring/*.xml";

	/**
	 * 
	 * Creates a new <code>OsgiBundleXmlApplicationContext</code> with no parent.
	 * 
	 */
	public OsgiBundleXmlApplicationContext() {
		this((String[]) null);
	}

	/**
	 * Creates a new <code>OsgiBundleXmlApplicationContext</code> with the given parent context.
	 * 
	 * @param parent the parent context
	 */
	public OsgiBundleXmlApplicationContext(ApplicationContext parent) {
		this(null, parent);
	}

	/**
	 * Creates a new <code>OsgiBundleXmlApplicationContext</code> with the given configLocations.
	 * 
	 * @param configLocations array of configuration resources
	 */
	public OsgiBundleXmlApplicationContext(String[] configLocations) {
		this(configLocations, null);
	}

	/**
	 * Creates a new <code>OsgiBundleXmlApplicationContext</code> with the given configLocations and parent context.
	 * 
	 * @param configLocations array of configuration resources
	 * @param parent the parent context
	 */
	public OsgiBundleXmlApplicationContext(String[] configLocations, ApplicationContext parent) {
		super(parent);
		setConfigLocations(configLocations);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * <p/> Loads the bean definitions via an <code>XmlBeanDefinitionReader</code>.
	 */
	protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws IOException {
		// Create a new XmlBeanDefinitionReader for the given BeanFactory.
		XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

		// Configure the bean definition reader with the context
		// resource loading environment.
		beanDefinitionReader.setResourceLoader(this);

		// add a specialized DocumentLoader to load blueprint configs w/o a schema location
		beanDefinitionReader.setDocumentLoader(new BlueprintDocumentLoader());

		final Object[] resolvers = new Object[2];

		final BundleContext ctx = getBundleContext();

		if (System.getSecurityManager() != null) {
			AccessController.doPrivileged(new PrivilegedAction<Object>() {
				public Object run() {
					String filter = BundleUtils.createNamespaceFilter(ctx);
					resolvers[0] = createNamespaceHandlerResolver(ctx, filter, getClassLoader());
					resolvers[1] = createEntityResolver(ctx, filter, getClassLoader());
					return null;
				}
			});
		} else {
			String filter = BundleUtils.createNamespaceFilter(ctx);
			resolvers[0] = createNamespaceHandlerResolver(ctx, filter, getClassLoader());
			resolvers[1] = createEntityResolver(ctx, filter, getClassLoader());
		}

		beanDefinitionReader.setNamespaceHandlerResolver((NamespaceHandlerResolver) resolvers[0]);
		beanDefinitionReader.setEntityResolver((EntityResolver) resolvers[1]);

		// Allow a subclass to provide custom initialisation of the reader,
		// then proceed with actually loading the bean definitions.
		initBeanDefinitionReader(beanDefinitionReader);
		loadBeanDefinitions(beanDefinitionReader);
	}

	/**
	 * Allows subclasses to do custom initialisation here.
	 * 
	 * @param beanDefinitionReader
	 */
	protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) {
	}

	/**
	 * Loads the bean definitions with the given <code>XmlBeanDefinitionReader</code>.
	 * 
	 * <p> The lifecycle of the bean factory is handled by the refreshBeanFactory method; therefore this method is just
	 * supposed to load and/or register bean definitions.
	 * 
	 * <p> Delegates to a ResourcePatternResolver for resolving location patterns into Resource instances.
	 * 
	 * @throws org.springframework.beans.BeansException in case of bean registration errors
	 * @throws java.io.IOException if the required XML document isn't found
	 * @see #refreshBeanFactory
	 * @see #getConfigLocations
	 * @see #getResources
	 * @see #getResourcePatternResolver
	 */
	protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
		String[] configLocations = expandLocations(getConfigLocations());
		if (configLocations != null) {
			for (int i = 0; i < configLocations.length; i++) {
				reader.loadBeanDefinitions(configLocations[i]);
			}
		}
	}

	/**
	 * Expands any folder entries supplied as configuration location. I.e. config/ becomes config/*.xml.
	 * 
	 * @param configLocations
	 * @return
	 */
	private String[] expandLocations(String[] configLocations) {
		String[] expanded = null;
		if (configLocations != null) {
			expanded = new String[configLocations.length];
			for (int i = 0; i < configLocations.length; i++) {
				String location = configLocations[i];
				if (location.endsWith("/")) {
					location = location + "*.xml";
				}
				expanded[i] = location;
			}
		}
		return expanded;
	}

	/**
	 * Provide default locations for XML files. This implementation returns <code>META-INF/spring/*.xml</code> relying
	 * on the default resource environment for actual localisation. By default, the bundle space will be used for
	 * locating the resources.
	 * 
	 * <p/> <strong>Note:</strong> Instead of overriding this method, consider using the Spring-DM specific header
	 * inside your manifest bundle.
	 * 
	 * @return default XML configuration locations
	 */
	protected String[] getDefaultConfigLocations() {
		return new String[] { DEFAULT_CONFIG_LOCATION };
	}

	/**
	 * Creates a special OSGi namespace handler resolver that first searches the bundle class path falling back to the
	 * namespace service published by Spring-DM. This allows embedded libraries that provide namespace handlers take
	 * priority over namespace provided by other bundles.
	 * 
	 * @param bundleContext the OSGi context of which the resolver should be aware of
	 * @param filter OSGi service filter
	 * @param bundleClassLoader classloader for creating the OSGi namespace resolver proxy
	 * @return a OSGi aware namespace handler resolver
	 */
	private NamespaceHandlerResolver createNamespaceHandlerResolver(BundleContext bundleContext, String filter,
			ClassLoader bundleClassLoader) {
		Assert.notNull(bundleContext, "bundleContext is required");
		// create local namespace resolver
		// we'll use the default resolver which uses the bundle local class-loader
		NamespaceHandlerResolver localNamespaceResolver = new DefaultNamespaceHandlerResolver(bundleClassLoader);

		// hook in OSGi namespace resolver
		NamespaceHandlerResolver osgiServiceNamespaceResolver =
				lookupNamespaceHandlerResolver(bundleContext, filter, localNamespaceResolver);

		DelegatedNamespaceHandlerResolver delegate = new DelegatedNamespaceHandlerResolver();
		delegate.addNamespaceHandler(localNamespaceResolver, "LocalNamespaceResolver for bundle "
				+ OsgiStringUtils.nullSafeNameAndSymName(bundleContext.getBundle()));
		delegate.addNamespaceHandler(osgiServiceNamespaceResolver, "OSGi Service resolver");

		return delegate;
	}

	/**
	 * Similar to {@link #createNamespaceHandlerResolver(BundleContext, ClassLoader, ClassLoader)} , this method creates
	 * a special OSGi entity resolver that considers the bundle class path first, falling back to the entity resolver
	 * service provided by the Spring DM extender.
	 * 
	 * @param bundleContext the OSGi context of which the resolver should be aware of
	 * @param filter OSGi service filter
	 * @param bundleClassLoader classloader for creating the OSGi namespace resolver proxy
	 * @return a OSGi aware entity resolver
	 */
	private EntityResolver createEntityResolver(BundleContext bundleContext, String filter,
			ClassLoader bundleClassLoader) {
		Assert.notNull(bundleContext, "bundleContext is required");
		// create local namespace resolver
		EntityResolver localEntityResolver = new DelegatingEntityResolver(bundleClassLoader);
		// hook in OSGi namespace resolver
		EntityResolver osgiServiceEntityResolver = lookupEntityResolver(bundleContext, filter, localEntityResolver);

		ChainedEntityResolver delegate = new ChainedEntityResolver();
		delegate.addEntityResolver(localEntityResolver, "LocalEntityResolver for bundle "
				+ OsgiStringUtils.nullSafeNameAndSymName(bundleContext.getBundle()));

		// hook in OSGi namespace resolver
		delegate.addEntityResolver(osgiServiceEntityResolver, "OSGi Service resolver");

		return delegate;
	}

	private NamespaceHandlerResolver lookupNamespaceHandlerResolver(final BundleContext bundleContext, String filter,
			final Object fallbackObject) {
		return (NamespaceHandlerResolver) TrackingUtil.getService(new Class<?>[] { NamespaceHandlerResolver.class },
				filter, NamespaceHandlerResolver.class.getClassLoader(), bundleContext, fallbackObject);
	}

	private EntityResolver lookupEntityResolver(final BundleContext bundleContext, String filter,
			final Object fallbackObject) {
		return (EntityResolver) TrackingUtil.getService(new Class<?>[] { EntityResolver.class }, filter,
				EntityResolver.class.getClassLoader(), bundleContext, fallbackObject);
	}

	public String[] getConfigLocations() {
		return super.getConfigLocations();
	}
}