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

org.scijava.Context Maven / Gradle / Ivy

Go to download

SciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by downstream projects in the SciJava ecosystem, such as ImageJ and SCIFIO.

There is a newer version: 2.99.0
Show newest version
/*
 * #%L
 * SciJava Common shared library for SciJava software.
 * %%
 * Copyright (C) 2009 - 2016 Board of Regents of the University of
 * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
 * Institute of Molecular Cell Biology and Genetics.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package org.scijava;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.scijava.event.ContextDisposingEvent;
import org.scijava.event.EventHandler;
import org.scijava.event.EventService;
import org.scijava.log.LogService;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.PluginIndex;
import org.scijava.service.Service;
import org.scijava.service.ServiceHelper;
import org.scijava.service.ServiceIndex;
import org.scijava.util.ClassUtils;
import org.scijava.util.Query;

/**
 * Top-level SciJava application context, which initializes and maintains a list
 * of services.
 * 
 * @author Curtis Rueden
 * @see Service
 */
public class Context implements Disposable {

	// -- Constants --

	/**
	 * System property indicating whether the context should fail fast when
	 * attempting to instantiate a required service which is invalid or missing.
	 * If this property is set to "false" then the context creation will attempt
	 * to continue even when a required service cannot be instantiated. Otherwise,
	 * the constructor will throw an {@link IllegalArgumentException} in that
	 * situation.
	 */
	public static final String STRICT_PROPERTY = "scijava.context.strict";

	// -- Fields --

	/** Index of the application context's services. */
	private final ServiceIndex serviceIndex;

	/** Master index of all plugins known to the application context. */
	private final PluginIndex pluginIndex;

