/******************************************************************************
 * 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.extender.internal.support;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.framework.Bundle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
import org.springframework.beans.factory.xml.DelegatingEntityResolver;
import org.springframework.beans.factory.xml.NamespaceHandler;
import org.springframework.beans.factory.xml.NamespaceHandlerResolver;
import org.eclipse.gemini.blueprint.util.BundleDelegatingClassLoader;
import org.eclipse.gemini.blueprint.util.OsgiStringUtils;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Spring schema handler/resolver for OSGi environments.
 * 
 * Besides delegation this class also does type filtering to avoid wiring the wrong bundle if multiple versions of the
 * same library (which support the same schema) are available.
 * 
 * Additionally, lazy handlers are supported so that they are checked (and thus loaded) only if no previous handler has
 * been able to satisfy the request.
 * 
 * @author Hal Hildebrand
 * @author Costin Leau
 * 
 */
public class NamespacePlugins implements NamespaceHandlerResolver, EntityResolver, DisposableBean {

	/**
	 * Wrapper class which implements both {@link EntityResolver} and {@link NamespaceHandlerResolver} interfaces.
	 * 
	 * Simply delegates to the actual implementation discovered in a specific bundle.
	 */
	private static class Plugin implements NamespaceHandlerResolver, EntityResolver {

		private final NamespaceHandlerResolver namespace;

		private final EntityResolver entity;

		private final Bundle bundle;

		private Plugin(Bundle bundle) {
			this.bundle = bundle;

			ClassLoader loader = BundleDelegatingClassLoader.createBundleClassLoaderFor(bundle);

			entity = new DelegatingEntityResolver(loader);
			namespace = new DefaultNamespaceHandlerResolver(loader);
		}

		public NamespaceHandler resolve(String namespaceUri) {
			return namespace.resolve(namespaceUri);
		}

		public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
			return entity.resolveEntity(publicId, systemId);
		}

