/*******************************************************************************
 * Copyright (c) 2005, 2006 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.osgi.baseadaptor;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.Properties;
import org.eclipse.core.runtime.adaptor.LocationManager;
import org.eclipse.osgi.baseadaptor.bundlefile.BundleFile;
import org.eclipse.osgi.baseadaptor.hooks.*;
import org.eclipse.osgi.framework.adaptor.*;
import org.eclipse.osgi.framework.debug.Debug;
import org.eclipse.osgi.framework.internal.core.*;
import org.eclipse.osgi.framework.internal.core.Constants;
import org.eclipse.osgi.framework.log.FrameworkLog;
import org.eclipse.osgi.framework.log.FrameworkLogEntry;
import org.eclipse.osgi.internal.baseadaptor.*;
import org.eclipse.osgi.service.resolver.PlatformAdmin;
import org.eclipse.osgi.service.resolver.State;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;

/**
 * A Framework adaptor implementation that allows additional functionality to be
 * hooked in.  Hooks are configured using {@link HookConfigurator}
 * objects.   A framework extension may add hook configurators which can be used
 * to add hooks to the {@link HookRegistry}.
 * @see HookConfigurator
 * @see HookRegistry
 * @see AdaptorHook
 * @since 3.2
 */
public class BaseAdaptor implements FrameworkAdaptor{
	// System property used to set the parent classloader type (boot is the default)
	private static final String PROP_PARENT_CLASSLOADER = "osgi.parentClassloader"; //$NON-NLS-1$
	// A parent classloader type that specifies the application classloader
	private static final String PARENT_CLASSLOADER_APP = "app"; //$NON-NLS-1$
	// A parent classloader type that specifies the extension classlaoder
	private static final String PARENT_CLASSLOADER_EXT = "ext"; //$NON-NLS-1$
	// A parent classloader type that specifies the boot classlaoder
	private static final String PARENT_CLASSLOADER_BOOT = "boot"; //$NON-NLS-1$
	// A parent classloader type that specifies the framework classlaoder
	private static final String PARENT_CLASSLOADER_FWK = "fwk"; //$NON-NLS-1$
	// The BundleClassLoader parent to use when creating BundleClassLoaders.
	private static ClassLoader bundleClassLoaderParent;
	static {
		// check property for specified parent
		String type = FrameworkProperties.getProperty(BaseAdaptor.PROP_PARENT_CLASSLOADER, BaseAdaptor.PARENT_CLASSLOADER_BOOT);
		if (BaseAdaptor.PARENT_CLASSLOADER_FWK.equalsIgnoreCase(type))
			bundleClassLoaderParent = FrameworkAdaptor.class.getClassLoader();
		else if (BaseAdaptor.PARENT_CLASSLOADER_APP.equalsIgnoreCase(type))
			bundleClassLoaderParent = ClassLoader.getSystemClassLoader();
		else if (BaseAdaptor.PARENT_CLASSLOADER_EXT.equalsIgnoreCase(type)) {
			ClassLoader appCL = ClassLoader.getSystemClassLoader();
			if (appCL != null)
				bundleClassLoaderParent = appCL.getParent();
		}
		// default to boot classloader
		if (bundleClassLoaderParent == null)
			bundleClassLoaderParent = new ParentClassLoader();
	}

	// Empty parent classloader.  This is used by default as the BundleClassLoader parent.
	private static class ParentClassLoader extends ClassLoader {
		protected ParentClassLoader() {
			super(null);
		}
	}

	private EventPublisher eventPublisher;
	private ServiceRegistry serviceRegistry;
	private boolean stopping;
	private HookRegistry hookRegistry;
	private FrameworkLog log;
	private BundleContext context;
	private BaseStorage storage;
	private BundleWatcher bundleWatcher;

	/**
	 * Constructs a BaseAdaptor.
	 * @param args arguments passed to the adaptor by the framework.
	 */
	public BaseAdaptor(String[] args) {
		if (LocationManager.getConfigurationLocation() == null)
			LocationManager.initializeLocations();
		hookRegistry = new HookRegistry(this);
		FrameworkLogEntry[] errors = hookRegistry.initialize();
		if (errors.length > 0)
			for (int i = 0; i < errors.length; i++)
				getFrameworkLog().log(errors[i]);
		// get the storage after the registry has been initialized
		storage = getStorage();
		// TODO consider passing args to BaseAdaptorHooks
	}

