/*******************************************************************************
 * Copyright (c) 2007, 2018 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 API and implementation
 *
 * Ericsson AB (Pascal Rapicault) - Bug 397216 -[Shared] Better shared
 * configuration change discovery
 *
 * Red Hat, Inc (Krzysztof Daniel) - Bug 421935: Extend simpleconfigurator to
 * read .info files from many locations
 *
 *******************************************************************************/
package org.eclipse.equinox.internal.simpleconfigurator.manipulator;

import java.io.*;
import java.net.*;
import java.util.*;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.equinox.frameworkadmin.BundleInfo;
import org.eclipse.equinox.internal.frameworkadmin.equinox.ParserUtils;
import org.eclipse.equinox.internal.frameworkadmin.utils.Utils;
import org.eclipse.equinox.internal.provisional.configuratormanipulator.ConfiguratorManipulator;
import org.eclipse.equinox.internal.provisional.frameworkadmin.*;
import org.eclipse.equinox.internal.simpleconfigurator.SimpleConfiguratorImpl;
import org.eclipse.equinox.internal.simpleconfigurator.utils.EquinoxUtils;
import org.eclipse.equinox.internal.simpleconfigurator.utils.SimpleConfiguratorUtils;
import org.eclipse.equinox.simpleconfigurator.manipulator.SimpleConfiguratorManipulator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;

public class SimpleConfiguratorManipulatorImpl implements SimpleConfiguratorManipulator, ConfiguratorManipulator {
	class LocationInfo {
		URI[] prerequisiteLocations = null;
		URI systemBundleLocation = null;
		URI[] systemFragmentedBundleLocations = null;
	}

	private final static boolean DEBUG = false;

	private static final BundleInfo[] NULL_BUNDLEINFOS = new BundleInfo[0];

	public static final String PROP_KEY_EXCLUSIVE_INSTALLATION = "org.eclipse.equinox.simpleconfigurator.exclusiveInstallation"; //$NON-NLS-1$
	public static final String CONFIG_LIST = "bundles.info"; //$NON-NLS-1$
	public static final String CONFIG_FOLDER = "configuration"; //$NON-NLS-1$
	public static final String CONFIGURATOR_FOLDER = "org.eclipse.equinox.simpleconfigurator"; //$NON-NLS-1$
	public static final String PROP_KEY_CONFIGURL = "org.eclipse.equinox.simpleconfigurator.configUrl"; //$NON-NLS-1$
	public static final String SHARED_BUNDLES_INFO = CONFIG_FOLDER + File.separatorChar + CONFIGURATOR_FOLDER
			+ File.separatorChar + CONFIG_LIST;

	private Set<Manipulator> manipulators = new HashSet<>();

	/**
	 * Return the ConfiguratorConfigFile which is determined by the parameters set
	 * in Manipulator.
	 *
	 * @param manipulator
	 * @return File
	 */
	private static File getConfigFile(Manipulator manipulator) throws IllegalStateException {
		File fwConfigLoc = manipulator.getLauncherData().getFwConfigLocation();
		File baseDir = null;
		if (fwConfigLoc == null) {
			baseDir = manipulator.getLauncherData().getHome();
			if (baseDir == null) {
				if (manipulator.getLauncherData().getLauncher() != null) {
					baseDir = manipulator.getLauncherData().getLauncher().getParentFile();
				} else {
					throw new IllegalStateException("All of fwConfigFile, home, launcher are not set."); //$NON-NLS-1$
				}
			}
		} else {
			if (fwConfigLoc.exists())
				if (fwConfigLoc.isDirectory())
					baseDir = fwConfigLoc;
				else
					baseDir = fwConfigLoc.getParentFile();
			else {
				// TODO We need to decide whether launcher data configLocation is the location
				// of a file or a directory
				if (fwConfigLoc.getName().endsWith(".ini")) //$NON-NLS-1$
					baseDir = fwConfigLoc.getParentFile();
				else
					baseDir = fwConfigLoc;
			}
		}
		File configuratorFolder = new File(baseDir, SimpleConfiguratorManipulatorImpl.CONFIGURATOR_FOLDER);
		File targetFile = new File(configuratorFolder, SimpleConfiguratorManipulatorImpl.CONFIG_LIST);
		if (!Utils.createParentDir(targetFile))
			return null;
		return targetFile;
	}

