/*******************************************************************************
 * Copyright (c) 2005, 2019 Cognos Incorporated, IBM Corporation and others.
 *
 * 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:
 *     Cognos Incorporated - initial API and implementation
 *     IBM Corporation - bug fixes and enhancements
 *     Code 9 - bug fixes and enhancements
 *******************************************************************************/
package org.eclipse.equinox.servletbridge;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.*;
import java.util.*;
import java.util.jar.*;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;

/**
 * The FrameworkLauncher provides the logic to:
 * 1) init
 * 2) deploy
 * 3) start
 * 4) stop
 * 5) undeploy
 * 6) destroy
 * an instance of the OSGi framework.
 * These 6 methods are provided to help manage the life-cycle and are called from outside this
 * class by the BridgeServlet. To create an extended FrameworkLauncher over-ride these methods to allow
 * custom behavior.
 */
public class FrameworkLauncher {

	private static final String REFERENCE_SCHEME = "reference:"; //$NON-NLS-1$
	private static final String CONFIG_INI = "config.ini"; //$NON-NLS-1$
	private static final String DOT_JAR = ".jar"; //$NON-NLS-1$
	private static final String WS_DELIM = " \t\n\r\f"; //$NON-NLS-1$
	protected static final String FILE_SCHEME = "file:"; //$NON-NLS-1$
	protected static final String FRAMEWORK_BUNDLE_NAME = "org.eclipse.osgi"; //$NON-NLS-1$
	protected static final String STARTER = "org.eclipse.core.runtime.adaptor.EclipseStarter"; //$NON-NLS-1$
	protected static final String NULL_IDENTIFIER = "@null"; //$NON-NLS-1$
	protected static final String OSGI_FRAMEWORK = "osgi.framework"; //$NON-NLS-1$
	protected static final String OSGI_FRAMEWORK_EXTENSIONS = "osgi.framework.extensions"; //$NON-NLS-1$
	protected static final String OSGI_INSTANCE_AREA = "osgi.instance.area"; //$NON-NLS-1$
	protected static final String OSGI_CONFIGURATION_AREA = "osgi.configuration.area"; //$NON-NLS-1$
	protected static final String OSGI_INSTALL_AREA = "osgi.install.area"; //$NON-NLS-1$
	protected static final String OSGI_FORCED_RESTART = "osgi.forcedRestart"; //$NON-NLS-1$
	protected static final String RESOURCE_BASE = "/WEB-INF/"; //$NON-NLS-1$
	protected static final String ECLIPSE = "eclipse/"; //$NON-NLS-1$
	protected static final String LAUNCH_INI = "launch.ini"; //$NON-NLS-1$

	private static final String EXTENSIONBUNDLE_DEFAULT_BSN = "org.eclipse.equinox.servletbridge.extensionbundle"; //$NON-NLS-1$
	private static final String EXTENSIONBUNDLE_DEFAULT_VERSION = "1.3.0"; //$NON-NLS-1$
	private static final String MANIFEST_VERSION = "Manifest-Version"; //$NON-NLS-1$
	private static final String BUNDLE_MANIFEST_VERSION = "Bundle-ManifestVersion"; //$NON-NLS-1$
	private static final String BUNDLE_NAME = "Bundle-Name"; //$NON-NLS-1$
	private static final String BUNDLE_SYMBOLIC_NAME = "Bundle-SymbolicName"; //$NON-NLS-1$
	private static final String BUNDLE_VERSION = "Bundle-Version"; //$NON-NLS-1$
	private static final String FRAGMENT_HOST = "Fragment-Host"; //$NON-NLS-1$
	private static final String EXPORT_PACKAGE = "Export-Package"; //$NON-NLS-1$

	private static final String CONFIG_COMMANDLINE = "commandline"; //$NON-NLS-1$
	private static final String CONFIG_EXTENDED_FRAMEWORK_EXPORTS = "extendedFrameworkExports"; //$NON-NLS-1$
	private static final String CONFIG_OVERRIDE_AND_REPLACE_EXTENSION_BUNDLE = "overrideAndReplaceExtensionBundle"; //$NON-NLS-1$

	static final PermissionCollection allPermissions = new PermissionCollection() {
		private static final long serialVersionUID = 482874725021998286L;
		// The AllPermission permission
		Permission allPermission = new AllPermission();

		// A simple PermissionCollection that only has AllPermission
		@Override
		public void add(Permission permission) {
			// do nothing
		}

		@Override
		public boolean implies(Permission permission) {
			return true;
		}

		@Override
		public Enumeration<Permission> elements() {
			return new Enumeration<Permission>() {
				int cur = 0;

				@Override
				public boolean hasMoreElements() {
					return cur < 1;
				}

				@Override
				public Permission nextElement() {
					if (cur == 0) {
						cur = 1;
						return allPermission;
					}
					throw new NoSuchElementException();
				}
			};
		}
	};

	static {
		// We do this to ensure the anonymous Enumeration class in allPermissions is pre-loaded
		if (allPermissions.elements() == null)
			throw new IllegalStateException();
	}

	protected ServletConfig config;
	protected ServletContext context;
	protected String resourceBase;
	private File platformDirectory;
	private ClassLoader frameworkContextClassLoader;
	private CloseableURLClassLoader frameworkClassLoader;

