/*******************************************************************************
 * Copyright (c) 2003, 2018 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.core.internal.registry.osgi;

import java.io.*;
import java.net.URL;
import java.util.*;
import org.eclipse.core.internal.registry.ExtensionRegistry;
import org.eclipse.core.internal.registry.RegistryMessages;
import org.eclipse.core.internal.runtime.ResourceTranslator;
import org.eclipse.core.internal.runtime.RuntimeLog;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.*;

/**
 * A listener for bundle events.  When a bundles come and go we look to see
 * if there are any extensions or extension points and update the registry accordingly.
 * Using a Synchronous listener here is important. If the
 * bundle activator code tries to access the registry to get its extension
 * points, we need to ensure that they are in the registry before the
 * bundle start is called. By listening sync we are able to ensure that
 * happens.
 */
public class EclipseBundleListener implements SynchronousBundleListener {
	private static final String PLUGIN_MANIFEST = "plugin.xml"; //$NON-NLS-1$
	private static final String FRAGMENT_MANIFEST = "fragment.xml"; //$NON-NLS-1$

	private final ExtensionRegistry registry;
	private final RegistryStrategyOSGI strategy;
	private final Object token;
	private final HashMap<String, Long> dynamicAddStateStamps = new HashMap<>();
	private final long currentStateStamp[] = new long[] {0};

	public EclipseBundleListener(ExtensionRegistry registry, Object key, RegistryStrategyOSGI strategy) {
		this.registry = registry;
		this.token = key;
		this.strategy = strategy;
	}

	@Override
	public void bundleChanged(BundleEvent event) {
		/* Only should listen for RESOLVED and UNRESOLVED events.
		 *
		 * When a bundle is updated the Framework will publish an UNRESOLVED and
		 * then a RESOLVED event which should cause the bundle to be removed
		 * and then added back into the registry.
		 *
		 * When a bundle is uninstalled the Framework should publish an UNRESOLVED
		 * event and then an UNINSTALLED event so the bundle will have been removed
		 * by the UNRESOLVED event before the UNINSTALLED event is published.
		 *
		 * When a bundle is refreshed from PackageAdmin an UNRESOLVED event will be
		 * published which will remove the bundle from the registry.  If the bundle
		 * can be RESOLVED after a refresh then a RESOLVED event will be published
		 * which will add the bundle back.  This is required because the classloader
		 * will have been refreshed for the bundle so all extensions and extension
		 * points for the bundle must be refreshed.
		 */
		Bundle bundle = event.getBundle();
		switch (event.getType()) {
			case BundleEvent.RESOLVED :
				synchronized (currentStateStamp) {
					long newStateStamp = registry.computeState();
					if (currentStateStamp[0] != newStateStamp) {
						// new state stamp; clear the dynamicaddStateStamps
						currentStateStamp[0] = newStateStamp;
						dynamicAddStateStamps.clear();
					}
				}
				addBundle(bundle, true);
				break;
			case BundleEvent.UNRESOLVED :
				removeBundle(bundle);
				break;
		}
	}

	public void processBundles(Bundle[] bundles) {
		for (Bundle bundle : bundles) {
			if (isBundleResolved(bundle)) {
				addBundle(bundle, false);
			} else {
				removeBundle(bundle);
			}
		}
	}

	private boolean isBundleResolved(Bundle bundle) {
		return (bundle.getState() & (Bundle.RESOLVED | Bundle.ACTIVE | Bundle.STARTING | Bundle.STOPPING)) != 0;
	}

	private void removeBundle(Bundle bundle) {
		long timestamp = 0;
		if (strategy.checkContributionsTimestamp()) {
			URL pluginManifest = getExtensionURL(bundle, false);
			if (pluginManifest != null)
				timestamp = strategy.getExtendedTimestamp(bundle, pluginManifest);
		}
		registry.remove(Long.toString(bundle.getBundleId()), timestamp);
	}