	static boolean isPrerequisiteBundles(URI location, LocationInfo info) {
		boolean ret = false;

		if (info.prerequisiteLocations == null)
			return false;
		for (URI prerequisiteLocation : info.prerequisiteLocations) {
			if (location.equals(prerequisiteLocation)) {
				ret = true;
				break;
			}
		}

		return ret;
	}

	static boolean isSystemBundle(URI location, LocationInfo info) {
		if (info.systemBundleLocation == null)
			return false;
		if (location.equals(info.systemBundleLocation))
			return true;
		return false;
	}

	static boolean isSystemFragmentBundle(URI location, LocationInfo info) {
		boolean ret = false;
		if (info.systemFragmentedBundleLocations == null)
			return false;
		for (URI systemFragmentedBundleLocation : info.systemFragmentedBundleLocations) {
			if (location.equals(systemFragmentedBundleLocation)) {
				ret = true;
				break;
			}
		}
		return ret;
	}

	private static boolean isTargetConfiguratorBundle(BundleInfo[] bInfos) {
		for (BundleInfo bInfo : bInfos) {
			if (isTargetConfiguratorBundle(bInfo.getLocation())) {
				return true;
				// TODO confirm that startlevel of configurator bundle must be no larger than
				// beginning start level of fw. However, there is no way to know the start level
				// of cached ones.
			}
		}
		return false;
	}

	private static boolean isTargetConfiguratorBundle(URI location) {
		final String symbolic = Utils
				.getPathFromClause(Utils.getManifestMainAttributes(location, Constants.BUNDLE_SYMBOLICNAME));
		return (SimpleConfiguratorManipulator.SERVICE_PROP_VALUE_CONFIGURATOR_SYMBOLICNAME.equals(symbolic));
	}

	private void algorithm(int initialSl, SortedMap<Integer, List<BundleInfo>> bslToList, BundleInfo configuratorBInfo,
			List<BundleInfo> setToInitialConfig, List<BundleInfo> setToSimpleConfig, LocationInfo info) {
		int configuratorSL = configuratorBInfo.getStartLevel();

		Integer sL0 = bslToList.keySet().iterator().next();// StartLevel == 0;
		List<BundleInfo> list0 = bslToList.get(sL0);
		if (sL0.intValue() == 0)
			for (BundleInfo bInfo : list0) {
				if (isSystemBundle(bInfo.getLocation(), info)) {
					setToSimpleConfig.add(bInfo);
					break;
				}
			}

		for (Integer sL : bslToList.keySet()) {
			List<BundleInfo> list = bslToList.get(sL);

			if (sL.intValue() < configuratorSL) {
				for (BundleInfo bInfo : list) {
					if (!isSystemBundle(bInfo.getLocation(), info))
						setToInitialConfig.add(bInfo);
				}
			} else if (sL.intValue() > configuratorSL) {
				for (BundleInfo bInfo : list) {
					if (isPrerequisiteBundles(bInfo.getLocation(), info)
							|| isSystemFragmentBundle(bInfo.getLocation(), info))
						if (!isSystemBundle(bInfo.getLocation(), info))
							setToInitialConfig.add(bInfo);
					setToSimpleConfig.add(bInfo);
				}
			} else {
				boolean found = false;
				for (BundleInfo bInfo : list) {
					if (found) {
						if (!isSystemBundle(bInfo.getLocation(), info))
							if (isPrerequisiteBundles(bInfo.getLocation(), info)
									|| isSystemFragmentBundle(bInfo.getLocation(), info))
								setToInitialConfig.add(bInfo);
						setToSimpleConfig.add(bInfo);
						continue;
					}
					if (isTargetConfiguratorBundle(bInfo.getLocation()))
						found = true;
					else if (!isSystemBundle(bInfo.getLocation(), info))
						setToInitialConfig.add(bInfo);
					setToSimpleConfig.add(bInfo);
				}
			}
		}

		setToInitialConfig.add(configuratorBInfo);
	}