	void init(ServletConfig servletConfig) {
		config = servletConfig;
		context = servletConfig.getServletContext();
		initResourceBase();
		init();
	}

	/**
	 * try to find the resource base for this webapp by looking for the launcher initialization file.
	 */
	protected void initResourceBase() {
		try {
			if (context.getResource(RESOURCE_BASE + LAUNCH_INI) != null) {
				resourceBase = RESOURCE_BASE;
				return;
			}
			if (context.getResource(RESOURCE_BASE + ECLIPSE + LAUNCH_INI) != null) {
				resourceBase = RESOURCE_BASE + ECLIPSE;
				return;
			}
		} catch (MalformedURLException e) {
			// ignore
		}
		// If things don't work out, use the default resource base
		resourceBase = RESOURCE_BASE;
	}

	/**
	 * init is the first method called on the FrameworkLauncher and can be used for any initial setup.
	 * The default behavior is to do nothing.
	 */
	public void init() {
		// do nothing for now
	}

	/**
	 * destroy is the last method called on the FrameworkLauncher and can be used for any final cleanup.
	 * The default behavior is to do nothing.
	 */
	public void destroy() {
		// do nothing for now
	}

	/**
	 * deploy is used to move the OSGi framework libraries into a location suitable for execution.
	 * The default behavior is to copy the contents of the webapp's WEB-INF/eclipse directory
	 * to the webapp's temp directory.
	 */
	public synchronized void deploy() {
		if (platformDirectory != null) {
			context.log("Framework is already deployed"); //$NON-NLS-1$
			return;
		}

		File servletTemp = (File) context.getAttribute("javax.servlet.context.tempdir"); //$NON-NLS-1$
		platformDirectory = new File(servletTemp, "eclipse"); //$NON-NLS-1$
		if (!platformDirectory.exists()) {
			platformDirectory.mkdirs();
		}

		copyResource(resourceBase + "configuration/", new File(platformDirectory, "configuration")); //$NON-NLS-1$ //$NON-NLS-2$
		copyResource(resourceBase + "features/", new File(platformDirectory, "features")); //$NON-NLS-1$ //$NON-NLS-2$
		File plugins = new File(platformDirectory, "plugins"); //$NON-NLS-1$
		copyResource(resourceBase + "plugins/", plugins); //$NON-NLS-1$
		copyResource(resourceBase + "p2/", new File(platformDirectory, "p2")); //$NON-NLS-1$ //$NON-NLS-2$
		deployExtensionBundle(plugins);
		copyResource(resourceBase + ".eclipseproduct", new File(platformDirectory, ".eclipseproduct")); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * deployExtensionBundle will generate the Servletbridge extensionbundle if it is not already present in the platform's
	 * plugin directory. By default it exports "org.eclipse.equinox.servletbridge" and a versioned export of the Servlet API.
	 * Additional exports can be added by using the "extendedFrameworkExports" initial-param in the ServletConfig
	 */
	private void deployExtensionBundle(File plugins) {
		// we might want to parameterize the extension bundle BSN in the future
		final String extensionBundleBSN = EXTENSIONBUNDLE_DEFAULT_BSN;
		File extensionBundleFile = findExtensionBundleFile(plugins, extensionBundleBSN);

		if (extensionBundleFile == null)
			generateExtensionBundle(plugins, extensionBundleBSN, EXTENSIONBUNDLE_DEFAULT_VERSION);
		else if (Boolean.valueOf(config.getInitParameter(CONFIG_OVERRIDE_AND_REPLACE_EXTENSION_BUNDLE)).booleanValue()) {
			String extensionBundleVersion = findExtensionBundleVersion(extensionBundleFile, extensionBundleBSN);
			if (extensionBundleFile.isDirectory()) {
				deleteDirectory(extensionBundleFile);
			} else {
				extensionBundleFile.delete();
			}
			generateExtensionBundle(plugins, extensionBundleBSN, extensionBundleVersion);
		} else {
			processExtensionBundle(extensionBundleFile);
		}
	}

	private File findExtensionBundleFile(File plugins, final String extensionBundleBSN) {
		FileFilter extensionBundleFilter = new FileFilter() {
			@Override
			public boolean accept(File candidate) {
				return candidate.getName().startsWith(extensionBundleBSN + "_"); //$NON-NLS-1$
			}
		};
		File[] extensionBundles = plugins.listFiles(extensionBundleFilter);
		if (extensionBundles.length == 0)
			return null;

		if (extensionBundles.length > 1) {
			for (int i = 1; i < extensionBundles.length; i++) {
				if (extensionBundles[i].isDirectory()) {
					deleteDirectory(extensionBundles[i]);
				} else {
					extensionBundles[i].delete();
				}
			}
		}
		return extensionBundles[0];
	}

	private String findExtensionBundleVersion(File extensionBundleFile, String extensionBundleBSN) {
		String fileName = extensionBundleFile.getName();
		if (fileName.endsWith(DOT_JAR)) {
			return fileName.substring(extensionBundleBSN.length() + 1, fileName.length() - DOT_JAR.length());
		}
		return fileName.substring(extensionBundleBSN.length() + 1);
	}

	private void generateExtensionBundle(File plugins, String extensionBundleBSN, String extensionBundleVersion) {
		Manifest mf = new Manifest();
		Attributes attribs = mf.getMainAttributes();
		attribs.putValue(MANIFEST_VERSION, "1.0"); //$NON-NLS-1$
		attribs.putValue(BUNDLE_MANIFEST_VERSION, "2"); //$NON-NLS-1$
		attribs.putValue(BUNDLE_NAME, "Servletbridge Extension Bundle"); //$NON-NLS-1$
		attribs.putValue(BUNDLE_SYMBOLIC_NAME, extensionBundleBSN);
		attribs.putValue(BUNDLE_VERSION, extensionBundleVersion);
		attribs.putValue(FRAGMENT_HOST, "system.bundle; extension:=framework"); //$NON-NLS-1$

		String packageExports = null;
		if (context.getMajorVersion() > 3) {
			// we really have no idea what the packages or versions are, it all just a guess ...
			String servletVersion = context.getMajorVersion() + "." + context.getMinorVersion(); //$NON-NLS-1$
			packageExports = "org.eclipse.equinox.servletbridge; version=1.1" + //$NON-NLS-1$
					", javax.servlet; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.annotation; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.descriptor; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.http; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.resources; version=" + servletVersion; //$NON-NLS-1$
		} else if (context.getMajorVersion() == 3) {
			// We know spec version 3.0 corresponds to package version 2.6
			// we are guessing future 3.x spec versions will increment package versions minor, so ...
			String servletVersion = (context.getMajorVersion() - 1) + "." + (context.getMinorVersion() + 6); //$NON-NLS-1$
			String specVersion = context.getMajorVersion() + "." + context.getMinorVersion(); //$NON-NLS-1$
			packageExports = "org.eclipse.equinox.servletbridge; version=1.1" + //$NON-NLS-1$
					", javax.servlet; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet; version=" + specVersion + //$NON-NLS-1$
					", javax.servlet.annotation; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.annotation; version=" + specVersion + //$NON-NLS-1$
					", javax.servlet.descriptor; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.descriptor; version=" + specVersion + //$NON-NLS-1$
					", javax.servlet.http; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.http; version=" + specVersion + //$NON-NLS-1$
					", javax.servlet.resources; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.resources; version=" + specVersion; //$NON-NLS-1$
		} else {
			// We know spec version 2.x directly correspond to package versions
			String servletVersion = context.getMajorVersion() + "." + context.getMinorVersion(); //$NON-NLS-1$
			packageExports = "org.eclipse.equinox.servletbridge; version=1.1" + //$NON-NLS-1$
					", javax.servlet; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.http; version=" + servletVersion + //$NON-NLS-1$
					", javax.servlet.resources; version=" + servletVersion; //$NON-NLS-1$
		}

		String extendedExports = config.getInitParameter(CONFIG_EXTENDED_FRAMEWORK_EXPORTS);
		if (extendedExports != null && extendedExports.trim().length() != 0)
			packageExports += ", " + extendedExports; //$NON-NLS-1$

		attribs.putValue(EXPORT_PACKAGE, packageExports);
		writeJarFile(new File(plugins, extensionBundleBSN + "_" + extensionBundleVersion + DOT_JAR), mf); //$NON-NLS-1$
	}

	private void processExtensionBundle(File extensionBundleFile) {
		String fileName = extensionBundleFile.getName();
		if (fileName.endsWith(DOT_JAR)) {
			Manifest mf = readJarFile(extensionBundleFile);
			if (mf == null)
				return;
			Attributes attributes = mf.getMainAttributes();
			String exportPackage = (String) attributes.remove(new Attributes.Name("X-Deploy-Export-Package")); //$NON-NLS-1$
			if (exportPackage != null) {
				attributes.putValue("Export-Package", exportPackage); //$NON-NLS-1$
				writeJarFile(extensionBundleFile, mf);
			}
		}
	}

	private void writeJarFile(File jarFile, Manifest mf) {
		try {
			JarOutputStream jos = null;
			try {
				jos = new JarOutputStream(new FileOutputStream(jarFile), mf);
				jos.finish();
			} finally {
				if (jos != null)
					jos.close();
			}
		} catch (IOException e) {
			context.log("Error writing extension bundle", e); //$NON-NLS-1$
		}
	}

	private Manifest readJarFile(File jarFile) {
		try {
			JarInputStream jis = null;
			try {
				jis = new JarInputStream(new FileInputStream(jarFile));
				return jis.getManifest();
			} finally {
				if (jis != null)
					jis.close();
			}
		} catch (IOException e) {
			context.log("Error reading extension bundle", e); //$NON-NLS-1$
		}
		return null;
	}

	/** undeploy is the reverse operation of deploy and removes the OSGi framework libraries from their
	 * execution location. Typically this method will only be called if a manual undeploy is requested in the
	 * ServletBridge.
	 * By default, this method removes the OSGi install and also removes the workspace.
	 */
	public synchronized void undeploy() {
		if (platformDirectory == null) {
			context.log("Undeploy unnecessary. - (not deployed)"); //$NON-NLS-1$
			return;
		}

		if (frameworkClassLoader != null) {
			throw new IllegalStateException("Could not undeploy Framework - (not stopped)"); //$NON-NLS-1$
		}

		deleteDirectory(new File(platformDirectory, "configuration")); //$NON-NLS-1$
		deleteDirectory(new File(platformDirectory, "features")); //$NON-NLS-1$
		deleteDirectory(new File(platformDirectory, "plugins")); //$NON-NLS-1$
		deleteDirectory(new File(platformDirectory, "workspace")); //$NON-NLS-1$
		deleteDirectory(new File(platformDirectory, "p2")); //$NON-NLS-1$

		new File(platformDirectory, ".eclipseproduct").delete(); //$NON-NLS-1$
		platformDirectory = null;
	}

	/** start is used to "start" a previously deployed OSGi framework
	 * The default behavior will read launcher.ini to create a set of initial properties and
	 * use the "commandline" configuration parameter to create the equivalent command line arguments
	 * available when starting Eclipse.
	 */
	public synchronized void start() {
		if (platformDirectory == null)
			throw new IllegalStateException("Could not start the Framework - (not deployed)"); //$NON-NLS-1$

		if (frameworkClassLoader != null) {
			context.log("Framework is already started"); //$NON-NLS-1$
			return;
		}

		Map<String, String> initialPropertyMap = buildInitialPropertyMap();
		String[] args = buildCommandLineArguments();

		// Handle commandline -D properties
		for (String arg : args) {
			if (arg.startsWith("-D")) { //$NON-NLS-1$
				int equalsIndex = arg.indexOf('=');
				if (equalsIndex == -1) {
					initialPropertyMap.put(arg.substring(2), ""); //$NON-NLS-1$
				} else {
					String key = arg.substring(2, equalsIndex);
					String value = arg.substring(equalsIndex + 1);
					if (value.startsWith("\"") && value.endsWith("\"")) //$NON-NLS-1$//$NON-NLS-2$
						value = value.substring(1, value.length() - 1);
					setInitialProperty(initialPropertyMap, key, value);
				}
			}
		}

		ClassLoader original = Thread.currentThread().getContextClassLoader();
		try {
			System.setProperty("osgi.framework.useSystemProperties", "false"); //$NON-NLS-1$ //$NON-NLS-2$

			URL[] frameworkURLs = findFrameworkURLs(initialPropertyMap);
			frameworkClassLoader = new ChildFirstURLClassLoader(frameworkURLs, this.getClass().getClassLoader());
			Class<?> clazz = frameworkClassLoader.loadClass(STARTER);

			Method setInitialProperties = clazz.getMethod("setInitialProperties", Map.class); //$NON-NLS-1$
			setInitialProperties.invoke(null, initialPropertyMap);

			registerRestartHandler(clazz);

			Method runMethod = clazz.getMethod("startup", String[].class, Runnable.class); //$NON-NLS-1$
			runMethod.invoke(null, args, null);

			frameworkContextClassLoader = Thread.currentThread().getContextClassLoader();
		} catch (

		InvocationTargetException ite) {
			Throwable t = ite.getTargetException();
			if (t == null)
				t = ite;
			context.log("Error while starting Framework", t); //$NON-NLS-1$
			throw new RuntimeException(t.getMessage());
		} catch (Exception e) {
			context.log("Error while starting Framework", e); //$NON-NLS-1$
			throw new RuntimeException(e.getMessage());
		} finally {
			Thread.currentThread().setContextClassLoader(original);
		}
	}

	private URL[] findFrameworkURLs(Map<String, String> initialPropertyMap) {
		List<URL> frameworkURLs = new ArrayList<>();
		String installArea = initialPropertyMap.get(OSGI_INSTALL_AREA);
		if (installArea.startsWith(FILE_SCHEME)) {
			installArea = installArea.substring(FILE_SCHEME.length());
		}
		File installBase = new File(installArea);

		// OSGi framework
		String osgiFramework = initialPropertyMap.get(OSGI_FRAMEWORK);
		File osgiFrameworkFile = null;
		if (osgiFramework == null) {
			// search for osgi.framework in osgi.install.area
			String path = new File(installBase, "plugins").toString(); //$NON-NLS-1$
			path = searchFor(FRAMEWORK_BUNDLE_NAME, path);
			if (path == null)
				throw new RuntimeException("Could not find framework"); //$NON-NLS-1$

			osgiFrameworkFile = new File(path);
		} else {
			if (osgiFramework.startsWith(FILE_SCHEME)) {
				osgiFramework = osgiFramework.substring(FILE_SCHEME.length());
			}
			osgiFrameworkFile = new File(osgiFramework);
			if (!osgiFrameworkFile.isAbsolute())
				osgiFrameworkFile = new File(installBase, osgiFramework);
		}

		try {
			URL frameworkURL = osgiFrameworkFile.toURL();
			frameworkURLs.add(frameworkURL);
			// ensure the framework URL is absolute
			initialPropertyMap.put(OSGI_FRAMEWORK, frameworkURL.toExternalForm());
		} catch (MalformedURLException e) {
			throw new RuntimeException("Could not find framework -- " + e.getMessage()); //$NON-NLS-1$
		}

		// OSGi framework extensions
		String osgiFrameworkExtensions = initialPropertyMap.get(OSGI_FRAMEWORK_EXTENSIONS);
		if (osgiFrameworkExtensions != null) {
			StringTokenizer tokenizer = new StringTokenizer(osgiFrameworkExtensions, ","); //$NON-NLS-1$
			while (tokenizer.hasMoreTokens()) {
				String extension = tokenizer.nextToken().trim();
				if (extension.length() == 0)
					continue;

				URL extensionURL = findExtensionURL(extension, osgiFrameworkFile);
				if (extensionURL != null) {
					frameworkURLs.add(extensionURL);
				}
			}
		}
		return frameworkURLs.toArray(new URL[frameworkURLs.size()]);
	}

	private URL findExtensionURL(String extension, File osgiFrameworkFile) {
		File extensionFile = null;
		if (extension.startsWith(REFERENCE_SCHEME)) {
			extension = extension.substring(REFERENCE_SCHEME.length());
			if (!extension.startsWith(FILE_SCHEME))
				throw new RuntimeException("Non-file scheme for framework extension URL -- " + extension); //$NON-NLS-1$
			extension = extension.substring(FILE_SCHEME.length());
			extensionFile = new File(extension);
			if (!extensionFile.isAbsolute())
				extensionFile = new File(osgiFrameworkFile.getParentFile(), extension);
		} else {
			String fullExtensionPath = searchFor(extension, osgiFrameworkFile.getParent());
			if (fullExtensionPath == null)
				return null;
			extensionFile = new File(fullExtensionPath);
		}

		try {
			return extensionFile.toURL();
		} catch (MalformedURLException e) {
			throw new RuntimeException("Could not find framework extension -- " + extensionFile.getAbsolutePath() + " : " + e.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	private void registerRestartHandler(Class<?> starterClazz) throws IllegalAccessException, InvocationTargetException {
		Method registerFrameworkShutdownHandler = null;
		try {
			registerFrameworkShutdownHandler = starterClazz.getDeclaredMethod("internalAddFrameworkShutdownHandler", Runnable.class); //$NON-NLS-1$
			if (!registerFrameworkShutdownHandler.isAccessible()) {
				registerFrameworkShutdownHandler.setAccessible(true);
			}
			Runnable restartHandler = createRestartHandler(starterClazz);
			registerFrameworkShutdownHandler.invoke(null, restartHandler);
		} catch (NoSuchMethodException e) {
			// Ok. However we will not support restart events. Log this as info
			context.log(starterClazz.getName() + " does not support setting a shutdown handler. Restart handling is disabled."); //$NON-NLS-1$
			return;
		}

	}

	private Runnable createRestartHandler(Class<?> starterClazz) throws NoSuchMethodException {
		final Method getProperty = starterClazz.getDeclaredMethod("getProperty", String.class); //$NON-NLS-1$
		if (!getProperty.isAccessible()) {
			getProperty.setAccessible(true);
		}
		Runnable restartHandler = new Runnable() {
			@Override
			public void run() {
				try {
					String forcedRestart = (String) getProperty.invoke(null, OSGI_FORCED_RESTART);
					if (Boolean.valueOf(forcedRestart).booleanValue()) {
						stop();
						start();
					}
				} catch (InvocationTargetException ite) {
					Throwable t = ite.getTargetException();
					if (t == null)
						t = ite;
					throw new RuntimeException(t.getMessage());
				} catch (Exception e) {
					throw new RuntimeException(e.getMessage());
				}
			}
		};
		return restartHandler;
	}

	/** buildInitialPropertyMap create the initial set of properties from the contents of launch.ini
	 * and for a few other properties necessary to launch defaults are supplied if not provided.
	 * The value '@null' will set the map value to null.
	 * @return a map containing the initial properties
	 */
	@SuppressWarnings("rawtypes")
	protected Map<String, String> buildInitialPropertyMap() {
		Map<String, String> initialPropertyMap = new HashMap<>();
		Properties launchProperties = loadProperties(resourceBase + LAUNCH_INI);
		for (Map.Entry entry : launchProperties.entrySet()) {
			String key = (String) entry.getKey();
			String value = (String) entry.getValue();
			setInitialProperty(initialPropertyMap, key, value);
		}

		try {
			// install.area if not specified
			if (initialPropertyMap.get(OSGI_INSTALL_AREA) == null)
				initialPropertyMap.put(OSGI_INSTALL_AREA, platformDirectory.toURL().toExternalForm());

			// configuration.area if not specified
			if (initialPropertyMap.get(OSGI_CONFIGURATION_AREA) == null) {
				File configurationDirectory = new File(platformDirectory, "configuration"); //$NON-NLS-1$
				if (!configurationDirectory.exists()) {
					configurationDirectory.mkdirs();
				}
				initialPropertyMap.put(OSGI_CONFIGURATION_AREA, configurationDirectory.toURL().toExternalForm());
			}

			// instance.area if not specified
			if (initialPropertyMap.get(OSGI_INSTANCE_AREA) == null) {
				File workspaceDirectory = new File(platformDirectory, "workspace"); //$NON-NLS-1$
				if (!workspaceDirectory.exists()) {
					workspaceDirectory.mkdirs();
				}
				initialPropertyMap.put(OSGI_INSTANCE_AREA, workspaceDirectory.toURL().toExternalForm());
			}

			// read values from config.ini
			Properties configurationProperties = loadConfigurationFile(initialPropertyMap);

			// osgi.framework if not specified
			if (initialPropertyMap.get(OSGI_FRAMEWORK) == null) {
				String osgiFramework = configurationProperties.getProperty(OSGI_FRAMEWORK);
				if (osgiFramework != null)
					initialPropertyMap.put(OSGI_FRAMEWORK, osgiFramework);
			}

			// osgi.framework.extensions if not specified
			if (initialPropertyMap.get(OSGI_FRAMEWORK_EXTENSIONS) == null) {
				String osgiFrameworkExtensions = configurationProperties.getProperty(OSGI_FRAMEWORK_EXTENSIONS);
				if (osgiFrameworkExtensions != null)
					initialPropertyMap.put(OSGI_FRAMEWORK_EXTENSIONS, osgiFrameworkExtensions);
			}

		} catch (MalformedURLException e) {
			throw new RuntimeException("Error establishing location"); //$NON-NLS-1$
		}

		return initialPropertyMap;
	}

	private void setInitialProperty(Map<String, String> initialPropertyMap, String key, String value) {
		if (key.endsWith("*")) { //$NON-NLS-1$
			if (value.equals(NULL_IDENTIFIER)) {
				clearPrefixedSystemProperties(key.substring(0, key.length() - 1), initialPropertyMap);
			}
		} else if (value.equals(NULL_IDENTIFIER))
			initialPropertyMap.put(key, null);
		else
			initialPropertyMap.put(key, value);
	}

	private Properties loadConfigurationFile(Map<String, String> initialPropertyMap) {
		InputStream is = null;
		try {
			String installArea = initialPropertyMap.get(OSGI_INSTALL_AREA);
			if (installArea.startsWith(FILE_SCHEME)) {
				installArea = installArea.substring(FILE_SCHEME.length());
			}
			File installBase = new File(installArea);

			String configurationArea = initialPropertyMap.get(OSGI_CONFIGURATION_AREA);
			if (configurationArea.startsWith(FILE_SCHEME)) {
				configurationArea = configurationArea.substring(FILE_SCHEME.length());
			}
			File configurationBase = new File(configurationArea);
			if (!configurationBase.isAbsolute())
				configurationBase = new File(installBase, configurationArea);

			File configurationFile = new File(configurationBase, CONFIG_INI);
			if (!configurationFile.exists())
				return null;

			Properties configProperties = new Properties();
			is = new BufferedInputStream(new FileInputStream(configurationFile));
			configProperties.load(is);
			return configProperties;
		} catch (Throwable t) {
			context.log("Error reading configuration file -- " + t.toString()); //$NON-NLS-1$
			return null;
		} finally {
			if (is != null)
				try {
					is.close();
				} catch (IOException e) {
					// unexpected
					e.printStackTrace();
				}
		}
	}

	/**
	 * clearPrefixedSystemProperties clears System Properties by writing null properties in the targetPropertyMap that match a prefix
	 */
	private static void clearPrefixedSystemProperties(String prefix, Map<String, String> targetPropertyMap) {
		for (Object key : System.getProperties().keySet()) {
			String propertyName = (String) key;
			if (propertyName.startsWith(prefix) && !targetPropertyMap.containsKey(propertyName)) {
				targetPropertyMap.put(propertyName, null);
			}
		}
	}

	/**
	 * buildCommandLineArguments parses the commandline config parameter into a set of arguments
	 * @return an array of String containing the commandline arguments
	 */
	protected String[] buildCommandLineArguments() {
		List<String> args = new ArrayList<>();

		String commandLine = config.getInitParameter(CONFIG_COMMANDLINE);
		if (commandLine != null) {
			StringTokenizer tokenizer = new StringTokenizer(commandLine, WS_DELIM);
			while (tokenizer.hasMoreTokens()) {
				String arg = tokenizer.nextToken();
				if (arg.startsWith("\"")) { //$NON-NLS-1$
					if (arg.endsWith("\"")) { //$NON-NLS-1$
						if (arg.length() >= 2) {
							// strip the beginning and ending quotes
							arg = arg.substring(1, arg.length() - 1);
						}
					} else {
						String remainingArg = tokenizer.nextToken("\""); //$NON-NLS-1$
						arg = arg.substring(1) + remainingArg;
						// skip to next whitespace separated token
						tokenizer.nextToken(WS_DELIM);
					}
				} else if (arg.startsWith("'")) { //$NON-NLS-1$
					if (arg.endsWith("'")) { //$NON-NLS-1$
						if (arg.length() >= 2) {
							// strip the beginning and ending quotes
							arg = arg.substring(1, arg.length() - 1);
						}
					} else {
						String remainingArg = tokenizer.nextToken("'"); //$NON-NLS-1$
						arg = arg.substring(1) + remainingArg;
						// skip to next whitespace separated token
						tokenizer.nextToken(WS_DELIM);
					}
				} else if (arg.startsWith("-D")) { //$NON-NLS-1$
					int matchIndex = arg.indexOf("=\""); //$NON-NLS-1$
					if (matchIndex != -1) {
						if (!arg.substring(matchIndex + 2).endsWith("\"") && tokenizer.hasMoreTokens()) { //$NON-NLS-1$
							arg += tokenizer.nextToken("\"") + "\""; //$NON-NLS-1$ //$NON-NLS-2$
							// skip to next whitespace separated token
							tokenizer.nextToken(WS_DELIM);
						}
					}
				}
				args.add(arg);
			}
		}
		return args.toArray(new String[] {});
	}

	/**
	 * stop is used to "shutdown" the framework and make it avialable for garbage collection.
	 * The default implementation also has special handling for Apache Commons Logging to "release" any
	 * resources associated with the frameworkContextClassLoader.
	 */
	public synchronized void stop() {
		if (platformDirectory == null) {
			context.log("Shutdown unnecessary. (not deployed)"); //$NON-NLS-1$
			return;
		}

		if (frameworkClassLoader == null) {
			context.log("Framework is already shutdown"); //$NON-NLS-1$
			return;
		}

		ClassLoader original = Thread.currentThread().getContextClassLoader();
		try {
			Class<?> clazz = frameworkClassLoader.loadClass(STARTER);
			Method method = clazz.getDeclaredMethod("shutdown"); //$NON-NLS-1$
			Thread.currentThread().setContextClassLoader(frameworkContextClassLoader);
			method.invoke(clazz);

			// ACL keys its loggers off of the ContextClassLoader which prevents GC without calling release.
			// This section explicitly calls release if ACL is used.
			try {
				clazz = this.getClass().getClassLoader().loadClass("org.apache.commons.logging.LogFactory"); //$NON-NLS-1$
				method = clazz.getDeclaredMethod("release", ClassLoader.class); //$NON-NLS-1$
				method.invoke(clazz, frameworkContextClassLoader);
			} catch (ClassNotFoundException e) {
				// ignore, ACL is not being used
			}

		} catch (Exception e) {
			context.log("Error while stopping Framework", e); //$NON-NLS-1$
			return;
		} finally {
			frameworkClassLoader.close();
			frameworkClassLoader = null;
			frameworkContextClassLoader = null;
			Thread.currentThread().setContextClassLoader(original);
		}
	}

	/**
	 * copyResource is a convenience method to recursively copy resources from the ServletContext to
	 * an installation target. The default behavior will create a directory if the resourcepath ends
	 * in '/' and a file otherwise.
	 * @param resourcePath - The resource root path
	 * @param target - The root location where resources are to be copied
	 */
	protected void copyResource(String resourcePath, File target) {
		if (resourcePath.endsWith("/")) { //$NON-NLS-1$
			target.mkdir();
			Set<String> paths = context.getResourcePaths(resourcePath);
			if (paths == null)
				return;
			for (String path : paths) {
				File newFile = new File(target, path.substring(resourcePath.length()));
				copyResource(path, newFile);
			}
		} else {
			try {
				if (target.createNewFile()) {
					InputStream is = null;
					OutputStream os = null;
					try {
						is = context.getResourceAsStream(resourcePath);
						if (is == null)
							return;
						os = new FileOutputStream(target);
						byte[] buffer = new byte[8192];
						int bytesRead = is.read(buffer);
						while (bytesRead != -1) {
							os.write(buffer, 0, bytesRead);
							bytesRead = is.read(buffer);
						}
					} finally {
						if (is != null)
							is.close();

						if (os != null)
							os.close();
					}
				}
			} catch (IOException e) {
				context.log("Error copying resources", e); //$NON-NLS-1$
			}
		}
	}

	/**
	 * deleteDirectory is a convenience method to recursively delete a directory
	 * @param directory - the directory to delete.
	 * @return was the delete successful
	 */
	protected static boolean deleteDirectory(File directory) {
		if (directory.isDirectory()) {
			File[] files = directory.listFiles();
			for (File file : files) {
				if (file.isDirectory()) {
					deleteDirectory(file);
				} else {
					file.delete();
				}
			}
		}
		return directory.delete();
	}

	/**
	 * Used when to set the ContextClassLoader when the BridgeServlet delegates to a Servlet
	 * inside the framework
	 * @return a Classloader with the OSGi framework's context class loader.
	 */
	public synchronized ClassLoader getFrameworkContextClassLoader() {
		return frameworkContextClassLoader;
	}

	/**
	 * Platfom Directory is where the OSGi software is installed
	 * @return the framework install location
	 */
	protected synchronized File getPlatformDirectory() {
		return platformDirectory;
	}

	/**
	 * loadProperties is a convenience method to load properties from a servlet context resource
	 * @param resource - The target to read properties from
	 * @return the properties
	 */
	protected Properties loadProperties(String resource) {
		Properties result = new Properties();
		InputStream in = null;
		try {
			URL location = context.getResource(resource);
			if (location != null) {
				in = location.openStream();
				result.load(in);
			}
		} catch (MalformedURLException e) {
			// no url to load from
		} catch (IOException e) {
			// its ok if there is no file
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					// ignore
				}
			}
		}
		return result;
	}

	/***************************************************************************
	 * See org.eclipse.core.launcher [copy of searchFor, findMax,
	 * compareVersion, getVersionElements] TODO: If these methods were made
	 * public and static we could use them directly
	 **************************************************************************/

	/**
	 * Searches for the given target directory starting in the "plugins" subdirectory
	 * of the given location.  If one is found then this location is returned;
	 * otherwise an exception is thrown.
	 * @param target
	 *
	 * @return the location where target directory was found
	 * @param start the location to begin searching
	 */
	protected String searchFor(final String target, String start) {
		FileFilter filter = new FileFilter() {
			@Override
			public boolean accept(File candidate) {
				return candidate.getName().equals(target) || candidate.getName().startsWith(target + "_"); //$NON-NLS-1$
			}
		};
		File[] candidates = new File(start).listFiles(filter);
		if (candidates == null)
			return null;
		String[] arrays = new String[candidates.length];
		for (int i = 0; i < arrays.length; i++) {
			arrays[i] = candidates[i].getName();
		}
		int result = findMax(arrays);
		if (result == -1)
			return null;
		return candidates[result].getAbsolutePath().replace(File.separatorChar, '/') + (candidates[result].isDirectory() ? "/" : ""); //$NON-NLS-1$//$NON-NLS-2$
	}

	protected int findMax(String[] candidates) {
		int result = -1;
		Object maxVersion = null;
		for (int i = 0; i < candidates.length; i++) {
			String name = candidates[i];
			String version = ""; //$NON-NLS-1$ // Note: directory with version suffix is always > than directory without version suffix
			int index = name.indexOf('_');
			if (index != -1)
				version = name.substring(index + 1);
			Object currentVersion = getVersionElements(version);
			if (maxVersion == null) {
				result = i;
				maxVersion = currentVersion;
			} else {
				if (compareVersion((Object[]) maxVersion, (Object[]) currentVersion) < 0) {
					result = i;
					maxVersion = currentVersion;
				}
			}
		}
		return result;
	}

	/**
	 * Compares version strings.
	 * @param left
	 * @param right
	 * @return result of comparison, as integer;
	 * <code><0</code> if left < right;
	 * <code>0</code> if left == right;
	 * <code>>0</code> if left > right;
	 */
	private int compareVersion(Object[] left, Object[] right) {

		int result = ((Integer) left[0]).compareTo((Integer) right[0]); // compare major
		if (result != 0)
			return result;

		result = ((Integer) left[1]).compareTo((Integer) right[1]); // compare minor
		if (result != 0)
			return result;

		result = ((Integer) left[2]).compareTo((Integer) right[2]); // compare service
		if (result != 0)
			return result;

		return ((String) left[3]).compareTo((String) right[3]); // compare qualifier
	}

	/**
	 * Do a quick parse of version identifier so its elements can be correctly compared.
	 * If we are unable to parse the full version, remaining elements are initialized
	 * with suitable defaults.
	 * @param version
	 * @return an array of size 4; first three elements are of type Integer (representing
	 * major, minor and service) and the fourth element is of type String (representing
	 * qualifier). Note, that returning anything else will cause exceptions in the caller.
	 */
	private Object[] getVersionElements(String version) {
		if (version.endsWith(DOT_JAR))
			version = version.substring(0, version.length() - 4);
		Object[] result = {Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0), ""}; //$NON-NLS-1$
		StringTokenizer t = new StringTokenizer(version, "."); //$NON-NLS-1$
		String token;
		int i = 0;
		while (t.hasMoreTokens() && i < 4) {
			token = t.nextToken();
			if (i < 3) {
				// major, minor or service ... numeric values
				try {
					result[i++] = Integer.valueOf(token);
				} catch (Exception e) {
					// invalid number format - use default numbers (0) for the rest
					break;
				}
			} else {
				// qualifier ... string value
				result[i++] = token;
			}
		}
		return result;
	}

	/**
	 * The ChildFirstURLClassLoader alters regular ClassLoader delegation and will check the URLs
	 * used in its initialization for matching classes before delegating to it's parent.
	 * Sometimes also referred to as a ParentLastClassLoader
	 */
	protected static class ChildFirstURLClassLoader extends CloseableURLClassLoader {
		private static final boolean CHILDFIRST_REGISTERED_AS_PARALLEL;

		static {
			boolean registeredAsParallel;
			try {
				Method parallelCapableMetod = ClassLoader.class.getDeclaredMethod("registerAsParallelCapable", (Class[]) null); //$NON-NLS-1$
				parallelCapableMetod.setAccessible(true);
				registeredAsParallel = ((Boolean) parallelCapableMetod.invoke(null, (Object[]) null)).booleanValue();
			} catch (Throwable e) {
				// must do everything to avoid failing in clinit
				registeredAsParallel = false;
			}
			CHILDFIRST_REGISTERED_AS_PARALLEL = registeredAsParallel;
		}

		public ChildFirstURLClassLoader(URL[] urls, ClassLoader parent) {
			super(urls, parent, false);
		}

		@Override
		public URL getResource(String name) {
			URL resource = findResource(name);
			if (resource == null) {
				ClassLoader parent = getParent();
				if (parent != null)
					resource = parent.getResource(name);
			}
			return resource;
		}

		@Override
		protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
			if (isRegisteredAsParallel()) {
				return loadClass0(name, resolve);
			}
			synchronized (this) {
				return loadClass0(name, resolve);
			}
		}

		private Class<?> loadClass0(String name, boolean resolve) throws ClassNotFoundException {
			Class<?> clazz = findLoadedClass(name);
			if (clazz == null) {
				try {
					clazz = findClass(name);
				} catch (ClassNotFoundException e) {
					ClassLoader parent = getParent();
					if (parent != null)
						clazz = parent.loadClass(name);
					else
						clazz = getSystemClassLoader().loadClass(name);
				}
			}

			if (resolve)
				resolveClass(clazz);

			return clazz;
		}

		// we want to ensure that the framework has AllPermissions
		@Override
		protected PermissionCollection getPermissions(CodeSource codesource) {
			return allPermissions;
		}

		@Override
		protected boolean isRegisteredAsParallel() {
			return CHILDFIRST_REGISTERED_AS_PARALLEL;
		}

	}

}