		public Bundle getBundle() {
			return bundle;
		}
	}

	private static final Log log = LogFactory.getLog(NamespacePlugins.class);

	final LazyBundleRegistry.Condition condition = new LazyBundleRegistry.Condition() {

		private final String NS_HANDLER_RESOLVER_CLASS_NAME = NamespaceHandlerResolver.class.getName();

		public boolean pass(Bundle bundle) {
			try {
				Class<?> type = bundle.loadClass(NS_HANDLER_RESOLVER_CLASS_NAME);
				return NamespaceHandlerResolver.class.equals(type);
			} catch (Throwable th) {
				// if the interface is not wired, ignore the bundle
				log.warn("Bundle " + OsgiStringUtils.nullSafeNameAndSymName(bundle) + " cannot see class ["
						+ NS_HANDLER_RESOLVER_CLASS_NAME + "]; ignoring it as a namespace resolver");

				return false;
			}
		}
	};

	private final LazyBundleRegistry.Activator<Plugin> activation = new LazyBundleRegistry.Activator<Plugin>() {

		public Plugin activate(Bundle bundle) {
			return new Plugin(bundle);
		}
	};

	private final LazyBundleRegistry<Plugin> pluginRegistry =
			new LazyBundleRegistry<Plugin>(condition, activation, log);

	/**
	 * Adds a bundle as a handler to plugin registry.
	 * 
	 * @param bundle
	 * @param lazyBundle
	 */
	void addPlugin(Bundle bundle, boolean lazyBundle, boolean applyCondition) {
		boolean debug = log.isDebugEnabled();

		if (debug)
			log.debug("Adding as " + (lazyBundle ? "lazy " : "") + "namespace handler bundle "
					+ OsgiStringUtils.nullSafeNameAndSymName(bundle));

		pluginRegistry.add(bundle, lazyBundle, applyCondition);
	}

	/**
	 * Checks the type compatibility check between the namespace parser wired to Spring DM and the discovered bundle
	 * class space.
	 * 
	 * @param bundle handler bundle
	 * @return true if there is type compatibility, false otherwise
	 */
	boolean isTypeCompatible(Bundle bundle) {
		return condition.pass(bundle);
	}

	/**
	 * Returns true if a handler mapping was removed for the given bundle.
	 * 
	 * @param bundle bundle to look at
	 * @return true if the bundle was used in the plugin map
	 */
	boolean removePlugin(Bundle bundle) {
		if (log.isDebugEnabled())
			log.debug("Removing handler " + OsgiStringUtils.nullSafeNameAndSymName(bundle));

		return pluginRegistry.remove(bundle);
	}

	public NamespaceHandler resolve(final String namespaceUri) {
		if (System.getSecurityManager() != null) {
			return AccessController.doPrivileged(new PrivilegedAction<NamespaceHandler>() {

				public NamespaceHandler run() {
					return doResolve(namespaceUri);
				}
			});

		} else {
			return doResolve(namespaceUri);
		}
	}

	public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException, IOException {
		if (System.getSecurityManager() != null) {
			try {
				return AccessController.doPrivileged(new PrivilegedExceptionAction<InputSource>() {

					public InputSource run() throws Exception {
						return doResolveEntity(publicId, systemId);
					}
				});
			} catch (PrivilegedActionException pae) {
				Exception cause = pae.getException();
				handleInputSourceException(cause);
			}
		} else {
			try {
				return doResolveEntity(publicId, systemId);
			} catch (Exception ex) {
				handleInputSourceException(ex);
			}
		}

		return null;
	}

	private NamespaceHandler doResolve(final String namespaceUri) {
		final boolean debug = log.isDebugEnabled();
		final boolean trace = log.isTraceEnabled();

		if (debug)
			log.debug("Trying to resolving namespace handler for " + namespaceUri);

		try {
			return pluginRegistry.apply(new LazyBundleRegistry.Operation<Plugin, NamespaceHandler>() {

				public NamespaceHandler operate(Plugin plugin) {
					try {
						NamespaceHandler handler = plugin.resolve(namespaceUri);
						if (handler != null) {
							if (debug)
								log.debug("Namespace handler for " + namespaceUri + " found inside bundle "
										+ OsgiStringUtils.nullSafeNameAndSymName(plugin.getBundle()));

							return handler;
						} else if (trace)
							log.trace("Namespace handler for " + namespaceUri + " not found inside bundle "
									+ OsgiStringUtils.nullSafeNameAndSymName(plugin.getBundle()));

					} catch (IllegalArgumentException ex) {
						if (trace)
							log.trace("Namespace handler for " + namespaceUri + " not found inside bundle "
									+ OsgiStringUtils.nullSafeNameAndSymName(plugin.getBundle()));
					}
					return null;
				}
			});
		} catch (Exception ex) {
			// the inner method doesn't declare any exceptions so the cast should be safe
			throw (RuntimeException) ex;
		}
	}

	private InputSource doResolveEntity(final String publicId, final String systemId) throws Exception {
		final boolean debug = log.isDebugEnabled();
		final boolean trace = log.isTraceEnabled();

		if (debug)
			log.debug("Trying to resolving entity for " + publicId + "|" + systemId);

		if (systemId != null) {

			return pluginRegistry.apply(new LazyBundleRegistry.Operation<Plugin, InputSource>() {

				public InputSource operate(Plugin plugin) throws SAXException, IOException {
					try {
						InputSource inputSource = plugin.resolveEntity(publicId, systemId);

						if (inputSource != null) {
							if (debug)
								log.debug("XML schema for " + publicId + "|" + systemId + " found inside bundle "
										+ OsgiStringUtils.nullSafeNameAndSymName(plugin.getBundle()));
							return inputSource;
						}

					} catch (FileNotFoundException ex) {
						if (trace)
							log.trace("XML schema for " + publicId + "|" + systemId + " not found inside bundle "
									+ OsgiStringUtils.nullSafeNameAndSymName(plugin.getBundle()), ex);
					}
					return null;
				}
			});
		}
		return null;
	}

	private void handleInputSourceException(Exception exception) throws SAXException, IOException {
		if (exception instanceof RuntimeException) {
			throw (RuntimeException) exception;
		}
		if (exception instanceof IOException) {
			throw (IOException) exception;
		}
		throw (SAXException) exception;
	}

	public void destroy() {
		pluginRegistry.clear();
	}
}