	private boolean checkResolve(BundleInfo bInfo, BundlesState state) {// throws ManipulatorException {
		if (bInfo == null)
			throw new IllegalArgumentException("bInfo is null."); //$NON-NLS-1$

		if (!state.isResolved())
			state.resolve(false);

		if (!state.isResolved(bInfo)) {
			printoutUnsatisfiedConstraints(bInfo, state);
			return false;
		}
		return true;
	}

	private boolean divideBundleInfos(Manipulator manipulator, List<BundleInfo> setToInitialConfig,
			List<BundleInfo> setToSimpleConfig, final int initialBSL) {
		BundlesState state = manipulator.getBundlesState();
		BundleInfo[] targetBundleInfos = null;
		if (state.isFullySupported()) {
			targetBundleInfos = state.getExpectedState();
		} else {
			targetBundleInfos = manipulator.getConfigData().getBundles();
		}
		BundleInfo configuratorBInfo = null;
		for (BundleInfo targetBundleInfo : targetBundleInfos) {
			if (isTargetConfiguratorBundle(targetBundleInfo.getLocation())) {
				if (targetBundleInfo.isMarkedAsStarted()) {
					configuratorBInfo = targetBundleInfo;
					break;
				}
			}
		}
		if (configuratorBInfo == null && !manipulators.contains(manipulator)) {
			return false;
		} else if (manipulators.contains(manipulator) && targetBundleInfos.length == 0) {
			// Resulting state will have no bundles - so is an uninstall, including
			// uninstall of the configurator. However, we have seen this manipulator
			// before with a target configurator bundle, so allow uninstall to proceed,
			// but only get one chance.
			manipulators.remove(manipulator);
		} else if (!manipulators.contains(manipulator)) {
			manipulators.add(manipulator);
		}

		if (state.isFullySupported()) {
			state.resolve(false);
		}

		LocationInfo info = new LocationInfo();
		setSystemBundles(state, info);
		if (configuratorBInfo != null) {
			setPrerequisiteBundles(configuratorBInfo, state, info);
			SortedMap<Integer, List<BundleInfo>> bslToList = getSortedMap(initialBSL, targetBundleInfos);
			algorithm(initialBSL, bslToList, configuratorBInfo, setToInitialConfig, setToSimpleConfig, info);
		}
		return true;
	}

	private SortedMap<Integer, List<BundleInfo>> getSortedMap(int initialSl, BundleInfo[] bInfos) {
		SortedMap<Integer, List<BundleInfo>> bslToList = new TreeMap<>();
		for (BundleInfo bInfo : bInfos) {
			Integer sL = Integer.valueOf(bInfo.getStartLevel());
			if (sL.intValue() == BundleInfo.NO_LEVEL)
				sL = Integer.valueOf(initialSl);
			List<BundleInfo> list = bslToList.get(sL);
			if (list == null) {
				list = new LinkedList<>();
				bslToList.put(sL, list);
			}
			list.add(bInfo);
		}
		return bslToList;
	}

	private BundleInfo[] orderingInitialConfig(List<BundleInfo> setToInitialConfig) {
		List<BundleInfo> notToBeStarted = new LinkedList<>();
		List<BundleInfo> toBeStarted = new LinkedList<>();
		for (BundleInfo bInfo : setToInitialConfig) {
			if (bInfo.isMarkedAsStarted())
				toBeStarted.add(bInfo);
			else
				notToBeStarted.add(bInfo);
		}
		setToInitialConfig.clear();
		setToInitialConfig.addAll(notToBeStarted);
		setToInitialConfig.addAll(toBeStarted);
		return Utils.getBundleInfosFromList(setToInitialConfig);

	}