	/**
	 * This method will call all configured adaptor hooks {@link AdaptorHook#initialize(BaseAdaptor)} method.
	 * @see FrameworkAdaptor#initialize(EventPublisher)
	 */
	public void initialize(EventPublisher publisher) {
		this.eventPublisher = publisher;
		serviceRegistry = new ServiceRegistryImpl();
		((ServiceRegistryImpl) serviceRegistry).initialize();
		// set the adaptor for the adaptor hooks
		AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
		for (int i = 0; i < adaptorHooks.length; i++)
			adaptorHooks[i].initialize(this);
	}

	/**
	 * @see FrameworkAdaptor#initializeStorage()
	 */
	public void initializeStorage() throws IOException {
		storage.initialize(this);
	}

	/**
	 * @see FrameworkAdaptor#compactStorage()
	 */
	public void compactStorage() throws IOException {
		storage.compact();
	}

	/**
	 * This method will call all the configured adaptor hook {@link AdaptorHook#addProperties(Properties)} methods.
	 * @see FrameworkAdaptor#getProperties()
	 */
	public Properties getProperties() {
		Properties props = new Properties();
		String resource = FrameworkProperties.getProperty(Constants.OSGI_PROPERTIES, Constants.DEFAULT_OSGI_PROPERTIES);
		try {
			InputStream in = null;
			File file = new File(resource);
			if (file.exists())
				in = new FileInputStream(file);
			if (in == null)
				in = getClass().getResourceAsStream(resource);
			if (in != null) {
				try {
					props.load(new BufferedInputStream(in));
				} finally {
					try {
						in.close();
					} catch (IOException ee) {
						// nothing to do
					}
				}
			} else {
				if (Debug.DEBUG && Debug.DEBUG_GENERAL)
					Debug.println("Skipping osgi.properties: " + resource); //$NON-NLS-1$
			}
		} catch (IOException e) {
			if (Debug.DEBUG && Debug.DEBUG_GENERAL)
				Debug.println("Unable to load osgi.properties: " + e.getMessage()); //$NON-NLS-1$
		}
		// add the storage properties
		storage.addProperties(props);
		// add the properties from each adaptor hook
		AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
		for (int i = 0; i < adaptorHooks.length; i++)
			adaptorHooks[i].addProperties(props);
		return props;
	}

	/**
	 * @see FrameworkAdaptor#getInstalledBundles()
	 */
	public BundleData[] getInstalledBundles() {
		return storage.getInstalledBundles();
	}

	/**
	 * This method will call each configured adaptor hook {@link AdaptorHook#mapLocationToURLConnection(String)} method
	 * until one returns a non-null value.  If none of the adaptor hooks return a non-null value then the 
	 * string is used to construct a new URL object to open a new url connection.
	 * 
	 * @see FrameworkAdaptor#mapLocationToURLConnection(String)
	 */
	public URLConnection mapLocationToURLConnection(String location) throws BundleException {
		try {
			URLConnection result = null;
			// try the adaptor hooks first;
			AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
			for (int i = 0; i < adaptorHooks.length; i++) {
				result = adaptorHooks[i].mapLocationToURLConnection(location);
				if (result != null)
					return result;
			}
			// just do the default
			return (new URL(location).openConnection());
		} catch (IOException e) {
			throw new BundleException(NLS.bind(AdaptorMsg.ADAPTOR_URL_CREATE_EXCEPTION, location), e);
		}
	}

	/**
	 * @see FrameworkAdaptor#installBundle(String, URLConnection)
	 */
	public BundleOperation installBundle(String location, URLConnection source) {
		return storage.installBundle(location, source);
	}

	/**
	 * @see FrameworkAdaptor#updateBundle(BundleData, URLConnection)
	 */
	public BundleOperation updateBundle(BundleData bundledata, URLConnection source) {
		return storage.updateBundle((BaseData) bundledata, source);
	}

	/**
	 * @see FrameworkAdaptor#uninstallBundle(BundleData)
	 */
	public BundleOperation uninstallBundle(BundleData bundledata) {
		return storage.uninstallBundle((BaseData) bundledata);
	}

	/**
	 * @see FrameworkAdaptor#getTotalFreeSpace()
	 */
	public long getTotalFreeSpace() throws IOException {
		return storage.getFreeSpace();
	}

	/**
	 * @see FrameworkAdaptor#getPermissionStorage()
	 */
	public PermissionStorage getPermissionStorage() throws IOException {
		return storage.getPermissionStorage();
	}

	/**
	 * @see FrameworkAdaptor#getServiceRegistry()
	 */
	public ServiceRegistry getServiceRegistry() {
		return serviceRegistry;
	}

