/*******************************************************************************
 * Copyright (c) 2000, 2010 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
 *     James D Miles (IBM Corp.) - bug 176250, Configurator needs to handle more platform urls 
 *******************************************************************************/
package org.eclipse.update.internal.configurator;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.SyncFailedException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Path;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.osgi.util.NLS;
import org.eclipse.update.configurator.IPlatformConfiguration;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * This class is responsible for providing the features and plugins (bundles) to 
 * the runtime. Configuration data is stored in the configuration/org.eclipse.update/platform.xml file.
 * When eclipse starts, it tries to load the config info from platform.xml.
 * If the file does not exist, then it also tries to read it from a temp or backup file.
 * If this does not succeed, a platform.xml is created by inspecting the eclipse 
 * installation directory (its features and plugin folders).
 * If platform.xml already exists, a check is made to see when it was last modified
 * and whether there are any file system changes that are newer (users may manually unzip 
 * features and plugins). In this case, the newly added features and plugins are picked up.
 * A check for existence of features and plugins is also performed, to detect deletions.
 */
public class PlatformConfiguration implements IPlatformConfiguration, IConfigurationConstants {

	private static PlatformConfiguration currentPlatformConfiguration = null;
	//private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
	//	private static final TransformerFactory transformerFactory = TransformerFactory.newInstance();
	private static final String XML_ENCODING = "UTF-8"; //$NON-NLS-1$

	private Configuration config;
	private URL configLocation;
	private HashMap externalLinkSites; // used to restore prior link site state
	private long changeStamp;
	private long featuresChangeStamp;
	private boolean featuresChangeStampIsValid;
	private long pluginsChangeStamp;
	private boolean pluginsChangeStampIsValid;
	//PAL nio optional
	//private FileLock lock;
	private Locker lock = null;
	private static int defaultPolicy = DEFAULT_POLICY_TYPE;
	private static boolean checkNio = false;
	private static boolean useNio;

	private static final String ECLIPSE = "eclipse"; //$NON-NLS-1$
	private static final String CONFIG_HISTORY = "history"; //$NON-NLS-1$
	private static final String PLATFORM_XML = "platform.xml"; //$NON-NLS-1$
	private static final String CONFIG_NAME = ConfigurationActivator.NAME_SPACE + "/" + PLATFORM_XML; //$NON-NLS-1$
	private static final String CONFIG_INI = "config.ini"; //NON-NLS-1$ //$NON-NLS-1$
	private static final String CONFIG_FILE_LOCK_SUFFIX = ".lock"; //$NON-NLS-1$
	private static final String CONFIG_FILE_TEMP_SUFFIX = ".tmp"; //$NON-NLS-1$
	private static final String LINKS = "links"; //$NON-NLS-1$
	private static final String[] BOOTSTRAP_PLUGINS = {};

	private static final String DEFAULT_FEATURE_APPLICATION = "org.eclipse.ui.ide.workbench"; //$NON-NLS-1$

	private static final String LINK_PATH = "path"; //$NON-NLS-1$
	private static final String LINK_READ = "r"; //$NON-NLS-1$
	private static final String LINK_READ_WRITE = "rw"; //$NON-NLS-1$
	private static URL installURL;

	private PlatformConfiguration(Location platformConfigLocation) throws CoreException, IOException {

		this.externalLinkSites = new HashMap();
		this.config = null;

		// initialize configuration
		initializeCurrent(platformConfigLocation);
		if (config != null)
			setDefaultPolicy();

		// Detect external links. These are "soft link" to additional sites. The link
		// files are usually provided by external installation programs. They are located
		// relative to this configuration URL.
		// Note: don't do it for self hosting or if update reconciler is disabled
		if (ConfigurationActivator.isReconciling()) {
			if (!isTransient())
				configureExternalLinks();

			// Validate sites in the configuration. Causes any sites that do not exist to
			// be removed from the configuration
			validateSites();
		}

		// compute differences between configuration and actual content of the sites
		// (base sites and link sites)
		// Note: when the config is transient (generated by PDE, etc.) we don't reconcile
		if (isTransient())
			return;

		// for 'osgi.clean' or osgi.checkConfiguration', force a refresh
		boolean osgiClean = "true".equals(ConfigurationActivator.getBundleContext().getProperty("osgi.clean")); //$NON-NLS-1$ //$NON-NLS-2$
		boolean osgiCheckConfiguration = "true".equals(ConfigurationActivator.getBundleContext().getProperty("osgi.checkConfiguration")); //$NON-NLS-1$ //$NON-NLS-2$

		if (osgiClean || osgiCheckConfiguration) {
			// We have to call refresh() for features to be rescanned correctly
			refresh();
			reconcile();
		} else {
			changeStamp = computeChangeStamp();
			if (changeStamp > config.getDate().getTime())
				reconcile();
		}
	}

	PlatformConfiguration(URL url) throws Exception {
		this.externalLinkSites = new HashMap();
		URL installLocation = Utils.getInstallURL();
		// Retrieve install location with respect to given url if possible
		try {
			if (url.getProtocol().equals("file")) {
				if (url.getPath().endsWith("configuration/org.eclipse.update/platform.xml"))
					installLocation = new Path(url.getPath()).removeLastSegments(3).toFile().toURL();
			}
		} catch (Exception e) {
			//
		}
		initialize(url, installLocation);
	}

