All Downloads are FREE. Search and download functionalities are using the official Maven repository.

aQute.bnd.osgi.PluginsContainer Maven / Gradle / Ivy

The newest version!
package aQute.bnd.osgi;

import static aQute.bnd.osgi.Processor.removeDuplicateMarker;
import static java.lang.invoke.MethodHandles.publicLookup;
import static java.lang.invoke.MethodType.methodType;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

import java.io.File;
import java.io.OutputStream;
import java.lang.invoke.MethodType;
import java.net.URL;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.Spliterators.AbstractSpliterator;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.osgi.annotation.versioning.ProviderType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.bnd.build.Project;
import aQute.bnd.build.Workspace;
import aQute.bnd.exceptions.SupplierWithException;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.Parameters;
import aQute.bnd.http.HttpClient;
import aQute.bnd.memoize.CloseableMemoize;
import aQute.bnd.memoize.Memoize;
import aQute.bnd.osgi.Processor.CL;
import aQute.bnd.result.Result;
import aQute.bnd.service.Registry;
import aQute.bnd.service.RegistryDonePlugin;
import aQute.bnd.service.url.URLConnectionHandler;
import aQute.lib.hex.Hex;
import aQute.lib.io.IO;
import aQute.lib.strings.Strings;
import aQute.libg.cryptography.SHA1;

/**
 * The plugin set for a Processor. Plugins are general service objects and can
 * be any type. The PluginsContainer treats the @{link {@link PluginProvider}
 * special. If it is used with a type, it will expand when it encounters a
 * {@link PluginProvider} plugin, see {@link #getPlugin(Class)} and
 * {@link #getPlugins(Class)}. These provided plugins are _not_ part of the this
 * set so they won't be explicitly returned when {@link #plugins()} is called.
 */
public class PluginsContainer extends AbstractSet implements Set, Registry {
	private static final Logger			logger				= LoggerFactory.getLogger(PluginsContainer.class);
	private static final MethodType		defaultConstructor	= methodType(void.class);

	private final Set			plugins				= new CopyOnWriteArraySet<>();

	// The following are only mutated during init(), so they don't need to
	// be concurrent-safe
	private final Set			missingCommand		= new HashSet<>();
	private final Set	closeablePlugins	= new HashSet<>();
	Processor							processor;

	/**
	 * A Plugin Provider provides plugins when accessed with a type selector.
	 * The provided plugins are not part of the container's plugin set.
	 */
	@ProviderType
	public interface PluginProvider {
		/**
		 * Provide the plugins for the given type if the type is equal.
		 *
		 * @param  the requested type
		 * @param type the class of the provider type, never null
		 * @return a stream supplying the provided plugins, can be empty
		 */
		 Stream provide(Class type);
	}

	final class AbstractPlugin implements PluginProvider, AutoCloseable {
		private final Class			serviceClass;
		private final Memoize>	externals;
		private final Attrs				attrs;
		private volatile Workspace		workspace;

		AbstractPlugin(Class type, Attrs attrs) {
			serviceClass = type;
			this.attrs = attrs;
			if (processor instanceof Workspace w) {
				workspace = w;
			} else if (processor instanceof Project project) {
				workspace = project.getWorkspace();
			} else {
				workspace = null;
			}
			externals = Memoize.supplier(() -> {
				Workspace ws = this.workspace;
				if (ws == null) {
					return Collections.emptyList();
				}
				logger.debug("Loading external plugins {}", this.serviceClass);
				Result> implementations = ws.getExternalPlugins()
					.getImplementations(this.serviceClass, this.attrs);
				implementations.accept(
					ok -> ok.forEach(p -> {
						logger.debug("Customizing external plugin {} with [{}]", p, this.attrs);
						processor.customize(p, this.attrs, PluginsContainer.this);
					}),
					error -> {
						logger.debug("Loading external plugins {} failed with error {}", this.serviceClass, error);
						processor.error("Loading external plugins %s failed with error %s", this.serviceClass, error);
					});
				return implementations.orElseGet(Collections::emptyList);
			});
		}

		@Override
		public  Stream provide(Class type) {
			if (type.isAssignableFrom(serviceClass)) {
				@SuppressWarnings("unchecked")
				Stream stream = (Stream) externals.get()
					.stream();
				return stream;
			}
			return Stream.empty();
		}

