From 35ca6b0510cee6a185ac560ad4b690f87adbf68e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 7 Oct 2014 17:40:50 +0200 Subject: [PATCH 01/29] Fix annotation processing when running javac without -d Samuel Inverso noticed, and Mark Hiner diagnosed, that compiling SciJava plugins using javac without specifying an output directory will write the annotation index into an incorrect location (instead of META-INF/json/ it is written into the top-level directory). This can be verified using a very simple example x1.java file: -- snip -- import org.scijava.plugin.Plugin; import org.scijava.plugin.SciJavaPlugin; @Plugin(type = SciJavaPlugin.class) public class x1 implements SciJavaPlugin { } -- snap -- The reason is that javac's DefaultFileManager will strip out any subdirectory in the path passed to the createResource() method unless an output directory is specified. Work around that by detecting the situation and creating the subdirectory explicitly. This fixes https://github.com/imagej/imagej-launcher/issues/22. Signed-off-by: Johannes Schindelin --- .../annotations/AnnotationProcessor.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scijava/annotations/AnnotationProcessor.java b/src/main/java/org/scijava/annotations/AnnotationProcessor.java index 778d909fa..4a7cf4880 100644 --- a/src/main/java/org/scijava/annotations/AnnotationProcessor.java +++ b/src/main/java/org/scijava/annotations/AnnotationProcessor.java @@ -32,7 +32,9 @@ package org.scijava.annotations; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -62,6 +64,7 @@ import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic.Kind; +import javax.tools.FileObject; import javax.tools.StandardLocation; import org.scijava.annotations.AbstractIndexWriter.StreamFactory; @@ -234,10 +237,27 @@ public InputStream openInput(String annotationName) throws IOException { @Override public OutputStream openOutput(String annotationName) throws IOException { final List originating = originatingElements.get(annotationName); - return filer.createResource(StandardLocation.CLASS_OUTPUT, "", - Index.INDEX_PREFIX + annotationName, - originating.toArray(new Element[originating.size()])) - .openOutputStream(); + final String path = Index.INDEX_PREFIX + annotationName; + final FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", + path, originating.toArray(new Element[originating.size()])); + + /* + * Verify that the generated file is in the META-INF/json/ subdirectory; + * Despite our asking for it explicitly, the DefaultFileManager will + * strip out the directory if javac was called without an explicit + * output directory (i.e. without -d option). + */ + final String uri = fileObject.toUri().toString(); + if (uri != null && uri.endsWith("/" + path)) { + return fileObject.openOutputStream(); + } + final String prefix = uri.substring(0, uri.length() - annotationName.length()); + final File file = new File(prefix + path); + final File parent = file.getParentFile(); + if (parent != null && !parent.isDirectory() && !parent.mkdirs()) { + throw new IOException("Could not create directory: " + parent); + } + return new FileOutputStream(file); } @Override From c637b291f67406944f55a436a7c12c441a9ec4ee Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 7 Oct 2014 18:11:59 +0200 Subject: [PATCH 02/29] Let Eclipse clean up AnnotationProcessor's source code Signed-off-by: Johannes Schindelin --- .../annotations/AnnotationProcessor.java | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/scijava/annotations/AnnotationProcessor.java b/src/main/java/org/scijava/annotations/AnnotationProcessor.java index 4a7cf4880..fe0a5c26b 100644 --- a/src/main/java/org/scijava/annotations/AnnotationProcessor.java +++ b/src/main/java/org/scijava/annotations/AnnotationProcessor.java @@ -93,14 +93,14 @@ public boolean process(final Set elements, try { writer.write(writer); } - catch (IOException e) { + catch (final IOException e) { final ByteArrayOutputStream out = new ByteArrayOutputStream(); e.printStackTrace(new PrintStream(out)); try { out.close(); processingEnv.getMessager().printMessage(Kind.ERROR, out.toString()); } - catch (IOException e2) { + catch (final IOException e2) { processingEnv.getMessager().printMessage(Kind.ERROR, e2.getMessage() + " while printing " + e.getMessage()); } @@ -155,8 +155,9 @@ public void add(final TypeElement element) { } @SuppressWarnings("unchecked") - private Map adapt(List mirrors, - TypeMirror annotationType) + private Map adapt( + final List mirrors, + final TypeMirror annotationType) { final Map result = new TreeMap(); for (final AnnotationMirror mirror : mirrors) { @@ -211,7 +212,8 @@ else if (o instanceof VariableElement) { } private AnnotationMirror getMirror(final TypeElement element) { - for (AnnotationMirror candidate : utils.getAllAnnotationMirrors(element)) + for (final AnnotationMirror candidate : utils + .getAllAnnotationMirrors(element)) { final Name binaryName = utils.getBinaryName((TypeElement) candidate.getAnnotationType() @@ -224,7 +226,9 @@ private AnnotationMirror getMirror(final TypeElement element) { } @Override - public InputStream openInput(String annotationName) throws IOException { + public InputStream openInput(final String annotationName) + throws IOException + { try { return filer.getResource(StandardLocation.CLASS_OUTPUT, "", Index.INDEX_PREFIX + annotationName).openInputStream(); @@ -235,11 +239,14 @@ public InputStream openInput(String annotationName) throws IOException { } @Override - public OutputStream openOutput(String annotationName) throws IOException { + public OutputStream openOutput(final String annotationName) + throws IOException + { final List originating = originatingElements.get(annotationName); final String path = Index.INDEX_PREFIX + annotationName; - final FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", - path, originating.toArray(new Element[originating.size()])); + final FileObject fileObject = + filer.createResource(StandardLocation.CLASS_OUTPUT, "", path, + originating.toArray(new Element[originating.size()])); /* * Verify that the generated file is in the META-INF/json/ subdirectory; @@ -251,7 +258,8 @@ public OutputStream openOutput(String annotationName) throws IOException { if (uri != null && uri.endsWith("/" + path)) { return fileObject.openOutputStream(); } - final String prefix = uri.substring(0, uri.length() - annotationName.length()); + final String prefix = + uri.substring(0, uri.length() - annotationName.length()); final File file = new File(prefix + path); final File parent = file.getParentFile(); if (parent != null && !parent.isDirectory() && !parent.mkdirs()) { @@ -261,7 +269,7 @@ public OutputStream openOutput(String annotationName) throws IOException { } @Override - public boolean isClassObsolete(String className) { + public boolean isClassObsolete(final String className) { return false; } From 8bbd4f872acbd815d5f83a4c575e1d3cfbe4b435 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 8 Oct 2014 16:21:24 +0200 Subject: [PATCH 03/29] Adapt instances of AnnotationValue in the annotation processor Kevin Mader reported that certain @interfaces were not handled gracefully because the annotation processor would encounter AnnotationValue instances and not know how to handle them. This fixes https://github.com/scijava/scijava-common/issues/130. Without these changes, the example provided below yielded the following exception: error: java.io.IOException: Cannot handle object of type class com.sun.tools.javac.code.Attribute$Constant at org.scijava.annotations.AbstractIndexWriter.writeObject(AbstractIndexWriter.java:252) at org.scijava.annotations.AbstractIndexWriter.writeArray(AbstractIndexWriter.java:306) at org.scijava.annotations.AbstractIndexWriter.writeObject(AbstractIndexWriter.java:243) at org.scijava.annotations.AbstractIndexWriter.writeMap(AbstractIndexWriter.java:288) at org.scijava.annotations.AbstractIndexWriter.writeObject(AbstractIndexWriter.java:249) at org.scijava.annotations.AbstractIndexWriter.writeMap(AbstractIndexWriter.java:288) at org.scijava.annotations.AbstractIndexWriter.writeObject(AbstractIndexWriter.java:249) at org.scijava.annotations.AbstractIndexWriter.write(AbstractIndexWriter.java:99) at org.scijava.annotations.AnnotationProcessor.process(AnnotationProcessor.java:91) at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:627) ... -- snip BlockIdentity.java -- import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.scijava.annotations.Indexable; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Indexable public @interface BlockIdentity { String blockName(); String desc() default ""; String[] inputNames(); String[] outputNames(); } -- snap -- -- snip Test.java -- @BlockIdentity(blockName = "GrowRegionsBlock", inputNames= {"labeled image", "mask image"}, outputNames= {"filled labels", "filled neighborhood"}) public class Test {} -- snap -- Signed-off-by: Johannes Schindelin --- .../java/org/scijava/annotations/AbstractIndexWriter.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/scijava/annotations/AbstractIndexWriter.java b/src/main/java/org/scijava/annotations/AbstractIndexWriter.java index 9266d6d26..52104745b 100644 --- a/src/main/java/org/scijava/annotations/AbstractIndexWriter.java +++ b/src/main/java/org/scijava/annotations/AbstractIndexWriter.java @@ -45,6 +45,8 @@ import java.util.Map.Entry; import java.util.TreeMap; +import javax.lang.model.element.AnnotationValue; + /** * Writes annotations as JSON-formatted files. *

@@ -170,6 +172,9 @@ protected Object adapt(final Object o) { if (o instanceof Annotation) { return adapt((Annotation) o); } + else if (o instanceof AnnotationValue) { + return adapt(((AnnotationValue) o).getValue()); + } else if (o instanceof Enum) { return adapt((Enum) o); } From d09c4f4ec2aa50a7d4e958ce31f6ca068e6d54da Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 11 Oct 2014 20:11:46 +0200 Subject: [PATCH 04/29] Fix OptionsTest's assumption that bar was not persisted previously Once the test ran successfully, bar is distinctively different from 0... Signed-off-by: Johannes Schindelin --- src/test/java/org/scijava/options/OptionsTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/scijava/options/OptionsTest.java b/src/test/java/org/scijava/options/OptionsTest.java index 6681ac4d3..d70feaac3 100644 --- a/src/test/java/org/scijava/options/OptionsTest.java +++ b/src/test/java/org/scijava/options/OptionsTest.java @@ -94,6 +94,7 @@ public void testPersistence() { final FooOptions fooOptions = optionsService.getOptions(FooOptions.class); // bar should initially be 0 + fooOptions.setBar(0); assertEquals(0, fooOptions.getBar()); // verify that we can set bar to a desired value at all From 13bf97f2ce7650f74ecd3c29ef172b9dfe81c033 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 27 Jun 2014 16:49:41 -0500 Subject: [PATCH 05/29] Fix Javadocs Signed-off-by: Johannes Schindelin --- src/main/java/org/scijava/util/Prefs.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/scijava/util/Prefs.java b/src/main/java/org/scijava/util/Prefs.java index 7dd12f503..5cc620a12 100644 --- a/src/main/java/org/scijava/util/Prefs.java +++ b/src/main/java/org/scijava/util/Prefs.java @@ -223,7 +223,7 @@ public static void remove(final Preferences preferences, final String key) { service().remove(preferences.absolutePath(), key); } - /** Puts a list into the preferences. */ + /** Puts a map into the preferences. */ public static void putMap(final Map map, final String key) { service().putMap(map, key); } @@ -234,7 +234,7 @@ public static void putMap(final Preferences preferences, service().putMap(preferences.absolutePath(), map, key); } - /** Puts a list into the preferences. */ + /** Puts a map into the preferences. */ public static void putMap(final Preferences preferences, final Map map) { From b3215739d8727ad164a7779d7dad4b15898101ce Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 30 Jun 2014 22:27:03 -0500 Subject: [PATCH 06/29] Avoid compiler warning Signed-off-by: Johannes Schindelin --- src/test/java/org/scijava/script/ScriptEngineTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/scijava/script/ScriptEngineTest.java b/src/test/java/org/scijava/script/ScriptEngineTest.java index 081def361..3c75f1297 100644 --- a/src/test/java/org/scijava/script/ScriptEngineTest.java +++ b/src/test/java/org/scijava/script/ScriptEngineTest.java @@ -138,5 +138,7 @@ public Object eval(Reader reader) throws ScriptException { } } - private static class Rot13Bindings extends HashMap implements Bindings { } + private static class Rot13Bindings extends HashMap implements Bindings { + private static final long serialVersionUID = 1L; + } } From 6470e08835de3ca7b8ecf121ea02bdba301944ef Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 27 Jun 2014 16:50:30 -0500 Subject: [PATCH 07/29] PrefService: Provide an API for Iterables The most generic way to provide a bunch of Strings to persist is by passing an Iterable for writing and handling an Iterable when reading. Signed-off-by: Johannes Schindelin --- .../org/scijava/prefs/DefaultPrefService.java | 76 +++++++++++++++++++ .../java/org/scijava/prefs/PrefService.java | 20 +++++ 2 files changed, 96 insertions(+) diff --git a/src/main/java/org/scijava/prefs/DefaultPrefService.java b/src/main/java/org/scijava/prefs/DefaultPrefService.java index f5bb0529b..df189324f 100644 --- a/src/main/java/org/scijava/prefs/DefaultPrefService.java +++ b/src/main/java/org/scijava/prefs/DefaultPrefService.java @@ -378,6 +378,28 @@ public List getList(final Class prefClass) { return getList(preferences); } + @Override + public Iterable getIterable(final String key) { + return getIterable((Class) null, key); + } + + @Override + public Iterable getIterable(final Class prefClass, final String key) { + final Preferences preferences = prefs(prefClass); + return getIterable(preferences.node(key)); + } + + @Override + public void putIterable(final Iterable iterable, final String key) { + putIterable((Class) null, iterable, key); + } + + @Override + public void putIterable(final Class prefClass, final Iterable iterable, final String key) { + final Preferences preferences = prefs(prefClass); + putIterable(preferences.node(key), iterable); + } + // -- Helper methods -- private void clear(final Preferences preferences, final String key) { @@ -460,6 +482,60 @@ private List getList(final Preferences preferences) { return list; } + private void putIterable(final Preferences preferences, + final Iterable iterable) + { + if (preferences == null) { + throw new IllegalArgumentException("Preferences not set."); + } + int index = 0; + for (final String value : iterable) { + preferences.put("" + index++, value == null ? null : value.toString()); + } + } + + private Iterable getIterable(final Preferences preferences) + { + if (preferences == null) { + throw new IllegalArgumentException("Preferences not set."); + } + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private String value; + private int index; + { + findNext(); + } + + @Override + public String next() { + final String result = value; + findNext(); + return result; + } + + @Override + public boolean hasNext() { + return value != null; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private void findNext() { + if (index < 0) return; + value = preferences.get("" + index, null); + index = value == null ? -1 : index + 1; + } + }; + } + }; + } + private Preferences prefs(final Class c) { return Preferences.userNodeForPackage(c == null ? PrefService.class : c); } diff --git a/src/main/java/org/scijava/prefs/PrefService.java b/src/main/java/org/scijava/prefs/PrefService.java index 590f8259c..21d4ac537 100644 --- a/src/main/java/org/scijava/prefs/PrefService.java +++ b/src/main/java/org/scijava/prefs/PrefService.java @@ -194,4 +194,24 @@ public interface PrefService extends SciJavaService { * prefs. */ List getList(Class prefClass); + + /** + * Puts an iterable into the preferences. + */ + void putIterable(Iterable iterable, String key); + + /** + * Puts an iterable into the preferences. + */ + void putIterable(Class prefClass, Iterable iterable, String key); + + /** + * Gets an iterable from the preferences. + */ + Iterable getIterable(String key); + + /** + * Gets an iterable from the preferences. + */ + Iterable getIterable(Class prefClass, String key); } From 71ca10e5a26a9cad1cb880128b2b579da8cc2a90 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 2 Jul 2014 09:36:43 -0500 Subject: [PATCH 08/29] Add a data structure keeping n unique last-recently-used items This is similar to the LinkedHashMap structure, except that an already-existing item can be easily put to the front of the list, or the back. Signed-off-by: Johannes Schindelin --- .../org/scijava/util/LastRecentlyUsed.java | 300 ++++++++++++++++++ .../scijava/util/LastRecentlyUsedTest.java | 69 ++++ 2 files changed, 369 insertions(+) create mode 100644 src/main/java/org/scijava/util/LastRecentlyUsed.java create mode 100644 src/test/java/org/scijava/util/LastRecentlyUsedTest.java diff --git a/src/main/java/org/scijava/util/LastRecentlyUsed.java b/src/main/java/org/scijava/util/LastRecentlyUsed.java new file mode 100644 index 000000000..678429fcb --- /dev/null +++ b/src/main/java/org/scijava/util/LastRecentlyUsed.java @@ -0,0 +1,300 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2014 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.util; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * A simple container for {@code N} last-recently-used items. + * + * @author Johannes Schindelin + */ +public class LastRecentlyUsed implements Iterable { + private final Object[] entries; + private final Map map; + /** + * The double-linked list pointers. + *

+ * The {@code top} variable points to the most recently added, the + * {@code bottom} variable to the oldest entry. The {@code next} and + * {@code previous} arrays point to the next newer/next older entry. + *

+ *

+ * For initialization performance, all of {@code next}, {@code previous}, + * {@code top} and {@code bottom} are initialized to {@code 0}, meaning that + * you need to decrement the values by one in order to obtain the entry index. + * Example: the index of the most recently added entry is {@code top -1}, and + * {@code next[top - 1]} is {@code 0} because there is no newer entry than the + * newest entry. + *

+ */ + private final int[] next, previous; + private int top, bottom; + + public LastRecentlyUsed(int size) { + entries = new Object[2 * size]; + next = new int[2 * size]; + previous = new int[2 * size]; + map = new HashMap(); + } + + /** + * Given the index of an entry, returns the index of the next newer entry. + * + * @param index the index of the current entry, or -1 to wrap around to the oldest entry. + * @return the index of the next newer entry, or -1 when there is no such entry. + */ + public int next(int index) { + return index < 0 ? bottom - 1 : next[index] - 1; + } + + /** + * Given the index of an entry, returns the index of the next older entry. + * + * @param index the index of the current entry, or -1 to wrap around to the newest entry. + * @return the index of the next older entry, or -1 when there is no such entry. + */ + public int previous(int index) { + return index < 0 ? top - 1 : previous[index] - 1; + } + + /** + * Returns the entry for the given index. + * + * @param index the index of the entry + * @return the entry + */ + @SuppressWarnings("unchecked") + public T get(int index) { + return (T) entries[index]; + } + + /** + * Looks up the index for a given entry. + * + * @param value the value of the entry to find + * @return the corresponding index, or {@code -1} if the entry was not found + */ + public int lookup(final T value) { + final Integer result = map.get(value); + return result == null ? -1 : (int) result; + } + + /** + * Add a new newest entry. + * + * @param value the value of the entry + * @return whether the entry was added + */ + public boolean add(final T value) { + return add(value, false); + } + + /** + * Add a new oldest entry. + *

+ * This method helps recreating {@link LastRecentlyUsed} instances given the + * entries in the order newest first, oldest last. + *

+ * + * @param value the value of the entry to add + */ + public void addToEnd(final T value) { + add(value, true); + } + + public boolean replace(final int index, T newValue) { + final Object previousValue = get(index); + if (previousValue == null) { + throw new IllegalArgumentException("No current entry at position " + + index); + } + if (newValue.equals(previous)) return false; + map.remove(previousValue); + map.put(newValue, index); + entries[index] = newValue; + return true; + } + + /** + * Empties the data structure. + */ + public void clear() { + top = bottom = 0; + map.clear(); + for (int i = 0; i < entries.length; i++) { + entries[i] = null; + next[i] = previous[i] = 0; + } + } + + /** + * Returns an {@link Iterator}. + * + * @return the iterator + */ + public Iterator iterator() { + return new Iterator() { + + private int position = top - 1; + + @Override + public boolean hasNext() { + return position >= 0; + } + + @Override + public T next() { + @SuppressWarnings("unchecked") + final T result = (T) entries[position]; + position = previous[position] - 1; + return result; + } + + @Override + public void remove() { + LastRecentlyUsed.this.remove(position == 0 ? top - 1 : next[position] - 1); + } + + }; + } + + // -- private methods + + private void remove(int position) { + assert(entries[position] != null); + map.remove(entries[position]); + entries[position] = null; + if (next[position] == 0) { + top = previous[position]; + } + else { + previous[next[position] - 1] = previous[position]; + } + if (previous[position] == 0) { + bottom = next[position]; + } + else { + next[previous[position] - 1] = next[position]; + } + next[position] = previous[position] = 0; + } + + private boolean add(final T value, boolean addAtEnd) { + final Integer existing = map.get(value); + int insert; + if (existing != null) { + insert = existing; + remove(insert); + } + else if (map.size() == entries.length / 2) { + insert = bottom - 1; + remove(insert); + } + else { + insert = value.hashCode() % entries.length; + if (insert < 0) insert += entries.length; + while (insert < entries.length && entries[insert] != null) insert++; + } + add(insert, value, addAtEnd); + return existing == null; + } + + private void add(int position, T value, boolean atEnd) { + assert(next[position] == 0); + assert(previous[position] == 0); + assert(entries[position] == null); + + map.put(value, position); + entries[position] = value; + if (atEnd) { + next[position] = bottom; + if (bottom > 0) previous[bottom - 1] = position + 1; + bottom = position + 1; + if (top == 0) top = bottom; + } + else { + previous[position] = top; + if (top > 0) { + next[top - 1] = position + 1; + } + top = position + 1; + if (bottom == 0) bottom = top; + } + } + + // For testing + protected void assertConsistency() { + if (top == 0) { + assert(bottom == 0); + assert(map.size() == 0); + for (int i = 0; i < entries.length; i++) { + assert(entries[i] == null); + assert(next[i] == 0); + assert(previous[i] == 0); + } + return; + } + assert(bottom != 0); + final Set indices = new HashSet(map.values()); + assert(indices.size() == map.size()); + for (int i = 0; i < entries.length; i++) { + if (indices.contains(i)) { + assert(entries[i] != null); + assert(map.get(entries[i]) == i); + if (i == top - 1 || top == bottom) { + assert(next[i] == 0); + } + else { + assert(next[i] > 0); + assert(previous[next[i] - 1] == i + 1); + } + if (i == bottom - 1 || top == bottom) { + assert(previous[i] == 0); + } + else { + assert(previous[i] > 0); + assert(next[previous[i] - 1] == i + 1); + } + } + else { + assert(entries[i] == null); + assert(next[i] == 0); + assert(previous[i] == 0); + } + } + } +} diff --git a/src/test/java/org/scijava/util/LastRecentlyUsedTest.java b/src/test/java/org/scijava/util/LastRecentlyUsedTest.java new file mode 100644 index 000000000..2051b9616 --- /dev/null +++ b/src/test/java/org/scijava/util/LastRecentlyUsedTest.java @@ -0,0 +1,69 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2014 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.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Tests the {@link LastRecentlyUsed} data structure. + * + * @author Johannes Schindelin + */ +public class LastRecentlyUsedTest { + + @Test + public void test() { + int count = 3; + final LastRecentlyUsed lru = new LastRecentlyUsed(count); + + for (int i = 1; i <= count; i++) { + lru.add("" + i); + } + + int position = -1; + for (int i = 1; i <= count; i++) { + position = lru.next(position); + assertEquals("" + i, lru.get(position)); + } + position = lru.next(position); + assertEquals(-1, position); + + for (int i = count; i >= 1; i--) { + position = lru.previous(position); + assertEquals("" + i, lru.get(position)); + } + position = lru.previous(position); + assertEquals(-1, position); + } +} From e89f212da17afa1ca69e6b67977ba9fc9e14accf Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 2 Jul 2014 09:38:22 -0500 Subject: [PATCH 09/29] Add a history class for the upcoming script interpreters The newly-introduced History class maintains the history of most recently executed statements and has functionality to persist and retrieve the list from the preferences. Signed-off-by: Johannes Schindelin --- src/main/java/org/scijava/script/History.java | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/main/java/org/scijava/script/History.java diff --git a/src/main/java/org/scijava/script/History.java b/src/main/java/org/scijava/script/History.java new file mode 100644 index 000000000..0879fceef --- /dev/null +++ b/src/main/java/org/scijava/script/History.java @@ -0,0 +1,140 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2014 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.script; + +import org.scijava.prefs.PrefService; +import org.scijava.util.LastRecentlyUsed; + +/** + * Container for a script language's interpreter history. + * + * @author Johannes Schindelin + */ +class History { + + protected static final long serialVersionUID = 1L; + + private static final String PREFIX = "History."; + private final int MAX_ENTRIES = 1000; + + private final PrefService prefs; + private final String name; + private final LastRecentlyUsed entries = new LastRecentlyUsed(MAX_ENTRIES); + private int position = -1; + + /** + * Constructs a history object for a given scripting language. + * + * @param name the name of the scripting language + */ + public History(final PrefService prefs, final String name) { + this.prefs = prefs; + this.name = name; + } + + /** + * Read back a persisted history. + */ + public void read() { + entries.clear(); + for (final String item : prefs.getIterable(getClass(), PREFIX + name)) { + entries.addToEnd(item); + }; + } + + /** + * Persist the history. + * + * @see {@link Prefs} + */ + public void write() { + prefs.putIterable(getClass(), entries, PREFIX + name); + } + + /** + * Adds the most recently issued command. + * + * @param command the most recent command to add to the history + */ + public void add(final String command) { + entries.add(command); + position = -1; + } + + public boolean replace(final String currentCommand) { + if (position < 0) return false; + return entries.replace(position, currentCommand); + } + + /** + * Navigates to the next (more recent) command. + *

+ * This method wraps around, i.e. it returns {@code null} when there is no + * more-recent command in the history. + *

+ * + * @return the next command + */ + public String next() { + position = entries.next(position); + return position < 0 ? null : entries.get(position); + } + + /** + * Navigates to the previous (i.e less recent) command. + *

+ * This method wraps around, i.e. it returns {@code null} when there is no + * less-recent command in the history. + *

+ * + * @return the previous command + */ + public String previous() { + position = entries.previous(position); + return position < 0 ? null : entries.get(position); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + int position = -1; + for (;;) { + position = entries.previous(position); + if (position < 0) break; + if (builder.length() > 0) builder.append(" -> "); + if (this.position == position) builder.append("["); + builder.append(entries.get(position)); + if (this.position == position) builder.append("]"); + } + return builder.toString(); + } +} From b7904e0dca18559af875f005d65de3378ff583bc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 2 Jul 2014 09:40:34 -0500 Subject: [PATCH 10/29] Add the UI-agnostic part of the script interpreters The newly-introduced ScriptInterpreter class provides all the functionality of a script interpreter sans the graphical user interface. It can execute statements and persists a history of most recently executed statements. Signed-off-by: Johannes Schindelin --- .../script/DefaultScriptInterpreter.java | 104 ++++++++++++++++++ .../org/scijava/script/ScriptInterpreter.java | 78 +++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 src/main/java/org/scijava/script/DefaultScriptInterpreter.java create mode 100644 src/main/java/org/scijava/script/ScriptInterpreter.java diff --git a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java new file mode 100644 index 000000000..56dc00553 --- /dev/null +++ b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java @@ -0,0 +1,104 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2014 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.script; + +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import org.scijava.prefs.PrefService; + +/** + * The default implementation of a {@link ScriptInterpreter}. + * + * @author Johannes Schindelin + */ +public class DefaultScriptInterpreter implements ScriptInterpreter { + + private ScriptEngine engine; + private History history; + private String currentCommand = ""; + + /** + * Constructs a new {@link DefaultScriptInterpreter}. + * + * @param scriptService the script service + * @param engine the script engine + */ + public DefaultScriptInterpreter(final PrefService prefs, final ScriptService scriptService, final ScriptEngine engine) { + this.engine = engine; + history = new History(prefs, engine.getClass().getName()); + readHistory(); + } + + @Override + public synchronized void readHistory() { + if (history == null) return; + history.read(); + } + + @Override + public synchronized void writeHistory() { + if (history == null) return; + history.write(); + } + + @Override + public synchronized String walkHistory(final String currentCommand, boolean forward) { + this.currentCommand = currentCommand; + if (history == null) return currentCommand; + history.replace(currentCommand); + return forward ? history.next() : history.previous(); + } + + /** + * Evaluates a command. + * + * @param command the command to evaluate + * @throws ScriptException + */ + @Override + public void eval(String command) throws ScriptException { + if (history != null) history.add(command); + if (engine == null) throw new java.lang.IllegalArgumentException(); + engine.eval(command); + } + + /** + * Returns the current script engine. + * + * @return + */ + @Override + public ScriptEngine getEngine() { + return engine; + } + +} diff --git a/src/main/java/org/scijava/script/ScriptInterpreter.java b/src/main/java/org/scijava/script/ScriptInterpreter.java new file mode 100644 index 000000000..3b8e3bb26 --- /dev/null +++ b/src/main/java/org/scijava/script/ScriptInterpreter.java @@ -0,0 +1,78 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2014 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.script; + +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +/** + * The contract for script interpreters. + * + * @author Johannes Schindelin + */ +public interface ScriptInterpreter { + + /** + * Reads the persisted history of the current script interpreter. + */ + public void readHistory(); + + /** + * Persists the history of the current script interpreter. + */ + public void writeHistory(); + + /** + * Obtains the next/previous command in the command history. + * + * @param currentCommand the current command (will be stored in the history) + * @param forward if true, the next history entry is returned (more recent), + * if false, the previous one + * @return the next/previous command + */ + public String walkHistory(final String currentCommand, boolean forward); + + /** + * Evaluates a command. + * + * @param command the command to evaluate + * @throws ScriptException + */ + public void eval(String command) throws ScriptException; + + /** + * Returns the associated {@link ScriptEngine} + * + * @return the script engine + */ + public ScriptEngine getEngine(); +} From 7e52d8fbcf4f79c5bb14ebcd8e8ea5058be26c12 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 26 Jul 2014 16:01:25 -0500 Subject: [PATCH 11/29] Let LastRecentlyUsed implement the full Collection contract Signed-off-by: Johannes Schindelin --- .../org/scijava/util/LastRecentlyUsed.java | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/util/LastRecentlyUsed.java b/src/main/java/org/scijava/util/LastRecentlyUsed.java index 678429fcb..c3fb19e9d 100644 --- a/src/main/java/org/scijava/util/LastRecentlyUsed.java +++ b/src/main/java/org/scijava/util/LastRecentlyUsed.java @@ -31,6 +31,8 @@ package org.scijava.util; +import java.lang.reflect.Array; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -42,7 +44,7 @@ * * @author Johannes Schindelin */ -public class LastRecentlyUsed implements Iterable { +public class LastRecentlyUsed implements Iterable, Collection { private final Object[] entries; private final Map map; /** @@ -161,6 +163,96 @@ public void clear() { } } + @Override + public boolean addAll(final Collection values) { + for (final T value : values) { + add(value); + } + return true; + } + + @Override + public boolean contains(final Object value) { + return map.containsKey(value); + } + + @Override + public boolean containsAll(final Collection values) { + return map.keySet().containsAll(values); + } + + @Override + public boolean isEmpty() { + return top == 0; + } + + @Override + public boolean remove(Object value) { + final Integer index = map.get(value); + if (index == null) return false; + remove(index.intValue()); + return true; + } + + @Override + public boolean removeAll(Collection values) { + boolean result = true; + for (final Object value : values) { + result = remove(value) && result; + } + return result; + } + + @Override + public boolean retainAll(Collection values) { + for (int index = top - 1; index >= 0; ) { + final int prev = previous[index] - 1; + if (!values.contains(get(index))) { + remove(index); + } + index = prev; + } + return containsAll(values); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public Object[] toArray() { + final Object[] result = new Object[size()]; + for (int i = 0, index = top - 1; index >= 0; i++, index = + previous[index] - 1) + { + result[i] = get(index); + } + return result; + } + + @SuppressWarnings("unchecked") + @Override + public S[] toArray(final S[] array) { + final int size = size(); + if (array.length >= size) { + for (int i = 0, index = top - 1; index >= 0; i++, index = + previous[index] - 1) + { + array[i] = (S) get(index); + } + return array; + } + final S[] result = + (S[]) Array.newInstance(array.getClass().getComponentType(), size); + for (int i = 0, index = top - 1; index >= 0; i++, index = + previous[index] - 1) + { + result[i] = (S) get(index); + } + return result; + } + /** * Returns an {@link Iterator}. * From 64e577ca95607c1a99cfa03c8b110d317d85d065 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 26 Jul 2014 16:24:52 -0500 Subject: [PATCH 12/29] Test LastRecentlyUsed#remove(Object) Signed-off-by: Johannes Schindelin --- .../scijava/util/LastRecentlyUsedTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/test/java/org/scijava/util/LastRecentlyUsedTest.java b/src/test/java/org/scijava/util/LastRecentlyUsedTest.java index 2051b9616..50dc5980a 100644 --- a/src/test/java/org/scijava/util/LastRecentlyUsedTest.java +++ b/src/test/java/org/scijava/util/LastRecentlyUsedTest.java @@ -32,6 +32,9 @@ package org.scijava.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.Iterator; import org.junit.Test; @@ -66,4 +69,36 @@ public void test() { position = lru.previous(position); assertEquals(-1, position); } + + @Test + public void testRemove() { + final LastRecentlyUsed lru = new LastRecentlyUsed(3); + lru.add("a"); + lru.add("b"); + lru.add("c"); + + lru.remove("b"); + + Iterator iter = lru.iterator(); + assertEquals("c", iter.next()); + assertEquals("a", iter.next()); + assertFalse(iter.hasNext()); + + lru.remove("a"); + + iter = lru.iterator(); + assertEquals("c", iter.next()); + assertFalse(iter.hasNext()); + + lru.remove("a"); + + iter = lru.iterator(); + assertEquals("c", iter.next()); + assertFalse(iter.hasNext()); + + lru.remove("c"); + + iter = lru.iterator(); + assertFalse(iter.hasNext()); + } } From b7eb86ae641521b76825f0273dff4b0fed12a743 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Oct 2014 17:00:14 -0500 Subject: [PATCH 13/29] ScriptInterpreter: remove irrelevant modifiers --- .../java/org/scijava/script/ScriptInterpreter.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/scijava/script/ScriptInterpreter.java b/src/main/java/org/scijava/script/ScriptInterpreter.java index 3b8e3bb26..63af267a8 100644 --- a/src/main/java/org/scijava/script/ScriptInterpreter.java +++ b/src/main/java/org/scijava/script/ScriptInterpreter.java @@ -44,12 +44,12 @@ public interface ScriptInterpreter { /** * Reads the persisted history of the current script interpreter. */ - public void readHistory(); + void readHistory(); /** * Persists the history of the current script interpreter. */ - public void writeHistory(); + void writeHistory(); /** * Obtains the next/previous command in the command history. @@ -59,7 +59,7 @@ public interface ScriptInterpreter { * if false, the previous one * @return the next/previous command */ - public String walkHistory(final String currentCommand, boolean forward); + String walkHistory(String currentCommand, boolean forward); /** * Evaluates a command. @@ -67,12 +67,12 @@ public interface ScriptInterpreter { * @param command the command to evaluate * @throws ScriptException */ - public void eval(String command) throws ScriptException; + void eval(String command) throws ScriptException; /** * Returns the associated {@link ScriptEngine} * * @return the script engine */ - public ScriptEngine getEngine(); + ScriptEngine getEngine(); } From e7b80c6662ad3e1b3162aab28f937136b2dd0e41 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Oct 2014 17:00:43 -0500 Subject: [PATCH 14/29] DefaultScriptInterpreter: remove redundant javadoc --- .../org/scijava/script/DefaultScriptInterpreter.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java index 56dc00553..56cdd8b19 100644 --- a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java +++ b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java @@ -78,12 +78,6 @@ public synchronized String walkHistory(final String currentCommand, boolean forw return forward ? history.next() : history.previous(); } - /** - * Evaluates a command. - * - * @param command the command to evaluate - * @throws ScriptException - */ @Override public void eval(String command) throws ScriptException { if (history != null) history.add(command); @@ -91,11 +85,6 @@ public void eval(String command) throws ScriptException { engine.eval(command); } - /** - * Returns the current script engine. - * - * @return - */ @Override public ScriptEngine getEngine() { return engine; From 52acdad77e7bf7399f523d5feef38f6bbfe735ca Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Oct 2014 17:01:53 -0500 Subject: [PATCH 15/29] DefaultScriptInterpreter: add final keywords --- .../java/org/scijava/script/DefaultScriptInterpreter.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java index 56cdd8b19..ac5c620d0 100644 --- a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java +++ b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java @@ -42,8 +42,8 @@ */ public class DefaultScriptInterpreter implements ScriptInterpreter { - private ScriptEngine engine; - private History history; + private final ScriptEngine engine; + private final History history; private String currentCommand = ""; /** @@ -52,7 +52,9 @@ public class DefaultScriptInterpreter implements ScriptInterpreter { * @param scriptService the script service * @param engine the script engine */ - public DefaultScriptInterpreter(final PrefService prefs, final ScriptService scriptService, final ScriptEngine engine) { + public DefaultScriptInterpreter(final PrefService prefs, + final ScriptService scriptService, final ScriptEngine engine) + { this.engine = engine; history = new History(prefs, engine.getClass().getName()); readHistory(); From f998ed8386c879fbc9771f668630f75003b88123 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Oct 2014 17:02:33 -0500 Subject: [PATCH 16/29] ScriptInterpreter: add missing punctuation --- src/main/java/org/scijava/script/ScriptInterpreter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/script/ScriptInterpreter.java b/src/main/java/org/scijava/script/ScriptInterpreter.java index 63af267a8..568dc7a55 100644 --- a/src/main/java/org/scijava/script/ScriptInterpreter.java +++ b/src/main/java/org/scijava/script/ScriptInterpreter.java @@ -70,7 +70,7 @@ public interface ScriptInterpreter { void eval(String command) throws ScriptException; /** - * Returns the associated {@link ScriptEngine} + * Returns the associated {@link ScriptEngine}. * * @return the script engine */ From 6b186e35c90e5c93de9dbd17fee46d0d5b366bf1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Oct 2014 17:02:46 -0500 Subject: [PATCH 17/29] ScriptInterpreter: remember the associated language It simplifies things downstream to be able to query the associated ScriptLanguage later, since it is known at construction time anyway. This does *not* persist the most recently used interpreter language! --- .../scijava/script/DefaultScriptInterpreter.java | 15 ++++++++++----- .../org/scijava/script/ScriptInterpreter.java | 5 +++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java index ac5c620d0..febac424a 100644 --- a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java +++ b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java @@ -42,20 +42,21 @@ */ public class DefaultScriptInterpreter implements ScriptInterpreter { + private final ScriptLanguage language; private final ScriptEngine engine; private final History history; - private String currentCommand = ""; /** * Constructs a new {@link DefaultScriptInterpreter}. * * @param scriptService the script service - * @param engine the script engine + * @param language the script language */ public DefaultScriptInterpreter(final PrefService prefs, - final ScriptService scriptService, final ScriptEngine engine) + final ScriptService scriptService, final ScriptLanguage language) { - this.engine = engine; + this.language = language; + engine = language.getScriptEngine(); history = new History(prefs, engine.getClass().getName()); readHistory(); } @@ -74,7 +75,6 @@ public synchronized void writeHistory() { @Override public synchronized String walkHistory(final String currentCommand, boolean forward) { - this.currentCommand = currentCommand; if (history == null) return currentCommand; history.replace(currentCommand); return forward ? history.next() : history.previous(); @@ -87,6 +87,11 @@ public void eval(String command) throws ScriptException { engine.eval(command); } + @Override + public ScriptLanguage getLanguage() { + return language; + } + @Override public ScriptEngine getEngine() { return engine; diff --git a/src/main/java/org/scijava/script/ScriptInterpreter.java b/src/main/java/org/scijava/script/ScriptInterpreter.java index 568dc7a55..936f211af 100644 --- a/src/main/java/org/scijava/script/ScriptInterpreter.java +++ b/src/main/java/org/scijava/script/ScriptInterpreter.java @@ -69,6 +69,11 @@ public interface ScriptInterpreter { */ void eval(String command) throws ScriptException; + /** + * Returns the associated {@link ScriptLanguage}. + */ + ScriptLanguage getLanguage(); + /** * Returns the associated {@link ScriptEngine}. * From b91b7034fc806e5c97546d89b05f6eae0b048101 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 6 Oct 2014 17:18:45 -0500 Subject: [PATCH 18/29] DefaultScriptInterpreter: add more final keywords --- .../java/org/scijava/script/DefaultScriptInterpreter.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java index febac424a..6f94198fe 100644 --- a/src/main/java/org/scijava/script/DefaultScriptInterpreter.java +++ b/src/main/java/org/scijava/script/DefaultScriptInterpreter.java @@ -74,14 +74,16 @@ public synchronized void writeHistory() { } @Override - public synchronized String walkHistory(final String currentCommand, boolean forward) { + public synchronized String walkHistory(final String currentCommand, + final boolean forward) + { if (history == null) return currentCommand; history.replace(currentCommand); return forward ? history.next() : history.previous(); } @Override - public void eval(String command) throws ScriptException { + public void eval(final String command) throws ScriptException { if (history != null) history.add(command); if (engine == null) throw new java.lang.IllegalArgumentException(); engine.eval(command); From f9c3057dd28fc077ec3849d903aa3ac0326b5415 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Oct 2014 16:14:31 -0500 Subject: [PATCH 19/29] Script Interpreter: keep a record of a 'current command' The 'current command' is the command that is not yet in the history. It is very convenient to be able to go back in history, just to have a look, before continuing to craft the current command (and it would be annoying if it was lost when going back in history). Of course, if the user decides to execute a different command from the history instead, the edits to the 'current command' are lost. This behavior is most in line with the Unix shell behavior power users might be used to. Signed-off-by: Johannes Schindelin --- src/main/java/org/scijava/script/History.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/script/History.java b/src/main/java/org/scijava/script/History.java index 0879fceef..cb0f181cb 100644 --- a/src/main/java/org/scijava/script/History.java +++ b/src/main/java/org/scijava/script/History.java @@ -49,6 +49,7 @@ class History { private final PrefService prefs; private final String name; private final LastRecentlyUsed entries = new LastRecentlyUsed(MAX_ENTRIES); + private String currentCommand = ""; private int position = -1; /** @@ -88,10 +89,14 @@ public void write() { public void add(final String command) { entries.add(command); position = -1; + currentCommand = ""; } public boolean replace(final String currentCommand) { - if (position < 0) return false; + if (position < 0) { + this.currentCommand = currentCommand; + return false; + } return entries.replace(position, currentCommand); } @@ -106,7 +111,7 @@ public boolean replace(final String currentCommand) { */ public String next() { position = entries.next(position); - return position < 0 ? null : entries.get(position); + return position < 0 ? currentCommand : entries.get(position); } /** @@ -120,7 +125,7 @@ public String next() { */ public String previous() { position = entries.previous(position); - return position < 0 ? null : entries.get(position); + return position < 0 ? currentCommand : entries.get(position); } @Override From dc938f142b15a671132abe6ec364ef6d37a5ae69 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 15 Oct 2014 18:34:09 -0500 Subject: [PATCH 20/29] Bump to next development cycle Signed-off-by: Jenkins --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 90b21a764..74f3568e5 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-common - 2.33.1-SNAPSHOT + 2.34.1-SNAPSHOT SciJava Common 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 both ImageJ and SCIFIO. From 2bc186d19dc2a86ee0cfbc2a89bcf10be9de1487 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 24 Oct 2014 10:14:53 -0500 Subject: [PATCH 21/29] Add an ol' classic: ReflectedUniverse, aka CurtisJ Sure, there is BeanShell. But that would add another dependency to the project! The ReflectedUniverse is simple, tried and true. Sure, you almost never want to actually use it, since compile-time safety is much preferred. But when you _do_ need to do some big reflection thing, it is quite handy. For example: suppose you want to remove a dependency from a project, and deprecate the methods that used it. You can leave the methods as is, so that they behave exactly the same way, but wrap the offending code in a ReflectedUniverse to avoid the compile-time dependency. That way, the deprecated methods will continue to work at least in the case where the removed dependency is still present on the classpath. --- .../org/scijava/util/ReflectException.java | 58 ++ .../org/scijava/util/ReflectedUniverse.java | 510 ++++++++++++++++++ 2 files changed, 568 insertions(+) create mode 100644 src/main/java/org/scijava/util/ReflectException.java create mode 100644 src/main/java/org/scijava/util/ReflectedUniverse.java diff --git a/src/main/java/org/scijava/util/ReflectException.java b/src/main/java/org/scijava/util/ReflectException.java new file mode 100644 index 000000000..d9814bf4a --- /dev/null +++ b/src/main/java/org/scijava/util/ReflectException.java @@ -0,0 +1,58 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2014 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.util; + +/** + * An exception thrown when something goes wrong performing a reflected + * operation with {@link ReflectedUniverse}. + * + * @author Curtis Rueden + */ +public class ReflectException extends Exception { + + public ReflectException() { + super(); + } + + public ReflectException(final String s) { + super(s); + } + + public ReflectException(final String s, final Throwable cause) { + super(s, cause); + } + + public ReflectException(final Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/org/scijava/util/ReflectedUniverse.java b/src/main/java/org/scijava/util/ReflectedUniverse.java new file mode 100644 index 000000000..2f460a102 --- /dev/null +++ b/src/main/java/org/scijava/util/ReflectedUniverse.java @@ -0,0 +1,510 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2014 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.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.StringTokenizer; + +import org.scijava.log.LogService; +import org.scijava.log.StderrLogService; + +/** + * A general-purpose reflection wrapper class. + *

+ * Note: use of this class is discouraged in favor of compile-time (i.e., + * type-safe) dependency linkages. However, there are times when it proves very + * useful for writing blocks of reflected code in a more succinct way than you + * can do with the vanilla {@link java.lang.reflect} API. Of course, debugging + * such reflected code becomes much more difficult—caveat emptor! + *

+ * + * @author Curtis Rueden + */ +public class ReflectedUniverse { + + // -- Fields -- + + /** Hashtable containing all variables present in the universe. */ + private final HashMap variables; + + /** Class loader for imported classes. */ + private final ClassLoader loader; + + /** Whether to force our way past restrictive access modifiers. */ + private boolean force; + + private final LogService log; + + // -- Constructors -- + + /** Constructs a new reflected universe. */ + public ReflectedUniverse() { + this(null, null); + } + + /** Constructs a new reflected universe. */ + public ReflectedUniverse(final LogService log) { + this(null, log); + } + + /** + * Constructs a new reflected universe, with the given URLs representing + * additional search paths for imported classes (in addition to the + * CLASSPATH). + */ + public ReflectedUniverse(final URL[] urls) { + this(urls == null ? null : new URLClassLoader(urls)); + } + + /** Constructs a new reflected universe that uses the given class loader. */ + public ReflectedUniverse(final ClassLoader loader) { + this(loader, null); + } + + public ReflectedUniverse(final ClassLoader loader, final LogService log) { + variables = new HashMap(); + this.loader = loader == null ? getClass().getClassLoader() : loader; + this.log = log == null ? new StderrLogService() : log; + } + + // -- Utility methods -- + + /** + * Returns whether the given object is compatible with the specified class for + * the purposes of reflection. + */ + public static boolean isInstance(final Class c, final Object o) { + return (o == null || c.isInstance(o) || + (c == byte.class && o instanceof Byte) || + (c == short.class && o instanceof Short) || + (c == int.class && o instanceof Integer) || + (c == long.class && o instanceof Long) || + (c == float.class && o instanceof Float) || + (c == double.class && o instanceof Double) || + (c == boolean.class && o instanceof Boolean) || (c == char.class && o instanceof Character)); + } + + // -- ReflectedUniverse API methods -- + + /** + * Executes a command in the universe. The following syntaxes are valid: + *
    + *
  • import fully.qualified.package.ClassName
  • + *
  • var = new ClassName(param1, ..., paramN)
  • + *
  • var.method(param1, ..., paramN)
  • + *
  • var2 = var.method(param1, ..., paramN)
  • + *
  • ClassName.method(param1, ..., paramN)
  • + *
  • var2 = ClassName.method(param1, ..., paramN)
  • + *
  • var2 = var
  • + *
+ * Important guidelines: + *
    + *
  • Any referenced class must be imported first using "import".
  • + *
  • Variables can be exported from the universe with getVar().
  • + *
  • Variables can be imported to the universe with setVar().
  • + *
  • Each parameter must be either: + *
      + *
    1. a variable in the universe
    2. + *
    3. a static or instance field (i.e., no nested methods)
    4. + *
    5. a string literal (remember to escape the double quotes)
    6. + *
    7. an integer literal
    8. + *
    9. a long literal (ending in L)
    10. + *
    11. a double literal (containing a decimal point)
    12. + *
    13. a boolean literal (true or false)
    14. + *
    15. the null keyword
    16. + *
    + *
  • + *
+ */ + public Object exec(String command) throws ReflectException { + command = command.trim(); + if (command.startsWith("import ")) { + // command is an import statement + command = command.substring(7).trim(); + final int dot = command.lastIndexOf("."); + final String varName = dot < 0 ? command : command.substring(dot + 1); + Class c; + try { + c = Class.forName(command, true, loader); + } + catch (final NoClassDefFoundError err) { + log.debug("No such class: " + command, err); + throw new ReflectException("No such class: " + command, err); + } + catch (final ClassNotFoundException exc) { + log.debug("No such class: " + command, exc); + throw new ReflectException("No such class: " + command, exc); + } + catch (final RuntimeException exc) { + // HACK: workaround for bug in Apache Axis2 + final String msg = exc.getMessage(); + if (msg != null && msg.indexOf("ClassNotFound") < 0) throw exc; + log.debug("No such class: " + command, exc); + throw new ReflectException("No such class: " + command, exc); + } + setVar(varName, c); + return null; + } + + // get variable where results of command should be stored + final int eqIndex = command.indexOf("="); + String target = null; + if (eqIndex >= 0) { + target = command.substring(0, eqIndex).trim(); + command = command.substring(eqIndex + 1).trim(); + } + + Object result = null; + + // parse parentheses + final int leftParen = command.indexOf("("); + if (leftParen < 0) { + // command is a simple assignment + result = getVar(command); + if (target != null) setVar(target, result); + return result; + } + else if (leftParen != command.lastIndexOf("(") || + command.indexOf(")") != command.length() - 1) + { + throw new ReflectException("Invalid parentheses"); + } + + // parse arguments + final String arglist = command.substring(leftParen + 1); + final StringTokenizer st = new StringTokenizer(arglist, "(,)"); + final int len = st.countTokens(); + final Object[] args = new Object[len]; + for (int i = 0; i < len; i++) { + final String arg = st.nextToken().trim(); + args[i] = getVar(arg); + } + command = command.substring(0, leftParen); + + if (command.startsWith("new ")) { + // command is a constructor call + final String className = command.substring(4).trim(); + final Object var = getVar(className); + if (var == null) { + throw new ReflectException("Class not found: " + className); + } + else if (!(var instanceof Class)) { + throw new ReflectException("Not a class: " + className); + } + final Class cl = (Class) var; + + // Search for a constructor that matches the arguments. Unfortunately, + // calling cl.getConstructor(argClasses) does not work, because + // getConstructor() is not flexible enough to detect when the arguments + // are subclasses of the constructor argument classes, making a brute + // force search through all public constructors necessary. + Constructor constructor = null; + final Constructor[] c = cl.getConstructors(); + for (int i = 0; i < c.length; i++) { + if (force) c[i].setAccessible(true); + final Class[] params = c[i].getParameterTypes(); + if (params.length == args.length) { + boolean match = true; + for (int j = 0; j < params.length; j++) { + if (!isInstance(params[j], args[j])) { + match = false; + break; + } + } + if (match) { + constructor = c[i]; + break; + } + } + } + if (constructor == null) { + final StringBuffer sb = new StringBuffer(command); + for (int i = 0; i < args.length; i++) { + sb.append(i == 0 ? "(" : ", "); + sb.append(args[i].getClass().getName()); + } + sb.append(")"); + throw new ReflectException("No such constructor: " + sb.toString()); + } + + // invoke constructor + Exception exc = null; + try { + result = constructor.newInstance(args); + } + catch (final InstantiationException e) { + exc = e; + } + catch (final IllegalAccessException e) { + exc = e; + } + catch (final InvocationTargetException e) { + exc = e; + } + if (exc != null) { + log.debug("Cannot instantiate object", exc); + throw new ReflectException("Cannot instantiate object", exc); + } + } + else { + // command is a method call + final int dot = command.indexOf("."); + if (dot < 0) throw new ReflectException("Syntax error"); + final String varName = command.substring(0, dot).trim(); + final String methodName = command.substring(dot + 1).trim(); + final Object var = getVar(varName); + if (var == null) { + throw new ReflectException("No such variable: " + varName); + } + final Class varClass = + var instanceof Class ? (Class) var : var.getClass(); + + // Search for a method that matches the arguments. Unfortunately, + // calling varClass.getMethod(methodName, argClasses) does not work, + // because getMethod() is not flexible enough to detect when the + // arguments are subclasses of the method argument classes, making a + // brute force search through all public methods necessary. + Method method = null; + final Method[] m = varClass.getMethods(); + for (int i = 0; i < m.length; i++) { + if (force) m[i].setAccessible(true); + if (methodName.equals(m[i].getName())) { + final Class[] params = m[i].getParameterTypes(); + if (params.length == args.length) { + boolean match = true; + for (int j = 0; j < params.length; j++) { + if (!isInstance(params[j], args[j])) { + match = false; + break; + } + } + if (match) { + method = m[i]; + break; + } + } + } + } + if (method == null) { + throw new ReflectException("No such method: " + methodName); + } + + // invoke method + Exception exc = null; + try { + result = method.invoke(var, args); + } + catch (final IllegalAccessException e) { + exc = e; + } + catch (final InvocationTargetException e) { + exc = e; + } + if (exc != null) { + log.debug("Cannot execute method: " + methodName, exc); + throw new ReflectException("Cannot execute method: " + methodName, exc); + } + } + + // assign result to proper variable + if (target != null) setVar(target, result); + return result; + } + + /** Registers a variable in the universe. */ + public void setVar(final String varName, final Object obj) { + if (obj == null) variables.remove(varName); + else variables.put(varName, obj); + } + + /** Registers a variable of primitive type boolean in the universe. */ + public void setVar(final String varName, final boolean b) { + setVar(varName, new Boolean(b)); + } + + /** Registers a variable of primitive type byte in the universe. */ + public void setVar(final String varName, final byte b) { + setVar(varName, new Byte(b)); + } + + /** Registers a variable of primitive type char in the universe. */ + public void setVar(final String varName, final char c) { + setVar(varName, Character.valueOf(c)); + } + + /** Registers a variable of primitive type double in the universe. */ + public void setVar(final String varName, final double d) { + setVar(varName, new Double(d)); + } + + /** Registers a variable of primitive type float in the universe. */ + public void setVar(final String varName, final float f) { + setVar(varName, new Float(f)); + } + + /** Registers a variable of primitive type int in the universe. */ + public void setVar(final String varName, final int i) { + setVar(varName, Integer.valueOf(i)); + } + + /** Registers a variable of primitive type long in the universe. */ + public void setVar(final String varName, final long l) { + setVar(varName, Long.valueOf(l)); + } + + /** Registers a variable of primitive type short in the universe. */ + public void setVar(final String varName, final short s) { + setVar(varName, Short.valueOf(s)); + } + + /** + * Returns the value of a variable or field in the universe. Primitive types + * will be wrapped in their Java Object wrapper classes. + */ + public Object getVar(final String varName) throws ReflectException { + if (varName.equals("null")) { + // variable is a null value + return null; + } + else if (varName.equals("true")) { + // variable is a boolean literal + return Boolean.TRUE; + } + else if (varName.equals("false")) { + // variable is a boolean literal + return Boolean.FALSE; + } + else if (varName.startsWith("\"") && varName.endsWith("\"")) { + // variable is a string literal + return varName.substring(1, varName.length() - 1); + } + try { + if (varName.matches("-?\\d+")) { + // variable is an int literal + return new Integer(varName); + } + else if (varName.matches("-?\\d+L")) { + // variable is a long literal + return new Long(varName); + } + else if (varName.matches("-?\\d*\\.\\d*")) { + // variable is a double literal + return new Double(varName); + } + } + catch (final NumberFormatException exc) { + throw new ReflectException("Invalid literal: " + varName, exc); + } + final int dot = varName.indexOf("."); + if (dot >= 0) { + // get field value of variable + final String className = varName.substring(0, dot).trim(); + final Object var = variables.get(className); + if (var == null) { + throw new ReflectException("No such class: " + className); + } + final Class varClass = + var instanceof Class ? (Class) var : var.getClass(); + final String fieldName = varName.substring(dot + 1).trim(); + Field field; + try { + field = varClass.getField(fieldName); + if (force) field.setAccessible(true); + } + catch (final NoSuchFieldException exc) { + log.debug("No such field: " + varName, exc); + throw new ReflectException("No such field: " + varName, exc); + } + Object fieldVal; + try { + fieldVal = field.get(var); + } + catch (final IllegalAccessException exc) { + log.debug("Cannot get field value: " + varName, exc); + throw new ReflectException("Cannot get field value: " + varName, exc); + } + return fieldVal; + } + // get variable + final Object var = variables.get(varName); + return var; + } + + /** Sets whether access modifiers (protected, private, etc.) are ignored. */ + public void setAccessibilityIgnored(final boolean ignore) { + force = ignore; + } + + /** Gets whether access modifiers (protected, private, etc.) are ignored. */ + public boolean isAccessibilityIgnored() { + return force; + } + + // -- Main method -- + + /** + * Allows exploration of a reflected universe in an interactive environment. + */ + public static void main(final String[] args) throws IOException { + final ReflectedUniverse r = new ReflectedUniverse(); + System.out.println("Reflected universe test environment. " + + "Type commands, or press ^D to quit."); + if (args.length > 0) { + r.setAccessibilityIgnored(true); + System.out.println("Ignoring accessibility modifiers."); + } + final BufferedReader in = + new BufferedReader(new InputStreamReader(System.in, "UTF-8")); + while (true) { + System.out.print("> "); + final String line = in.readLine(); + if (line == null) break; + try { + r.exec(line); + } + catch (final ReflectException exc) { + r.log.debug("Could not execute '" + line + "'", exc); + } + } + System.out.println(); + } + +} From ea3c75060e126a186e2042afffa84cc99d0d7622 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 24 Oct 2014 10:22:49 -0500 Subject: [PATCH 22/29] ReflectedUniverse: remove LogService usage The LogService was only used for debugging, and only to emit the same information already thrown by the checked ReflectException. In other words: totally superfluous. The only really valid usage was in the main method, which is merely a test driver. That method uses System.out.println elsewhere, so using System.err.println for that one situation is more consistent anyway. --- .../org/scijava/util/ReflectedUniverse.java | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/scijava/util/ReflectedUniverse.java b/src/main/java/org/scijava/util/ReflectedUniverse.java index 2f460a102..20ecc1ee2 100644 --- a/src/main/java/org/scijava/util/ReflectedUniverse.java +++ b/src/main/java/org/scijava/util/ReflectedUniverse.java @@ -43,9 +43,6 @@ import java.util.HashMap; import java.util.StringTokenizer; -import org.scijava.log.LogService; -import org.scijava.log.StderrLogService; - /** * A general-purpose reflection wrapper class. *

@@ -71,18 +68,11 @@ public class ReflectedUniverse { /** Whether to force our way past restrictive access modifiers. */ private boolean force; - private final LogService log; - // -- Constructors -- /** Constructs a new reflected universe. */ public ReflectedUniverse() { - this(null, null); - } - - /** Constructs a new reflected universe. */ - public ReflectedUniverse(final LogService log) { - this(null, log); + this((ClassLoader) null); } /** @@ -96,13 +86,8 @@ public ReflectedUniverse(final URL[] urls) { /** Constructs a new reflected universe that uses the given class loader. */ public ReflectedUniverse(final ClassLoader loader) { - this(loader, null); - } - - public ReflectedUniverse(final ClassLoader loader, final LogService log) { variables = new HashMap(); this.loader = loader == null ? getClass().getClassLoader() : loader; - this.log = log == null ? new StderrLogService() : log; } // -- Utility methods -- @@ -166,18 +151,15 @@ public Object exec(String command) throws ReflectException { c = Class.forName(command, true, loader); } catch (final NoClassDefFoundError err) { - log.debug("No such class: " + command, err); throw new ReflectException("No such class: " + command, err); } catch (final ClassNotFoundException exc) { - log.debug("No such class: " + command, exc); throw new ReflectException("No such class: " + command, exc); } catch (final RuntimeException exc) { // HACK: workaround for bug in Apache Axis2 final String msg = exc.getMessage(); if (msg != null && msg.indexOf("ClassNotFound") < 0) throw exc; - log.debug("No such class: " + command, exc); throw new ReflectException("No such class: " + command, exc); } setVar(varName, c); @@ -280,7 +262,6 @@ else if (!(var instanceof Class)) { exc = e; } if (exc != null) { - log.debug("Cannot instantiate object", exc); throw new ReflectException("Cannot instantiate object", exc); } } @@ -339,7 +320,6 @@ else if (!(var instanceof Class)) { exc = e; } if (exc != null) { - log.debug("Cannot execute method: " + methodName, exc); throw new ReflectException("Cannot execute method: " + methodName, exc); } } @@ -450,7 +430,6 @@ else if (varName.matches("-?\\d*\\.\\d*")) { if (force) field.setAccessible(true); } catch (final NoSuchFieldException exc) { - log.debug("No such field: " + varName, exc); throw new ReflectException("No such field: " + varName, exc); } Object fieldVal; @@ -458,7 +437,6 @@ else if (varName.matches("-?\\d*\\.\\d*")) { fieldVal = field.get(var); } catch (final IllegalAccessException exc) { - log.debug("Cannot get field value: " + varName, exc); throw new ReflectException("Cannot get field value: " + varName, exc); } return fieldVal; @@ -501,7 +479,8 @@ public static void main(final String[] args) throws IOException { r.exec(line); } catch (final ReflectException exc) { - r.log.debug("Could not execute '" + line + "'", exc); + System.err.println("Could not execute '" + line + "':"); + exc.printStackTrace(); } } System.out.println(); From 44d2aaf8bff9c4cc0a37adbe5625ec5bb6624ad3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 24 Oct 2014 12:52:15 -0500 Subject: [PATCH 23/29] POM: bump to the latest pom-scijava 4.2 release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 74f3568e5..7c2fbe541 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 3.6 + 4.2 From 2d161863aad8afb9d39cb1e062757df43918bb17 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 24 Oct 2014 13:05:19 -0500 Subject: [PATCH 24/29] Bump to next development cycle Signed-off-by: Jenkins --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7c2fbe541..eb97143e1 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-common - 2.34.1-SNAPSHOT + 2.35.1-SNAPSHOT SciJava Common 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 both ImageJ and SCIFIO. From 11e439c0cd1ecc109a3fe8152fa1940d5faafae8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 30 Oct 2014 12:55:21 -0500 Subject: [PATCH 25/29] ShadowMenu: avoid NPE when child module is null See: http://fiji.sc/bugzilla/show_bug.cgi?id=944 Of course, it is quite odd for the child module to be null. It may be a symptom of a larger issue. But it's still better not to throw NPE here. --- src/main/java/org/scijava/menu/ShadowMenu.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scijava/menu/ShadowMenu.java b/src/main/java/org/scijava/menu/ShadowMenu.java index 6c65355a1..b117f1e0c 100644 --- a/src/main/java/org/scijava/menu/ShadowMenu.java +++ b/src/main/java/org/scijava/menu/ShadowMenu.java @@ -529,14 +529,15 @@ private ShadowMenu addChild(final ModuleInfo info, final int depth) { if (!leaf) child.addChild(info, depth + 1); else if (existingChild != null) { if (log != null) { - if (info.getPriority() == existingChild.getModuleInfo().getPriority()) { + final ModuleInfo childInfo = existingChild.getModuleInfo(); + if (childInfo != null && info.getPriority() == childInfo.getPriority()) + { log.warn("ShadowMenu: menu item already exists:\n\texisting: " + - existingChild.getModuleInfo() + "\n\t ignored: " + info); + childInfo + "\n\t ignored: " + info); } else { log.debug("ShadowMenu: higher-priority menu item already exists:\n" + - "\texisting: " + existingChild.getModuleInfo() + "\n\t ignored: " + - info); + "\texisting: " + childInfo + "\n\t ignored: " + info); } } } From bce7b69391791f39b5019dca14bf110816c97265 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 4 Nov 2014 13:25:49 -0600 Subject: [PATCH 26/29] AbstractUIDetails: fix potential comparison NPE Theoretically, no object should have a null title. But just in case, let's use the null-friendly MiscUtils.compare method for titles, too. --- src/main/java/org/scijava/AbstractUIDetails.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/AbstractUIDetails.java b/src/main/java/org/scijava/AbstractUIDetails.java index ec90ebc82..cc4d36cbf 100644 --- a/src/main/java/org/scijava/AbstractUIDetails.java +++ b/src/main/java/org/scijava/AbstractUIDetails.java @@ -235,7 +235,7 @@ public int compareTo(final Prioritized that) { // compare titles final String thisTitle = getTitle(); final String thatTitle = uiDetails.getTitle(); - return thisTitle.compareTo(thatTitle); + return MiscUtils.compare(thisTitle, thatTitle); } } From 93c340f472401567dee50f2edbb70abbff261749 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 4 Nov 2014 13:31:09 -0600 Subject: [PATCH 27/29] AbstractUIDetails: do not let getTitle return null Since it is possible for getIdentifier() to return null, we need to guard against that circumstance, and only return non-null identifiers. --- src/main/java/org/scijava/AbstractUIDetails.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/AbstractUIDetails.java b/src/main/java/org/scijava/AbstractUIDetails.java index cc4d36cbf..fb0b78493 100644 --- a/src/main/java/org/scijava/AbstractUIDetails.java +++ b/src/main/java/org/scijava/AbstractUIDetails.java @@ -105,7 +105,8 @@ public String getTitle() { // use the unique identifier, if available if (this instanceof Identifiable) { - return ((Identifiable) this).getIdentifier(); + final String id = ((Identifiable) this).getIdentifier(); + if (id != null) return id; } // use class name as a last resort From e9bc21f92c2317e0e43ea224399dbc7bee8d5dfc Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Fri, 14 Nov 2014 11:43:04 -0600 Subject: [PATCH 28/29] Bump to latest pom-scijava Updated to pom-scijava 5.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eb97143e1..c5d3b1085 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 4.2 + 5.1 From 28759bcec11837828f7dfb1f0e3752e2327a2ea4 Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Fri, 14 Nov 2014 11:51:08 -0600 Subject: [PATCH 29/29] Bump to next development cycle Signed-off-by: Jenkins --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c5d3b1085..cde8d72fd 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ scijava-common - 2.35.1-SNAPSHOT + 2.35.2-SNAPSHOT SciJava Common 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 both ImageJ and SCIFIO.