	public PlatformConfiguration(URL url, URL installLocation) throws Exception {
		this.externalLinkSites = new HashMap();
		initialize(url, installLocation);
	}

	private void setDefaultPolicy() {
		// Assumption: If the configuration that we initialize with 
		// has a MANAGED_ONLY policy, then all sites should have default policy 
		// of MANAGED_ONLY.  
		ISiteEntry[] sentries = getConfiguredSites();
		if (sentries != null && sentries.length > 0) {
			int policyType = sentries[0].getSitePolicy().getType();
			if (policyType == ISitePolicy.MANAGED_ONLY) {
				defaultPolicy = policyType;
			}
		}
	}

	public static int getDefaultPolicy() {
		return defaultPolicy;
	}

	/*
	 * @see IPlatformConfiguration#createSiteEntry(URL, ISitePolicy)
	 */
	public ISiteEntry createSiteEntry(URL url, ISitePolicy policy) {
		return new SiteEntry(url, policy);
	}

	/*
	 * @see IPlatformConfiguration#createSitePolicy(int, String[])
	 */
	public ISitePolicy createSitePolicy(int type, String[] list) {
		return new SitePolicy(type, list);
	}

	/*
	 * @see IPlatformConfiguration#createFeatureEntry(String, String, String, boolean, String, URL)
	 */
	public IFeatureEntry createFeatureEntry(String id, String version, String pluginVersion, boolean primary, String application, URL[] root) {
		return new FeatureEntry(id, version, pluginVersion, primary, application, root);
	}

	/*
	 * @see IPlatformConfiguration#createFeatureEntry(String, String, String,
	 * String, boolean, String, URL)
	 */
	public IFeatureEntry createFeatureEntry(String id, String version, String pluginIdentifier, String pluginVersion, boolean primary, String application, URL[] root) {
		return new FeatureEntry(id, version, pluginIdentifier, pluginVersion, primary, application, root);
	}

	/*
	 * @see IPlatformConfiguration#configureSite(ISiteEntry)
	 */
	public void configureSite(ISiteEntry entry) {
		configureSite(entry, false);
	}

	/*
	 * @see IPlatformConfiguration#configureSite(ISiteEntry, boolean)
	 */
	public synchronized void configureSite(ISiteEntry entry, boolean replace) {

		if (entry == null)
			return;

		URL url = entry.getURL();
		if (url == null)
			return;

		String key = url.toExternalForm();
		if (config.getSiteEntry(key) != null && !replace)
			return;

		if (entry instanceof SiteEntry)
			config.addSiteEntry(key, (SiteEntry) entry);
	}

	/*
	 * @see IPlatformConfiguration#unconfigureSite(ISiteEntry)
	 */
	public synchronized void unconfigureSite(ISiteEntry entry) {
		if (entry == null)
			return;

		URL url = entry.getURL();
		if (url == null)
			return;

		String key = url.toExternalForm();
		if (entry instanceof SiteEntry)
			config.removeSiteEntry(key);
	}

	/*
	 * @see IPlatformConfiguration#getConfiguredSites()
	 */
	public ISiteEntry[] getConfiguredSites() {
		if (config == null)
			return new ISiteEntry[0];

		SiteEntry[] sites = config.getSites();
		ArrayList enabledSites = new ArrayList(sites.length);
		for (int i = 0; i < sites.length; i++) {
			if (sites[i].isEnabled())
				enabledSites.add(sites[i]);
		}
		return (ISiteEntry[]) enabledSites.toArray(new ISiteEntry[enabledSites.size()]);
	}

	/*
	 * @see IPlatformConfiguration#findConfiguredSite(URL)
	 */
	public ISiteEntry findConfiguredSite(URL url) {
		return findConfiguredSite(url, true);
	}

	/**
	 * 
	 * @param url site url
	 * @param checkPlatformURL if true, check for url format that is platform:/...
	 * @return
	 */
	public SiteEntry findConfiguredSite(URL url, boolean checkPlatformURL) {
		if (url == null)
			return null;
		String key = url.toExternalForm();

		SiteEntry result = config.getSiteEntry(key);
		if (result == null) { // retry with decoded URL string
			try {
				//PAL foundation
				//key = URLDecoder.decode(key, "UTF-8"); //$NON-NLS-1$
				key = UpdateURLDecoder.decode(key, "UTF-8"); //$NON-NLS-1$
			} catch (UnsupportedEncodingException e) {
				// ignore
			}
			result = config.getSiteEntry(key);
		}

		if (result == null && checkPlatformURL) {
			try {
				result = findConfiguredSite(config.asPlatformURL(url), false);
			} catch (Exception e) {
				//ignore
			}
		}
		return result;
	}