	/**
	 * This method calls all the configured adaptor hook {@link AdaptorHook#frameworkStart(BundleContext)} methods.
	 * @see FrameworkAdaptor#frameworkStart(BundleContext)
	 */
	public void frameworkStart(BundleContext fwContext) throws BundleException {
		this.context = fwContext;
		stopping = false;
		BundleResourceHandler.setContext(fwContext);
		// always start the storage first
		storage.frameworkStart(fwContext);
		AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
		for (int i = 0; i < adaptorHooks.length; i++)
			adaptorHooks[i].frameworkStart(fwContext);
	}

	/**
	 * This method calls all the configured adaptor hook {@link AdaptorHook#frameworkStop(BundleContext)} methods.
	 * @see FrameworkAdaptor#frameworkStop(BundleContext)
	 */
	public void frameworkStop(BundleContext fwContext) throws BundleException {
		// first inform all configured adaptor hooks
		AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
		for (int i = 0; i < adaptorHooks.length; i++)
			adaptorHooks[i].frameworkStop(fwContext);
		// stop the storage last
		storage.frameworkStop(fwContext);
		fwContext = null;
	}

	/**
	 * This method calls all the configured adaptor hook {@link AdaptorHook#frameworkStopping(BundleContext)} methods.
	 * @see FrameworkAdaptor#frameworkStopping(BundleContext)
	 */
	public void frameworkStopping(BundleContext fwContext) {
		stopping = true;
		// always tell storage of stopping first
		storage.frameworkStopping(fwContext);
		// inform all configured adaptor hooks last
		AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
		for (int i = 0; i < adaptorHooks.length; i++)
			adaptorHooks[i].frameworkStopping(fwContext);
	}

	/**
	 * @see FrameworkAdaptor#getInitialBundleStartLevel()
	 */
	public int getInitialBundleStartLevel() {
		return storage.getInitialBundleStartLevel();
	}

	/**
	 * @see FrameworkAdaptor#setInitialBundleStartLevel(int)
	 */
	public void setInitialBundleStartLevel(int value) {
		storage.setInitialBundleStartLevel(value);
	}

	/**
	 * This method calls all configured adaptor hook  {@link AdaptorHook#createFrameworkLog()} methods 
	 * until the first one returns a non-null value.  If none of the adaptor hooks return a non-null
	 * value then a framework log implementation which does nothing is returned.
	 * @see FrameworkAdaptor#getFrameworkLog()
	 */
	public FrameworkLog getFrameworkLog() {
		if (log != null)
			return log;
		AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
		for (int i = 0; i < adaptorHooks.length; i++) {
			log = adaptorHooks[i].createFrameworkLog();
			if (log != null)
				return log;
		}
		log = new FrameworkLog() {
			public void log(FrameworkEvent frameworkEvent) {
				log(new FrameworkLogEntry(frameworkEvent.getBundle().getSymbolicName() == null ? frameworkEvent.getBundle().getLocation() : frameworkEvent.getBundle().getSymbolicName(), FrameworkLogEntry.ERROR, 0, "FrameworkEvent.ERROR", 0, frameworkEvent.getThrowable(), null)); //$NON-NLS-1$
			}

			public void log(FrameworkLogEntry logEntry) {
				System.err.print(logEntry.getEntry() + " "); //$NON-NLS-1$
				System.err.println(logEntry.getMessage());
				if (logEntry.getThrowable() != null)
					logEntry.getThrowable().printStackTrace(System.err);
			}

			public void setWriter(Writer newWriter, boolean append) {
				// do nothing
			}

			public void setFile(File newFile, boolean append) throws IOException {
				// do nothing
			}

			public File getFile() {
				// do nothing
				return null;
			}

			public void setConsoleLog(boolean consoleLog) {
				// do nothing
			}

			public void close() {
				// do nothing
			}
		};
		return log;
	}

	/**
	 * @see FrameworkAdaptor#createSystemBundleData()
	 */
	public BundleData createSystemBundleData() throws BundleException {
		return new SystemBundleData(this);
	}

	/**
	 * @see FrameworkAdaptor#getBundleWatcher()
	 */
	public BundleWatcher getBundleWatcher() {
		if (bundleWatcher != null)
			return bundleWatcher;
		final BundleWatcher[] watchers = hookRegistry.getWatchers();
		if (watchers.length == 0)
			return null;
		bundleWatcher = new BundleWatcher() {
			public void watchBundle(Bundle bundle, int type) {
				for (int i = 0; i < watchers.length; i++)
					watchers[i].watchBundle(bundle, type);
			}	
		};
		return bundleWatcher;
	}

	/**
	 * @see FrameworkAdaptor#getPlatformAdmin()
	 */
	public PlatformAdmin getPlatformAdmin() {
		return storage.getStateManager();
	}

	/**
	 * @see FrameworkAdaptor#getState()
	 */
	public State getState() {
		return storage.getStateManager().getSystemState();
	}