	/**
	 * Whether context creation and injection should behave strictly, failing fast
	 * when attempting to instantiate a required service which is invalid or
	 * missing.
	 * 
    *
  • If the flag is false, then the context creation will attempt to * continue even when a required service cannot be instantiated. Otherwise, * the constructor will throw an {@link IllegalArgumentException} in that * situation.
  • *
  • If this flag is false, then a call to {@link Context#inject(Object)} * will attempt to catch any errors that occur during context injection * (notably: {@link NoClassDefFoundError} when scanning for event handler * methods), logging them as errors.
  • *
*/ private boolean strict; /** * Creates a new SciJava application context with all available services. * * @see #Context(Collection, PluginIndex, boolean) */ public Context() { this(false); } /** * Creates a new SciJava application context. * * @param empty If true, the context will be empty of services; otherwise, it * will be initialized with all available services. * @see #Context(boolean, boolean) */ public Context(final boolean empty) { this(empty, false); } /** * Creates a new SciJava application context. * * @param noServices If true, the context will contain no services; otherwise, * it will be initialized with all available services. * @param noPlugins If true, the context will contain no plugins; otherwise, * it will be initialized with all available plugins. * @see #Context(Collection, PluginIndex, boolean) */ public Context(final boolean noServices, final boolean noPlugins) { this(services(noServices), plugins(noPlugins)); } /** * Creates a new SciJava application context with the specified services (and * any required service dependencies). *

* Developer's note: This constructor's argument is raw (i.e., * {@code Class...} instead of {@code Class...}) because * otherwise, downstream invocations (e.g., * {@code new Context(DisplayService.class)}) yield the potentially confusing * warning: *

*
Type safety: A generic array of Class is * created for a varargs parameter
*

* To avoid this, we have opted to use raw types and suppress the relevant * warnings here instead. *

* * @param serviceClasses A list of types that implement the {@link Service} * interface (e.g., {@code DisplayService.class}). Compatible * services will be loaded in the order given, * regardless of their relative priorities. * @see #Context(Collection, PluginIndex, boolean) * @throws ClassCastException If any of the given arguments do not implement * the {@link Service} interface. */ public Context(@SuppressWarnings("rawtypes") final Class... serviceClasses) { this(serviceClassList(serviceClasses)); } /** * Creates a new SciJava application context with the specified services (and * any required service dependencies). * * @param serviceClasses A collection of types that implement the * {@link Service} interface (e.g., {@code DisplayService.class}). * Compatible services will be loaded according to the order of the * collection, regardless of their relative priorities. * @see #Context(Collection, PluginIndex, boolean) */ public Context(final Collection> serviceClasses) { this(serviceClasses, null); } /** * Creates a new SciJava application context with the specified services (and * any required service dependencies). * * @param serviceClasses A collection of types that implement the * {@link Service} interface (e.g., {@code DisplayService.class}). * Compatible services will be loaded according to the order of the * collection, regardless of their relative priorities. * @param strict Whether context creation will fail fast when there is * an error instantiating a required service. * @see #Context(Collection, PluginIndex, boolean) */ public Context(final Collection> serviceClasses, final boolean strict) { this(serviceClasses, null, strict); } /** * Creates a new SciJava application context with all available services from * the specified PluginIndex. This allows a base set of available plugins to * be defined, and is useful when plugins that would not be returned by the * {@link PluginIndex}'s {@link org.scijava.plugin.PluginFinder} are desired. * * @param pluginIndex The plugin index to use when discovering and indexing * plugins. If you wish to completely control how services are * discovered (i.e., use your own * {@link org.scijava.plugin.PluginFinder} implementation), then you * can pass a custom {@link PluginIndex} here. Passing null will * result in a default plugin index being constructed and used. * @see #Context(Collection, PluginIndex, boolean) */ public Context(final PluginIndex pluginIndex) { this(services(false), pluginIndex); } /** * Creates a new SciJava application context with the specified services (and * any required service dependencies). Service dependency candidates are * selected from those discovered by the given {@link PluginIndex}'s * associated {@link org.scijava.plugin.PluginFinder}. * * @param serviceClasses A collection of types that implement the * {@link Service} interface (e.g., {@code DisplayService.class}). * Compatible services will be loaded according to the order of the * collection, regardless of their relative priorities. * @param pluginIndex The plugin index to use when discovering and indexing * plugins. If you wish to completely control how services are * discovered (i.e., use your own * {@link org.scijava.plugin.PluginFinder} implementation), then you * can pass a custom {@link PluginIndex} here. Passing null will * result in a default plugin index being constructed and used. * @see #Context(Collection, PluginIndex, boolean) */ public Context(final Collection> serviceClasses, final PluginIndex pluginIndex) { this(serviceClasses, pluginIndex, strict()); } /** * Creates a new SciJava application context with the specified services (and * any required service dependencies). Service dependency candidates are * selected from those discovered by the given {@link PluginIndex}'s * associated {@link org.scijava.plugin.PluginFinder}. *

* NB: Context creation is an important step of a SciJava application's * lifecycle. Particularly in environments where more than one implementation * exists for various services, careful consideration should be exercised * regarding what classes and plugins are provided to the Context, and what * needs to occur during the initialization of these services (especially * those of lower priority). See {@link ServiceHelper#loadServices()} for more * information. *

* * @param serviceClasses A collection of types that implement the * {@link Service} interface (e.g., {@code DisplayService.class}). * Compatible services will be loaded according to the order of the * collection, regardless of their relative priorities. * @param pluginIndex The plugin index to use when discovering and indexing * plugins. If you wish to completely control how services are * discovered (i.e., use your own * {@link org.scijava.plugin.PluginFinder} implementation), then you * can pass a custom {@link PluginIndex} here. Passing null will * result in a default plugin index being constructed and used. * @param strict Whether context creation will fail fast when there is * an error instantiating a required service. */ public Context(final Collection> serviceClasses, final PluginIndex pluginIndex, final boolean strict) { serviceIndex = new ServiceIndex(); this.pluginIndex = pluginIndex == null ? new PluginIndex() : pluginIndex; this.pluginIndex.discover(); setStrict(strict); if (!serviceClasses.isEmpty()){ final ServiceHelper serviceHelper = new ServiceHelper(this, serviceClasses, strict); serviceHelper.loadServices(); } } // -- Context methods -- public ServiceIndex getServiceIndex() { return serviceIndex; } public PluginIndex getPluginIndex() { return pluginIndex; } public boolean isStrict() { return strict; } public void setStrict(final boolean strict) { this.strict = strict; } /** * Gets the service of the given class. * * @throws NoSuchServiceException if the context does not have the requested * service. */ public S service(final Class c) { final S service = getService(c); if (service == null) { throw new NoSuchServiceException("Service " + c.getName() + " not found."); } return service; } /** * Gets the service of the given class name (useful for scripts). * * @throws IllegalArgumentException if the class does not exist, or is not a * service class. * @throws NoSuchServiceException if the context does not have the requested * service. */ public Service service(final String className) { final Class c = ClassUtils.loadClass(className, false); if (!Service.class.isAssignableFrom(c)) { throw new IllegalArgumentException("Not a service class: " + c.getName()); } @SuppressWarnings("unchecked") final Class serviceClass = (Class) c; return service(serviceClass); } /** * Gets the service of the given class, or null if there is no matching * service. */ public S getService(final Class c) { return serviceIndex.getService(c); } /** Gets the service of the given class name (useful for scripts). */ public Service getService(final String className) { final Class c = ClassUtils.loadClass(className); if (c == null) return null; if (!Service.class.isAssignableFrom(c)) return null; // not a service class @SuppressWarnings("unchecked") final Class serviceClass = (Class) c; return getService(serviceClass); } /** * Injects the application context into the given object. This does three * distinct things: *
    *
  • If the given object has any non-final {@link Context} fields annotated * with @{@link Parameter}, sets the value of those fields to this context.
  • *
  • If the given object has any non-final {@link Service} fields annotated * with @{@link Parameter}, sets the value of those fields to the * corresponding service available from this context.
  • *
  • Calls {@link EventService#subscribe(Object)} with the object to * register any @{@link EventHandler} annotated methods as event subscribers.
  • * . *
* * @param o The object to which the context should be assigned. * @throws IllegalStateException If the object already has a context. * @throws IllegalArgumentException If the object has a required * {@link Service} parameter (see {@link Parameter#required()}) * which is not available from this context. */ public void inject(final Object o) { // Ensure parameter fields and event handler methods are cached for this // object. Query query = new Query(); query.put(Parameter.class, Field.class); query.put(EventHandler.class, Method.class); ClassUtils.cacheAnnotatedObjects(o.getClass(), query); // iterate over all @Parameter annotated fields final List fields = getParameterFields(o); for (final Field f : fields) { inject(f, o); } // NB: Subscribe to all events handled by this object. // This greatly simplifies event handling. subscribeToEvents(o); } // -- Disposable methods -- @Override public void dispose() { final EventService eventService = getService(EventService.class); if (eventService != null) eventService.publish(new ContextDisposingEvent()); // NB: Dispose services in reverse order. // This may or may not actually be necessary, but seems safer, since // dependent services will be disposed *before* their dependencies. final List services = serviceIndex.getAll(); for (int s = services.size() - 1; s >= 0; s--) { services.get(s).dispose(); } } // -- Utility methods -- /** * Utility method for converting a varargs list of service classes to a * {@link List} of those classes. */ @SuppressWarnings({ "rawtypes", "unchecked" }) public static List> serviceClassList( final Class... serviceClasses) { return serviceClasses != null ? (List) Arrays.asList(serviceClasses) : Arrays.asList(Service.class); } // -- Helper methods -- private List getParameterFields(Object o) { try { return ClassUtils.getAnnotatedFields(o.getClass(), Parameter.class); } catch (final Throwable t) { handleSafely(t); } return Collections.emptyList(); } private void inject(final Field f, final Object o) { try { f.setAccessible(true); // expose private fields final Class type = f.getType(); if (Service.class.isAssignableFrom(type)) { final Service existingService = (Service) ClassUtils.getValue(f, o); if (strict && existingService != null) { throw new IllegalStateException("Context already injected: " + f.getDeclaringClass().getName() + "#" + f.getName()); } // populate Service parameter @SuppressWarnings("unchecked") final Class serviceType = (Class) type; final Service service = getService(serviceType); if (service == null && f.getAnnotation(Parameter.class).required()) { throw new IllegalArgumentException( createMissingServiceMessage(serviceType)); } if (existingService != null && existingService != service) { // NB: Can only happen in non-strict mode. throw new IllegalStateException("Mismatched context: " + f.getDeclaringClass().getName() + "#" + f.getName()); } ClassUtils.setValue(f, o, service); } else if (Context.class.isAssignableFrom(type) && type.isInstance(this)) { final Context existingContext = (Context) ClassUtils.getValue(f, o); if (strict && existingContext != null) { throw new IllegalStateException("Context already injected: " + f.getDeclaringClass().getName() + "#" + f.getName()); } if (existingContext != null && existingContext != this) { // NB: Can only happen in non-strict mode. throw new IllegalStateException("Mismatched context: " + f.getDeclaringClass().getName() + "#" + f.getName()); } // populate Context parameter ClassUtils.setValue(f, o, this); } } catch (final Throwable t) { handleSafely(t); } } private void subscribeToEvents(final Object o) { try { final EventService eventService = getService(EventService.class); if (eventService != null) eventService.subscribe(o); } catch (final Throwable t) { handleSafely(t); } } private void handleSafely(final Throwable t) { if (isStrict()) { // NB: Only rethrow unchecked exceptions. if (t instanceof RuntimeException) throw (RuntimeException) t; if (t instanceof Error) throw (Error) t; } final LogService log = getService(LogService.class); if (log != null) log.error(t); } private String createMissingServiceMessage( final Class serviceType) { final String nl = System.getProperty("line.separator"); final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); final StringBuilder msg = new StringBuilder("Required service is missing: " + serviceType.getName() + nl); msg.append("Context: " + this + nl); msg.append("ClassLoader: " + classLoader + nl); // Add list of services known to context msg.append(nl + "-- Services known to context --" + nl); for (final Service knownService : serviceIndex.getAll()) { msg.append(knownService + nl); } // Add list of classes known to classloader msg.append(nl + "-- Classpath of ClassLoader --" + nl); if (classLoader instanceof URLClassLoader) { for (final URL url : ((URLClassLoader) classLoader).getURLs()) { msg.append(url.getPath() + nl); } } else { msg .append("ClassLoader was not a URLClassLoader. Could not print classpath."); } return msg.toString(); } private static PluginIndex plugins(final boolean empty) { return empty ? new PluginIndex(null) : null; } @SuppressWarnings("unchecked") private static List> services(final boolean empty) { if (empty) return Collections.> emptyList(); return Arrays.> asList(Service.class); } private static boolean strict() { return !"false".equals(System.getProperty(STRICT_PROPERTY)); } }