		@Override
		public void close() throws Exception {
			workspace = null;
			externals.accept(list -> list.forEach(p -> {
				if (p instanceof AutoCloseable closeable) {
					IO.close(closeable);
				}
			}));
		}

		@Override
		public String toString() {
			return "AbstractPlugin [serviceClass=" + serviceClass + ", externals=" + externals.peek() + ", attrs="
				+ attrs + ", inited=" + externals.isPresent() + "]";
		}
	}

	protected PluginsContainer() {}

	/**
	 * Init actions occur inside of the first-level memoizer.
	 */
	protected void init(Processor processor) {
		this.processor = processor;

		String spe = processor.getProperty(Constants.PLUGIN);
		if (Constants.NONE.equals(spe)) {
			return;
		}

		// The owner of the plugin is always in there.
		add(processor);
		processor.setTypeSpecificPlugins(this);

		/*
		 * Look only local
		 */
		spe = processor.mergeLocalProperties(Constants.PLUGIN);
		String pluginPath = processor.mergeProperties(Constants.PLUGINPATH);
		loadPlugins(processor, spe, pluginPath);

	}

	/**
	 * Post init actions must occur outside of the first level memoizer. This
	 * means these actions can reentrantly see the current state of the
	 * PluginsContainer, through the Processor, which may be partially complete
	 * if addExtensions adds more plugins.
	 */
	protected void postInit(Processor processor) {
		processor.addExtensions(this);

		for (RegistryDonePlugin rdp : getPlugins(RegistryDonePlugin.class)) {
			try {
				rdp.done();
			} catch (Exception e) {
				processor.exception(e, "Calling done on %s, gives an exception", rdp);
			}
		}
	}

