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

org.scijava.command.CommandInfo 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 - 2017 Board of Regents of the University of
 * Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck
 * Institute of Molecular Cell Biology and Genetics, University of
 * Konstanz, and KNIME GmbH.
 * %%
 * 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.command;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.scijava.Cancelable;
import org.scijava.InstantiableException;
import org.scijava.ItemIO;
import org.scijava.ItemVisibility;
import org.scijava.ValidityProblem;
import org.scijava.event.EventService;
import org.scijava.module.Module;
import org.scijava.module.ModuleException;
import org.scijava.module.ModuleInfo;
import org.scijava.module.ModuleItem;
import org.scijava.module.event.ModulesUpdatedEvent;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.Plugin;
import org.scijava.plugin.PluginInfo;
import org.scijava.util.ClassUtils;
import org.scijava.util.StringMaker;
import org.scijava.util.Types;

/**
 * A collection of metadata about a particular {@link Command}.
 * 

* Unlike its more general superclass {@link PluginInfo}, a {@code CommandInfo} * implements {@link ModuleInfo}, allowing it to describe and instantiate the * command in {@link Module} form. *

* * @author Curtis Rueden * @author Johannes Schindelin * @author Grant Harris * @see ModuleInfo - The interface which provides many methods for interrogating * structural details of a {@link Module}. * @see CommandModule - An adapter class that bridges the gap between * {@link Command}s and the rich {@link Module} interface. */ public class CommandInfo extends PluginInfo implements ModuleInfo { /** Wrapped {@link PluginInfo}, if any. */ private final PluginInfo info; /** List of items with fixed, preset values. */ private Map presets; // TODO: Reconcile more with AbstractModuleInfo? /** * Flag indicating whether the command parameters have been parsed. Parsing * the parameters requires loading the command class, so doing so is deferred * until information about the parameters is actively needed. */ private boolean paramsParsed; /** List of problems detected when parsing command parameters. */ private final List problems = new ArrayList<>(); /** Table of inputs, keyed on name. */ private final Map> inputMap = new HashMap<>(); /** Table of outputs, keyed on name. */ private final Map> outputMap = new HashMap<>(); /** Ordered list of input items. */ private final List> inputList = new ArrayList<>(); /** Ordered list of output items. */ private final List> outputList = new ArrayList<>(); // -- Constructors -- /** * Creates a new command metadata object. * * @param className The name of the class, which must implement * {@link Command}. */ public CommandInfo(final String className) { this(null, className, null, null); } /** * Creates a new command metadata object. * * @param className The name of the class, which must implement * {@link Command}. * @param annotation The @{@link Plugin} annotation to associate with this * metadata object. */ public CommandInfo(final String className, final Plugin annotation) { this(null, className, null, annotation); } /** * Creates a new command metadata object. * * @param commandClass The plugin class, which must implement {@link Command}. */ public CommandInfo(final Class commandClass) { this(null, null, commandClass, null); } /** * Creates a new command metadata object. * * @param commandClass The plugin class, which must implement {@link Command}. * @param annotation The @{@link Plugin} annotation to associate with this * metadata object. */ public CommandInfo(final Class commandClass, final Plugin annotation) { this(null, null, commandClass, annotation); } /** * Creates a new command metadata object describing the same command as the * given {@link PluginInfo}. * * @param info The plugin metadata to wrap. */ public CommandInfo(final PluginInfo info) { this(info, null, null, info.getAnnotation()); } protected CommandInfo(final PluginInfo info, final String className, final Class commandClass, final Plugin annotation) { super(className, commandClass, Command.class, annotation, null); this.info = info; setPresets(null); } // -- CommandInfo methods -- /** Sets the table of items with fixed, preset values. */ public void setPresets(final Map presets) { if (presets == null) { this.presets = new HashMap<>(); } else { this.presets = presets; } } /** Gets the table of items with fixed, preset values. */ public Map getPresets() { return presets; } /** * Instantiates the module described by this module info, around the specified * existing command instance. */ public Module createModule(final Command commandInstance) { // if the command implements Module, return the instance directly if (commandInstance instanceof Module) return (Module) commandInstance; // command does not implement Module; wrap it in a CommandModule instance return new CommandModule(this, commandInstance); } // -- PluginInfo methods -- @Override public void setPluginClass(final Class pluginClass) { if (info == null) super.setPluginClass(pluginClass); else info.setPluginClass(pluginClass); } @Override public Class getPluginClass() { return info == null ? super.getPluginClass() : info.getPluginClass(); } @Override public URL getIconURL() throws InstantiableException { return info == null ? super.getIconURL() : info.getIconURL(); } // -- Object methods -- @Override public String toString() { final StringMaker sm = new StringMaker(super.toString()); for (final String key : presets.keySet()) { final Object value = presets.get(key); sm.append(key, value); } return sm.toString(); } // -- Instantiable methods -- @Override public String getClassName() { return info == null ? super.getClassName() : info.getClassName(); } @Override public Class loadClass() throws InstantiableException { return info == null ? super.loadClass() : info.loadClass(); } @Override public Command createInstance() throws InstantiableException { return info == null ? super.createInstance() : info.createInstance(); } // -- ModuleInfo methods -- @Override public CommandModuleItem getInput(final String name) { parseParams(); return (CommandModuleItem) inputMap.get(name); } @Override public CommandModuleItem getInput(final String name, final Class type) { return castItem(getInput(name), type); } @Override public CommandModuleItem getOutput(final String name) { parseParams(); return (CommandModuleItem) outputMap.get(name); } @Override public CommandModuleItem getOutput(final String name, final Class type) { return castItem(getOutput(name), type); } @Override public Iterable> inputs() { parseParams(); return Collections.unmodifiableList(inputList); } @Override public Iterable> outputs() { parseParams(); return Collections.unmodifiableList(outputList); } @Override public String getDelegateClassName() { return getClassName(); } @Override public Class loadDelegateClass() throws ClassNotFoundException { try { return loadClass(); } catch (final InstantiableException exc) { throw new ClassNotFoundException(null, exc); } } @Override public Module createModule() throws ModuleException { // if the command implements Module, return a new instance directly try { final Class commandClass = loadClass(); if (Module.class.isAssignableFrom(commandClass)) { return (Module) commandClass.newInstance(); } } catch (final InstantiableException e) { throw new ModuleException(e); } catch (final InstantiationException e) { throw new ModuleException(e); } catch (final IllegalAccessException e) { throw new ModuleException(e); } // command does not implement Module; wrap it in a CommandModule instance return new CommandModule(this); } @Override public boolean isInteractive() { final Class commandClass = loadCommandClass(); if (commandClass == null) return false; return Interactive.class.isAssignableFrom(commandClass); } @Override public boolean canPreview() { final Class commandClass = loadCommandClass(); if (commandClass == null) return false; return Previewable.class.isAssignableFrom(commandClass); } @Override public boolean canCancel() { final Class commandClass = loadCommandClass(); if (commandClass == null) return false; return Cancelable.class.isAssignableFrom(commandClass); } @Override public boolean canRunHeadless() { return getAnnotation() == null ? false : getAnnotation().headless(); } @Override public String getInitializer() { return getAnnotation() == null ? null : getAnnotation().initializer(); } @Override public void update(final EventService eventService) { eventService.publish(new ModulesUpdatedEvent(this)); } // -- UIDetails methods -- @Override public String getTitle() { final String title = super.getTitle(); if (!title.equals(getClass().getSimpleName())) return title; // use delegate class name rather than actual class name final String className = getDelegateClassName(); final int dot = className.lastIndexOf("."); return dot < 0 ? className : className.substring(dot + 1); } // -- Validated methods -- @Override public boolean isValid() { parseParams(); return problems.isEmpty(); } @Override public List getProblems() { parseParams(); return Collections.unmodifiableList(problems); } // -- Identifiable methods -- @Override public String getIdentifier() { // NB: The delegate class name, together with the list of presets, is // typically enough to uniquely distinguish this command from others. final StringBuilder sb = new StringBuilder(); sb.append("command:" + getDelegateClassName()); final Map pre = getPresets(); if (!pre.isEmpty()) { sb.append("("); boolean first = true; for (final String name : pre.keySet()) { final Object value = pre.get(name); final String sValue = value == null ? "" : value.toString().replaceAll("[^\\w]", "_"); if (first) { sb.append(", "); first = false; } sb.append(name + " = " + sValue); } sb.append(")"); } return sb.toString(); } // -- Helper methods -- /** * Parses the command's inputs and outputs. Invoked lazily, as needed, to * defer class loading as long as possible. */ private void parseParams() { if (!paramsParsed) initParams(); } private synchronized void initParams() { if (paramsParsed) return; // already initialized checkFields(loadCommandClass()); paramsParsed = true; } /** Processes the given class's @{@link Parameter}-annotated fields. */ private void checkFields(final Class type) { if (type == null) return; // NB: Reject abstract classes. if (Modifier.isAbstract(type.getModifiers())) { problems.add(new ValidityProblem("Delegate class is abstract")); } final List fields = ClassUtils.getAnnotatedFields(type, Parameter.class); for (final Field f : fields) { f.setAccessible(true); // expose private fields final Parameter param = f.getAnnotation(Parameter.class); boolean valid = true; final boolean isFinal = Modifier.isFinal(f.getModifiers()); final boolean isMessage = param.visibility() == ItemVisibility.MESSAGE; if (isFinal && !isMessage) { // NB: Final parameters are bad because they cannot be modified. final String error = "Invalid final parameter: " + f; problems.add(new ValidityProblem(error)); valid = false; } final String name = f.getName(); if (inputMap.containsKey(name) || outputMap.containsKey(name)) { // NB: Shadowed parameters are bad because they are ambiguous. final String error = "Invalid duplicate parameter: " + f; problems.add(new ValidityProblem(error)); valid = false; } if (param.type() == ItemIO.BOTH && isImmutable(f.getType())) { // NB: The BOTH type signifies that the parameter will be changed // in-place somehow. But immutable parameters cannot be changed in // such a manner, so it makes no sense to label them as BOTH. final String error = "Immutable BOTH parameter: " + f; problems.add(new ValidityProblem(error)); valid = false; } if (!valid) { // NB: Skip invalid parameters. continue; } final boolean isPreset = presets.containsKey(name); // add item to the relevant list (inputs or outputs) final CommandModuleItem item = new CommandModuleItem<>(this, f); if (item.isInput()) { inputMap.put(name, item); if (!isPreset) inputList.add(item); } if (item.isOutput()) { outputMap.put(name, item); if (!isPreset) outputList.add(item); } } } private boolean isImmutable(final Class type) { // NB: All eight primitive types, as well as the boxed primitive // wrapper classes, as well as strings, are immutable objects. return Types.isNumber(type) || Types.isText(type) || Types.isBoolean(type); } private Class loadCommandClass() { try { return loadClass(); } catch (final InstantiableException e) { final String error = "Could not initialize command class: " + getClassName(); problems.add(new ValidityProblem(error, e)); } return null; } private CommandModuleItem castItem(final CommandModuleItem item, final Class type) { final Class itemType = item.getType(); if (!type.isAssignableFrom(itemType)) { throw new IllegalArgumentException("Type " + type.getName() + " is incompatible with item of type " + itemType.getName()); } @SuppressWarnings("unchecked") final CommandModuleItem typedItem = (CommandModuleItem) item; return typedItem; } }