	private void printoutUnsatisfiedConstraints(BundleInfo bInfo, BundlesState state) {
		if (DEBUG) {
			StringBuffer sb = new StringBuffer();
			sb.append("Missing constraints:\n"); //$NON-NLS-1$
			String[] missings = state.getUnsatisfiedConstraints(bInfo);
			for (String missing : missings) {
				sb.append(" " + missing + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			System.out.println(sb.toString());
		}
	}

	/**
	 * Like {@link SimpleConfiguratorImpl#chooseConfigurationURL(URL, URL[])} but it
	 * doesn't check file timestamps because if the
	 * {@link SimpleConfiguratorImpl#PROP_IGNORE_USER_CONFIGURATION} property is set
	 * then we already know that timestamps have been checked and we need to ignore
	 * the user config.
	 */
	private URL chooseConfigurationURL(String relativePath, URL[] configURL) throws MalformedURLException {
		if (configURL != null) {
			File userConfig = new File(configURL[0].getFile(), relativePath);
			if (configURL.length == 1) {
				return userConfig.exists() ? userConfig.toURI().toURL() : null;
			}

			File sharedConfig = new File(configURL[1].getFile(), relativePath);
			if (!userConfig.exists()) {
				return sharedConfig.exists() ? sharedConfig.toURI().toURL() : null;
			}

			if (!sharedConfig.exists()) {
				return userConfig.toURI().toURL();
			}

			if (Boolean.getBoolean(SimpleConfiguratorImpl.PROP_IGNORE_USER_CONFIGURATION)) {
				return sharedConfig.toURI().toURL();
			}
			return userConfig.toURI().toURL();
		}
		return null;
	}

	@Override
	public BundleInfo[] loadConfiguration(BundleContext context, String infoPath) throws IOException {
		URI installArea = EquinoxUtils.getInstallLocationURI(context);

		URL configURL = null;

		if (infoPath == null) {
			SimpleConfiguratorImpl simpleImpl = new SimpleConfiguratorImpl(context, null);
			configURL = simpleImpl.getConfigurationURL();
		} else {
			// == (not .equals) use the default source info, currently SOURCE_INFO_PATH
			boolean defaultSource = (infoPath == SOURCE_INFO);
			if (defaultSource)
				infoPath = SOURCE_INFO_PATH;

			URL[] configURLs = EquinoxUtils.getConfigAreaURL(context);
			configURL = chooseConfigurationURL(infoPath, configURLs);
		}

		// At this point the file specified by configURL should definitely exist or be
		// null
		if (configURL == null) {
			return NULL_BUNDLEINFOS;
		}

		List<BundleInfo> result = new ArrayList<>();
		// Stream will be closed by loadConfiguration
		result.addAll(Arrays.asList(loadConfiguration(configURL.openStream(), installArea)));

		try {
			List<File> infoFiles = SimpleConfiguratorUtils.getInfoFiles();
			for (File infoFile : infoFiles) {
				// Stream will be closed by loadConfiguration
				BundleInfo[] info = loadConfiguration(infoFile.toURL().openStream(), infoFile.getParentFile().toURI());
				result.addAll(Arrays.asList(info));
			}
		} catch (URISyntaxException e) {
			// ignore the extended configurations
		}

		return result.toArray(new BundleInfo[result.size()]);
	}

	/*
	 * InputStream must be closed
	 */
	@Override
	public BundleInfo[] loadConfiguration(InputStream stream, URI installArea) throws IOException {
		if (stream == null)
			return NULL_BUNDLEINFOS;

		List<org.eclipse.equinox.internal.simpleconfigurator.utils.BundleInfo> simpleBundles = SimpleConfiguratorUtils
				.readConfiguration(stream, installArea);

		// convert to FrameworkAdmin BundleInfo Type
		BundleInfo[] result = new BundleInfo[simpleBundles.size()];
		int i = 0;
		for (org.eclipse.equinox.internal.simpleconfigurator.utils.BundleInfo simpleInfo : simpleBundles) {
			URI location = simpleInfo.getLocation();
			if (!location.isAbsolute() && simpleInfo.getBaseLocation() != null)
				location = URIUtil.makeAbsolute(location, simpleInfo.getBaseLocation());

			BundleInfo bundleInfo = new BundleInfo(simpleInfo.getSymbolicName(), simpleInfo.getVersion(), location,
					simpleInfo.getStartLevel(), simpleInfo.isMarkedAsStarted());
			bundleInfo.setBaseLocation(simpleInfo.getBaseLocation());
			result[i++] = bundleInfo;
		}
		return result;
	}

	@Override
	public void saveConfiguration(BundleInfo[] configuration, OutputStream stream, URI installArea) throws IOException {
		org.eclipse.equinox.internal.simpleconfigurator.utils.BundleInfo[] simpleInfos = convertBundleInfos(
				configuration, installArea);
		SimpleConfiguratorManipulatorUtils.writeConfiguration(simpleInfos, stream);
	}

	@Override
	public void saveConfiguration(BundleInfo[] configuration, File outputFile, URI installArea) throws IOException {
		saveConfiguration(configuration, outputFile, installArea, false);
	}

	private void saveConfiguration(BundleInfo[] configuration, File outputFile, URI installArea, boolean backup)
			throws IOException {
		if (backup && outputFile.exists()) {
			File backupFile = Utils.getSimpleDataFormattedFile(outputFile);
			if (!outputFile.renameTo(backupFile)) {
				throw new IOException("Fail to rename from (" + outputFile + ") to (" + backupFile + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			}
		}

		org.eclipse.equinox.internal.simpleconfigurator.utils.BundleInfo[] simpleInfos = convertBundleInfos(
				configuration, installArea);

		// if empty remove the configuration file
		if (simpleInfos == null || simpleInfos.length == 0) {
			if (outputFile.exists()) {
				outputFile.delete();
			}
			File parentDir = outputFile.getParentFile();
			if (parentDir.exists()) {
				parentDir.delete();
			}
			return;
		}
		SimpleConfiguratorManipulatorUtils.writeConfiguration(simpleInfos, outputFile);
		if (CONFIG_LIST.equals(outputFile.getName()) && installArea != null
				&& isSharedInstallSetup(URIUtil.toFile(installArea), outputFile))
			rememberSharedBundlesInfoTimestamp(installArea, outputFile.getParentFile());
	}

	private void rememberSharedBundlesInfoTimestamp(URI installArea, File outputFolder) {
		if (installArea == null)
			return;

		File sharedBundlesInfo = new File(URIUtil.append(installArea, SHARED_BUNDLES_INFO));
		if (!sharedBundlesInfo.exists())
			return;

		Properties timestampToPersist = new Properties();
		timestampToPersist.put(SimpleConfiguratorImpl.KEY_BUNDLESINFO_TIMESTAMP,
				Long.toString(SimpleConfiguratorUtils.getFileLastModified(sharedBundlesInfo)));
		timestampToPersist.put(SimpleConfiguratorImpl.KEY_EXT_TIMESTAMP,
				Long.toString(SimpleConfiguratorUtils.getExtendedTimeStamp()));
		OutputStream os = null;
		try {
			try {
				File outputFile = new File(outputFolder, SimpleConfiguratorImpl.BASE_TIMESTAMP_FILE_BUNDLESINFO);
				os = new BufferedOutputStream(new FileOutputStream(outputFile));
				timestampToPersist.store(os, "Written by " + this.getClass()); //$NON-NLS-1$
			} finally {
				if (os != null)
					os.close();
			}
		} catch (IOException e) {
			return;
		}
	}

	private org.eclipse.equinox.internal.simpleconfigurator.utils.BundleInfo[] convertBundleInfos(
			BundleInfo[] configuration, URI installArea) {
		// convert to SimpleConfigurator BundleInfo Type
		org.eclipse.equinox.internal.simpleconfigurator.utils.BundleInfo[] simpleInfos = new org.eclipse.equinox.internal.simpleconfigurator.utils.BundleInfo[configuration.length];
		for (int i = 0; i < configuration.length; i++) {
			BundleInfo bundleInfo = configuration[i];
			URI location = bundleInfo.getLocation();
			if (bundleInfo.getSymbolicName() == null || bundleInfo.getVersion() == null || location == null)
				throw new IllegalArgumentException("Cannot persist bundleinfo: " + bundleInfo.toString()); //$NON-NLS-1$
			// only need to make a new BundleInfo if we are changing it.
			if (installArea != null)
				location = URIUtil.makeRelative(location, installArea);
			simpleInfos[i] = new org.eclipse.equinox.internal.simpleconfigurator.utils.BundleInfo(
					bundleInfo.getSymbolicName(), bundleInfo.getVersion(), location, bundleInfo.getStartLevel(),
					bundleInfo.isMarkedAsStarted());
			simpleInfos[i].setBaseLocation(bundleInfo.getBaseLocation());
		}
		return simpleInfos;
	}

	@Override
	public BundleInfo[] save(Manipulator manipulator, boolean backup) throws IOException {
		List<BundleInfo> setToInitialConfig = new LinkedList<>();
		List<BundleInfo> setToSimpleConfig = new LinkedList<>();
		ConfigData configData = manipulator.getConfigData();

		if (!divideBundleInfos(manipulator, setToInitialConfig, setToSimpleConfig,
				configData.getInitialBundleStartLevel()))
			return configData.getBundles();

		File outputFile = getConfigFile(manipulator);
		URI installArea = ParserUtils.getOSGiInstallArea(Arrays.asList(manipulator.getLauncherData().getProgramArgs()),
				manipulator.getConfigData().getProperties(), manipulator.getLauncherData()).toURI();
		saveConfiguration(setToSimpleConfig.toArray(new BundleInfo[setToSimpleConfig.size()]), outputFile, installArea,
				backup);
		configData.setProperty(SimpleConfiguratorManipulatorImpl.PROP_KEY_CONFIGURL,
				outputFile.toURL().toExternalForm());
		return orderingInitialConfig(setToInitialConfig);
	}

	void setPrerequisiteBundles(BundleInfo configuratorBundleInfo, BundlesState state, LocationInfo info) {
		if (state.isFullySupported())
			if (!this.checkResolve(configuratorBundleInfo, state)) {
				printoutUnsatisfiedConstraints(configuratorBundleInfo, state);
				return;
			}
		BundleInfo[] prerequisites = state.getPrerequisteBundles(configuratorBundleInfo);
		info.prerequisiteLocations = new URI[prerequisites.length];
		for (int i = 0; i < prerequisites.length; i++)
			info.prerequisiteLocations[i] = prerequisites[i].getLocation();
		return;

	}

	void setSystemBundles(BundlesState state, LocationInfo info) {
		BundleInfo systemBundleInfo = state.getSystemBundle();
		if (systemBundleInfo == null) {
			// TODO Log
			// throw new IllegalStateException("There is no systemBundle.\n");
			return;
		}
		if (state.isFullySupported())
			if (!this.checkResolve(systemBundleInfo, state)) {
				printoutUnsatisfiedConstraints(systemBundleInfo, state);
				return;
			}
		info.systemBundleLocation = systemBundleInfo.getLocation();
		BundleInfo[] fragments = state.getSystemFragmentedBundles();
		info.systemFragmentedBundleLocations = new URI[fragments.length];
		for (int i = 0; i < fragments.length; i++)
			info.systemFragmentedBundleLocations[i] = fragments[i].getLocation();
	}

	@Override
	public void updateBundles(Manipulator manipulator) throws IOException {
		if (DEBUG)
			System.out.println("SimpleConfiguratorManipulatorImpl#updateBundles()"); //$NON-NLS-1$

		BundlesState bundleState = manipulator.getBundlesState();

		if (bundleState == null)
			return;
		if (bundleState.isFullySupported())
			bundleState.resolve(true);

		BundleInfo[] currentBInfos = bundleState.getExpectedState();
		if (!isTargetConfiguratorBundle(currentBInfos))
			return;
		Properties properties = new Properties();
		String[] jvmArgs = manipulator.getLauncherData().getJvmArgs();
		for (String jvmArg : jvmArgs) {
			if (jvmArg.startsWith("-D")) {
				// $NON-NLS-1$
				int index = jvmArg.indexOf("="); //$NON-NLS-1$
				if (index > 0 && jvmArg.length() > 2) {
					String key = jvmArg.substring(2, index);
					String value = jvmArg.substring(index + 1);
					properties.setProperty(key, value);
				}
			}
		}

		Utils.appendProperties(properties, manipulator.getConfigData().getProperties());
		boolean exclusiveInstallation = Boolean.parseBoolean(
				properties.getProperty(SimpleConfiguratorManipulatorImpl.PROP_KEY_EXCLUSIVE_INSTALLATION));
		File configFile = getConfigFile(manipulator);

		File installArea = ParserUtils.getOSGiInstallArea(Arrays.asList(manipulator.getLauncherData().getProgramArgs()),
				manipulator.getConfigData().getProperties(), manipulator.getLauncherData());
		BundleInfo[] toInstall = new BundleInfo[0];

		boolean isShared = isSharedInstallSetup(installArea, configFile);
		if (!isShared || (isShared && !hasBaseChanged(installArea.toURI(), configFile.getParentFile()))) {
			try {
				// input stream will be closed for us
				toInstall = loadConfiguration(new FileInputStream(configFile), installArea.toURI());
			} catch (FileNotFoundException e) {
				// no file, just return an empty list
				toInstall = new BundleInfo[0];
			}
		}

		List<BundleInfo> toUninstall = new LinkedList<>();
		if (exclusiveInstallation)
			for (BundleInfo currentBInfo : currentBInfos) {
				boolean install = false;
				for (BundleInfo toInstall1 : toInstall) {
					if (currentBInfo.getLocation().equals(toInstall1.getLocation())) {
						install = true;
						break;
					}
				}
				if (!install) {
					toUninstall.add(currentBInfo);
				}
			}

		for (BundleInfo toInstall1 : toInstall) {
			try {
				bundleState.installBundle(toInstall1);
			} catch (RuntimeException e) {
				// Ignore
			}
		}
		if (exclusiveInstallation)
			for (BundleInfo bInfo : toUninstall) {
				bundleState.uninstallBundle(bInfo);
			}

		bundleState.resolve(true);
		manipulator.getConfigData().setBundles(bundleState.getExpectedState());
	}

	@Override
	public void cleanup(Manipulator manipulator) {
		File outputFile = getConfigFile(manipulator);
		outputFile.delete();

		if (outputFile.getParentFile().isDirectory())
			outputFile.getParentFile().delete();
	}

	private boolean hasBaseChanged(URI installArea, File outputFolder) {
		String rememberedTimestamp;
		String extensionTimestsamp;
		try {
			rememberedTimestamp = (String) loadProperties(
					new File(outputFolder, SimpleConfiguratorImpl.BASE_TIMESTAMP_FILE_BUNDLESINFO))
							.get(SimpleConfiguratorImpl.KEY_BUNDLESINFO_TIMESTAMP);
			extensionTimestsamp = (String) loadProperties(
					new File(outputFolder, SimpleConfiguratorImpl.BASE_TIMESTAMP_FILE_BUNDLESINFO))
							.get(SimpleConfiguratorImpl.KEY_EXT_TIMESTAMP);
		} catch (IOException e) {
			return false;
		}
		if (rememberedTimestamp == null)
			return false;

		File sharedBundlesInfo = new File(URIUtil.append(installArea, SHARED_BUNDLES_INFO));
		if (!sharedBundlesInfo.exists())
			return true;
		return !(String.valueOf(SimpleConfiguratorUtils.getFileLastModified(sharedBundlesInfo))
				.equals(rememberedTimestamp)
				&& String.valueOf(SimpleConfiguratorUtils.getExtendedTimeStamp()).equals(extensionTimestsamp));
	}

	private boolean isSharedInstallSetup(File installArea, File outputFile) {
		// An instance is treated as shared if the bundles.info file is not located in
		// the install area.
		return !new File(installArea, SHARED_BUNDLES_INFO).equals(outputFile);
	}

	private Properties loadProperties(File inputFile) throws FileNotFoundException, IOException {
		Properties props = new Properties();
		InputStream is = null;
		try {
			is = new FileInputStream(inputFile);
			props.load(is);
		} finally {
			try {
				if (is != null)
					is.close();
			} catch (IOException e) {
				// Do nothing
			}
			is = null;
		}
		return props;
	}
}