	static public URL getExtensionURL(Bundle bundle, boolean report) {
		// bail out if the bundle does not have a symbolic name
		if (bundle.getSymbolicName() == null)
			return null;

		boolean isFragment = OSGIUtils.getDefault().isFragment(bundle);
		String manifestName = isFragment ? FRAGMENT_MANIFEST : PLUGIN_MANIFEST;
		URL extensionURL = bundle.getEntry(manifestName);
		if (extensionURL == null)
			return null;

		// If the bundle is not a singleton, then it is not added
		if (!isSingleton(bundle)) {
			if (report && !isGeneratedManifest(bundle)) {
				String message = NLS.bind(RegistryMessages.parse_nonSingleton, bundle.getSymbolicName());
				RuntimeLog.log(new Status(IStatus.WARNING, RegistryMessages.OWNER_NAME, 0, message, null));
			}
			return null;
		}
		if (!isFragment)
			return extensionURL;

		// If the bundle is a fragment being added to a non singleton host, then it is not added
		Bundle[] hosts = OSGIUtils.getDefault().getHosts(bundle);
		if (hosts == null)
			return null; // should never happen?

		if (isSingleton(hosts[0]))
			return extensionURL;

		if (report) {
			// if the host is not a singleton we always report the error; even if the host has a generated manifest
			String message = NLS.bind(RegistryMessages.parse_nonSingletonFragment, bundle.getSymbolicName(), hosts[0].getSymbolicName());
			RuntimeLog.log(new Status(IStatus.WARNING, RegistryMessages.OWNER_NAME, 0, message, null));
		}
		return null;
	}

	private static boolean isGeneratedManifest(Bundle bundle) {
		return bundle.getHeaders("").get("Generated-from") != null; //$NON-NLS-1$ //$NON-NLS-2$
	}

	private void addBundle(Bundle bundle, boolean checkNLSFragments) {
		if (checkNLSFragments)
			checkForNLSFragment(bundle);
		// if the given bundle already exists in the registry then return.
		// note that this does not work for update cases.
		IContributor contributor = ContributorFactoryOSGi.createContributor(bundle);
		if (registry.hasContributor(contributor))
			return;
		URL pluginManifest = getExtensionURL(bundle, true);
		if (pluginManifest == null)
			return;
		InputStream is;
		try {
			is = new BufferedInputStream(pluginManifest.openStream());
		} catch (IOException ex) {
			is = null;
		}
		if (is == null)
			return;

		ResourceBundle translationBundle = null;
		try {
			translationBundle = ResourceTranslator.getResourceBundle(bundle);
		} catch (MissingResourceException e) {
			//Ignore the exception
		}
		long timestamp = 0;
		if (strategy.checkContributionsTimestamp())
			timestamp = strategy.getExtendedTimestamp(bundle, pluginManifest);
		registry.addContribution(is, contributor, true, pluginManifest.getPath(), translationBundle, token, timestamp);
	}

	private void checkForNLSFragment(Bundle bundle) {
		if (!OSGIUtils.getDefault().isFragment(bundle)) {
			// only need to worry about fragments
			synchronized (currentStateStamp) {
				// mark this host as processed for the current state stamp.
				dynamicAddStateStamps.put(Long.toString(bundle.getBundleId()), Long.valueOf(currentStateStamp[0]));
			}
			return;
		}
		Bundle[] hosts = OSGIUtils.getDefault().getHosts(bundle);
		if (hosts == null)
			return;
		// check to see if the hosts should be refreshed because the fragment contains NLS properties files.
		for (Bundle host : hosts) {
			checkForNLSFiles(host, bundle);
		}
	}