	/**
	 * Return the set of plugins added to this container.
	 * 

* This will include {@link PluginProvider}s themselves, if any, rather than * any plugins the {@link PluginProvider}s may provide. *

* Use {@link #stream(Class)} or @{link {@link #spliterator(Class)} to * return the complete plugins including any plugins the * {@link PluginProvider}s may provide as well as the plugins from the * parent processor. The complete plugins may have duplicates if the same * plugin is added into different processors in the hierarchy. */ protected Set plugins() { return plugins; } /** * Returns a stream of plugins of the specified type ordered by the * processor hierarchy. *

* The supplied plugins may have duplicates if the same plugin is added into * different processors in the hierarchy. */ protected Stream stream(Class type) { return StreamSupport.stream(spliterator(type), false); } /** * Returns a spliterator of plugins of the specified type ordered by the * processor hierarchy. *

* The supplied plugins may have duplicates if the same plugin is added into * different processors in the hierarchy. */ protected Spliterator spliterator(Class type) { return new PluginsSpliterator<>(type); } /** * A spliterator of plugins of the specified type ordered by the processor * hierarchy. *

* The supplied plugins may have duplicates if the same plugin is added into * different processors in the hierarchy. *

* We supply the parent's plugins after this container's plugins so that * there is a hierarchical ordering where this container's plugins come * before the parent's plugins. Then {@link #getPlugin(Class)} will select a * matching plugin from this container, if it exists, versus one from the * parent. */ final class PluginsSpliterator extends AbstractSpliterator implements Consumer { private final Class type; private final Spliterator self; private Spliterator provider; private Spliterator parent; private Object plugin; PluginsSpliterator(Class type) { super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.ORDERED | Spliterator.NONNULL); this.type = requireNonNull(type); this.self = plugins().spliterator(); this.provider = Spliterators.emptySpliterator(); } /** * Defer creating the parent spliterator since we may not need it if the * stream operation short circuits such as findFirst. */ private Spliterator parent() { Spliterator spliterator = this.parent; if (spliterator != null) { return spliterator; } Processor parent = processor.getParent(); return this.parent = (parent != null) ? parent.getPlugins() .spliterator(type) : Spliterators.emptySpliterator(); } @Override public boolean tryAdvance(Consumer action) { if (provider.tryAdvance(action)) { return true; } while (self.tryAdvance(this)) { boolean instance = type.isInstance(plugin); if (plugin instanceof PluginProvider pluginProvider) { provider = pluginProvider.provide(type) .spliterator(); if (!instance && provider.tryAdvance(action)) { return true; } } if (instance) { @SuppressWarnings("unchecked") T p = (T) plugin; action.accept(p); return true; } } return parent().tryAdvance(action); } @Override public void forEachRemaining(Consumer action) { provider.forEachRemaining(action); while (self.tryAdvance(this)) { if (type.isInstance(plugin)) { @SuppressWarnings("unchecked") T p = (T) plugin; action.accept(p); } if (plugin instanceof PluginProvider pluginProvider) { pluginProvider.provide(type) .forEachOrdered(action); } } parent().forEachRemaining(action); } @Override public void accept(Object plugin) { this.plugin = plugin; } } /** * Return the first plugin of the specified type. *

* This may invoke any {@link PluginProvider}s that match the specified * type. */ @Override public T getPlugin(Class type) { Optional first = stream(type).findFirst(); return first.orElse(null); } /** * Return all plugins of the specified type. Duplicates are removed. *

* This will invoke any {@link PluginProvider}s that match the specified * type. */ @Override public List getPlugins(Class type) { List list = stream(type).distinct() .collect(toList()); return list; } @Override public boolean add(Object plugin) { return plugins().add(plugin); } @Override public boolean addAll(Collection collection) { return plugins().addAll(collection); } @Override public boolean remove(Object plugin) { return plugins().remove(plugin); } @Override public Iterator iterator() { return stream().iterator(); } @Override public Spliterator spliterator() { return stream().spliterator(); } @Override public Stream stream() { return stream(Object.class).distinct(); } @Override public int size() { return plugins().size(); } @Override public String toString() { return plugins().toString(); } /** * Magic to load the plugins. This is quite tricky actually since we allow * plugins to be downloaded (this is mainly intended for repositories since * in general plugins should use extensions, however to bootstrap the * extensions we need more). Since downloads might need plugins for * passwords and protocols we need to first load the paths specified on the * plugin clause, then check if there are any local plugins (starting with * aQute.bnd and be able to load from our own class loader). *

* After that, we load the plugin paths, these can use the built in * connectors. *

* Last but not least, we load the remaining plugins. */ protected void loadPlugins(Processor processor, String pluginString, String pluginPathString) { Parameters pluginParameters = new Parameters(pluginString, processor, true); CL loader = processor.getLoader(); // First add the plugin-specific paths from their path: directives pluginParameters.stream() .flatMapValue(v -> Strings.splitAsStream(v.get(Constants.PATH_DIRECTIVE))) .forEachOrdered((key, path) -> { try { File f = processor.getFile(path) .getAbsoluteFile(); loader.add(f); } catch (Exception e) { processor.exception(e, "Problem adding path %s to loader for plugin %s", path, removeDuplicateMarker(key)); } }); /* * Try to load any plugins that are local these must start with * aQute.bnd.* and and be possible to load. The main intention of this * code is to load the URL connectors so that any access to remote * plugins can use the connector model. */ Set loaded = new HashSet<>(); for (Entry entry : pluginParameters.entrySet()) { String className = removeDuplicateMarker(entry.getKey()); Attrs attrs = entry.getValue(); logger.debug("Trying pre-plugin {}", className); Object plugin = loadPlugin(processor, processor.getClass() .getClassLoader(), attrs, className, true); if (plugin != null) { // with the marker!! loaded.add(entry.getKey()); } } /* * Make sure we load each plugin only once by removing the entries that * were successfully loaded */ pluginParameters.keySet() .removeAll(loaded); loadPluginPath(processor, pluginPathString, loader); /* * Load the remaining plugins */ for (Entry entry : pluginParameters.entrySet()) { String className = removeDuplicateMarker(entry.getKey()); Attrs attrs = entry.getValue(); logger.debug("Loading secondary plugin {}", className); /* * We can defer the error if the plugin specifies a command name. In * that case, we'll verify that a bnd file does not contain any * references to a plugin command. The reason this feature was added * was to compile plugin classes with the same build. */ String commands = attrs.get(Constants.COMMAND_DIRECTIVE); Object plugin = loadPlugin(processor, loader, attrs, className, commands != null); if (plugin == null) { Strings.splitAsStream(commands) .forEach(missingCommand::add); } } } /** * Add the @link {@link Constants#PLUGINPATH} entries (which are file names) * to the class loader. If this file does not exist, and there is a * {@link Constants#PLUGINPATH_URL_ATTR} attribute then we download it first * from that url. You can then also specify a * {@link Constants#PLUGINPATH_SHA1_ATTR} attribute to verify the file. * * @see PLUGINPATH * @param pluginPath the clauses for the plugin path * @param loader The class loader to extend */ private void loadPluginPath(Processor processor, String pluginPath, CL loader) { CloseableMemoize client = CloseableMemoize .closeableSupplier(SupplierWithException.asSupplier(() -> { HttpClient c = new HttpClient(); c.setRegistry(this); c.readSettings(processor); /* * Allow the URLConnectionHandlers to interact with the * connection so they can sign it or decorate it with a password * etc. */ getPlugins(URLConnectionHandler.class).forEach(c::addURLConnectionHandler); return c; })); Parameters pluginPathParameters = new Parameters(pluginPath, processor); try { nextClause: for (Entry entry : pluginPathParameters.entrySet()) { File f = processor.getFile(entry.getKey()) .getAbsoluteFile(); if (!f.isFile()) { /* * File does not exist! Check if we need to download */ String url = entry.getValue() .get(Constants.PLUGINPATH_URL_ATTR); if (url != null) { try { logger.debug("downloading {} to {}", url, f.getAbsoluteFile()); URL u = new URL(url); /* * Copy the url to the file */ IO.mkdirs(f.getParentFile()); try (Resource resource = Resource.fromURL(u, client.get())) { try (OutputStream out = IO.outputStream(f)) { resource.write(out); } long lastModified = resource.lastModified(); if (lastModified > 0L) { f.setLastModified(lastModified); } } /* * If there is a sha specified, we verify the * download of the the file. */ String digest = entry.getValue() .get(Constants.PLUGINPATH_SHA1_ATTR); if (digest != null) { if (Hex.isHex(digest.trim())) { byte[] sha1 = Hex.toByteArray(digest); byte[] filesha1 = SHA1.digest(f) .digest(); if (!Arrays.equals(sha1, filesha1)) { processor.error( "Plugin path: %s, specified url %s and a sha1 but the file does not match the sha", entry.getKey(), url); } } else { processor.error( "Plugin path: %s, specified url %s and a sha1 '%s' but this is not a hexadecimal", entry.getKey(), url, digest); } } } catch (Exception e) { processor.exception(e, "Failed to download plugin %s from %s", entry.getKey(), url); continue nextClause; } } else { processor.error( "No such file %s from %s and no 'url' attribute on the path so it can be downloaded", entry.getKey(), processor); continue nextClause; } } logger.debug("Adding {} to loader for plugins", f); loader.add(f); } } finally { IO.close(client); } } /** * Load a plugin and customize it. If the plugin cannot be loaded then we * return null. * * @param loader Name of the loader * @param attrs * @param className */ private Object loadPlugin(Processor processor, ClassLoader loader, Attrs attrs, String className, boolean ignoreError) { try { Class c = loader.loadClass(className); Object plugin; if (c.isInterface()) { plugin = new AbstractPlugin<>(c, attrs); } else { plugin = publicLookup().findConstructor(c, defaultConstructor) .invoke(); processor.customize(plugin, attrs, this); } add(plugin); if (plugin instanceof AutoCloseable closeable) { closeablePlugins.add(closeable); } return plugin; } catch (NoClassDefFoundError e) { if (!ignoreError) processor.exception(e, "Failed to load plugin %s;%s", className, attrs); } catch (ClassNotFoundException e) { if (!ignoreError) processor.exception(e, "Failed to load plugin %s;%s", className, attrs); } catch (Error e) { throw e; } catch (Throwable e) { processor.exception(e, "Unexpected error loading plugin %s-%s", className, attrs); } return null; } boolean isMissingPlugin(String name) { return missingCommand.contains(name); } protected void close() { closeablePlugins.forEach(IO::close); closeablePlugins.clear(); plugins.clear(); missingCommand.clear(); } }