diff --git a/src/main/java/org/scijava/command/DynamicCommand.java b/src/main/java/org/scijava/command/DynamicCommand.java
index a8bf63220..ce2848139 100644
--- a/src/main/java/org/scijava/command/DynamicCommand.java
+++ b/src/main/java/org/scijava/command/DynamicCommand.java
@@ -63,7 +63,7 @@ public abstract class DynamicCommand extends DefaultMutableModule implements
private CommandService commandService;
@Parameter
- private PluginService pluginService;
+ protected PluginService pluginService;
@Parameter
protected ModuleService moduleService;
@@ -79,7 +79,8 @@ public abstract class DynamicCommand extends DefaultMutableModule implements
public DynamicCommandInfo getInfo() {
if (info == null) {
// NB: Create dynamic metadata lazily.
- final CommandInfo commandInfo = commandService.getCommand(getClass());
+ CommandInfo commandInfo = commandService.getCommand(getClass());
+ if (commandInfo == null) commandInfo = new CommandInfo(getClass());
info = new DynamicCommandInfo(commandInfo, getClass());
}
return info;
diff --git a/src/main/java/org/scijava/command/DynamicCommandInfo.java b/src/main/java/org/scijava/command/DynamicCommandInfo.java
index a45f4b1ac..790f7064a 100644
--- a/src/main/java/org/scijava/command/DynamicCommandInfo.java
+++ b/src/main/java/org/scijava/command/DynamicCommandInfo.java
@@ -51,9 +51,9 @@
* Helper class for maintaining a {@link DynamicCommand}'s associated
* {@link ModuleInfo}.
*
- * The {@link CommandService} has a plain {@link CommandInfo} object in its
- * index, populated from the {@link DynamicCommand}'s @{@link Plugin}
- * annotation. So this class adapts that object, delegating to it for the
+ * This class wraps a plain {@link CommandInfo} object (e.g. from the
+ * {@link CommandService}'s index, present due to an @{@link Plugin} annotation
+ * on the {@link DynamicCommand} class), delegating to it for the
* {@link UIDetails} methods. The plain {@link CommandInfo} cannot be used
* as-is, however, because we need to override the {@link ModuleInfo} methods as
* well as provide metadata manipulation functionality such as
diff --git a/src/main/java/org/scijava/command/Inputs.java b/src/main/java/org/scijava/command/Inputs.java
new file mode 100644
index 000000000..ad7ba18f4
--- /dev/null
+++ b/src/main/java/org/scijava/command/Inputs.java
@@ -0,0 +1,105 @@
+/*
+ * #%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.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import org.scijava.Context;
+import org.scijava.module.process.PreprocessorPlugin;
+
+/**
+ * A way to build a dynamic set of inputs, whose values are then harvested by
+ * the preprocessing framework.
+ *
+ * The {@link #run()} method of this command does nothing. If you want something
+ * custom to happen during execution, use a normal {@link Command} instead:
+ * either implement {@link Command directly}, or extend {@link ContextCommand}
+ * or {@link DynamicCommand}.
+ *
+ *
+ * Here is are some examples of usage:
+ *
+ *
+ *
+ * {@code
+ * // Single input, no configuration.
+ * Inputs inputs = new Inputs(context);
+ * inputs.addInput("sigma", Double.class);
+ * Double sigma = (Double) inputs.harvest().get("sigma");
+ *
+ * // Two inputs, no configuration.
+ * Inputs inputs = new Inputs(context);
+ * inputs.addInput("name", String.class);
+ * inputs.addInput("age", Integer.class);
+ * Map values = inputs.harvest();
+ * String name = (String) values.get("name");
+ * Integer age = (Integer) values.get("age");
+ *
+ * // Inputs with configuration.
+ * Inputs inputs = new Inputs(context);
+ * MutableModuleItem wordInput = inputs.addInput("word", String.class);
+ * wordInput.setLabel("Favorite word");
+ * wordInput.setChoices(Arrays.asList("quick", "brown", "fox"));
+ * wordInput.setDefaultValue("fox");
+ * MutableModuleItem opacityInput = inputs.addInput("opacity", Double.class);
+ * opacityInput.setMinimumValue(0.0);
+ * opacityInput.setMaximumValue(1.0);
+ * opacityInput.setDefaultValue(0.5);
+ * opacityInput.setWidgetStyle(NumberWidget.SCROLL_BAR_STYLE);
+ * inputs.harvest();
+ * String word = wordInput.getValue(inputs);
+ * Double opacity = opacityInput.getValue(inputs);
+ * }
+ *
+ *
+ * @author Curtis Rueden
+ */
+public final class Inputs extends DynamicCommand {
+
+ public Inputs(final Context context) {
+ context.inject(this);
+ }
+
+ public Map harvest() {
+ try {
+ final List pre = //
+ pluginService.createInstancesOfType(PreprocessorPlugin.class);
+ return moduleService.run(this, pre, null).get().getInputs();
+ }
+ catch (final InterruptedException | ExecutionException exc) {
+ throw new RuntimeException(exc);
+ }
+ }
+}
diff --git a/src/main/java/org/scijava/plugin/DefaultPluginService.java b/src/main/java/org/scijava/plugin/DefaultPluginService.java
index 45fcdf656..24612e3d4 100644
--- a/src/main/java/org/scijava/plugin/DefaultPluginService.java
+++ b/src/main/java/org/scijava/plugin/DefaultPluginService.java
@@ -239,7 +239,8 @@ public List createInstances(
return p;
}
catch (final Throwable t) {
- final String errorMessage = "Cannot create plugin: " + info;
+ final String errorMessage = //
+ "Cannot create plugin: " + info.getClassName();
if (log.isDebug()) log.debug(errorMessage, t);
else log.error(errorMessage);
}
diff --git a/src/test/java/org/scijava/command/InputsTest.java b/src/test/java/org/scijava/command/InputsTest.java
new file mode 100644
index 000000000..8c06e0bbe
--- /dev/null
+++ b/src/test/java/org/scijava/command/InputsTest.java
@@ -0,0 +1,170 @@
+/*
+ * #%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 static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.scijava.Context;
+import org.scijava.InstantiableException;
+import org.scijava.log.LogLevel;
+import org.scijava.log.LogService;
+import org.scijava.module.Module;
+import org.scijava.module.ModuleItem;
+import org.scijava.module.MutableModuleItem;
+import org.scijava.module.process.AbstractPreprocessorPlugin;
+import org.scijava.module.process.PreprocessorPlugin;
+import org.scijava.plugin.PluginInfo;
+import org.scijava.plugin.PluginService;
+import org.scijava.widget.InputHarvester;
+import org.scijava.widget.NumberWidget;
+
+/**
+ * Tests {@link Inputs}.
+ *
+ * @author Curtis Rueden
+ * @author Deborah Schmidt
+ */
+public class InputsTest {
+
+ private Context context;
+
+ @Before
+ public void setUp() {
+ context = new Context();
+ context.service(PluginService.class);
+ }
+
+ @After
+ public void tearDown() {
+ context.dispose();
+ }
+
+ /** Tests single input, no configuration. */
+ @Test
+ public void testSingleInput() {
+ setExpected(new HashMap() {{
+ put("sigma", 3.9f);
+ }});
+ Inputs inputs = new Inputs(context);
+ inputs.getInfo().setName("testSingleInput");//TEMP
+ inputs.addInput("sigma", Float.class);
+ float sigma = (Float) inputs.harvest().get("sigma");
+ assertEquals(3.9f, sigma, 0);
+ }
+
+ /** Tests two inputs, no configuration. */
+ @Test
+ public void testTwoInputs() {
+ setExpected(new HashMap() {{
+ put("name", "Chuckles");
+ put("age", 37);
+ }});
+ Inputs inputs = new Inputs(context);
+ inputs.getInfo().setName("testTwoInputs");//TEMP
+ inputs.addInput("name", String.class);
+ inputs.addInput("age", Integer.class);
+ Map values = inputs.harvest();
+ String name = (String) values.get("name");
+ int age = (Integer) values.get("age");
+ assertEquals("Chuckles", name);
+ assertEquals(37, age);
+ }
+
+ /** Tests inputs with configuration. */
+ @Test
+ public void testWithConfiguration() {
+ setExpected(new HashMap() {{
+ put("word", "brown");
+ put("opacity", 0.8);
+ }});
+ Inputs inputs = new Inputs(context);
+ inputs.getInfo().setName("testWithConfiguration");//TEMP
+ MutableModuleItem wordInput = inputs.addInput("word", String.class);
+ wordInput.setLabel("Favorite word");
+ wordInput.setChoices(Arrays.asList("quick", "brown", "fox"));
+ wordInput.setDefaultValue("fox");
+ MutableModuleItem opacityInput = inputs.addInput("opacity", Double.class);
+ opacityInput.setMinimumValue(0.0);
+ opacityInput.setMaximumValue(1.0);
+ opacityInput.setDefaultValue(0.5);
+ opacityInput.setWidgetStyle(NumberWidget.SCROLL_BAR_STYLE);
+ inputs.harvest();
+ String word = wordInput.getValue(inputs);
+ double opacity = opacityInput.getValue(inputs);
+ assertEquals("brown", word);
+ assertEquals(0.8, opacity, 0);
+ }
+
+ public void setExpected(final Map expected) {
+ final PluginInfo info =
+ new PluginInfo(MockInputHarvester.class,
+ PreprocessorPlugin.class)
+ {
+ @Override
+ public PreprocessorPlugin createInstance() throws InstantiableException {
+ final PreprocessorPlugin pp = super.createInstance();
+ ((MockInputHarvester) pp).setExpected(expected);
+ return pp;
+ }
+ };
+ info.setPriority(InputHarvester.PRIORITY);
+ context.service(PluginService.class).addPlugin(info);
+ }
+
+ public static class MockInputHarvester extends AbstractPreprocessorPlugin {
+ private Map expected;
+ public void setExpected(final Map expected) {
+ this.expected = expected;
+ }
+
+ @Override
+ public void process(final Module module) {
+ for (final ModuleItem> input : module.getInfo().inputs()) {
+ if (module.isInputResolved(input.getName())) continue;
+ final String name = input.getName();
+ if (!expected.containsKey(name)) {
+ throw new AssertionError("No value for input: " + input.getName());
+ }
+ final Object value = expected.get(name);
+ module.setInput(name, value);
+ }
+ }
+ }
+}