	/*
	 * @see IPlatformConfiguration#configureFeatureEntry(IFeatureEntry)
	 */
	public synchronized void configureFeatureEntry(IFeatureEntry entry) {
		if (entry == null)
			return;

		String key = entry.getFeatureIdentifier();
		if (key == null)
			return;

		// we should check each site and find where the feature is
		// located and then configure it
		if (config == null)
			config = new Configuration();

		SiteEntry[] sites = config.getSites();
		for (int i = 0; i < sites.length; i++) {
			// find out what site contains the feature and configure it
			try {
				URL url = new URL(sites[i].getURL(), FEATURES + "/" + entry.getFeatureIdentifier() + "_" + entry.getFeatureVersion() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				try {
					url = resolvePlatformURL(url, getBasePathLocation(url, config.getInstallURL(), config.getURL()));
				} catch (IOException e) {
				}
				if (new File(url.getFile()).exists())
					sites[i].addFeatureEntry(entry);
				else {
					url = new URL(sites[i].getURL(), FEATURES + "/" + entry.getFeatureIdentifier() + "/"); //$NON-NLS-1$ //$NON-NLS-2$
					if (new File(url.getFile()).exists())
						sites[i].addFeatureEntry(entry);
				}
			} catch (MalformedURLException e) {
			}
		}
	}

	/*
	 * @see IPlatformConfiguration#unconfigureFeatureEntry(IFeatureEntry)
	 */
	public synchronized void unconfigureFeatureEntry(IFeatureEntry entry) {
		if (entry == null)
			return;

		String key = entry.getFeatureIdentifier();
		if (key == null)
			return;

		config.unconfigureFeatureEntry(entry);
	}

	/*
	 * @see IPlatformConfiguration#getConfiguredFeatureEntries()
	 */
	public IFeatureEntry[] getConfiguredFeatureEntries() {
		ArrayList configFeatures = new ArrayList();
		SiteEntry[] sites = config.getSites();
		for (int i = 0; i < sites.length; i++) {
			FeatureEntry[] features = sites[i].getFeatureEntries();
			for (int j = 0; j < features.length; j++)
				configFeatures.add(features[j]);
		}
		return (IFeatureEntry[]) configFeatures.toArray(new FeatureEntry[configFeatures.size()]);
	}

	/*
	 * @see IPlatformConfiguration#findConfiguredFeatureEntry(String)
	 */
	public IFeatureEntry findConfiguredFeatureEntry(String id) {
		if (id == null)
			return null;

		SiteEntry[] sites = config.getSites();
		for (int i = 0; i < sites.length; i++) {
			FeatureEntry f = sites[i].getFeatureEntry(id);
			if (f != null)
				return f;
		}
		return null;
	}

	/*
	 * @see IPlatformConfiguration#getConfigurationLocation()
	 */
	public URL getConfigurationLocation() {
		return configLocation;
	}

	/*
	 * @see IPlatformConfiguration#getChangeStamp()
	 */
	public long getChangeStamp() {
		if (config.getLinkedConfig() == null)
			return config.getDate().getTime();
		return Math.max(config.getDate().getTime(), config.getLinkedConfig().getDate().getTime());
	}

	/*
	 * @see IPlatformConfiguration#getFeaturesChangeStamp()
	 * @deprecated Don't use this method
	 */
	public long getFeaturesChangeStamp() {
		return 0;
	}

	/*
	 * @see IPlatformConfiguration#getPluginsChangeStamp()
	 * @deprecated Don't use this method
	 */
	public long getPluginsChangeStamp() {
		return 0;
	}

	public String getApplicationIdentifier() {
		// Return the app if defined in system properties
		String application = ConfigurationActivator.getBundleContext().getProperty(ECLIPSE_APPLICATION);
		if (application != null)
			return application;

		// Otherwise, try to get it from the primary feature (aka product)
		String feature = getPrimaryFeatureIdentifier();

		// lookup application for feature (specified or defaulted)
		if (feature != null) {
			IFeatureEntry fe = findConfiguredFeatureEntry(feature);
			if (fe != null) {
				if (fe.getFeatureApplication() != null)
					return fe.getFeatureApplication();
			}
		}

		// return hardcoded default if we failed
		return DEFAULT_FEATURE_APPLICATION;
	}

	/*
	 * @see IPlatformConfiguration#getPrimaryFeatureIdentifier()
	 */
	public String getPrimaryFeatureIdentifier() {
		// Return the product if defined in system properties
		String primaryFeatureId = ConfigurationActivator.getBundleContext().getProperty(ECLIPSE_PRODUCT);
		if (primaryFeatureId != null) {
			// check if feature exists
			IFeatureEntry feature = findConfiguredFeatureEntry(primaryFeatureId);
			if (feature != null && feature.canBePrimary())
				return primaryFeatureId;
		}
		return null;
	}

	/*
	 * @see IPlatformConfiguration#getPluginPath()
	 */
	public URL[] getPluginPath() {
		ArrayList path = new ArrayList();
		Utils.debug("computed plug-in path:"); //$NON-NLS-1$

		ISiteEntry[] sites = getConfiguredSites();
		URL pathURL;
		for (int i = 0; i < sites.length; i++) {
			String[] plugins = sites[i].getPlugins();
			for (int j = 0; j < plugins.length; j++) {
				try {
					pathURL = new URL(((SiteEntry) sites[i]).getResolvedURL(), plugins[j]);
					path.add(pathURL);
					Utils.debug("   " + pathURL.toString()); //$NON-NLS-1$
				} catch (MalformedURLException e) {
					// skip entry ...
					Utils.debug("   bad URL: " + e); //$NON-NLS-1$
				}
			}
		}
		return (URL[]) path.toArray(new URL[0]);
	}

	public Set getPluginPaths() {

		HashSet paths = new HashSet();
		ISiteEntry[] sites = getConfiguredSites();

		for (int i = 0; i < sites.length; i++) {
			String[] plugins = sites[i].getPlugins();
			for (int j = 0; j < plugins.length; j++) {
				paths.add(plugins[j]);
			}
		}

		return paths;
	}

	/*
	 * A variation of the getPluginPath, but it returns the actual plugin entries
	 */
	public PluginEntry[] getPlugins() {
		ArrayList allPlugins = new ArrayList();
		Utils.debug("computed plug-ins:"); //$NON-NLS-1$

		ISiteEntry[] sites = getConfiguredSites();
		for (int i = 0; i < sites.length; i++) {
			if (!(sites[i] instanceof SiteEntry)) {
				Utils.debug("Site " + sites[i].getURL() + " is not a SiteEntry"); //$NON-NLS-1$ //$NON-NLS-2$
				continue;
			}
			PluginEntry[] plugins = ((SiteEntry) sites[i]).getPluginEntries();
			for (int j = 0; j < plugins.length; j++) {
				allPlugins.add(plugins[j]);
				Utils.debug("   " + plugins[j].getURL()); //$NON-NLS-1$
			}
		}
		return (PluginEntry[]) allPlugins.toArray(new PluginEntry[0]);
	}

	/*
	 * @see IPlatformConfiguration#getBootstrapPluginIdentifiers()
	 */
	public String[] getBootstrapPluginIdentifiers() {
		return BOOTSTRAP_PLUGINS;
	}

	/*
	 * @see IPlatformConfiguration#setBootstrapPluginLocation(String, URL)
	 */
	public void setBootstrapPluginLocation(String id, URL location) {
	}

	/*
	 * @see IPlatformConfiguration#isUpdateable()
	 */
	public boolean isUpdateable() {
		return true;
	}

	/*
	 * @see IPlatformConfiguration#isTransient()
	 */
	public boolean isTransient() {
		return (config != null) ? config.isTransient() : false;
	}

	/*
	 * @see IPlatformConfiguration#isTransient(boolean)
	 */
	public void isTransient(boolean value) {
		if (this != getCurrent() && config != null)
			config.setTransient(value);
	}

	/*
	 * @see IPlatformConfiguration#refresh()
	 */
	public synchronized void refresh() {
		// Reset computed values. Will be lazily refreshed
		// on next access
		ISiteEntry[] sites = getConfiguredSites();
		for (int i = 0; i < sites.length; i++) {
			if (sites[i].isUpdateable()) {
				// reset site entry
				((SiteEntry) sites[i]).refresh();
			}
		}
	}

	/*
	 * @see IPlatformConfiguration#save()
	 */
	public void save() throws IOException {
		if (isUpdateable())
			save(configLocation);
	}

	/*
	 * @see IPlatformConfiguration#save(URL)
	 */
	public synchronized void save(URL url) throws IOException {
		if (url == null)
			throw new IOException(Messages.cfig_unableToSave_noURL);

		OutputStream os = null;
		if (!url.getProtocol().equals("file")) { //$NON-NLS-1$
			// not a file protocol - attempt to save to the URL
			URLConnection uc = url.openConnection();
			uc.setDoOutput(true);
			os = uc.getOutputStream();
			try {
				saveAsXML(os);
				config.setDirty(false);
			} catch (CoreException e) {
				Utils.log(e.getMessage());
				Utils.log(e.getStatus());
				throw new IOException(NLS.bind(Messages.cfig_unableToSave, (new String[] {url.toExternalForm()})));
			} finally {
				os.close();
			}
		} else {
			// file protocol - do safe i/o
			File cfigFile = new File(url.getFile().replace('/', File.separatorChar));
			if (!cfigFile.getName().equals(PLATFORM_XML)) {
				if (cfigFile.exists() && cfigFile.isFile()) {
					Utils.log(Messages.PlatformConfiguration_expectingPlatformXMLorDirectory + cfigFile.getName());
					cfigFile = cfigFile.getParentFile();
				}
				cfigFile = new File(cfigFile, CONFIG_NAME);
			}
			File workingDir = cfigFile.getParentFile();
			if (workingDir != null && !workingDir.exists())
				workingDir.mkdirs();

			// Do safe i/o: 
			//    - backup current config, by moving it to the history folder
			//    - write new config to platform.xml.tmp file
			//    - rename the temp file to platform.xml
			File cfigFileOriginal = new File(cfigFile.getAbsolutePath());
			File cfigTmp = new File(cfigFile.getAbsolutePath() + CONFIG_FILE_TEMP_SUFFIX);

			// Backup old file
			if (cfigFile.exists()) {
				File backupDir = new File(workingDir, CONFIG_HISTORY);
				if (!backupDir.exists())
					backupDir.mkdir();
				long timestamp = cfigFile.lastModified();
				File preservedFile = new File(backupDir, String.valueOf(timestamp) + ".xml"); //$NON-NLS-1$
				// If the target file exists, increment the timestamp. Try at most 100 times.
				long increment = 1;
				while (preservedFile.exists() && increment < 100) {
					preservedFile = new File(backupDir, String.valueOf(timestamp + increment++) + ".xml"); //$NON-NLS-1$
				}
				if (!preservedFile.exists()) {
					// try renaming current config to backup copy
					if (!cfigFile.renameTo(preservedFile))
						Utils.log(Messages.PlatformConfiguration_cannotBackupConfig);
				}
			}

			// first save the file as temp
			os = new FileOutputStream(cfigTmp);

			try {
				saveAsXML(os);
				// Try flushing any internal buffers, and synchronize with the disk
				try {
					os.flush();
					((FileOutputStream) os).getFD().sync();
				} catch (SyncFailedException e2) {
					Utils.log(e2.getMessage());
				} catch (IOException e2) {
					Utils.log(e2.getMessage());
				}
				try {
					os.close();
					os = null;
				} catch (IOException e1) {
					Utils.log(Messages.PlatformConfiguration_cannotCloseStream + cfigTmp);
					Utils.log(e1.getMessage());
				}
				// set file time stamp to match that of the config element
				cfigTmp.setLastModified(config.getDate().getTime());
				// set this on config, in case the value was rounded off
				config.setLastModified(cfigTmp.lastModified());
				// make the change stamp to be the same as the config file
				changeStamp = config.getDate().getTime();
				config.setDirty(false);
			} catch (CoreException e) {
				throw new IOException(NLS.bind(Messages.cfig_unableToSave, (new String[] {cfigTmp.getAbsolutePath()})));
			} finally {
				if (os != null)
					try {
						os.close();
					} catch (IOException e1) {
						Utils.log(Messages.PlatformConfiguration_cannotCloseTempFile + cfigTmp);
					}
			}

			// at this point we have old config (if existed) as "bak" and the
			// new config as "tmp".
			boolean ok = cfigTmp.renameTo(cfigFileOriginal);
			if (!ok) {
				// this codepath represents a tiny failure window. The load processing
				// on startup will detect missing config and will attempt to start
				// with "tmp" (latest), then "bak" (the previous). We can also end up
				// here if we failed to rename the current config to "bak". In that
				// case we will restart with the previous state.
				Utils.log(Messages.PlatformConfiguration_cannotRenameTempFile);

				throw new IOException(NLS.bind(Messages.cfig_unableToSave, (new String[] {cfigTmp.getAbsolutePath()})));
			}
		}
	}

	public static PlatformConfiguration getCurrent() {
		return currentPlatformConfiguration;
	}

	/**
	 * Starts a platform installed at specified installURL using configuration located at platformConfigLocation.
	 */
	public static synchronized void startup(URL installURL, Location platformConfigLocation) throws Exception {
		PlatformConfiguration.installURL = installURL;

		// create current configuration
		if (currentPlatformConfiguration == null) {
			currentPlatformConfiguration = new PlatformConfiguration(platformConfigLocation);
			if (currentPlatformConfiguration.config == null)
				throw new Exception(Messages.PlatformConfiguration_cannotLoadConfig + platformConfigLocation.getURL());
			if (currentPlatformConfiguration.config.isDirty())
				// If this is a transient config (generated by PDE),do nothing
				// otherwise, save the configuration with proper date
				if (!currentPlatformConfiguration.isTransient())
					currentPlatformConfiguration.save();
		}
	}

	public static synchronized void shutdown() throws IOException {

		// save platform configuration
		PlatformConfiguration config = getCurrent();
		if (config != null) {
			// only save if there are changes in the config
			if (config.config.isDirty() && !config.isTransient()) {
				try {
					config.save();
				} catch (IOException e) {
					Utils.debug("Unable to save configuration " + e.toString()); //$NON-NLS-1$
					// will recover on next startup
				}
			}
		}
	}

	private synchronized void initializeCurrent(Location platformConfigLocation) throws IOException {

		// Configuration URL was is specified by the OSGi layer. 
		// Default behavior is to look
		// for configuration in the specified meta area. If not found, look
		// for pre-initialized configuration in the installation location.
		// If it is found it is used as the initial configuration. Otherwise
		// a new configuration is created. In either case the resulting
		// configuration is written into the specified configuration area.

		URL configFileURL = new URL(platformConfigLocation.getURL(), CONFIG_NAME);
		try {
			// check concurrent use lock
			getConfigurationLock(platformConfigLocation.getURL());

			// try loading the configuration
			try {
				config = loadConfig(configFileURL, installURL);
				Utils.debug("Using configuration " + configFileURL.toString()); //$NON-NLS-1$
			} catch (Exception e) {
				// failed to load, see if we can find pre-initialized configuration.
				try {
					Location parentLocation = platformConfigLocation.getParentLocation();
					if (parentLocation == null)
						throw new IOException(); // no platform.xml found, need to create default site

					URL sharedConfigFileURL = new URL(parentLocation.getURL(), CONFIG_NAME);
					config = loadConfig(sharedConfigFileURL, installURL);

					// pre-initialized config loaded OK ... copy any remaining update metadata
					// Only copy if the default config location is not the install location
					if (!sharedConfigFileURL.equals(configFileURL)) {
						// need to link config info instead of using a copy
						linkInitializedState(config, parentLocation, platformConfigLocation);
						Utils.debug("Configuration initialized from    " + sharedConfigFileURL.toString()); //$NON-NLS-1$
					}
					return;
				} catch (Exception ioe) {
					Utils.debug("Creating default configuration from " + configFileURL.toExternalForm()); //$NON-NLS-1$
					createDefaultConfiguration(configFileURL, installURL);
				}
			} finally {
				// if config == null an unhandled exception has been thrown and we allow it to propagate
				if (config != null) {
					configLocation = configFileURL;
					if (config.getURL() == null)
						config.setURL(configFileURL);
					verifyPath(configLocation, config.getInstallURL());
					Utils.debug("Creating configuration " + configFileURL.toString()); //$NON-NLS-1$
				}
			}
		} finally {
			// releaes concurrent use lock
			clearConfigurationLock();
		}
	}

	private synchronized void initialize(URL url, URL installLocation) throws Exception {
		if (url != null) {
			config = loadConfig(url, installLocation);
			Utils.debug("Using configuration " + url.toString()); //$NON-NLS-1$
		}
		if (config == null) {
			config = new Configuration();
			Utils.debug("Creating empty configuration object"); //$NON-NLS-1$
		}
		config.setURL(url);
		config.setInstallLocation(installLocation);
		configLocation = url;
	}

	private void createDefaultConfiguration(URL url, URL installLocation) throws IOException {
		// we are creating new configuration
		config = new Configuration();
		config.setURL(url);
		config.setInstallLocation(installLocation);
		SiteEntry defaultSite = (SiteEntry) getRootSite();
		configureSite(defaultSite);
		try {
			// parse the site directory to discover features
			defaultSite.loadFromDisk(0);
		} catch (CoreException e1) {
			Utils.log(Messages.PlatformConfiguration_cannotLoadDefaultSite + defaultSite.getResolvedURL());
			return;
		}
	}

	private ISiteEntry getRootSite() {
		// create default site entry for the root
		ISitePolicy defaultPolicy = createSitePolicy(getDefaultPolicy(), DEFAULT_POLICY_LIST);
		URL siteURL = null;
		try {
			siteURL = new URL("platform:/base/"); //$NON-NLS-1$  // try using platform-relative URL
		} catch (MalformedURLException e) {
			siteURL = getInstallURL(); // ensure we come up ... use absolute file URL
		}
		ISiteEntry defaultSite = createSiteEntry(siteURL, defaultPolicy);
		return defaultSite;
	}

	/**
	 * Gets the configuration lock
	 * @param url configuration directory
	 */
	private void getConfigurationLock(URL url) {
		if (!url.getProtocol().equals("file")) //$NON-NLS-1$
			return;

		File lockFile = new File(url.getFile(), ConfigurationActivator.NAME_SPACE + File.separator + CONFIG_FILE_LOCK_SUFFIX);
		verifyPath(url, config == null ? null : config.getInstallURL());
		// PAL nio optional
		lock = createLocker(lockFile);
		try {
			lock.lock();
		} catch (IOException ioe) {
			lock = null;
		}
	}

	private void clearConfigurationLock() {
		// PAL nio optional
		if (lock != null) {
			lock.release();
		}
	}

	/**
	 * Create a locker using java new I/O or regular I/O
	 * depending whether we run in J2SE or cdcFoundation
	 * PAL nio optional
	 */
	private static Locker createLocker(File lock) {
		if (!checkNio) {
			useNio = true;
			try {
				Class.forName("java.nio.channels.FileLock"); //$NON-NLS-1$
			} catch (ClassNotFoundException e) {
				useNio = false;
			}
		}
		if (useNio)
			return new Locker_JavaNio(lock);

		return new Locker_JavaIo(lock);
	}

	private long computeChangeStamp() {
		featuresChangeStamp = computeFeaturesChangeStamp();
		pluginsChangeStamp = computePluginsChangeStamp();
		changeStamp = Math.max(featuresChangeStamp, pluginsChangeStamp);
		// round off to seconds
		changeStamp = (changeStamp / 1000) * 1000;
		return changeStamp;
	}

	private long computeFeaturesChangeStamp() {
		if (featuresChangeStampIsValid)
			return featuresChangeStamp;

		long result = 0;
		ISiteEntry[] sites = config.getSites();
		for (int i = 0; i < sites.length; i++) {
			result = Math.max(result, sites[i].getFeaturesChangeStamp());
		}
		featuresChangeStamp = result;
		featuresChangeStampIsValid = true;
		return featuresChangeStamp;
	}

	private long computePluginsChangeStamp() {
		if (pluginsChangeStampIsValid)
			return pluginsChangeStamp;

		long result = 0;
		ISiteEntry[] sites = config.getSites();
		for (int i = 0; i < sites.length; i++) {
			result = Math.max(result, sites[i].getPluginsChangeStamp());
		}
		pluginsChangeStamp = result;
		pluginsChangeStampIsValid = true;
		return pluginsChangeStamp;
	}

	private void configureExternalLinks() {
		URL linkURL = getInstallURL();
		if (!supportsDetection(linkURL, config.getInstallURL()))
			return;

		try {
			linkURL = new URL(linkURL, LINKS + "/"); //$NON-NLS-1$
		} catch (MalformedURLException e) {
			// skip bad links ...
			Utils.debug("Unable to obtain link URL"); //$NON-NLS-1$
			return;
		}

		File linkDir = new File(linkURL.getFile());
		File[] links = linkDir.listFiles();
		if (links == null || links.length == 0) {
			Utils.debug("No links detected in " + linkURL.toExternalForm()); //$NON-NLS-1$
			return;
		}

		for (int i = 0; i < links.length; i++) {
			if (links[i].isDirectory())
				continue;
			Utils.debug("Link file " + links[i].getAbsolutePath()); //$NON-NLS-1$
			Properties props = new Properties();
			FileInputStream is = null;
			try {
				is = new FileInputStream(links[i]);
				props.load(is);
				configureExternalLinkSite(links[i], props);
			} catch (IOException e) {
				// skip bad links ...
				Utils.debug("   unable to load link file " + e); //$NON-NLS-1$
				continue;
			} finally {
				if (is != null) {
					try {
						is.close();
					} catch (IOException e) {
						// ignore ...
					}
				}
			}
		}
	}

	private void configureExternalLinkSite(File linkFile, Properties props) {
		String path = props.getProperty(LINK_PATH);
		if (path == null) {
			Utils.debug("   no path definition"); //$NON-NLS-1$
			return;
		}

		String link;
		boolean updateable = true;
		URL siteURL;

		// parse out link information
		if (path.startsWith(LINK_READ + " ")) { //$NON-NLS-1$
			updateable = false;
			link = path.substring(2).trim();
		} else if (path.startsWith(LINK_READ_WRITE + " ")) { //$NON-NLS-1$
			link = path.substring(3).trim();
		} else {
			link = path.trim();
		}

		// 	make sure we have a valid link specification
		try {
			File siteFile = new File(link);
			siteFile = new File(siteFile, ECLIPSE);
			siteURL = siteFile.toURL();
			if (findConfiguredSite(siteURL, true) != null)
				// linked site is already known
				return;
		} catch (MalformedURLException e) {
			// ignore bad links ...
			Utils.debug("  bad URL " + e); //$NON-NLS-1$
			return;
		}

		// process the link
		SiteEntry linkSite = (SiteEntry) externalLinkSites.get(siteURL);
		if (linkSite == null) {
			// this is a link to a new target so create site for it
			ISitePolicy linkSitePolicy = createSitePolicy(getDefaultPolicy(), DEFAULT_POLICY_LIST);
			linkSite = (SiteEntry) createSiteEntry(siteURL, linkSitePolicy);
		}
		// update site entry if needed
		linkSite.setUpdateable(updateable);
		linkSite.setLinkFileName(linkFile.getAbsolutePath());

		// configure the new site
		// NOTE: duplicates are not replaced (first one in wins)
		configureSite(linkSite);
		// there are changes in the config
		config.setDirty(true);
		Utils.debug("   " + (updateable ? "R/W -> " : "R/O -> ") + siteURL.toString()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	private void validateSites() {

		// check to see if all sites are valid. Remove any sites that do not exist.
		SiteEntry[] list = config.getSites();
		for (int i = 0; i < list.length; i++) {
			URL siteURL = list[i].getResolvedURL();
			if (!supportsDetection(siteURL, config.getInstallURL()))
				continue;

			File siteRoot = new File(siteURL.getFile().replace('/', File.separatorChar));
			if (!siteRoot.exists()) {
				unconfigureSite(list[i]);
				Utils.debug("Site " + siteURL + " does not exist ... removing from configuration"); //$NON-NLS-1$ //$NON-NLS-2$
			}

			// If multiple paths are defined in the same link file
			// or if the path changes, the old site will still be kept.
			// A better algorithm could be implemented by keeping track 
			// of the previous content of the link file.
			// TODO do the above
			String linkName = list[i].getLinkFileName();
			if (linkName != null) {
				File linkFile = new File(linkName);
				if (!linkFile.exists()) {
					unconfigureSite(list[i]);
					config.setDirty(true);
					Utils.debug("Site " + siteURL + " is no longer linked ... removing from configuration"); //$NON-NLS-1$ //$NON-NLS-2$	
				}
			}
		}
	}

	private void linkInitializedState(Configuration sharedConfig, Location sharedConfigLocation, Location newConfigLocation) {
		try {
			URL newConfigIniURL = new URL(newConfigLocation.getURL(), CONFIG_INI);
			if (!newConfigIniURL.getProtocol().equals("file")) //$NON-NLS-1$
				return; // need to be able to do write

			// modify config.ini and platform.xml to only link original files
			File configIni = new File(newConfigIniURL.getFile());
			Properties props = new Properties();
			String externalForm = Utils.makeRelative(config.getInstallURL(), sharedConfigLocation.getURL()).toExternalForm();
			props.put("osgi.sharedConfiguration.area", externalForm); //$NON-NLS-1$
			props.store(new FileOutputStream(configIni), "Linked configuration"); //$NON-NLS-1$

			config = new Configuration(new Date());
			config.setURL(new URL(newConfigLocation.getURL(), CONFIG_NAME));
			config.setLinkedConfig(sharedConfig);
			config.setDirty(true);
		} catch (IOException e) {
			// this is an optimistic copy. If we fail, the state will be reconciled
			// when the update manager is triggered.
			System.out.println(e);
		}
	}

	private Configuration loadConfig(URL url, URL installLocation) throws Exception {
		if (url == null)
			throw new IOException(Messages.cfig_unableToLoad_noURL);

		// try to load saved configuration file (watch for failed prior save())
		ConfigurationParser parser = null;
		try {
			parser = new ConfigurationParser();
		} catch (InvocationTargetException e) {
			throw (Exception) e.getTargetException();
		}

		config = null;
		Exception originalException = null;
		try {
			config = parser.parse(url, installLocation);
			if (config == null)
				throw new Exception(Messages.PlatformConfiguration_cannotFindConfigFile);
		} catch (Exception e1) {
			// check for save failures, so open temp and backup configurations
			originalException = e1;
			try {
				URL tempURL = new URL(url.toExternalForm() + CONFIG_FILE_TEMP_SUFFIX);
				config = parser.parse(tempURL, installLocation);
				if (config == null)
					throw new Exception();
				config.setDirty(true); // force saving to platform.xml
			} catch (Exception e2) {
				try {
					// check the backup
					if ("file".equals(url.getProtocol())) { //$NON-NLS-1$
						File cfigFile = new File(url.getFile().replace('/', File.separatorChar));
						File workingDir = cfigFile.getParentFile();
						if (workingDir != null && workingDir.exists()) {
							File[] backups = workingDir.listFiles(new FileFilter() {
								public boolean accept(File pathname) {
									return pathname.isFile() && pathname.getName().endsWith(".xml"); //$NON-NLS-1$
								}
							});
							if (backups != null && backups.length > 0) {
								URL backupUrl = backups[backups.length - 1].toURL();
								config = parser.parse(backupUrl, installLocation);
							}
						}
					}
					if (config == null)
						throw originalException; // we tried, but no config here ...
					config.setDirty(true); // force saving to platform.xml
				} catch (IOException e3) {
					throw originalException; // we tried, but no config here ...
				}
			}
		}

		return config;
	}

	public static boolean supportsDetection(URL url, URL installLocation) {
		String protocol = url.getProtocol();
		if (protocol.equals("file")) //$NON-NLS-1$
			return true;
		else if (protocol.equals("platform")) { //$NON-NLS-1$
			URL resolved = null;
			try {
				resolved = resolvePlatformURL(url, installLocation); // 19536
			} catch (IOException e) {
				return false; // we tried but failed to resolve the platform URL
			}
			return resolved.getProtocol().equals("file"); //$NON-NLS-1$
		} else
			return false;
	}

	private static void verifyPath(URL url, URL installLocation) {
		String protocol = url.getProtocol();
		String path = null;
		if (protocol.equals("file")) //$NON-NLS-1$
			path = url.getFile();
		else if (protocol.equals("platform")) { //$NON-NLS-1$
			URL resolved = null;
			try {
				resolved = resolvePlatformURL(url, installLocation); // 19536
				if (resolved.getProtocol().equals("file")) //$NON-NLS-1$
					path = resolved.getFile();
			} catch (IOException e) {
				// continue ...
			}
		}

		if (path != null) {
			File dir = new File(path).getParentFile();
			if (dir != null)
				dir.mkdirs();
		}
	}

	public static URL resolvePlatformURL(URL url, URL base_path_Location) throws IOException {
		if (url.getProtocol().equals("platform")) { //$NON-NLS-1$
			if (base_path_Location == null) {
				url = FileLocator.toFileURL(url);
				File f = new File(url.getFile());
				url = f.toURL();
			} else {
				final String BASE = "platform:/base/";
				final String CONFIG = "platform:/config/";
				String toResolve = url.toExternalForm();
				if (toResolve.startsWith(BASE))
					url = new URL(base_path_Location, toResolve.substring(BASE.length()));
				else if (toResolve.startsWith(CONFIG)) {
					url = new URL(base_path_Location, toResolve.substring(CONFIG.length()));
				} else
					url = base_path_Location;
			}
		}
		return url;
	}

	private URL getBasePathLocation(URL url, URL installLocation, URL configLocation) {
		final String BASE = "platform:/base/";
		final String CONFIG = "platform:/config/";
		String toResolve = url.toExternalForm();
		if (toResolve.startsWith(BASE)) {
			return installLocation;
		} else if (toResolve.startsWith(CONFIG)) {
			URL config_loc;
			try {
				config_loc = new URL(configLocation, "..");
			} catch (MalformedURLException e) {
				return configLocation;
			}
			return config_loc;
		}
		return url;
	}

	public static URL getInstallURL() {
		return installURL;
	}

	private void saveAsXML(OutputStream stream) throws CoreException, IOException {
		BufferedWriter xmlWriter = new BufferedWriter(new OutputStreamWriter(stream, XML_ENCODING));
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setExpandEntityReferences(false);
			factory.setValidating(false);
			factory.setIgnoringComments(true);
			DocumentBuilder docBuilder = factory.newDocumentBuilder();
			Document doc = docBuilder.newDocument();

			if (config == null)
				throw Utils.newCoreException(Messages.PlatformConfiguration_cannotSaveNonExistingConfig, null);

			config.setDate(new Date());
			Element configElement = config.toXML(doc);
			doc.appendChild(configElement);

			// This is not DBCS friendly... PAL
			//XMLPrintHandler.printComment(xmlWriter,"Created on " + config.getDate().toString());
			XMLPrintHandler.printNode(xmlWriter, doc, XML_ENCODING);

		} catch (Exception e) {
			throw Utils.newCoreException("", e); //$NON-NLS-1$
		} finally {
			xmlWriter.flush();
			// will close the stream in the caller	
			//xmlWriter.close();
		}
	}

	private void reconcile() throws CoreException {
		long lastChange = config.getDate().getTime();
		SiteEntry[] sites = config.getSites();
		for (int s = 0; s < sites.length; s++) {
			if (sites[s].isUpdateable()) {
				long siteTimestamp = sites[s].getChangeStamp();
				if (siteTimestamp > lastChange)
					sites[s].loadFromDisk(lastChange);
			}
		}
		config.setDirty(true);
	}

	public Configuration getConfiguration() {
		return config;
	}
}