	/**
	 * This method calls all the configured classloading hooks {@link ClassLoadingHook#getBundleClassLoaderParent()} methods 
	 * until one returns a non-null value.
	 * @see FrameworkAdaptor#getBundleClassLoaderParent()
	 */
	public ClassLoader getBundleClassLoaderParent() {
		// ask the configured adaptor hooks first
		ClassLoader result = null;
		ClassLoadingHook[] cpManagerHooks = getHookRegistry().getClassLoadingHooks();
		for (int i = 0; i < cpManagerHooks.length; i++) {
			result = cpManagerHooks[i].getBundleClassLoaderParent();
			if (result != null)
				return result;
		}
		// none of the configured adaptor hooks gave use a parent loader; use the default
		return bundleClassLoaderParent;
	}

	/**
	 * This method calls all the configured adaptor hooks  {@link AdaptorHook#handleRuntimeError(Throwable)} methods.
	 * @see FrameworkAdaptor#handleRuntimeError(Throwable)
	 */
	public void handleRuntimeError(Throwable error) {
		AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
		for (int i = 0; i < adaptorHooks.length; i++)
			adaptorHooks[i].handleRuntimeError(error);
	}

	/**
	 * This method calls all the configured adaptor hooks {@link AdaptorHook#matchDNChain(String, String[])} methods 
	 * until one returns a true value.
	 * @see FrameworkAdaptor#matchDNChain(String, String[])
	 */
	public boolean matchDNChain(String pattern, String[] dnChain) {
		AdaptorHook[] adaptorHooks = getHookRegistry().getAdaptorHooks();
		for (int i = 0; i < adaptorHooks.length; i++)
			if (adaptorHooks[i].matchDNChain(pattern, dnChain))
				return true;
		return false;
	}

	/**
	 * Returns true if the {@link #frameworkStopping(BundleContext)} method has been called
	 * @return true if the framework is stopping
	 */
	public boolean isStopping() {
		return stopping;
	}

	/**
	 * Returns the event publisher for this BaseAdaptor
	 * @return the event publisher for this BaseAdaptor
	 */
	public EventPublisher getEventPublisher() {
		return eventPublisher;
	}

	/**
	 * Returns the <code>HookRegistry</code> object for this adaptor.
	 * @return the <code>HookRegistry</code> object for this adaptor.
	 */
	public HookRegistry getHookRegistry() {
		return hookRegistry;
	}

	/**
	 * Returns the system bundle's context
	 * @return the system bundle's context
	 */
	public BundleContext getContext() {
		return context;
	}

	/**
	 * Creates a bundle file object for the given content and base data. 
	 * This method must delegate to each configured bundle file factory 
	 * {@link BundleFileFactoryHook#createBundleFile(Object, BaseData, boolean)} method until one 
	 * factory returns a non-null value.  If no bundle file factory returns a non-null value 
	 * then the the default behavior will be performed.
	 * <p>
	 * If the specified content is <code>null</code> then the base content of the specified 
	 * bundledata must be found before calling any bundle file factories.
	 * </p>
	 * <p>
	 * After the bundle file has been created each configured bundle file wrapper factory
	 * {@link BundleFileWrapperFactoryHook#wrapBundleFile(BundleFile, Object, BaseData, boolean)}
	 * method is called to wrap the bundle file.
	 * </p>
	 * @param content The object which contains the content of a bundle file. A value of 
	 * <code>null</code> indicates that the storage must find the base content for the 
	 * specified BaseData.
	 * @param data The BaseData associated with the content
	 * @return a BundleFile object.
	 * @throws IOException if an error occured while creating the BundleFile
	 */
	public BundleFile createBundleFile(Object content, BaseData data) throws IOException {
		return storage.createBundleFile(content, data);
	}

	/**
	 * Returns true if the persistent storage is read-only
	 * @return true if the persistent storage is read-only
	 */
	public boolean isReadOnly() {
		return storage.isReadOnly();
	}

	/*
	 * This is an experimental method to allow adaptors to replace the storage implementation by 
	 * extending BaseAdaptor and overriding this method.  This method is experimental.
	 * @return a base storage object.
	 */
	protected BaseStorage getStorage() {
		if (storage != null)
			return storage;
		// this bit of code assumes the registry is initialized with a BaseStorageHook
		// we want to make sure we are using the same BaseStorage instance as the BaseStorageHook
		StorageHook[] hooks = hookRegistry.getStorageHooks();
		for (int i = 0; i < hooks.length && storage == null; i++)
			if (hooks[i] instanceof BaseStorageHook)
				storage = ((BaseStorageHook)hooks[i]).getStorage();
		return storage;
	}
}
