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); + } + } + } +}