/*******************************************************************************
 * Copyright (c) 2007, 2017 IBM Corporation and others.
 * All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which accompanies this distribution,
 * and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 * IBM Corporation - initial implementation and ideas
 * Red Hat Inc. - Bug 460967
 ******************************************************************************/
package org.eclipse.equinox.internal.p2.reconciler.dropins;

import java.io.*;
import java.net.URI;
import java.net.URL;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.artifact.repository.ArtifactRepositoryManager;
import org.eclipse.equinox.internal.p2.core.helpers.*;
import org.eclipse.equinox.internal.p2.extensionlocation.*;
import org.eclipse.equinox.internal.p2.metadata.repository.MetadataRepositoryManager;
import org.eclipse.equinox.internal.p2.reconciler.dropins.DropinsRepositoryListener.LinkedRepository;
import org.eclipse.equinox.internal.p2.update.Configuration;
import org.eclipse.equinox.internal.p2.update.PathUtil;
import org.eclipse.equinox.internal.provisional.p2.directorywatcher.DirectoryWatcher;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.engine.IProfile;
import org.eclipse.equinox.p2.engine.IProfileRegistry;
import org.eclipse.equinox.p2.repository.IRepository;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.osgi.service.datalocation.Location;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class Activator implements BundleActivator {

	static final String PROP_APPLICATION_STATUS = "org.eclipse.equinox.p2.reconciler.application.status"; //$NON-NLS-1$
	public static final String ID = "org.eclipse.equinox.p2.reconciler.dropins"; //$NON-NLS-1$
	private static final String DROPINS_DIRECTORY = "org.eclipse.equinox.p2.reconciler.dropins.directory"; //$NON-NLS-1$
	private static final String DROPINS = "dropins"; //$NON-NLS-1$
	private static final String LINKS = "links"; //$NON-NLS-1$
	private static final String CONFIG_INI = "config.ini"; //$NON-NLS-1$
	private static final String PLATFORM_CFG = "org.eclipse.update/platform.xml"; //$NON-NLS-1$
	private static final String CACHE_FILENAME = "cache.timestamps"; //$NON-NLS-1$
	private static final String DIR_ECLIPSE = "eclipse"; //$NON-NLS-1$
	private static final String DIR_PLUGINS = "plugins"; //$NON-NLS-1$
	private static final String DIR_FEATURES = "features"; //$NON-NLS-1$
	private static final String EXT_LINK = ".link"; //$NON-NLS-1$
	public static final String TRACING_PREFIX = "[reconciler] "; //$NON-NLS-1$
	private static BundleContext bundleContext;
	private final static Set<IMetadataRepository> repositories = new HashSet<>();
	private Collection<File> filesToCheck = null;

	/**
	 * Helper method to create an extension location metadata repository at the given URI.
	 * If one already exists at that location then an exception will be thrown.
	 *
	 * This method never returns <code>null</code>.
	 *
	 * @throws IllegalStateException
	 * @throws ProvisionException
	 */
	public static IMetadataRepository createExtensionLocationMetadataRepository(URI location, String name, Map<String, String> properties) throws ProvisionException {
		IProvisioningAgent agent = getAgent();
		IMetadataRepositoryManager manager = agent.getService(IMetadataRepositoryManager.class);
		if (manager == null)
			throw new IllegalStateException("MetadataRepositoryManager not registered."); //$NON-NLS-1$
		ExtensionLocationMetadataRepositoryFactory factory = new ExtensionLocationMetadataRepositoryFactory();
		factory.setAgent(agent);
		// always compress repositories that we are creating.
		Map<String, String> repositoryProperties = new HashMap<>();
		repositoryProperties.put(IRepository.PROP_COMPRESSED, Boolean.TRUE.toString());
		if (properties != null)
			repositoryProperties.putAll(properties);
		IMetadataRepository repository = factory.create(location, name, ExtensionLocationMetadataRepository.TYPE, repositoryProperties);
		//we need to add the concrete repository to the repository manager, or its properties will not be correct
		((MetadataRepositoryManager) manager).addRepository(repository);
		manager.setRepositoryProperty(location, IRepository.PROP_SYSTEM, String.valueOf(true));
		return repository;
	}

	private static IProvisioningAgent getAgent() {
		return ServiceHelper.getService(getContext(), IProvisioningAgent.class);
	}

	/**
	 * Helper method to load an extension location metadata repository from the given URL.
	 *
	 * @throws IllegalStateException
	 * @throws ProvisionException
	 */
	public static IMetadataRepository loadMetadataRepository(URI location, IProgressMonitor monitor) throws ProvisionException {
		IMetadataRepositoryManager manager = getAgent().getService(IMetadataRepositoryManager.class);
		if (manager == null)
			throw new IllegalStateException("MetadataRepositoryManager not registered."); //$NON-NLS-1$
		IMetadataRepository repository = manager.loadRepository(location, monitor);
		manager.setRepositoryProperty(location, IRepository.PROP_SYSTEM, String.valueOf(true));
		return repository;
	}

	/**
	 * Helper method to create an extension location artifact repository at the given URL.
	 * If one already exists at that location then an exception will be thrown.
	 *
	 * This method never returns <code>null</code>.
	 *
	 * @throws IllegalStateException
	 * @throws ProvisionException
	 */
	public static IArtifactRepository createExtensionLocationArtifactRepository(URI location, String name, Map<String, String> properties) throws ProvisionException {
		IProvisioningAgent agent = getAgent();
		IArtifactRepositoryManager manager = agent.getService(IArtifactRepositoryManager.class);
		if (manager == null)
			throw new IllegalStateException("ArtifactRepositoryManager not registered."); //$NON-NLS-1$
		ExtensionLocationArtifactRepositoryFactory factory = new ExtensionLocationArtifactRepositoryFactory();
		factory.setAgent(agent);
		// always compress repositories that we are creating.
		Map<String, String> repositoryProperties = new HashMap<>();
		repositoryProperties.put(IRepository.PROP_COMPRESSED, Boolean.TRUE.toString());
		if (properties != null)
			repositoryProperties.putAll(properties);
		IArtifactRepository repository = factory.create(location, name, ExtensionLocationArtifactRepository.TYPE, repositoryProperties);
		//we need to add the concrete repository to the repository manager, or its properties will not be correct
		((ArtifactRepositoryManager) manager).addRepository(repository);
		manager.setRepositoryProperty(location, IRepository.PROP_SYSTEM, String.valueOf(true));
		return repository;
	}

	/**
	 * Helper method to load an extension location metadata repository from the given URL.
	 *
	 * @throws IllegalStateException
	 * @throws ProvisionException
	 */
	public static IArtifactRepository loadArtifactRepository(URI location, IProgressMonitor monitor) throws ProvisionException {
		IArtifactRepositoryManager manager = getAgent().getService(IArtifactRepositoryManager.class);
		if (manager == null)
			throw new IllegalStateException("ArtifactRepositoryManager not registered."); //$NON-NLS-1$
		IArtifactRepository repository = manager.loadRepository(location, monitor);
		manager.setRepositoryProperty(location, IRepository.PROP_SYSTEM, String.valueOf(true));
		return repository;
	}

	/*
	 * Return the set of metadata repositories known to this bundle. It is constructed from the repos
	 * for the drop-ins as well as the ones in the configuration.
	 */
	public static Set<IMetadataRepository> getRepositories() {
		return repositories;
	}

	@Override
	public void start(BundleContext context) throws Exception {
		bundleContext = context;

		// check to see if there is really any work to do. Do this after setting the context, and
		// doing other initialization in case others call our public methods later.
		if (isUpToDate()) {
			// clear the cache
			filesToCheck = null;
			return;
		}

		checkConfigIni();

		// create the watcher for the "drop-ins" folder
		watchDropins();
		// keep an eye on the platform.xml
		watchConfiguration();

		synchronize(null);
		writeTimestamps();

		// we should probably be holding on to these repos by URL
		// see Bug 223422
		// for now explicitly nulling out these repos to allow GC to occur
		repositories.clear();
		filesToCheck = null;
	}

	private void checkConfigIni() {
		File configuration = getConfigurationLocation();
		if (configuration == null) {
			LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to determine configuration location.")); //$NON-NLS-1$
			return;
		}

		File configIni = new File(configuration, CONFIG_INI);
		if (!configIni.exists()) {
			// try parent configuration
			File parentConfiguration = getParentConfigurationLocation();
			if (parentConfiguration == null)
				return;

			// write shared configuration
			Properties props = new Properties();
			try {
				try (OutputStream os = new BufferedOutputStream(new FileOutputStream(configIni))) {
					String externalForm = PathUtil.makeRelative(parentConfiguration.toURL().toExternalForm(), getOSGiInstallArea()).replace('\\', '/');
					props.put("osgi.sharedConfiguration.area", externalForm); //$NON-NLS-1$
					props.store(os, "Linked configuration"); //$NON-NLS-1$
				}
			} catch (IOException e) {
				LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to create linked configuration location.", e)); //$NON-NLS-1$
			}
		}
	}

	/*
	 * Return a boolean value indicating whether or not we need to run
	 * the reconciler due to changes in the file-system.
	 */
	private boolean isUpToDate() {
		// the user might want to force a reconciliation
		if ("true".equals(getContext().getProperty("osgi.checkConfiguration"))) { //$NON-NLS-1$//$NON-NLS-2$
			trace("User requested forced reconciliation via \"osgi.checkConfiguration=true\" System property."); //$NON-NLS-1$
			trace("Performing reconciliation."); //$NON-NLS-1$
			return false;
		}
		// master configuration changed. Reconcile.
		if (Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty(ProfileSynchronizer.PROP_IGNORE_USER_CONFIGURATION))) {
			Activator.trace("Master profile changed."); //$NON-NLS-1$
			Activator.trace("Performing reconciliation."); //$NON-NLS-1$
			return false;
		}

		// read timestamps
		Properties timestamps = readTimestamps();
		if (timestamps.isEmpty()) {
			trace("Cached timestamp file empty."); //$NON-NLS-1$
			trace("Performing reconciliation."); //$NON-NLS-1$
			return false;
		}

		// gather the list of files/folders that we need to check
		Collection<File> files = getFilesToCheck();
		for (File file : files) {
			String key = file.getAbsolutePath();
			String timestamp = timestamps.getProperty(key);
			if (timestamp == null) {
				trace("Missing timestamp for file: " + key); //$NON-NLS-1$
				trace("Performing reconciliation."); //$NON-NLS-1$
				return false;
			}
			long lastModified = file.lastModified();
			if (!Long.toString(lastModified).equals(timestamp)) {
				trace("Timestamp has been updated for file: " + key + ", expected: " + timestamp + ", actual: " + lastModified); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				trace("Performing reconciliation."); //$NON-NLS-1$
				return false;
			}
			timestamps.remove(key);
		}

		// if we had some extra timestamps in the file, then signal that something has
		// changed and we need to reconcile
		boolean result = timestamps.isEmpty();
		if (result) {
			trace("Cached timestamp values up to date."); //$NON-NLS-1$
			trace("Reconciliation skipped."); //$NON-NLS-1$
		} else {
			if (Tracing.DEBUG_RECONCILER) {
				trace("Found extra values in cached timestamp file: "); //$NON-NLS-1$
				for (Object object : timestamps.keySet())
					trace(object);
				trace("Performing reconciliation. "); //$NON-NLS-1$
			}
		}
		return result;
	}

	/*
	 * Restore the cached timestamp values.
	 */
	private Properties readTimestamps() {
		Properties result = new Properties();
		File file = Activator.getContext().getDataFile(CACHE_FILENAME);
		if (!file.exists())
			return result;
		trace("Reading timestamps from file: " + file.getAbsolutePath()); //$NON-NLS-1$
		try (InputStream input = new BufferedInputStream(new FileInputStream(file))) {
			result.load(input);
		} catch (IOException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Error occurred while reading cached timestamps for reconciliation.", e)); //$NON-NLS-1$
		}
		if (Tracing.DEBUG_RECONCILER) {
			for (Object key : result.keySet()) {
				Object value = result.get(key);
				trace(key.toString() + '=' + value);
			}
		}
		return result;
	}

	/*
	 * Return a collection of files which are interesting to us when we want to record timestamps
	 * to figure out if something has changed and perhaps avoid an unnecessary reconcilation.
	 */
	private Collection<File> getFilesToCheck() {
		if (filesToCheck != null)
			return filesToCheck;

		Set<File> result = new HashSet<>();

		// configuration/org.eclipse.update/platform.xml, configuration/../plugins, configuration/../features
		File configuration = getConfigurationLocation();
		if (configuration != null) {
			result.add(new File(configuration, PLATFORM_CFG));
			File parent = configuration.getParentFile();
			if (parent != null) {
				File plugins = new File(parent, "plugins"); //$NON-NLS-1$
				result.add(plugins);
				File features = new File(parent, "features"); //$NON-NLS-1$
				result.add(features);
			}
		}

		// if we are in shared mode then record the same files for the parent configuration
		File parentConfiguration = getParentConfigurationLocation();
		if (parentConfiguration != null) {
			result.add(new File(parentConfiguration, PLATFORM_CFG));
			File parent = parentConfiguration.getParentFile();
			if (parent != null) {
				File plugins = new File(parent, "plugins"); //$NON-NLS-1$
				result.add(plugins);
				File features = new File(parent, "features"); //$NON-NLS-1$
				result.add(features);
			}
		}

		// dropins folders
		File[] dropins = getDropinsDirectories();
		result.addAll(getDropinsToCheck(dropins));

		// links folders
		File[] links = getLinksDirectories();
		result.addAll(getDropinsToCheck(links));

		filesToCheck = result;
		return filesToCheck;
	}

	/*
	 * Iterate over the given collection of files (could be dropins or links folders) and
	 * return a collection of files that might be interesting to check the timestamps of.
	 */
	private Collection<File> getDropinsToCheck(File[] files) {
		Collection<File> result = new HashSet<>();
		for (File file : files) {
			// add top-level file/folder
			result.add(file);

			File[] children = file.listFiles();
			for (int inner = 0; children != null && inner < children.length; inner++) {
				File child = children[inner];
				if (child.isFile() && child.getName().toLowerCase().endsWith(EXT_LINK)) {
					// if we have a link file then add the link file and its target
					LinkedRepository repo = DropinsRepositoryListener.getLinkedRepository(child);
					if (repo == null || !repo.exists())
						continue;
					File target = repo.getLocation();
					result.add(child);
					result.add(target);
					File eclipse = new File(target, DIR_ECLIPSE);
					result.add(eclipse);
					result.add(new File(eclipse, DIR_PLUGINS));
					result.add(new File(eclipse, DIR_FEATURES));

				} else if (child.getName().equalsIgnoreCase(DIR_ECLIPSE)) {
					// if it is an "eclipse" dir then add it as well as "plugins" and "features"
					result.add(child);
					result.add(new File(child, DIR_PLUGINS));
					result.add(new File(child, DIR_FEATURES));

				} else if (child.isDirectory()) {
					// look for "dropins/foo/plugins" (and "features") and
					// "dropins/foo/eclipse/plugins" (and "features")
					// Note: we could have a directory-based bundle here but we
					// will still add it since it won't hurt anything (one extra timestamp check)
					result.add(child);
					File parent;
					File eclipse = new File(child, DIR_ECLIPSE);
					if (eclipse.exists()) {
						result.add(eclipse);
						parent = eclipse;
					} else {
						parent = child;
					}
					File plugins = new File(parent, DIR_PLUGINS);
					if (plugins.exists())
						result.add(plugins);
					File features = new File(parent, DIR_FEATURES);
					if (features.exists())
						result.add(features);
				}
			}
		}
		return result;
	}

	/*
	 * Persist the cache timestamp values.
	 */
	private void writeTimestamps() {
		Properties timestamps = new Properties();
		Collection<File> files = getFilesToCheck();
		for (File file : files) {
			timestamps.put(file.getAbsolutePath(), Long.toString(file.lastModified()));
		}

		// write out the file
		File file = Activator.getContext().getDataFile(CACHE_FILENAME);
		trace("Writing out timestamps to file : " + file.getAbsolutePath()); //$NON-NLS-1$
		file.delete();
		try (OutputStream output = new BufferedOutputStream(new FileOutputStream(file))) {
			timestamps.store(output, null);
			if (Tracing.DEBUG_RECONCILER) {
				for (Object key : timestamps.keySet()) {
					Object value = timestamps.get(key);
					trace(key.toString() + '=' + value);
				}
			}
		} catch (IOException e) {
			LogHelper.log(new Status(IStatus.ERROR, ID, "Error occurred while writing cache timestamps for reconciliation.", e)); //$NON-NLS-1$
		}
	}

	/*
	 * Synchronize the profile.
	 */
	public static synchronized void synchronize(IProgressMonitor monitor) {
		IProfile profile = getCurrentProfile(bundleContext);
		if (profile == null)
			return;
		// create the profile synchronizer on all available repositories
		ProfileSynchronizer synchronizer = new ProfileSynchronizer(getAgent(), profile, repositories);
		IStatus result = synchronizer.synchronize(monitor);
		if (ProfileSynchronizer.isReconciliationApplicationRunning()) {
			System.getProperties().put(PROP_APPLICATION_STATUS, result);
		}
		if (!result.isOK() && !(result.getSeverity() == IStatus.CANCEL))
			LogHelper.log(result);
	}

	/*
	 * Watch the platform.xml file.
	 */
	private void watchConfiguration() {
		File configFile = getConfigurationLocation();
		if (configFile == null) {
			LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to determine configuration location.")); //$NON-NLS-1$
			return;
		}

		configFile = new File(configFile, PLATFORM_CFG);
		if (!configFile.exists()) {
			// try parent configuration
			File parentConfiguration = getParentConfigurationLocation();
			if (parentConfiguration == null)
				return;

			File shareConfigFile = new File(parentConfiguration, PLATFORM_CFG);
			if (!shareConfigFile.exists())
				return;

			Configuration config = new Configuration();
			config.setDate(Long.toString(new Date().getTime()));
			config.setVersion("3.0"); //$NON-NLS-1$
			try {
				String sharedUR = PathUtil.makeRelative(shareConfigFile.toURL().toExternalForm(), getOSGiInstallArea()).replace('\\', '/');
				config.setSharedUR(sharedUR);
				// ensure that org.eclipse.update directory that holds platform.xml is pre-created.
				configFile.getParentFile().mkdirs();
				config.save(configFile, getOSGiInstallArea());
			} catch (IOException e) {
				LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to create linked platform.xml.", e)); //$NON-NLS-1$
				return;
			} catch (ProvisionException e) {
				LogHelper.log(new Status(IStatus.ERROR, ID, "Unable to create linked platform.xml.", e)); //$NON-NLS-1$
				return;
			}

		}
		DirectoryWatcher watcher = new DirectoryWatcher(configFile.getParentFile());
		PlatformXmlListener listener = new PlatformXmlListener(configFile);
		watcher.addListener(listener);
		watcher.poll();
		repositories.addAll(listener.getMetadataRepositories());
	}

	/*
	 * Create a new directory watcher with a repository listener on the drop-ins folder.
	 */
	private void watchDropins() {
		List<File> directories = new ArrayList<>();
		File[] dropinsDirectories = getDropinsDirectories();
		directories.addAll(Arrays.asList(dropinsDirectories));
		File[] linksDirectories = getLinksDirectories();
		directories.addAll(Arrays.asList(linksDirectories));
		if (directories.isEmpty())
			return;

		// we will compress the repositories and mark them hidden as "system" repos.
		Map<String, String> properties = new HashMap<>();
		properties.put(IRepository.PROP_COMPRESSED, Boolean.TRUE.toString());
		properties.put(IRepository.PROP_SYSTEM, Boolean.TRUE.toString());

		DropinsRepositoryListener listener = new DropinsRepositoryListener(getAgent(), DROPINS, properties);
		DirectoryWatcher watcher = new DirectoryWatcher(directories.toArray(new File[directories.size()]));
		watcher.addListener(listener);
		watcher.poll();
		repositories.addAll(listener.getMetadataRepositories());
	}

	@Override
	public void stop(BundleContext context) throws Exception {
		bundleContext = null;
	}

	/*
	 * Return the bundle context for this bundle.
	 */
	public static BundleContext getContext() {
		return bundleContext;
	}

	/*
	 * Helper method to get the configuration location. Return null if
	 * it is unavailable.
	 */
	public static File getConfigurationLocation() {
		Location configurationLocation = ServiceHelper.getService(getContext(), Location.class, Location.CONFIGURATION_FILTER);
		if (configurationLocation == null || !configurationLocation.isSet())
			return null;
		URL url = configurationLocation.getURL();
		if (url == null)
			return null;
		return URLUtil.toFile(url);
	}

	/*
	 * Helper method to get the shared configuration location. Return null if
	 * it is unavailable.
	 */
	public static File getParentConfigurationLocation() {
		Location configurationLocation = ServiceHelper.getService(getContext(), Location.class, Location.CONFIGURATION_FILTER);
		if (configurationLocation == null || !configurationLocation.isSet())
			return null;

		Location sharedConfigurationLocation = configurationLocation.getParentLocation();
		if (sharedConfigurationLocation == null)
			return null;

		URL url = sharedConfigurationLocation.getURL();
		if (url == null)
			return null;
		return URLUtil.toFile(url);
	}

	/*
	 * Do a look-up and return the OSGi install area if it is set.
	 */
	public static URL getOSGiInstallArea() {
		Location location = ServiceHelper.getService(Activator.getContext(), Location.class, Location.INSTALL_FILTER);
		if (location == null)
			return null;
		if (!location.isSet())
			return null;
		return location.getURL();
	}

	/*
	 * Perform variable substitution on the given string. Replace vars in the form %foo%
	 * with the equivalent property set in the System properties.
	 */
	public static String substituteVariables(String path) {
		if (path == null)
			return path;
		int beginIndex = path.indexOf('%');
		// no variable
		if (beginIndex == -1)
			return path;
		beginIndex++;
		int endIndex = path.indexOf('%', beginIndex);
		// no matching end % to indicate variable
		if (endIndex == -1)
			return path;
		// get the variable name and do a lookup
		String var = path.substring(beginIndex, endIndex);
		if (var.length() == 0 || var.indexOf(File.pathSeparatorChar) != -1)
			return path;
		var = getContext().getProperty(var);
		if (var == null)
			return path;
		return path.substring(0, beginIndex - 1) + var + path.substring(endIndex + 1);
	}

	/*
	 * Helper method to return the eclipse.home location. Return
	 * null if it is unavailable.
	 */
	public static File getEclipseHome() {
		Location eclipseHome = ServiceHelper.getService(getContext(), Location.class, Location.ECLIPSE_HOME_FILTER);
		if (eclipseHome == null || !eclipseHome.isSet())
			return null;
		URL url = eclipseHome.getURL();
		if (url == null)
			return null;
		return URLUtil.toFile(url);
	}

	/*
	 * Return the locations of the links directories. There is a potential for
	 * more than one to be returned here if we are running in shared mode.
	 */
	private static File[] getLinksDirectories() {
		List<File> linksDirectories = new ArrayList<>();
		File root = getEclipseHome();
		if (root != null)
			linksDirectories.add(new File(root, LINKS));

		// check to see if we are in shared mode. if so, then add the user's local
		// links directory. (the shared one will have been added above with the
		// reference to Eclipse home)
		if (getParentConfigurationLocation() != null) {
			File configuration = getConfigurationLocation();
			if (configuration != null && configuration.getParentFile() != null)
				linksDirectories.add(new File(configuration.getParentFile(), LINKS));
		}
		return linksDirectories.toArray(new File[linksDirectories.size()]);
	}

	/*
	 * Return the location of the dropins directories. These include the one specified by
	 * the "org.eclipse.equinox.p2.reconciler.dropins.directory" System property and the one
	 * in the Eclipse home directory. If we are in shared mode, then also add the user's
	 * local dropins directory.
	 */
	private static File[] getDropinsDirectories() {
		List<File> dropinsDirectories = new ArrayList<>();
		// did the user specify one via System properties?
		String watchedDirectoryProperty = bundleContext.getProperty(DROPINS_DIRECTORY);
		if (watchedDirectoryProperty != null) {
			// perform a variable substitution if necessary
			watchedDirectoryProperty = substituteVariables(watchedDirectoryProperty);
			dropinsDirectories.add(new File(watchedDirectoryProperty));
		}

		// always add the one in the Eclipse home directory
		File root = getEclipseHome();
		if (root != null)
			dropinsDirectories.add(new File(root, DROPINS));

		// check to see if we are in shared mode. if so, then add the user's local
		// dropins directory. (the shared one will have been added above with the
		// reference to Eclipse home)
		if (getParentConfigurationLocation() != null) {
			File configuration = getConfigurationLocation();
			if (configuration != null && configuration.getParentFile() != null)
				dropinsDirectories.add(new File(configuration.getParentFile(), DROPINS));
		}
		return dropinsDirectories.toArray(new File[dropinsDirectories.size()]);
	}

	/*
	 * Return the current profile or null if it cannot be retrieved.
	 */
	public static IProfile getCurrentProfile(BundleContext context) {
		IProvisioningAgent agent = getAgent();
		IProfileRegistry profileRegistry = agent.getService(IProfileRegistry.class);
		if (profileRegistry == null)
			return null;
		return profileRegistry.getProfile(IProfileRegistry.SELF);
	}

	/*
	 * If tracing is enabled, then write out the given message.
	 */
	public static void trace(Object message) {
		if (Tracing.DEBUG_RECONCILER)
			Tracing.debug(TRACING_PREFIX + message);
	}

}