	private void checkForNLSFiles(Bundle host, Bundle fragment) {
		String hostID = Long.toString(host.getBundleId());

		synchronized (currentStateStamp) {
			Long hostStateStamp = dynamicAddStateStamps.get(hostID);
			if (hostStateStamp != null && currentStateStamp[0] == hostStateStamp.longValue())
				return; // already processed this host
		}

		Bundle[] fragments = OSGIUtils.getDefault().getFragments(host);
		boolean refresh = false;
		// check host first
		if (hasNLSFilesFor(host, fragment)) {
			refresh = true;
		} else {
			// check the fragment provides NLS for other fragments of this host
			for (int i = 0; i < fragments.length && !refresh; i++) {
				if (fragment.equals(fragments[i]))
					continue; // skip fragment that was just resolved; it will be added in by the caller
				if (hasNLSFilesFor(fragments[i], fragment)) {
					refresh = true;
				}
			}
		}
		if (refresh) {
			// force the host and fragments to be removed and added back
			removeBundle(host);
			addBundle(host, false);
			for (Bundle b : fragments) {
				if (fragment.equals(b)) {
					continue; // skip fragment that was just resolved; it will be added in by the caller
				}
				removeBundle(b);
				addBundle(b, false);
			}
			synchronized (currentStateStamp) {
				// mark this host as processed for the current state stamp.
				dynamicAddStateStamps.put(hostID, Long.valueOf(currentStateStamp[0]));
			}
		}
	}

	private boolean hasNLSFilesFor(Bundle target, Bundle fragment) {
		if (!registry.hasContributor(Long.toString(target.getBundleId())))
			return false;
		// get the base localization path from the target
		Dictionary<?, ?> targetHeaders = target.getHeaders(""); //$NON-NLS-1$
		String localization = (String) targetHeaders.get(Constants.BUNDLE_LOCALIZATION);
		if (localization == null)
			// localization may be empty in which case we should check the default
			localization = Constants.BUNDLE_LOCALIZATION_DEFAULT_BASENAME;
		// we do a simple check to make sure the default nls path exists in the target;
		// this is for performance reasons, but I'm not sure it is valid because a target could ship without the default nls properties file but this seems very unlikely
		URL baseNLS = target.getEntry(localization + ".properties"); //$NON-NLS-1$
		if (baseNLS == null)
			return false;
		int lastSlash = localization.lastIndexOf('/');
		if (lastSlash == localization.length() - 1)
			return false; // just to be safe
		String baseDir = lastSlash < 0 ? "" : localization.substring(0, lastSlash); //$NON-NLS-1$
		String filePattern = (lastSlash < 0 ? localization : localization.substring(lastSlash + 1)) + "_*.properties"; //$NON-NLS-1$
		Enumeration<?> nlsFiles = fragment.findEntries(baseDir, filePattern, false);
		return nlsFiles != null;
	}

	private static boolean isSingleton(Bundle bundle) {
		Dictionary<?, ?> allHeaders = bundle.getHeaders(""); //$NON-NLS-1$
		String symbolicNameHeader = (String) allHeaders.get(Constants.BUNDLE_SYMBOLICNAME);
		try {
			if (symbolicNameHeader != null) {
				ManifestElement[] symbolicNameElements = ManifestElement.parseHeader(Constants.BUNDLE_SYMBOLICNAME, symbolicNameHeader);
				if (symbolicNameElements.length > 0) {
					String singleton = symbolicNameElements[0].getDirective(Constants.SINGLETON_DIRECTIVE);
					if (singleton == null)
						singleton = symbolicNameElements[0].getAttribute(Constants.SINGLETON_DIRECTIVE);

					if (!"true".equalsIgnoreCase(singleton)) { //$NON-NLS-1$
						String manifestVersion = (String) allHeaders.get(org.osgi.framework.Constants.BUNDLE_MANIFESTVERSION);
						if (manifestVersion == null) {//the header was not defined for previous versions of the bundle
							//3.0 bundles without a singleton attributes are still being accepted
							if (OSGIUtils.getDefault().getBundle(symbolicNameElements[0].getValue()) == bundle)
								return true;
						}
						return false;
					}
				}
			}
		} catch (BundleException e1) {
			//This can't happen because the fwk would have rejected the bundle
		}
		return true;
	}
}
