/******************************************************************************
 * Copyright (c) 2006, 2010 VMware 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.
 *****************************************************************************/

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

import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.gemini.blueprint.util.internal.ClassUtils;
import org.springframework.util.Assert;

/**
 * Chaining class loader implementation that delegates the resource and class loading to a number of class loaders
 * passed in.
 * 
 * <p/> This class loader parent (by default the AppClassLoader) can be specified and will be added automatically as the
 * last entry in the list.
 * 
 * <p/> Additionally, the class space of this class loader can be extended at runtime (by allowing more class loaders to
 * be added).
 * 
 * <strong>Note:</strong>non-OSGi class loaders are considered as special cases. As there are classes that are loaded by
 * the Boot, Ext, App and Fwk ClassLoaders through boot delegation, this implementation tries to identify them and place
 * them last in the chain. Otherwise, these loaders can pull in classes from outside OSGi causing
 * {@link ClassCastException}s.
 * 
 * @author Costin Leau
 */
public class ChainedClassLoader extends ClassLoader {

	/** list of loaders */
	private final List<ClassLoader> loaders = new ArrayList<ClassLoader>();

	/** list of special, non-osgi loaders, added by the user */
	private final List<ClassLoader> nonOsgiLoaders = new ArrayList<ClassLoader>();

	/** parent class loader */
	private final ClassLoader parent;

	/**
	 * Constructs a new <code>ChainedClassLoader</code> instance.
	 * 
	 * Equivalent to {@link #ChainedClassLoader(ClassLoader[], ClassLoader)} with the parent class loader initialized to
	 * the AppClassLoader (practically the system bundle class loader).
	 * 
	 * Note that the AppClassLoader can be different then the {@link #getSystemClassLoader()}, used by
	 * {@link #ChainedClassLoader(ClassLoader[], ClassLoader)} if no parent is specified.
	 * 
	 * @param loaders array of non-null class loaders
	 */
	public ChainedClassLoader(ClassLoader[] loaders) {
		this(loaders, ClassUtils.getFwkClassLoader());
	}

	/**
	 * Constructs a new <code>ChainedClassLoader</code> instance.
	 * 
	 * @param loaders array of non-null class loaders
	 * @param parent parent class loader (can be null)
	 */
	public ChainedClassLoader(ClassLoader[] loaders, ClassLoader parent) {
		super(parent);

		this.parent = parent;

		Assert.notEmpty(loaders);

		synchronized (this.loaders) {
			for (int i = 0; i < loaders.length; i++) {
				ClassLoader classLoader = loaders[i];
				Assert.notNull(classLoader, "null classloaders not allowed");
				addClassLoader(classLoader);
			}
		}
	}

	public URL getResource(final String name) {
		if (System.getSecurityManager() != null) {
			return AccessController.doPrivileged(new PrivilegedAction<URL>() {

				public URL run() {
					return doGetResource(name);
				}
			});
		} else {
			return doGetResource(name);
		}
	}

	private URL doGetResource(String name) {
		URL url = doGetResource(name, loaders);

		if (url != null) {
			return url;
		} else {
			url = doGetResource(name, nonOsgiLoaders);
		}

		if (url != null) {
			return url;
		}

		return (parent != null ? parent.getResource(name) : url);
	}

	private URL doGetResource(String name, List<ClassLoader> classLoaders) {
		URL url = null;
		synchronized (classLoaders) {
			for (int i = 0; i < classLoaders.size(); i++) {
				ClassLoader loader = classLoaders.get(i);
				url = loader.getResource(name);
				if (url != null)
					return url;
			}
		}
		return url;
	}

	public Class<?> loadClass(final String name) throws ClassNotFoundException {

		if (System.getSecurityManager() != null) {
			try {
				return AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {

					public Class<?> run() throws Exception {
						return doLoadClass(name);
					}
				});
			} catch (PrivilegedActionException pae) {
				throw (ClassNotFoundException) pae.getException();
			}
		} else {
			return doLoadClass(name);
		}
	}

	private Class<?> doLoadClass(String name) throws ClassNotFoundException {
		Class<?> clazz = doLoadClass(name, loaders);

		if (clazz != null) {
			return clazz;
		} else {
			clazz = doLoadClass(name, nonOsgiLoaders);
		}

		if (clazz != null) {
			return clazz;
		}

		if (parent != null) {
			return parent.loadClass(name);
		}

		else {
			throw new ClassNotFoundException(name);
		}
	}

	private Class<?> doLoadClass(String name, List<ClassLoader> classLoaders) throws ClassNotFoundException {
		Class<?> clazz = null;

		synchronized (classLoaders) {
			for (int i = 0; i < classLoaders.size(); i++) {
				ClassLoader loader = classLoaders.get(i);
				try {
					clazz = loader.loadClass(name);
					return clazz;
				} catch (ClassNotFoundException e) {
					// keep moving through the class loaders
				}
			}
		}

		return clazz;
	}

	/**
	 * Adds a class loader defining the given class, to the chained class loader space.
	 * 
	 * @param clazz
	 */
	public void addClassLoader(final Class<?> clazz) {
		Assert.notNull(clazz, "a non-null class required");
		if (System.getSecurityManager() != null) {
			addClassLoader(AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
				public ClassLoader run() {
					return ClassUtils.getClassLoader(clazz);
				}
			}));
		} else {
			addClassLoader(ClassUtils.getClassLoader(clazz));
		}
	}

	/**
	 * Adds the given class loader to the existing list.
	 * 
	 * @param classLoader class loader to load classes from
	 */
	public void addClassLoader(ClassLoader classLoader) {
		Assert.notNull(classLoader, "a non-null classLoader required");

		if (!addNonOsgiLoader(classLoader)) {
			addOsgiLoader(classLoader);
		}
	}

	/**
	 * Checks if the given classloader is a known, non-OSGi loader. If it is, then it is added to a specific list based
	 * on the known ordering (ignoring the user defined one). This is done so that the discovered hierarchy is respected
	 * regardless of the user configuration.
	 * 
	 * @param classLoader
	 * @return true if the class loader was added/is known, false otherwise
	 */
	private boolean addNonOsgiLoader(ClassLoader classLoader) {
		// check if the classloader is known or not before doing any locking
		if (ClassUtils.knownNonOsgiLoadersSet.contains(classLoader)) {
			synchronized (nonOsgiLoaders) {
				if (!nonOsgiLoaders.contains(classLoader)) {
					int index = ClassUtils.knownNonOsgiLoaders.indexOf(classLoader);
					// add the class loader to the list if there is a match
					if (index >= 0) {
						int insertIndex = 0;
						// but consider the defined order
						for (int i = 0; i < nonOsgiLoaders.size(); i++) {
							int presentLoaderIndex = ClassUtils.knownNonOsgiLoaders.indexOf(nonOsgiLoaders.get(i));
							if (presentLoaderIndex >= 0 && presentLoaderIndex < index) {
								insertIndex = i + 1;
							} else {
								continue;
							}
						}
						nonOsgiLoaders.add(insertIndex, classLoader);
						return true;
					}
				}
			}
			return true;
		}
		return false;
	}

	private void addOsgiLoader(ClassLoader classLoader) {
		synchronized (loaders) {
			if (!loaders.contains(classLoader)) {
				loaders.add(classLoader);
			}
		}
	}
}