diff --git a/.github/build.sh b/.github/build.sh
new file mode 100755
index 000000000..523abeb87
--- /dev/null
+++ b/.github/build.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/main/ci-build.sh
+sh ci-build.sh
diff --git a/.github/setup.sh b/.github/setup.sh
new file mode 100755
index 000000000..0ebca586f
--- /dev/null
+++ b/.github/setup.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/main/ci-setup-github-actions.sh
+sh ci-setup-github-actions.sh
+
+# Let the Linux build handle artifact deployment.
+if [ "$(uname)" != Linux ]
+then
+ echo "No deploy -- non-Linux build"
+ echo "NO_DEPLOY=1" >> $GITHUB_ENV
+fi
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..c0ac58830
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,42 @@
+name: build
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - "*-[0-9]+.*"
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build:
+ name: build-${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'zulu'
+ cache: 'maven'
+ - name: Set up CI environment
+ run: .github/setup.sh
+ shell: bash
+ - name: Execute the build
+ run: .github/build.sh
+ shell: bash
+ env:
+ GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }}
+ GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
+ MAVEN_USER: ${{ secrets.MAVEN_USER }}
+ MAVEN_PASS: ${{ secrets.MAVEN_PASS }}
+ CENTRAL_USER: ${{ secrets.CENTRAL_USER }}
+ CENTRAL_PASS: ${{ secrets.CENTRAL_PASS }}
+ SIGNING_ASC: ${{ secrets.SIGNING_ASC }}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 3bc57c558..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-language: java
-jdk: openjdk8
-branches:
- only:
- - master
- - "/.*-[0-9]+\\..*/"
-install: true
-script: ".travis/build.sh"
-cache:
- directories:
- - "~/.m2/repository"
-env:
- global:
- - secure: Xkwu2u/v0+kBqW2Mt80FpMS+2NRMnfRaZ6ptiEW44sViKkaENnJegmYVjdSm9RAwAJx9DEp6KYTLH4Hh56JbWp6s8qGcxm2vCbAWBE6ZesOj3oTvv/T4lucxarocZ8hK9NqfY9iCePjl8R10UDePThZgWBFidUQjvPbH+LcXo9g=
- - secure: Jgj204N4hKv+7sUrXGtXMvUoXYuHAerhUvwbHsm0BJFpzUSyHP5QWJH2tHvCUL9Yb4VhNEjc46RLFVmiIXynJwAdAWJodT/VEnOTb5Cf7+0UHK4qCyjyjVHiWq1cf+R8Lr9fuKRqxUAdz9Hw6g0Yt23TX8kCa+wQW9lwchn0IG8=
- - secure: RuGzuPrxyvBRJwsCpSJ10p9PdP2Hzc9lxb29L//Y/Fkc3bJwNMlNa0ST83oGNHJapYc4dWuFk22Ooy/2YioNgsVvR4n5MAslqXQXSY0VDle1Nbf1pudAlPU+quaiYxcAyyysjUUtqc5wADVgpkrDA7GJzAuCOWb4aSwiiIn6u4A=
- - secure: bnsjVlj9MBJto8ahZdMwGpVPjZeyF7r5tZh+t+NhDuZsP0WMlURzGZ9/lJOO4quklgiWCyeVDB5uvDvsdG9BReBe5Q8Q6WZyxhxYtP2iLV0zRWSWsGKk0stGcrmJKk8wbicIJdXD8suiP+2Li5GfxTge/hkhL4CL0OOr6b1JqrM=
diff --git a/.travis/build.sh b/.travis/build.sh
deleted file mode 100755
index 094a570ee..000000000
--- a/.travis/build.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/travis-build.sh
-sh travis-build.sh $encrypted_d2fbfc37eea9_key $encrypted_d2fbfc37eea9_iv
diff --git a/.travis/signingkey.asc.enc b/.travis/signingkey.asc.enc
deleted file mode 100644
index 4e4802b46..000000000
Binary files a/.travis/signingkey.asc.enc and /dev/null differ
diff --git a/LICENSE.txt b/LICENSE.txt
index 7d75d811c..55024a7c5 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2009 - 2020, SciJava developers.
+Copyright (c) 2009 - 2026, SciJava developers.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
diff --git a/NOTICE.txt b/NOTICE.txt
index 74385d300..5b605b01d 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -1,11 +1,12 @@
This project contains code adapted from Apache Commons Lang
(https://commons.apache.org/proper/commons-lang/) version 3.4,
as well as GenTyRef (https://github.com/coekie/gentyref) version 1.1.0,
-both of which are licensed under the Apache 2.0 license, as follows:
+and EventBus (https://github.com/michaelbushe/EventBus) version 1.4,
+each of which is licensed under the Apache 2.0 license, as follows:
Apache License
Version 2.0, January 2004
- http://www.apache.org/licenses/
+ https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@@ -197,7 +198,7 @@ both of which are licensed under the Apache 2.0 license, as follows:
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/README.md b/README.md
index 0838f7b7b..ba1f15173 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-[](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.scijava%22%20AND%20a%3A%22scijava-common%22)
-[](https://travis-ci.org/scijava/scijava-common)
-[](https://gitter.im/scijava/scijava-common?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.scijava%22%20AND%20a%3A%22scijava-common%22)
+[](https://github.com/scijava/scijava-common/actions/workflows/build.yml)
+[](https://imagesc.zulipchat.com/#narrow/stream/327237-SciJava)
SciJava Common is a common 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](https://github.com/imagej/imagej) and
+It is used by both [ImageJ2](https://github.com/imagej/imagej2) and
[SCIFIO](https://github.com/scifio/scifio).
diff --git a/pom.xml b/pom.xml
index 8b8fef07a..771234c9d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,16 +1,16 @@
-
+ * For each annotated class, this processor writes a JSON record of the
+ * annotation's attributes into {@code META-INF/json/} under a file named after
+ * the annotation type. At runtime, the index can be read to discover annotated
+ * classes and inspect their annotation values without loading (and therefore
+ * initializing) those classes — avoiding the cost and side-effects of a full
+ * classpath scan.
+ *
+ * For example, SciJava Common's {@link org.scijava.plugin.Plugin} annotation
+ * enables the {@link org.scijava.Context} application container to discover
+ * and prepare the application including all its plugins without needing to
+ * load all of those plugins in advance. Rather, they can be loaded upon first
+ * use in each appropriate context, greatly reducing application startup cost.
+ *
- * SciJava conforms to the Semantic
+ * SciJava conforms to the Semantic
* Versioning specification.
*
- * The command service keeps a master index of all commands known to the system.
+ * The command service keeps a central index of all commands known to the system.
* It asks the {@link PluginService} for available commands, then takes care of
* registering them with the {@link ModuleService}.
*
+ * Events and/or topic data can be cached, but are not by default. To cache events or topic data, call + * {@link #setDefaultCacheSizePerClassOrTopic(int)}, {@link #setCacheSizeForEventClass(Class, int)}, or + * {@link #setCacheSizeForTopic(String, int)}, {@link #setCacheSizeForTopic(Pattern, int)}. Retrieve cached values + * with {@link #getLastEvent(Class)}, {@link #getLastTopicData(String)}, {@link #getCachedEvents(Class)}, or + * {@link #getCachedTopicData(String)}. Using caching while subscribing is most likely to make sense only if you + * subscribe and publish on the same thread (so caching is very useful for Swing applications since both happen on + * the EDT in a single-threaded manner). In multithreaded applications, you never know if your subscriber has handled + * an event while it was being subscribed (before the subscribe() method returned) that is newer or older than the + * retrieved cached value (taken before or after subscribe() respectively). + *
+ *+ * There is nothing special about the term "Event," this could just as easily be called a "Message" Service, this term + * is already taken by the JMS, which is similar, but is used across processes and networks. + *
+ * + * @author Michael Bushe michael@bushe.com + * @see {@link ThreadSafeEventService} for the default implementation + */ +interface EventService { + + /** + * Publishes an object so that subscribers will be notified if they subscribed to the object's class, one of its + * subclasses, or to one of the interfaces it implements. + * + * @param event the object to publish + */ + public void publish(Object event); + + /** + * Use this method to publish generified objects to subscribers of Types, i.e. subscribers that use + * {@link #subscribe(Type, EventSubscriber)}, and to publish to subscribers of the non-generic type. + *+ * Due to generic type erasure, the type must be supplied by the caller. You can get a declared object's + * type by using the {@link org.scijava.event.bushe.TypeReference} class. For Example: + *
+ *
+ * TypeReference<List<Trade>> subscribingTypeReference = new TypeReference<List<Trade>>(){};
+ * EventBus.subscribe(subscribingTypeReference.getType(), mySubscriber);
+ * EventBus.subscribe(List.class, thisSubscriberWillGetCalledToo);
+ * ...
+ * //Likely in some other class
+ * TypeReference<List<Trade>> publishingTypeReference = new TypeReference<List<Trade>>(){};
+ * List<Trade> trades = new ArrayList<Trade>();
+ * EventBus.publish(publishingTypeReference.getType(), trades);
+ * trades.add(trade);
+ * EventBus.publish(publishingTypeReference.getType(), trades);
+ *
+ * @param genericType the generified type of the published object.
+ * @param event The event that occurred
+ */
+ public void publish(Type genericType, Object event);
+
+ /**
+ * Publishes an object on a topic name so that all subscribers to that name or a Regular Expression that matches
+ * the topic name will be notified.
+ *
+ * @param topic The name of the topic subscribed to
+ * @param o the object to publish
+ */
+ public void publish(String topic, Object o);
+
+ /**
+ * Subscribes an EventSubscriber to the publication of objects matching a type. Only a WeakReference to
+ * the subscriber is held by the EventService.
+ * + * Subscribing to a class means the subscriber will be called when objects of that class are published, when + * objects of subclasses of the class are published, when objects implementing any of the interfaces of the + * class are published, or when generic types are published with the class' raw type. + *
+ *+ * Subscription is weak by default to avoid having to call unsubscribe(), and to avoid the memory leaks that would + * occur if unsubscribe was not called. The service will respect the WeakReference semantics. In other words, if + * the subscriber has not been garbage collected, then onEvent(Object) will be called normally. If the hard + * reference has been garbage collected, the service will unsubscribe it's WeakReference. + *
+ *+ * It's allowable to call unsubscribe() with the same EventSubscriber hard reference to stop a subscription + * immediately. + *
+ *+ * The service will create the WeakReference on behalf of the caller. + *
+ * + * @param eventClass the class of published objects to subscriber listen to + * @param subscriber The subscriber that will accept the events of the event class when published. + * + * @return true if the subscriber was subscribed successfully, false otherwise + */ + public boolean subscribe(Class eventClass, EventSubscriber subscriber); + + /** + * Subscribe an EventSubscriber to publication of generic Types. + * Subscribers will only be notified for publications using {@link #publish(java.lang.reflect.Type, Object)}. + *+ * Due to generic type erasure, the type must be supplied by the publisher. You can get a declared object's + * type by using the {@link org.scijava.event.bushe.TypeReference} class. For Example: + *
+ *
+ * TypeReference<List<Trade>> subscribingTypeReference = new TypeReference<List<Trade>>(){};
+ * EventBus.subscribe(subscribingTypeReference.getType(), mySubscriber);
+ * EventBus.subscribe(List.class, thisSubscriberWillGetCalledToo);
+ * ...
+ * //Likely in some other class
+ * TypeReference<List<Trade>> publishingTypeReference = new TypeReference<List<Trade>>(){};
+ * List<Trade> trades = new ArrayList<Trade>();
+ * EventBus.publish(publishingTypeReference.getType(), trades);
+ * trades.add(trade);
+ * EventBus.publish(publishingTypeReference.getType(), trades);
+ *
+ * @param type the generic type to subscribe to
+ * @param subscriber the subscriber to the type
+ * @return true if a new subscription is made, false if it already existed
+ */
+ public boolean subscribe(Type type, EventSubscriber subscriber);
+
+ /**
+ * Subscribes an EventSubscriber to the publication of objects exactly matching a type. Only a WeakReference
+ * to the subscriber is held by the EventService.
+ * + * Subscription is weak by default to avoid having to call unsubscribe(), and to avoid the memory leaks that would + * occur if unsubscribe was not called. The service will respect the WeakReference semantics. In other words, if + * the subscriber has not been garbage collected, then the onEvent will be called normally. If the hard reference + * has been garbage collected, the service will unsubscribe it's WeakReference. + *
+ *+ * It's allowable to call unsubscribe() with the same EventSubscriber hard reference to stop a subscription + * immediately. + *
+ *+ * The service will create the WeakReference on behalf of the caller. + *
+ * + * @param eventClass the class of published objects to listen to + * @param subscriber The subscriber that will accept the events when published. + * + * @return true if the subscriber was subscribed successfully, false otherwise + */ + public boolean subscribeExactly(Class eventClass, EventSubscriber subscriber); + + /** + * Subscribes an EventTopicSubscriber to the publication of a topic name. Only a WeakReference + * to the subscriber is held by the EventService. + *+ * Subscription is weak by default to avoid having to call unsubscribe(), and to avoid the memory leaks that would + * occur if unsubscribe was not called. The service will respect the WeakReference semantics. In other words, if + * the subscriber has not been garbage collected, then the onEvent will be called normally. If the hard reference + * has been garbage collected, the service will unsubscribe it's WeakReference. + *
+ *+ * It's allowable to call unsubscribe() with the same EventSubscriber hard reference to stop a subscription + * immediately. + *
+ * + * @param topic the name of the topic listened to + * @param subscriber The topic subscriber that will accept the events when published. + * + * @return true if the subscriber was subscribed successfully, false otherwise + */ + public boolean subscribe(String topic, EventTopicSubscriber subscriber); + + /** + * Subscribes an EventSubscriber to the publication of all the topic names that match a RegEx Pattern. Only a + * WeakReference to the subscriber is held by the EventService. + *+ * Subscription is weak by default to avoid having to call unsubscribe(), and to avoid the memory leaks that would + * occur if unsubscribe was not called. The service will respect the WeakReference semantics. In other words, if + * the subscriber has not been garbage collected, then the onEvent will be called normally. If the hard reference + * has been garbage collected, the service will unsubscribe it's WeakReference. + *
+ *+ * It's allowable to call unsubscribe() with the same EventSubscriber hard reference to stop a subscription + * immediately. + *
+ * + * @param topicPattern pattern that matches to the name of the topic published to + * @param subscriber The topic subscriber that will accept the events when published. + * + * @return true if the subscriber was subscribed successfully, false otherwise + */ + public boolean subscribe(Pattern topicPattern, EventTopicSubscriber subscriber); + + /** + * Subscribes an EventSubscriber to the publication of objects matching a type. + *+ * The semantics are the same as {@link #subscribe(Class, EventSubscriber)}, except that the EventService holds + * a regularly reference, not a WeakReference. + *
+ *+ * The subscriber will remain subscribed until {@link #unsubscribe(Class,EventSubscriber)} is called. + *
+ * + * @param eventClass the class of published objects to listen to + * @param subscriber The subscriber that will accept the events when published. + * + * @return true if the subscriber was subscribed successfully, false otherwise + */ + public boolean subscribeStrongly(Class eventClass, EventSubscriber subscriber); + + /** + * Subscribes an EventSubscriber to the publication of objects matching a type exactly. + *+ * The semantics are the same as {@link #subscribeExactly(Class, EventSubscriber)}, except that the EventService + * holds a regularly reference, not a WeakReference. + *
+ *+ * The subscriber will remain subscribed until {@link #unsubscribe(Class,EventSubscriber)} is called. + *
+ * + * @param eventClass the class of published objects to listen to + * @param subscriber The subscriber that will accept the events when published. + * + * @return true if the subscriber was subscribed successfully, false otherwise + */ + public boolean subscribeExactlyStrongly(Class eventClass, EventSubscriber subscriber); + + /** + * Subscribes a subscriber to an event topic name. + *+ * The semantics are the same as {@link #subscribe(String, EventTopicSubscriber)}, except that the EventService + * holds a regularly reference, not a WeakReference. + *
+ *+ * The subscriber will remain subscribed until {@link #unsubscribe(String,EventTopicSubscriber)} is called. + *
+ * + * @param topic the name of the topic listened to + * @param subscriber The topic subscriber that will accept the events when published. + * + * @return true if the subscriber was subscribed successfully, false otherwise + */ + public boolean subscribeStrongly(String topic, EventTopicSubscriber subscriber); + + /** + * Subscribes a subscriber to all the event topic names that match a RegEx expression. + *+ * The semantics are the same as {@link #subscribe(java.util.regex.Pattern, EventTopicSubscriber)}, except that the + * EventService holds a regularly reference, not a WeakReference. + *
+ *+ * The subscriber will remain subscribed until {@link #unsubscribe(String,EventTopicSubscriber)} is called. + *
+ * + * @param topicPattern the name of the topic listened to + * @param subscriber The topic subscriber that will accept the events when published. + * + * @return true if the subscriber was subscribed successfully, false otherwise + */ + public boolean subscribeStrongly(Pattern topicPattern, EventTopicSubscriber subscriber); + + /** + * Stop the subscription for a subscriber that is subscribed to a class. + * + * @param eventClass the class of published objects to listen to + * @param subscriber The subscriber that is subscribed to the event. The same reference as the one subscribed. + * + * @return true if the subscriber was subscribed to the event, false if it wasn't + */ + public boolean unsubscribe(Class eventClass, EventSubscriber subscriber); + + /** + * Stop the subscription for a subscriber that is subscribed to an exact class. + * + * @param eventClass the class of published objects to listen to + * @param subscriber The subscriber that is subscribed to the event. The same reference as the one subscribed. + * + * @return true if the subscriber was subscribed to the event, false if it wasn't + */ + public boolean unsubscribeExactly(Class eventClass, EventSubscriber subscriber); + + /** + * Stop the subscription for a subscriber that is subscribed to an event topic. + * + * @param topic the topic listened to + * @param subscriber The subscriber that is subscribed to the topic. The same reference as the one subscribed. + * + * @return true if the subscriber was subscribed to the event, false if it wasn't + */ + public boolean unsubscribe(String topic, EventTopicSubscriber subscriber); + + /** + * Stop the subscription for a subscriber that is subscribed to event topics via a Pattern. + * + * @param topicPattern the regex expression matching topics listened to + * @param subscriber The subscriber that is subscribed to the topic. The same reference as the one subscribed. + * + * @return true if the subscriber was subscribed to the event, false if it wasn't + */ + public boolean unsubscribe(Pattern topicPattern, EventTopicSubscriber subscriber); + + /** + * Subscribes a VetoEventListener to publication of event matching a class. Only a WeakReference to the + * VetoEventListener is held by the EventService. + *+ * Use this method to avoid having to call unsubscribe(), though with care since garbage collection semantics is + * indeterminate. The service will respect the WeakReference semantics. In other words, if the vetoListener has not + * been garbage collected, then the onEvent will be called normally. If the hard reference has been garbage + * collected, the service will unsubscribe it's WeakReference. + *
+ *+ * It's allowable to call unsubscribe() with the same VetoEventListener hard reference to stop a subscription + * immediately. + *
+ *+ * The service will create the WeakReference on behalf of the caller. + *
+ * + * @param eventClass the class of published objects that can be vetoed + * @param vetoListener The VetoEventListener that can determine whether an event is published. + * + * @return true if the VetoEventListener was subscribed successfully, false otherwise + */ + public boolean subscribeVetoListener(Class eventClass, VetoEventListener vetoListener); + + /** + * Subscribes a VetoEventListener to publication of an exact event class. Only a WeakReference to the + * VetoEventListener is held by the EventService. + *+ * Use this method to avoid having to call unsubscribe(), though with care since garbage collection semantics is + * indeterminate. The service will respect the WeakReference semantics. In other words, if the vetoListener has not + * been garbage collected, then the onEvent will be called normally. If the hard reference has been garbage + * collected, the service will unsubscribe it's WeakReference. + *
+ *+ * It's allowable to call unsubscribe() with the same VetoEventListener hard reference to stop a subscription + * immediately. + *
+ *+ * The service will create the WeakReference on behalf of the caller. + *
+ * + * @param eventClass the class of published objects that can be vetoed + * @param vetoListener The vetoListener that can determine whether an event is published. + * + * @return true if the vetoListener was subscribed successfully, false otherwise + */ + public boolean subscribeVetoListenerExactly(Class eventClass, VetoEventListener vetoListener); + + /** + * Subscribes a VetoTopicEventListener to a topic name. Only a WeakReference to the + * VetoEventListener is held by the EventService. + * + * @param topic the name of the topic listened to + * @param vetoListener The vetoListener that can determine whether an event is published. + * + * @return true if the vetoListener was subscribed successfully, false otherwise + */ + public boolean subscribeVetoListener(String topic, VetoTopicEventListener vetoListener); + + /** + * Subscribes an VetoTopicEventListener to all the topic names that match the RegEx Pattern. Only a + * WeakReference to the VetoEventListener is held by the EventService. + * + * @param topicPattern the RegEx pattern to match topics with + * @param vetoListener The vetoListener that can determine whether an event is published. + * + * @return true if the vetoListener was subscribed successfully, false otherwise + */ + public boolean subscribeVetoListener(Pattern topicPattern, VetoTopicEventListener vetoListener); + + /** + * Subscribes a VetoEventListener for an event class and its subclasses. Only a WeakReference to the + * VetoEventListener is held by the EventService. + *+ * The VetoEventListener will remain subscribed until {@link #unsubscribeVetoListener(Class,VetoEventListener)} is + * called. + *
+ * + * @param eventClass the class of published objects to listen to + * @param vetoListener The vetoListener that will accept the events when published. + * + * @return true if the vetoListener was subscribed successfully, false otherwise + */ + public boolean subscribeVetoListenerStrongly(Class eventClass, VetoEventListener vetoListener); + + /** + * Subscribes a VetoEventListener for an event class (but not its subclasses). + *+ * The VetoEventListener will remain subscribed until {@link #unsubscribeVetoListener(Class,VetoEventListener)} is + * called. + *
+ * + * @param eventClass the class of published objects to listen to + * @param vetoListener The vetoListener that will accept the events when published. + * + * @return true if the vetoListener was subscribed successfully, false otherwise + */ + public boolean subscribeVetoListenerExactlyStrongly(Class eventClass, VetoEventListener vetoListener); + + /** + * Subscribes a VetoEventListener to a topic name. + *+ * The VetoEventListener will remain subscribed until {@link #unsubscribeVetoListener(String,VetoTopicEventListener)} is + * called. + *
+ * + * @param topic the name of the topic listened to + * @param vetoListener The topic vetoListener that will accept or reject publication. + * + * @return true if the vetoListener was subscribed successfully, false otherwise + * + * @see #subscribeVetoListenerStrongly(Class,VetoEventListener) + */ + public boolean subscribeVetoListenerStrongly(String topic, VetoTopicEventListener vetoListener); + + /** + * Subscribes a VetoTopicEventListener to a set of topics that match a RegEx expression. + *+ * The VetoEventListener will remain subscribed until {@link #unsubscribeVetoListener(Pattern,VetoTopicEventListener)} is + * called. + *
+ * + * @param topicPattern the RegEx pattern that matches the name of the topics listened to + * @param vetoListener The topic vetoListener that will accept or reject publication. + * + * @return true if the vetoListener was subscribed successfully, false otherwise + * + * @see #subscribeVetoListenerStrongly(Pattern,VetoTopicEventListener) + */ + public boolean subscribeVetoListenerStrongly(Pattern topicPattern, VetoTopicEventListener vetoListener); + + /** + * Stop the subscription for a vetoListener that is subscribed to an event class and its subclasses. + * + * @param eventClass the class of published objects that can be vetoed + * @param vetoListener The vetoListener that will accept or reject publication of an event. + * + * @return true if the vetoListener was subscribed to the event, false if it wasn't + */ + public boolean unsubscribeVetoListener(Class eventClass, VetoEventListener vetoListener); + + /** + * Stop the subscription for a vetoListener that is subscribed to an event class (but not its subclasses). + * + * @param eventClass the class of published objects that can be vetoed + * @param vetoListener The vetoListener that will accept or reject publication of an event. + * + * @return true if the vetoListener was subscribed to the event, false if it wasn't + */ + public boolean unsubscribeVetoListenerExactly(Class eventClass, VetoEventListener vetoListener); + + /** + * Stop the subscription for a VetoTopicEventListener that is subscribed to an event topic name. + * + * @param topic the name of the topic that is listened to + * @param vetoListener The vetoListener that can determine whether an event is published on that topic + * + * @return true if the vetoListener was subscribed to the topic, false if it wasn't + */ + public boolean unsubscribeVetoListener(String topic, VetoTopicEventListener vetoListener); + + /** + * Stop the subscription for a VetoTopicEventListener that is subscribed to an event topic RegEx pattern. + * + * @param topicPattern the RegEx pattern matching the name of the topics listened to + * @param vetoListener The vetoListener that can determine whether an event is published on that topic + * + * @return true if the vetoListener was subscribed to the topicPattern, false if it wasn't + */ + public boolean unsubscribeVetoListener(Pattern topicPattern, VetoTopicEventListener vetoListener); + + /** + * Union of getSubscribersToClass(Class) and getSubscribersToExactClass(Class) + * + * @param eventClass the eventClass of interest + * + * @return the subscribers that will be called when an event of eventClass is published, this includes those + * subscribed that match by exact class and those that match to a class and its supertypes + */ + public+ * If this value is set to a positive number, then when an event is published, the EventService caches the event or + * topic payload data for later retrieval. This allows subscribers to find out what has most recently happened + * before they subscribed. The cached event(s) are returned from #getLastEvent(Class), #getLastTopicData(String), + * #getCachedEvents(Class), or #getCachedTopicData(String) + *
+ *+ * The default can be overridden on a by-event-class or by-topic basis. + *
+ * + * @param defaultCacheSizePerClassOrTopic the cache size per event + */ + public void setDefaultCacheSizePerClassOrTopic(int defaultCacheSizePerClassOrTopic); + + /** + * The default number of events or payloads kept per event class or topic + * @return the default number of event payloads kept per event class or topic + */ + public int getDefaultCacheSizePerClassOrTopic(); + + /** + * Set the number of events cached for a particular class of event. By default, no events are cached. + *+ * This overrides any setting for the DefaultCacheSizePerClassOrTopic. + *
+ *+ * Class hierarchy semantics are respected. That is, if there are three events, A, X and Y, and X and Y are both + * derived from A, then setting the cache size for A applies the cache size for all three. Setting the cache size + * for X applies to X and leaves the settings for A and Y in tact. Interfaces can be passed to this method, but they + * only take effect if the cache size of a class or it's superclasses has been set. Just like Class.getInterfaces(), + * if multiple cache sizes are set, the interface names declared earliest in the implements clause of the eventClass + * takes effect. + *
+ *+ * The cache for an event is not adjusted until the next event of that class is published. + *
+ * + * @param eventClass the class of event + * @param cacheSize the number of published events to cache for this event + */ + public void setCacheSizeForEventClass(Class eventClass, int cacheSize); + + /** + * Returns the number of events cached for a particular class of event. By default, no events are cached. + *+ * This result is computed for a particular class from the values passed to #setCacheSizeForEventClass(Class, int), + * and respects the class hierarchy. + *
+ * + * @param eventClass the class of event + * + * @return the maximum size of the event cache for the given event class + * + * @see #setCacheSizeForEventClass(Class,int) + */ + public int getCacheSizeForEventClass(Class eventClass); + + /** + * Set the number of published data objects cached for a particular event topic. By default, no data are cached. + *+ * This overrides any setting for the DefaultCacheSizePerClassOrTopic. + *
+ *+ * Exact topic names take precedence over pattern matching. + *
+ *+ * The cache for a topic is not adjusted until the next publication on that topic. + *
+ * + * @param topicName the topic name + * @param cacheSize the number of published data Objects to cache for this topic + */ + public void setCacheSizeForTopic(String topicName, int cacheSize); + + /** + * Set the number of published data objects cached for a topics matching a pattern. By default, no data are cached. + *+ * This overrides any setting for the DefaultCacheSizePerClassOrTopic. + *
+ *+ * Exact topic names take precedence over pattern matching. + *
+ *+ * The cache for a topic is not adjusted until the next publication on that topic. + *
+ * + * @param pattern the pattern matching topic names + * @param cacheSize the number of data Objects to cache for this topic + */ + public void setCacheSizeForTopic(Pattern pattern, int cacheSize); + + /** + * Returns the number of cached data objects published on a particular topic. + *+ * This result is computed for a particular class from the values passed to #setCacheSizeForEventClass(Class, int), + * and respects the class hierarchy. + *
+ * + * @param topic the topic name + * + * @return the maximum size of the data Object cache for the given topic + * + * @see #setCacheSizeForTopic(String,int) + * @see #setCacheSizeForTopic(java.util.regex.Pattern,int) + */ + public int getCacheSizeForTopic(String topic); + + /** + * When caching, returns the last event publish for the type supplied. + * @param eventClass an index into the cache + * + * @return the last event published for this event class, or null if caching is turned off (the default) + */ + public+ * If an object is subscribed by proxy and it implements EventSubscriber, then the normal unsubscribe methods will + * still unsubscribe the object. + *
+ * + * @param eventClass class this object is subscribed to by proxy + * @param subscribedByProxy object subscribed by proxy + * @return true if the subscription was cancelled, false if it never existed + */ + boolean unsubscribe(Class eventClass, Object subscribedByProxy); + + /** + * Stop a subscription for an object that is subscribed exactly with a ProxySubscriber. + *+ * If an object is subscribed by proxy and it implements EventSubscriber, then the normal unsubscribe methods will + * still unsubscribe the object. + *
+ * + * @param eventClass class this object is subscribed to by proxy + * @param subscribedByProxy object subscribed by proxy + * @return true if the subscription was cancelled, false if it never existed + */ + boolean unsubscribeExactly(Class eventClass, Object subscribedByProxy); + + /** + * Stop a subscription for an object that is subscribed to a topic with a ProxySubscriber. + *+ * If an object is subscribed by proxy and it implements EventSubscriber, then the normal unsubscribe methods will + * still unsubscribe the object. + *
+ * + * @param topic the topic this object is subscribed to by proxy + * @param subscribedByProxy object subscribed by proxy + * @return true if the subscription was cancelled, false if it never existed + */ + boolean unsubscribe(String topic, Object subscribedByProxy); + + /** + * When using annotations, an object may be subscribed by proxy. This unsubscribe method will unsubscribe an object + * that is subscribed with a ProxySubscriber. + *+ * If an object is subscribed by proxy and it implements EventSubscriber, then the normal unsubscribe methods will + * still unsubscribe the object. + *
+ * + * @param pattern the RegEx expression this object is subscribed to by proxy + * @param subscribedByProxy object subscribed by proxy + * @return true if the subscription was cancelled, false if it never existed + */ + boolean unsubscribe(Pattern pattern, Object subscribedByProxy); + + /** + * Stop a veto subscription for an object that is subscribed with a ProxySubscriber. + *+ * If an object is subscribed by proxy and it implements VetoSubscriber, then the normal unsubscribe methods will + * still unsubscribe the object. + *
+ * + * @param eventClass class this object is subscribed to by proxy + * @param subscribedByProxy object subscribed by proxy + * @return true if the subscription was cancelled, false if it never existed + */ + boolean unsubscribeVeto(Class eventClass, Object subscribedByProxy); + + /** + * Stop a veto subscription for an object that is subscribed exactly with a ProxySubscriber. + *+ * If an object is subscribed by proxy and it implements VetoSubscriber, then the normal unsubscribe methods will + * still unsubscribe the object. + *
+ * + * @param eventClass class this object is subscribed to by proxy + * @param subscribedByProxy object subscribed by proxy + * @return true if the subscription was cancelled, false if it never existed + */ + boolean unsubscribeVetoExactly(Class eventClass, Object subscribedByProxy); + + /** + * Stop a veto subscription for an object that is subscribed to a topic with a ProxySubscriber. + *+ * If an object is subscribed by proxy and it implements EventSubscriber, then the normal unsubscribe methods will + * still unsubscribe the object. + *
+ * + * @param topic the topic this object is subscribed to by proxy + * @param subscribedByProxy object subscribed by proxy + * @return true if the subscription was cancelled, false if it never existed + */ + boolean unsubscribeVeto(String topic, Object subscribedByProxy); + + /** + * When using annotations, an object may be subscribed by proxy. This unsubscribe method will unsubscribe an object + * that is subscribed with a ProxySubscriber. + *+ * If an object is subscribed by proxy and it implements EventSubscriber, then the normal unsubscribe methods will + * still unsubscribe the object. + *
+ * + * @param pattern the RegEx expression this object is subscribed to by proxy + * @param subscribedByProxy object subscribed by proxy + * @return true if the subscription was cancelled, false if it never existed + */ + boolean unsubscribeVeto(Pattern pattern, Object subscribedByProxy); +} diff --git a/src/main/java/org/scijava/event/bushe/EventSubscriber.java b/src/main/java/org/scijava/event/bushe/EventSubscriber.java new file mode 100644 index 000000000..03e8f9227 --- /dev/null +++ b/src/main/java/org/scijava/event/bushe/EventSubscriber.java @@ -0,0 +1,35 @@ +/** + * Copyright 2005 Bushe Enterprises, Inc., Hopkinton, MA, USA, www.bushe.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.scijava.event.bushe; + +/** + * Callback interface for class-based subscribers of an {@link EventService}. + * + * @author Michael Bushe michael@bushe.com + */ +public interface EventSubscriberThe EventService calls this method on each publication of an object that matches the
+ * class or interface passed to one of the EventService's class-based subscribe methods, specifically, {@link
+ * EventService#subscribe(Class,EventSubscriber)} {@link EventService#subscribeExactly(Class,EventSubscriber)}
+ * {@link EventService#subscribeStrongly(Class,EventSubscriber)} and {@link EventService#subscribeExactlyStrongly(Class,
+ *EventSubscriber)}.
+ *
+ * @param event The Object that is being published.
+ */
+ public void onEvent(T event);
+}
diff --git a/src/main/java/org/scijava/event/bushe/EventTopicSubscriber.java b/src/main/java/org/scijava/event/bushe/EventTopicSubscriber.java
new file mode 100644
index 000000000..37bf575e4
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/EventTopicSubscriber.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2005 Bushe Enterprises, Inc., Hopkinton, MA, USA, www.bushe.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.scijava.event.bushe;
+
+/**
+ * Callback interface for topic-based subscribers of an {@link EventService}.
+ *
+ * @author Michael Bushe michael@bushe.com
+ */
+interface EventTopicSubscriber
+ * The EventService calls this method on each publication on a matching topic name passed to one of the
+ * EventService's topic-based subscribe methods, specifically, {@link EventService#subscribe(String,
+ *EventTopicSubscriber)} {@link EventService#subscribe(java.util.regex.Pattern,EventTopicSubscriber)} {@link
+ * EventService#subscribeStrongly(String,EventTopicSubscriber)} and {@link EventService#subscribeStrongly(java.util.regex.Pattern,
+ *EventTopicSubscriber)}.
+ *
+ * The EventBus allows operation in two modes - using java.util.logging so that
+ * the EventBus can be deployed in its own jar or using any logging system supported
+ * by apache commons logging, which of course requires other jars.
+ *
+ * The EventBus logging uses the names of its classes as the log, primarily
+ * "org.scijava.event.bushe.EventService". This aids in debugging which subscription and publication issues.
+ *
+ * Implementation note: There are no imports in this class to make things
+ * explicit. There is also no explicit use of classes outside java.util,
+ * anything else is used by reflection to avoid NoClassDefFound errors on class load.
+ *
+ * Subscribers that do not implement this interface are called on a FIFO basis, as are subscribers that implement this
+ * interface and return 0. If the priority returned from this interface is negative, then this subscriber will be
+ * called before non-Prioritized subscribers, the more negative, the earlier it is called. If the priority returned
+ * from this interface is positive, then this subscriber will be called after non-Prioritized subscribers, the more
+ * positive, the later it is called.
+ */
+interface Prioritized {
+ int getPriority();
+}
diff --git a/src/main/java/org/scijava/event/bushe/ProxySubscriber.java b/src/main/java/org/scijava/event/bushe/ProxySubscriber.java
new file mode 100644
index 000000000..04ac3ce0a
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/ProxySubscriber.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2007 Bushe Enterprises, Inc., Hopkinton, MA, USA, www.bushe.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.scijava.event.bushe;
+
+/**
+ * An interface that can be implemented when proxies are used for subscription, not needed in normal usage. When an
+ * unsubscribe method is called on an EventService, the EventService is required to check if any of subscribed objects
+ * are ProxySubscribers and if the object to be unsubscribed is the ProxySubscriber's proxiedSubscriber. If so, the
+ * EventService proxy is unsubscribed and the ProxySubscriber's proxyUnsubscribed() method is called to allow the proxy
+ * to perform any cleanup if necessary. ProxySubscribers should set their references to their proxied objects to null
+ * for strong subscriptions to allow garbage collection.
+ *
+ * @author Michael Bushe
+ */
+interface ProxySubscriber {
+
+ /** @return the object this proxy is subscribed on behalf of */
+ public Object getProxiedSubscriber();
+
+ /**
+ * Called by EventServices to inform the proxy that it is unsubscribed. The ProxySubscriber should null the
+ * reference to it's proxied subscriber
+ */
+ public void proxyUnsubscribed();
+
+ /**
+ * @return the reference strength from this proxy to the proxied subscriber
+ */
+ public ReferenceStrength getReferenceStrength();
+}
diff --git a/src/main/java/org/scijava/event/bushe/PublicationStatus.java b/src/main/java/org/scijava/event/bushe/PublicationStatus.java
new file mode 100644
index 000000000..84c0090d0
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/PublicationStatus.java
@@ -0,0 +1,30 @@
+package org.scijava.event.bushe;
+
+/**
+ * The status of an event as it makes its way from publication through processing by subscribers.
+ *
+ * EventServices are required to stamp any event object or payload that implements the PublicationStatusTracker
+ * with the corresponding PublicationStatus as the event object is processed. The EventService is not
+ * required to set the Unpublished state.
+ */
+enum PublicationStatus {
+ /** Recommended default.*/
+ Unpublished,
+ /** Set directly after publication on an EventService.*/
+ Initiated,
+ /** End status for events that are vetoed and never sent to subscribers.*/
+ Vetoed,
+ /** State set after veto test is passed before the event is send to any subscribers.*/
+ Queued,
+ /** Set while the event is sent to it's subscribers. EventService implementations
+ * such as the ThreadSafeEventService and the SwingEventService will transition from Queued to
+ * Publishing immediately. Others implementations that call subscribers on threads different
+ * from veto subscribers are free to leave an event in the Queued state and wait until
+ * the event is passed to the thread(s) that subscribers are called on to set the
+ * Publishing state */
+ Publishing,
+ /**
+ * Called when all subscribers have finished handling the event publication.
+ */
+ Completed
+}
diff --git a/src/main/java/org/scijava/event/bushe/PublicationStatusTracker.java b/src/main/java/org/scijava/event/bushe/PublicationStatusTracker.java
new file mode 100644
index 000000000..cc6a98595
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/PublicationStatusTracker.java
@@ -0,0 +1,24 @@
+package org.scijava.event.bushe;
+
+/**
+ * An optional interface that can be implemented by Events objects or topic Payloads
+ * to enable the events' status to be stamped on the event by an event service.
+ *
+ * EventService implementations must call setEventStatus(status) on event objects and
+ * payloads that implement this interface.
+ */
+interface PublicationStatusTracker {
+
+ /**
+ * Implementations of this method must be made thread safe.
+ * @return last value set by setPublicationStatus(), or
+ * {@link PublicationStatus#Unpublished} if setPublicationStatus was never called.
+ */
+ public PublicationStatus getPublicationStatus();
+
+ /**
+ * Implementations of this method must be made thread safe.
+ * @param status the status of the event during it's current publication
+ */
+ public void setPublicationStatus(PublicationStatus status);
+}
diff --git a/src/main/java/org/scijava/event/bushe/ReferenceStrength.java b/src/main/java/org/scijava/event/bushe/ReferenceStrength.java
new file mode 100644
index 000000000..15e7a7eb5
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/ReferenceStrength.java
@@ -0,0 +1,11 @@
+package org.scijava.event.bushe;
+
+/**
+ * The two kinds of references that are used in the EventBus.
+ *
+ * @author Michael Bushe
+ */
+public enum ReferenceStrength {
+ WEAK,
+ STRONG
+}
diff --git a/src/main/java/org/scijava/event/bushe/SwingException.java b/src/main/java/org/scijava/event/bushe/SwingException.java
new file mode 100644
index 000000000..f36fc36d6
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/SwingException.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright 2005 Bushe Enterprises, Inc., Hopkinton, MA, USA, www.bushe.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.scijava.event.bushe;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * Aids in troubleshooting Swing application exceptions or any exception where the caller's stack may not be the
+ * exception stack (such as producer-consumer patterns that cross threads).
+ *
+ * Swing exceptions usually occur on the Swing Event Dispatch Thread, and often occur when code puts events on the EDT.
+ * This code is often in a non-EDT thread such as a thread that is receiving data from a server. If the non-EDT threads
+ * puts a call on the EDT and that EDT call causes and exception, the stack trace of the exception is lost, and it often
+ * difficult or impossible to determine where the non-EDT call came from.
+ *
+ * This Exception class is used to handle exceptions that occur when events are posted on the Swing EDT or occur on
+ * another thread from the Swing EDT. It includes a "swing" call stack to record from where the event occurred, and
+ * overrides so that the exception and the swing calling stack print nicely to logs.
+ *
+ * The swing calling stack is different from the cause of the exception since it is gathered before the exception occurs
+ * in a different stack from the cause and used after the exception in a new thread occurs.
+ *
+ * This implementation is not Swing thread-safe. If publication occurs on a thread other than the Swing
+ * EventDispatchThread, subscribers will receive the event on the calling thread, and not the EDT. Swing components
+ * should use the SwingEventService instead, which is the implementation used by the EventBus.
+ *
+ * Two threads may be accessing the ThreadSafeEventService at the same time, one unsubscribing a
+ * listener for topic "A" and the other publishing on topic "A". If the unsubscribing thread gets the lock first,
+ * then it is unsubscribed, end of story. If the publisher gets the lock first, then a snapshot copy of the current
+ * subscribers is made during the publication, the lock is released and the subscribers are called. Between the time
+ * the lock is released and the time that the listener is called, the unsubscribing thread can unsubscribe, resulting
+ * in an unsubscribed object receiving notification of the event after it was unsubscribed (but just once).
+ *
+ * On event publication, subscribers are called in the order in which they subscribed.
+ *
+ * Events and/or topic data can be cached, but are not by default. To cache events or topic data, call
+ * {@link #setDefaultCacheSizePerClassOrTopic(int)}, {@link #setCacheSizeForEventClass(Class, int)}, or
+ * {@link #setCacheSizeForTopic(String, int)}, {@link #setCacheSizeForTopic(Pattern, int)}. Retrieve cached values
+ * with {@link #getLastEvent(Class)}, {@link #getLastTopicData(String)}, {@link #getCachedEvents(Class)}, or
+ * {@link #getCachedTopicData(String)}. Using caching while subscribing
+ * is most likely to make sense only if you subscribe and publish on the same thread (so caching is very useful for
+ * Swing applications since both happen on the EDT in a single-threaded manner). In multithreaded applications, you
+ * never know if your subscriber has handled an event while it was being subscribed (before the subscribe() method
+ * returned) that is newer or older than the retrieved cached value (taken before or after subscribe() respectively).
+ *
+ * All logging goes through the {@link Logger}. The Logger is configurable and supports multiple logging systems.
+ *
+ * Exceptions are logged by default, override {@link #handleException(String,Object,String,Object,Throwable,
+ * StackTraceElement[],String)} to handleException exceptions in another way. Each call to a subscriber is wrapped in
+ * a try block to ensure one listener does not interfere with another.
+ *
+ * The EventService may need to clean up stale WeakReferences and ProxySubscribers created for EventBus annotations. (Aside: EventBus
+ * Annotations are handled by the creation of proxies to the annotated objects. Since the annotations create weak references
+ * by default, annotation proxies must held strongly by the EventService, otherwise the proxy is garbage collected.) When
+ * a WeakReference's referent or an ProxySubscriber's proxiedObject (the annotated object) is claimed by the garbage collector,
+ * the EventService still holds onto the actual WeakReference or ProxySubscriber subscribed to the EventService (which are pretty tiny).
+ *
+ * There are two ways that these stale WeakReferences and ProxySubscribers are cleaned up.
+ *
+ * If a topic or class is never published to again, WeakReferences and ProxySubscribers can be left behind if they
+ * are not cleaned up. To prevent loitering stale subscribers, the ThreadSafeEventService may periodically run through
+ * all the EventSubscribers and VetoSubscribers for all topics and classes and clean up stale proxies. Proxies for
+ * Annotations that have a ReferenceStrength.STRONG are never cleaned up in normal usage. (By specifying
+ * ReferenceStrength.STRONG, the programmer is buying into unsubscribing annotated objects themselves. There is
+ * one caveat: If getProxiedSubscriber() returns null, even for a ProxySubscriber with a STRONG reference strength, that proxy
+ * is cleaned up as it is assumed it is stale or just wrong. This would not occur normally in EventBus usage, but only
+ * if someone is implementing their own custom ProxySubscriber and/or AnnotationProcessor.)
+ *
+ * Cleanup is pretty rare in general. Not only are stale subscribers cleaned up with regular usage, stale
+ * subscribers on abandoned topics and classes do not take up a lot of memory, hence, they are allowed to build up to a certain degree.
+ * Cleanup does not occur until the number of WeakReferences and SubscriptionsProxy's with WeakReference strength
+ * subscribed to an EventService for all the EventService's subscriptions in total exceed the
+ * Once the cleanup start threshold is exceeded, a
+ * After a cleanup cycle completes, if the number of stale subscribers falls at or below the
+ * All cleanup parameters are tunable "live" and checked after each subscription and after each cleanup cycle.
+ * To make cleanup never run, set cleanupStartThreshhold to Integer.MAX_VALUE and cleanupPeriodMS to null.
+ * To get cleanup to run continuously, set set cleanupStartThreshhold to 0 and cleanupPeriodMS to some reasonable value,
+ * perhaps 1000 (1 second) or so (not recommended, cleanup is conducted with regular usage and the cleanup thread is
+ * rarely created or invoked).
+ *
+ * Cleanup is not run in a daemon thread, and thus will not stop the JVM from exiting.
+ *
+ * Overriding this method is only for the adventurous. This basically gives you just enough rope to hang yourself.
+ *
+ * Using protected visibility to open the caching to other implementations.
+ *
+ * If this value is set to a positive number, then when an event is published, the EventService caches the event or
+ * topic payload data for later retrieval. This allows subscribers to find out what has most recently happened
+ * before they subscribed. The cached event(s) are returned from #getLastEvent(Class), #getLastTopicData(String),
+ * #getCachedEvents(Class), or #getCachedTopicData(String)
+ *
+ * The default can be overridden on a by-event-class or by-topic basis.
+ *
+ * This overrides any setting for the DefaultCacheSizePerClassOrTopic.
+ *
+ * Class hierarchy semantics are respected. That is, if there are three events, A, X and Y, and X and Y are both
+ * derived from A, then setting the cache size for A applies the cache size for all three. Setting the cache size
+ * for X applies to X and leaves the settings for A and Y in tact. Interfaces can be passed to this method, but they
+ * only take effect if the cache size of a class or it's superclasses has been set. Just like Class.getInterfaces(),
+ * if multiple cache sizes are set, the interface names declared earliest in the implements clause of the eventClass
+ * takes effect.
+ *
+ * The cache for an event is not adjusted until the next event of that class is published.
+ *
+ * This result is computed for a particular class from the values passed to #setCacheSizeForEventClass(Class, int),
+ * and respects the class hierarchy.
+ *
+ * This overrides any setting for the DefaultCacheSizePerClassOrTopic.
+ *
+ * Settings for exact topic names take precedence over pattern matching.
+ *
+ * The cache for a topic is not adjusted until the next publication on that topic.
+ *
+ * This overrides any setting for the DefaultCacheSizePerClassOrTopic.
+ *
+ * Settings for exact topic names take precedence over pattern matching. If a topic matches the cache settings for
+ * more than one pattern, the cache size chosen is an undetermined one from one of the matched pattern settings.
+ *
+ * The cache for a topic is not adjusted until the next publication on that topic.
+ *
+ * This result is computed for a particular topic from the values passed to #setCacheSizeForTopic(String, int) and
+ * #setCacheSizeForTopic(Pattern, int).
+ *
+ * Not private since I don't claim I'm smart enough to anticipate all needs, but I
+ * am smart enough to doc the rules you must follow to override this method. Those
+ * rules may change (changes will be doc'ed), override at your own risk.
+ *
+ * Overriders MUST call iterator.remove() to unsubscribe the proxy if the subscriber is
+ * a ProxySubscriber and is stale and should be cleaned up. If the ProxySubscriber
+ * is unsubscribed, then implementers MUST also call proxyUnsubscribed() on the subscriber.
+ * Overriders MUST also remove the proxy from the weakProxySubscriber list by calling
+ * removeStaleProxyFromList. Method assumes caller is holding the listenerList
+ * lock (else how can you pass the iterator?).
+ * Multithreading
+ * Logging
+ * Cleanup of Stale WeakReferences and Stale Annotation Proxies
+ *
+ *
+ * The Cleanup Thread
+ * cleanupStartThreshhold,
+ * which is set to CLEANUP_START_THRESHOLD_DEFAULT (500) by default. The default is overridable in the constructor
+ * or via #setCleanupStartThreshhold(Integer). If set to null, cleanup will never start.
+ * java.util.Timer is started to clean up stale subscribers periodically
+ * in another thread. The timer will fire every cleanupPeriodMS milliseconds, which is set to the
+ * CLEANUP_PERIOD_MS_DEFAULT (20 minutes) by default. The default is overridable in the constructor or
+ * via #setCleanupPeriodMS(Integer). If set to null, cleanup will not start. This is implemented with a java.util.Timer,
+ * so Timer's warnings apply - setting this too low will cause cleanups to bunch up and hog the cleanup thread.
+ * cleanupStopThreshhold
+ * cleanup stops until the cleanupStartThreshhold is exceeded again. The cleanupStopThreshhold is set
+ * to CLEANUP_STOP_THRESHOLD_DEFAULT (100) by default. The default is overridable in the constructor or via
+ * #setCleanupStopThreshhold(Integer). If set to null or 0, cleanup will not stop if it is ever started.
+ *
+ * The EventService calls this method before publication of on a topic name. If any of the + * VetoTopicEventListeners return true, then none of the subscribers to that topic are called.
Prerequisite: + * VetoTopicEventListener has to be subscribed with the EventService for the topic name.
Guaranteed to be + * called in the SwingEventThread when using the SwingEventService (EventBus). See {@link EventService}
+ * + * + * @param topic The topic name the data object is published on. + * @param data The data object being published on the topic. + * + * @return true if the publication on the topic should be vetoed and not published, false if the data should be + * published on the topic. + */ + public boolean shouldVeto(String topic, T data); +} diff --git a/src/main/java/org/scijava/input/Accelerator.java b/src/main/java/org/scijava/input/Accelerator.java index c5fefc304..73878ca15 100644 --- a/src/main/java/org/scijava/input/Accelerator.java +++ b/src/main/java/org/scijava/input/Accelerator.java @@ -2,7 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2020 SciJava developers. + * Copyright (C) 2009 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/input/DefaultInputService.java b/src/main/java/org/scijava/input/DefaultInputService.java index 5901b3fbe..392dc250d 100644 --- a/src/main/java/org/scijava/input/DefaultInputService.java +++ b/src/main/java/org/scijava/input/DefaultInputService.java @@ -2,7 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2020 SciJava developers. + * Copyright (C) 2009 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/input/InputModifiers.java b/src/main/java/org/scijava/input/InputModifiers.java index 6df1198cb..50dba7820 100644 --- a/src/main/java/org/scijava/input/InputModifiers.java +++ b/src/main/java/org/scijava/input/InputModifiers.java @@ -2,7 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2020 SciJava developers. + * Copyright (C) 2009 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/input/InputService.java b/src/main/java/org/scijava/input/InputService.java index d402ebd85..0c7adae95 100644 --- a/src/main/java/org/scijava/input/InputService.java +++ b/src/main/java/org/scijava/input/InputService.java @@ -2,7 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2020 SciJava developers. + * Copyright (C) 2009 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/input/KeyCode.java b/src/main/java/org/scijava/input/KeyCode.java index 5a0ae0139..a8eeccaad 100644 --- a/src/main/java/org/scijava/input/KeyCode.java +++ b/src/main/java/org/scijava/input/KeyCode.java @@ -2,7 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2020 SciJava developers. + * Copyright (C) 2009 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -52,7 +52,7 @@ public enum KeyCode { CANCEL(0x03), /** Clear. */ - CLEAR(0x0C), + CLEAR(0x0c), /** Shift (left or right). */ SHIFT(0x10), @@ -70,7 +70,7 @@ public enum KeyCode { CAPS_LOCK(0x14), /** Escape. */ - ESCAPE(0x1B), + ESCAPE(0x1b), /** Space (' '). */ SPACE(0x20), @@ -100,16 +100,16 @@ public enum KeyCode { DOWN(0x28), /** Comma (','). */ - COMMA(0x2C), + COMMA(0x2c), /** Minus ('-'). */ - MINUS(0x2D), + MINUS(0x2d), /** Period ('.'). */ - PERIOD(0x2E), + PERIOD(0x2e), /** Forward slash ('/'). */ - SLASH(0x2F), + SLASH(0x2f), /** Zero ('0', non-numpad). */ NUM0(0x30), @@ -142,10 +142,10 @@ public enum KeyCode { NUM9(0x39), /** Semicolon (';'). */ - SEMICOLON(0x3B), + SEMICOLON(0x3b), /** Equals ('='). */ - EQUALS(0x3D), + EQUALS(0x3d), /** The letter A. */ A(0x41), @@ -175,22 +175,22 @@ public enum KeyCode { I(0x49), /** The letter J. */ - J(0x4A), + J(0x4a), /** The letter K. */ - K(0x4B), + K(0x4b), /** The letter L. */ - L(0x4C), + L(0x4c), /** The letter M. */ - M(0x4D), + M(0x4d), /** The letter N. */ - N(0x4E), + N(0x4e), /** The letter O. */ - O(0x4F), + O(0x4f), /** The letter P. */ P(0x50), @@ -223,16 +223,16 @@ public enum KeyCode { Y(0x59), /** The letter Z. */ - Z(0x5A), + Z(0x5a), /** Left bracket ('['). */ - OPEN_BRACKET(0x5B), + OPEN_BRACKET(0x5b), /** Backslash ('\\'). */ - BACK_SLASH(0x5C), + BACK_SLASH(0x5c), /** Right bracket (']'). */ - CLOSE_BRACKET(0x5D), + CLOSE_BRACKET(0x5d), /** Zero ('0') on numeric keypad. */ NUMPAD_0(0x60), @@ -265,24 +265,24 @@ public enum KeyCode { NUMPAD_9(0x69), /** Asterisk ('*') on numeric keypad. */ - NUMPAD_ASTERISK(0x6A), + NUMPAD_ASTERISK(0x6a), /** Plus ('+') on numeric keypad. */ - NUMPAD_PLUS(0x6B), + NUMPAD_PLUS(0x6b), - NUMPAD_SEPARATOR(0x6C), + NUMPAD_SEPARATOR(0x6c), /** Minus ('-') on numeric keypad. */ - NUMPAD_MINUS(0x6D), + NUMPAD_MINUS(0x6d), /** Period ('.') on numeric keypad. */ - NUMPAD_PERIOD(0x6E), + NUMPAD_PERIOD(0x6e), /** Slash ('/') on numeric keypad. */ - NUMPAD_SLASH(0x6F), + NUMPAD_SLASH(0x6f), /** Delete (non-numpad). */ - DELETE(0x7F), + DELETE(0x7f), /** Num Lock. */ NUM_LOCK(0x90), @@ -321,76 +321,76 @@ public enum KeyCode { F10(0x79), /** F11. */ - F11(0x7A), + F11(0x7a), /** F12. */ - F12(0x7B), + F12(0x7b), /** F13. */ - F13(0xF000), + F13(0xf000), /** F14. */ - F14(0xF001), + F14(0xf001), /** F15. */ - F15(0xF002), + F15(0xf002), /** F16. */ - F16(0xF003), + F16(0xf003), /** F17. */ - F17(0xF004), + F17(0xf004), /** F18 */ - F18(0xF005), + F18(0xf005), /** F19. */ - F19(0xF006), + F19(0xf006), /** F20. */ - F20(0xF007), + F20(0xf007), /** F21. */ - F21(0xF008), + F21(0xf008), /** F22. */ - F22(0xF009), + F22(0xf009), /** F23. */ - F23(0xF00A), + F23(0xf00a), /** F24. */ - F24(0xF00B), + F24(0xf00b), /** Print Screen. */ - PRINTSCREEN(0x9A), + PRINTSCREEN(0x9a), /** Insert. */ - INSERT(0x9B), + INSERT(0x9b), /** Help. */ - HELP(0x9C), + HELP(0x9c), /** Meta. */ - META(0x9D), + META(0x9d), /** Backquote ('`'). */ - BACK_QUOTE(0xC0), + BACK_QUOTE(0xc0), /** Single quote ('\''). */ - QUOTE(0xDE), + QUOTE(0xde), /** Up arrow on numeric keypad. */ - KP_UP(0xE0), + KP_UP(0xe0), /** Down arrow on numeric keypad. */ - KP_DOWN(0xE1), + KP_DOWN(0xe1), /** Left arrow on numeric keypad. */ - KP_LEFT(0xE2), + KP_LEFT(0xe2), /** Right arrow on numeric keypad. */ - KP_RIGHT(0xE3), + KP_RIGHT(0xe3), /** TODO. */ DEAD_GRAVE(0x80), @@ -492,51 +492,51 @@ public enum KeyCode { PLUS(0x0209), /** Right parenthesis (')'). */ - RIGHT_PARENTHESIS(0x020A), + RIGHT_PARENTHESIS(0x020a), /** Underscore ('_'). */ - UNDERSCORE(0x020B), + UNDERSCORE(0x020b), /** Windows key (both left and right). */ - WINDOWS(0x020C), + WINDOWS(0x020c), /** Windows Context Menu key. */ - CONTEXT_MENU(0x020D), + CONTEXT_MENU(0x020d), FINAL(0x0018), /** Convert function key. */ - CONVERT(0x001C), + CONVERT(0x001c), /** Don't Convert function key. */ - NONCONVERT(0x001D), + NONCONVERT(0x001d), /** Accept or Commit function key. */ - ACCEPT(0x001E), + ACCEPT(0x001e), - MODECHANGE(0x001F), + MODECHANGE(0x001f), KANA(0x0015), KANJI(0x0019), /** Alphanumeric function key. */ - ALPHANUMERIC(0x00F0), + ALPHANUMERIC(0x00f0), /** Katakana function key. */ - KATAKANA(0x00F1), + KATAKANA(0x00f1), /** Hiragana function key. */ - HIRAGANA(0x00F2), + HIRAGANA(0x00f2), /** Full-Width Characters function key. */ - FULL_WIDTH(0x00F3), + FULL_WIDTH(0x00f3), /** Half-Width Characters function key. */ - HALF_WIDTH(0x00F4), + HALF_WIDTH(0x00f4), /** Roman Characters function key. */ - ROMAN_CHARACTERS(0x00F5), + ROMAN_CHARACTERS(0x00f5), /** All Candidates function key. */ ALL_CANDIDATES(0x0100), @@ -563,37 +563,37 @@ public enum KeyCode { INPUT_METHOD_ON_OFF(0x0107), /** Cut (Sun keyboard). */ - CUT(0xFFD1), + CUT(0xffd1), /** Copy (Sun keyboard). */ - COPY(0xFFCD), + COPY(0xffcd), /** Paste (Sun keyboard). */ - PASTE(0xFFCF), + PASTE(0xffcf), /** Undo (Sun keyboard). */ - UNDO(0xFFCB), + UNDO(0xffcb), /** Again (Sun keyboard). */ - AGAIN(0xFFC9), + AGAIN(0xffc9), /** Find (Sun keyboard). */ - FIND(0xFFD0), + FIND(0xffd0), /** Props (Sun keyboard). */ - PROPS(0xFFCA), + PROPS(0xffca), /** Stop (Sun keyboard). */ - STOP(0xFFC8), + STOP(0xffc8), /** Compose function key. */ - COMPOSE(0xFF20), + COMPOSE(0xff20), /** AltGraph function key. */ - ALT_GRAPH(0xFF7E), + ALT_GRAPH(0xff7e), /** Begin key. */ - BEGIN(0xFF58), + BEGIN(0xff58), /** Unknown code. */ UNDEFINED(0x0); @@ -632,14 +632,97 @@ public static KeyCode get(final int code) { return keyCode; } + /** + * Gets the KeyCode corresponding to the given character, + * or {@link #UNDEFINED} if no such code. + */ + public static KeyCode get(final char c) { + switch (c) { + case '\n': case '\r': return ENTER; + case '\b': return BACK_SPACE; + case '\t': return TAB; + case 0x1b: return ESCAPE; + case ' ': return SPACE; + case ',': return COMMA; + case '-': return MINUS; + case '.': return PERIOD; + case '/': return SLASH; + case '0': return NUM0; + case '1': return NUM1; + case '2': return NUM2; + case '3': return NUM3; + case '4': return NUM4; + case '5': return NUM5; + case '6': return NUM6; + case '7': return NUM7; + case '8': return NUM8; + case '9': return NUM9; + case ';': return SEMICOLON; + case '=': return EQUALS; + case 'a': case 'A': return A; + case 'b': case 'B': return B; + case 'c': case 'C': return C; + case 'd': case 'D': return D; + case 'e': case 'E': return E; + case 'f': case 'F': return F; + case 'g': case 'G': return G; + case 'h': case 'H': return H; + case 'i': case 'I': return I; + case 'j': case 'J': return J; + case 'k': case 'K': return K; + case 'l': case 'L': return L; + case 'm': case 'M': return M; + case 'n': case 'N': return N; + case 'o': case 'O': return O; + case 'p': case 'P': return P; + case 'q': case 'Q': return Q; + case 'r': case 'R': return R; + case 's': case 'S': return S; + case 't': case 'T': return T; + case 'u': case 'U': return U; + case 'v': case 'V': return V; + case 'w': case 'W': return W; + case 'x': case 'X': return X; + case 'y': case 'Y': return Y; + case 'z': case 'Z': return Z; + case '[': return OPEN_BRACKET; + case '\\': return BACK_SLASH; + case ']': return CLOSE_BRACKET; + case '`': return BACK_QUOTE; + case '\'': return QUOTE; + case '&': return AMPERSAND; + case '*': return ASTERISK; + case '"': return QUOTEDBL; + case '<': return LESS; + case '>': return GREATER; + case '{': return BRACELEFT; + case '}': return BRACERIGHT; + case '@': return AT; + case ':': return COLON; + case '^': return CIRCUMFLEX; + case '$': return DOLLAR; + case '€': return EURO_SIGN; + case '!': return EXCLAMATION_MARK; + case 161: return INVERTED_EXCLAMATION_MARK; + case '(': return LEFT_PARENTHESIS; + case '#': return NUMBER_SIGN; + case '+': return PLUS; + case ')': return RIGHT_PARENTHESIS; + case '_': return UNDERSCORE; + } + return UNDEFINED; + } + /** * Gets the KeyCode with the given name, or {@link #UNDEFINED} if no such * code. */ public static KeyCode get(final String name) { final KeyCode keyCode = NAMES.get(name); - if (keyCode == null) return UNDEFINED; - return keyCode; + if (keyCode != null) return keyCode; + // Not a code name, but maybe a direct character value? + if (name.length() == 1) return KeyCode.get(name.charAt(0)); + return UNDEFINED; } } diff --git a/src/main/java/org/scijava/input/MouseCursor.java b/src/main/java/org/scijava/input/MouseCursor.java index 3c0521552..15bb0ccc8 100644 --- a/src/main/java/org/scijava/input/MouseCursor.java +++ b/src/main/java/org/scijava/input/MouseCursor.java @@ -2,7 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2020 SciJava developers. + * Copyright (C) 2009 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/io/AbstractIOPlugin.java b/src/main/java/org/scijava/io/AbstractIOPlugin.java index 98dd6fef7..3c965739a 100644 --- a/src/main/java/org/scijava/io/AbstractIOPlugin.java +++ b/src/main/java/org/scijava/io/AbstractIOPlugin.java @@ -2,7 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2020 SciJava developers. + * Copyright (C) 2009 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/org/scijava/io/AbstractTypedIOService.java b/src/main/java/org/scijava/io/AbstractTypedIOService.java index 45403af49..fd7effc63 100644 --- a/src/main/java/org/scijava/io/AbstractTypedIOService.java +++ b/src/main/java/org/scijava/io/AbstractTypedIOService.java @@ -2,7 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2020 SciJava developers. + * Copyright (C) 2009 - 2026 SciJava developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -56,7 +56,8 @@ public abstract class AbstractTypedIOService
+ * If every class on the stack trace is annotated, then the class at the
+ * root of the stack trace is returned.
+ */
+ public static String getCallingClassName() {
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ for (int i = 1; i < stackTrace.length - 2; i++) {
+ String className = stackTrace[i].getClassName();
+ if (!hasIgnoreAsCallingClassAnnotation(className)) return className;
+ }
+ return stackTrace[stackTrace.length - 1].getClassName();
+ }
+
+ private static boolean hasIgnoreAsCallingClassAnnotation(String className) {
+ try {
+ Class< ? > clazz = Context.getClassLoader().loadClass(className);
+ return clazz.isAnnotationPresent(IgnoreAsCallingClass.class);
+ }
+ catch (ClassNotFoundException ignore) {
+ return false;
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #getCallingClassName()} instead.
+ *
+ * Warning: This method throws a IllegalStateException as soon as it comes
+ * across a class that can't be loaded with the default class loader.
+ *
* Inspects the stack trace to return the class that calls this method, but
* ignores every class annotated with @IgnoreAsCallingClass.
*
* @throws IllegalStateException if every method on the stack, is in a class
* annotated with @IgnoreAsCallingClass.
*/
+ @Deprecated
public static Class> getCallingClass() {
try {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
diff --git a/src/main/java/org/scijava/log/DefaultLogger.java b/src/main/java/org/scijava/log/DefaultLogger.java
index 506b02447..a5522aa50 100644
--- a/src/main/java/org/scijava/log/DefaultLogger.java
+++ b/src/main/java/org/scijava/log/DefaultLogger.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/DefaultUncaughtExceptionHandler.java b/src/main/java/org/scijava/log/DefaultUncaughtExceptionHandler.java
index 0c313435f..d562921e5 100644
--- a/src/main/java/org/scijava/log/DefaultUncaughtExceptionHandler.java
+++ b/src/main/java/org/scijava/log/DefaultUncaughtExceptionHandler.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/IgnoreAsCallingClass.java b/src/main/java/org/scijava/log/IgnoreAsCallingClass.java
index d6ff175f1..5aa4a690a 100644
--- a/src/main/java/org/scijava/log/IgnoreAsCallingClass.java
+++ b/src/main/java/org/scijava/log/IgnoreAsCallingClass.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -34,7 +34,7 @@
/**
* Classes annotated with {@link IgnoreAsCallingClass} are ignored by
- * {@link CallingClassUtils#getCallingClass()}.
+ * {@link CallingClassUtils#getCallingClassName()}.
*
* @author Matthias Arzt
*/
diff --git a/src/main/java/org/scijava/log/LogLevel.java b/src/main/java/org/scijava/log/LogLevel.java
index dab6e9583..aabab92aa 100644
--- a/src/main/java/org/scijava/log/LogLevel.java
+++ b/src/main/java/org/scijava/log/LogLevel.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/LogListener.java b/src/main/java/org/scijava/log/LogListener.java
index 918895891..19c03c71f 100644
--- a/src/main/java/org/scijava/log/LogListener.java
+++ b/src/main/java/org/scijava/log/LogListener.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/LogMessage.java b/src/main/java/org/scijava/log/LogMessage.java
index a9755e034..dce591878 100644
--- a/src/main/java/org/scijava/log/LogMessage.java
+++ b/src/main/java/org/scijava/log/LogMessage.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/LogService.java b/src/main/java/org/scijava/log/LogService.java
index d8dd1c2b0..ad9cdb40b 100644
--- a/src/main/java/org/scijava/log/LogService.java
+++ b/src/main/java/org/scijava/log/LogService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/LogSource.java b/src/main/java/org/scijava/log/LogSource.java
index 16fdd6c6b..5f03b1870 100644
--- a/src/main/java/org/scijava/log/LogSource.java
+++ b/src/main/java/org/scijava/log/LogSource.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/Logged.java b/src/main/java/org/scijava/log/Logged.java
index 3f31f51c5..bee37e0fa 100644
--- a/src/main/java/org/scijava/log/Logged.java
+++ b/src/main/java/org/scijava/log/Logged.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/Logger.java b/src/main/java/org/scijava/log/Logger.java
index 492203115..c2657b0b0 100644
--- a/src/main/java/org/scijava/log/Logger.java
+++ b/src/main/java/org/scijava/log/Logger.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/log/StderrLogService.java b/src/main/java/org/scijava/log/StderrLogService.java
index 39f09ac2d..8c67b082b 100644
--- a/src/main/java/org/scijava/log/StderrLogService.java
+++ b/src/main/java/org/scijava/log/StderrLogService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/main/DefaultMainService.java b/src/main/java/org/scijava/main/DefaultMainService.java
index 17ef070f6..699a78132 100644
--- a/src/main/java/org/scijava/main/DefaultMainService.java
+++ b/src/main/java/org/scijava/main/DefaultMainService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/main/MainService.java b/src/main/java/org/scijava/main/MainService.java
index 7ecc9ff67..36b92b89b 100644
--- a/src/main/java/org/scijava/main/MainService.java
+++ b/src/main/java/org/scijava/main/MainService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/main/console/MainArgument.java b/src/main/java/org/scijava/main/console/MainArgument.java
index b01f55636..0e0ebe29d 100644
--- a/src/main/java/org/scijava/main/console/MainArgument.java
+++ b/src/main/java/org/scijava/main/console/MainArgument.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/main/run/MainCodeRunner.java b/src/main/java/org/scijava/main/run/MainCodeRunner.java
index 5b6a17acf..a18ae6b3b 100644
--- a/src/main/java/org/scijava/main/run/MainCodeRunner.java
+++ b/src/main/java/org/scijava/main/run/MainCodeRunner.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/AbstractMenuCreator.java b/src/main/java/org/scijava/menu/AbstractMenuCreator.java
index acda55b48..726639ae8 100644
--- a/src/main/java/org/scijava/menu/AbstractMenuCreator.java
+++ b/src/main/java/org/scijava/menu/AbstractMenuCreator.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/DefaultMenuService.java b/src/main/java/org/scijava/menu/DefaultMenuService.java
index dc43dd7d5..74ed27285 100644
--- a/src/main/java/org/scijava/menu/DefaultMenuService.java
+++ b/src/main/java/org/scijava/menu/DefaultMenuService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/MenuConstants.java b/src/main/java/org/scijava/menu/MenuConstants.java
index eb80cbc89..8c38942c9 100644
--- a/src/main/java/org/scijava/menu/MenuConstants.java
+++ b/src/main/java/org/scijava/menu/MenuConstants.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/MenuCreator.java b/src/main/java/org/scijava/menu/MenuCreator.java
index a9d510f9c..386771672 100644
--- a/src/main/java/org/scijava/menu/MenuCreator.java
+++ b/src/main/java/org/scijava/menu/MenuCreator.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/MenuService.java b/src/main/java/org/scijava/menu/MenuService.java
index f7fc4a7e7..ce9a5d36c 100644
--- a/src/main/java/org/scijava/menu/MenuService.java
+++ b/src/main/java/org/scijava/menu/MenuService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/ShadowMenu.java b/src/main/java/org/scijava/menu/ShadowMenu.java
index 7872caaa3..c5eaea4cd 100644
--- a/src/main/java/org/scijava/menu/ShadowMenu.java
+++ b/src/main/java/org/scijava/menu/ShadowMenu.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -230,17 +230,24 @@ public URL getIconURL() {
if (isLeaf()) iconPath = DEFAULT_ICON_PATH;
else return null;
}
- final String className = moduleInfo.getDelegateClassName();
try {
- final Class> c = Types.load(className, false);
+ final Class> c = moduleInfo.loadDelegateClass();
final URL iconURL = c.getResource(iconPath);
if (iconURL == null) {
if (log != null) log.error("Could not load icon: " + iconPath);
}
return iconURL;
}
+ catch (final ClassNotFoundException exc) {
+ final String message = "Failed to load class: " +
+ moduleInfo.getDelegateClassName();
+ if (log.isDebug()) log.debug(message, exc);
+ else log.error(message);
+ return null;
+ }
catch (final IllegalArgumentException exc) {
- final String message = "Could not load icon for class: " + className;
+ final String message = "Could not load icon for class: " +
+ moduleInfo.getDelegateClassName();
if (log.isDebug()) log.debug(message, exc);
else log.error(message);
return null;
diff --git a/src/main/java/org/scijava/menu/ShadowMenuIterator.java b/src/main/java/org/scijava/menu/ShadowMenuIterator.java
index 42e0d1970..ea69e7a2f 100644
--- a/src/main/java/org/scijava/menu/ShadowMenuIterator.java
+++ b/src/main/java/org/scijava/menu/ShadowMenuIterator.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/event/MenuEvent.java b/src/main/java/org/scijava/menu/event/MenuEvent.java
index 3dbeca85c..ae0deaa13 100644
--- a/src/main/java/org/scijava/menu/event/MenuEvent.java
+++ b/src/main/java/org/scijava/menu/event/MenuEvent.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/event/MenusAddedEvent.java b/src/main/java/org/scijava/menu/event/MenusAddedEvent.java
index f824c3ad6..a3fd70e2f 100644
--- a/src/main/java/org/scijava/menu/event/MenusAddedEvent.java
+++ b/src/main/java/org/scijava/menu/event/MenusAddedEvent.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/event/MenusRemovedEvent.java b/src/main/java/org/scijava/menu/event/MenusRemovedEvent.java
index 70926115c..24e3a8f93 100644
--- a/src/main/java/org/scijava/menu/event/MenusRemovedEvent.java
+++ b/src/main/java/org/scijava/menu/event/MenusRemovedEvent.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/menu/event/MenusUpdatedEvent.java b/src/main/java/org/scijava/menu/event/MenusUpdatedEvent.java
index df5b30223..951daace8 100644
--- a/src/main/java/org/scijava/menu/event/MenusUpdatedEvent.java
+++ b/src/main/java/org/scijava/menu/event/MenusUpdatedEvent.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/module/AbstractModule.java b/src/main/java/org/scijava/module/AbstractModule.java
index 04ad15e3a..4ca6a4528 100644
--- a/src/main/java/org/scijava/module/AbstractModule.java
+++ b/src/main/java/org/scijava/module/AbstractModule.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -160,8 +160,13 @@ public void resolveInput(final String name) {
item.validate(this);
}
catch (final MethodCallException exc) {
- // NB: Hacky, but avoids changing the API signature.
- throw new RuntimeException(exc);
+ // NB: resolveInput cannot declare checked exceptions, so we wrap.
+ // Prefer the cause's message (the user-facing validation error) when
+ // available; otherwise fall back to the MethodCallException's message.
+ final Throwable cause = exc.getCause();
+ final String message = (cause != null && cause.getMessage() != null &&
+ !cause.getMessage().isEmpty()) ? cause.getMessage() : exc.getMessage();
+ throw new RuntimeException(message, exc);
}
}
resolvedInputs.add(name);
diff --git a/src/main/java/org/scijava/module/AbstractModuleInfo.java b/src/main/java/org/scijava/module/AbstractModuleInfo.java
index 334f9f37e..c11351573 100644
--- a/src/main/java/org/scijava/module/AbstractModuleInfo.java
+++ b/src/main/java/org/scijava/module/AbstractModuleInfo.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -158,23 +158,6 @@ protected List
+ * The validation function may signal failure either by throwing an exception
+ * or by returning a non-empty {@link String} error message.
+ *
+ * The validation function may signal failure either by throwing an exception
+ * or by returning a non-empty {@link String} error message. This method
+ * catches both cases and returns the error message as a string, or
+ * {@code null} if the value is valid.
+ *
diff --git a/src/main/java/org/scijava/module/ModuleRunner.java b/src/main/java/org/scijava/module/ModuleRunner.java
index b8ec3a20a..ece995d8a 100644
--- a/src/main/java/org/scijava/module/ModuleRunner.java
+++ b/src/main/java/org/scijava/module/ModuleRunner.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -39,6 +39,7 @@
import org.scijava.event.EventService;
import org.scijava.log.LogService;
import org.scijava.module.event.ModuleCanceledEvent;
+import org.scijava.module.event.ModuleErroredEvent;
import org.scijava.module.event.ModuleExecutedEvent;
import org.scijava.module.event.ModuleExecutingEvent;
import org.scijava.module.event.ModuleFinishedEvent;
@@ -144,36 +145,42 @@ public void run() {
final String title = module.getInfo().getTitle();
- // announce start of execution process
- if (ss != null) ss.showStatus("Running command: " + title);
- if (es != null) es.publish(new ModuleStartedEvent(module));
-
- // execute preprocessors
- final ModulePreprocessor canceler = preProcess();
- if (canceler != null) {
- // module execution was canceled by preprocessor
- final String reason = canceler.getCancelReason();
- cancel(reason);
- cleanupAndBroadcastCancelation(title, reason);
- return;
+ try {
+ // announce start of execution process
+ if (ss != null) ss.showStatus("Running command: " + title);
+ if (es != null) es.publish(new ModuleStartedEvent(module));
+
+ // execute preprocessors
+ final ModulePreprocessor canceler = preProcess();
+ if (canceler != null) {
+ // module execution was canceled by preprocessor
+ final String reason = canceler.getCancelReason();
+ cancel(reason);
+ cleanupAndBroadcastCancelation(title, reason);
+ return;
+ }
+
+ // execute module
+ if (es != null) es.publish(new ModuleExecutingEvent(module));
+ module.run();
+ if (isCanceled()) {
+ // module execution was canceled by the module itself
+ cleanupAndBroadcastCancelation(title, getCancelReason());
+ return;
+ }
+ if (es != null) es.publish(new ModuleExecutedEvent(module));
+
+ // execute postprocessors
+ postProcess();
+
+ // announce completion of execution process
+ if (es != null) es.publish(new ModuleFinishedEvent(module));
+ if (ss != null) ss.showStatus("Command finished: " + title);
}
-
- // execute module
- if (es != null) es.publish(new ModuleExecutingEvent(module));
- module.run();
- if (isCanceled()) {
- // module execution was canceled by the module itself
- cleanupAndBroadcastCancelation(title, getCancelReason());
- return;
+ catch (final Throwable t) {
+ cleanupAndBroadcastException(title, t);
+ throw t;
}
- if (es != null) es.publish(new ModuleExecutedEvent(module));
-
- // execute postprocessors
- postProcess();
-
- // announce completion of execution process
- if (es != null) es.publish(new ModuleFinishedEvent(module));
- if (ss != null) ss.showStatus("Command finished: " + title);
}
// -- Helper methods --
@@ -190,6 +197,17 @@ private void cleanupAndBroadcastCancelation(final String title,
}
}
+ private void cleanupAndBroadcastException(final String title,
+ final Throwable t)
+ {
+ final ModuleErroredEvent evt = new ModuleErroredEvent(module, t);
+ if (es != null) es.publish(evt);
+ if (log != null && !evt.isConsumed()) {
+ // Nothing else handled the error, so log it.
+ log.error("Command errored: " + title, t);
+ }
+ }
+
private boolean isCanceled() {
return module instanceof Cancelable && ((Cancelable) module).isCanceled();
}
diff --git a/src/main/java/org/scijava/module/ModuleService.java b/src/main/java/org/scijava/module/ModuleService.java
index 0767fb2f4..25bc64947 100644
--- a/src/main/java/org/scijava/module/ModuleService.java
+++ b/src/main/java/org/scijava/module/ModuleService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -47,7 +47,7 @@
/**
* Interface for service that tracks and executes available modules.
*
- * The module service keeps a master index of all modules known to the system.
+ * The module service keeps a central index of all modules known to the system.
* At heart, a module is a {@link Runnable} piece of code, but with explicit
* typed input and output parameters.
*
- * The plugin service keeps a master index of all plugins known to the system.
+ * The plugin service keeps a central index of all plugins known to the system.
* At heart, a plugin is a piece of functionality that extends a program's
* capabilities. Plugins take many forms; see {@link SciJavaPlugin} for details.
*
* Credit to Jason Sachs for the multi-line evaluation (see
- * his post on StackOverflow).
+ * his post on StackOverflow).
*
- * For further details, see SO
+ * For further details, see SO
* #5584674.
*
+ * Asynchronous case:
+ * A job (runnable) is sent for execution to the linked {@link ThreadService}.
+ * It reports status updates via the linked {@link EventService}.
+ * A {@link org.scijava.task.event.TaskEvent} is sent before the job
+ * is started and when finished.
+ * In the asynchronous case, upon task cancellation ({@link Task#cancel(String)} call),
+ * the runnable associated to the ThreadService is attempted to be stopped
+ * by calling {@link Future#cancel(boolean)}.
+ * This default behaviour can be supplemented by an additional
+ * custom callback which can be set in {@link Task#setCancelCallBack(Runnable)}.
+ *
+ * Synchronous case:
+ * A job that reports its status in between calls of {@link Task#start()},
+ * and {@link Task#finish()}. It also reports its status via
+ * the linked {@link EventService}.
+ * Start and finish calls allow publishing proper {@link org.scijava.task.event.TaskEvent}
+ * to subscribers (with the EventService).
+ * Upon cancellation of a synchronous task, it is the responsibility
+ * of the synchronous task to handle its own cancellation through
+ * a custom callback which can be set via {@link Task#setCancelCallBack(Runnable)}.
+ *
+ * Note that if the system itself is headless—which can be detected via
+ * the {@code java.awt.headless} system property or by calling
+ * {@link java.awt.GraphicsEnvironment#isHeadless()}—then calling
+ * {@code setHeadless(false)} will have no effect; the system will still be
+ * headless, and {@link #isHeadless()} will still return true.
+ *
+ * But if the system itself is not headless, calling
+ * {@code setHeadless(true)} will force {@link #isHeadless()} to return true,
+ * instructing the application to behave in a headless manner insofar as it
+ * can.
+ *
+ * More precisely: returns true when {@code java.awt.headless} system
+ * property is set, and/or {@link java.awt.GraphicsEnvironment#isHeadless()}
+ * returns true, and/or {@link #setHeadless(boolean)} was called with {@code
+ * true} to force headless behavior in an otherwise headful environment.
+ *
* Its primary purpose is to provide the code necessary to keep ImageJ Mirror up-to-date.
+ * href="https://mirror.imagej.net/">ImageJ Mirror up-to-date.
*
+ * If the string matches one of the enum values directly, that value will be
+ * returned via {@link #enumValue(String, Class)}. Otherwise, the result of
+ * {@link #enumFromLabel} is returned.
+ *
* For example, with
- * class StringList implements List<String>,
- * getExactSuperType(StringList.class, Collection.class) returns a
- * {@link ParameterizedType} representing Collection<String>.
+ *
* This is mostly useful if you get a type from one of the other methods in
- * GenericTypeReflector, but you don't want to deal with all the
+ * inputPanel,
}
if (item.isRequired()) {
- throw new ModuleException("A " + type.getSimpleName() +
- " is required but none exist.");
+ final List inputPanel,
}
/** Asks the object service and convert service for valid choices */
- @SuppressWarnings("unchecked")
- private List> getObjects(final Class> type) {
- @SuppressWarnings("rawtypes")
- Set compatibleInputs =
- new HashSet(convertService.getCompatibleInputs(type));
- compatibleInputs.addAll(objectService.getObjects(type));
- return new ArrayList<>(compatibleInputs);
+ private List
+ * This should be called on all widgets whenever any parameter value changes,
+ * since a change to one parameter may affect the validity of others.
+ *
+ * For example, for {@code style="format:#0.00"}, this will return
+ * {@code "#0.00"}.
+ *
+ * For example, for {@code style="extensions:png/gif/bmp"}, this will return {@code ["png", "gif", "bmp"]}.
+ * probability
+ of returning true, else returning false. probability must
be between 0.0 and 1.0, inclusive. Not as precise a random real
event as nextBoolean(double), but twice as fast. To explicitly
use this, remember you may need to cast to float first. */
@@ -567,8 +567,8 @@ public final boolean nextBoolean(final float probability)
}
- /** This generates a coin flip with a probability probability
- of returning true, else returning false. probability must
+ /** This generates a coin flip with a probability probability
+ of returning true, else returning false. probability must
be between 0.0 and 1.0, inclusive. */
public final boolean nextBoolean(final double probability)
@@ -928,7 +928,7 @@ public final double nextDouble()
/** Returns a double in the range from 0.0 to 1.0, possibly inclusive of 0.0 and 1.0 themselves. Thus:
-
+
Expression Interval nextDouble(false, false) (0.0, 1.0)
@@ -1141,7 +1141,7 @@ public final float nextFloat()
/** Returns a float in the range from 0.0f to 1.0f, possibly inclusive of 0.0f and 1.0f themselves. Thus:
- nextDouble(true, false) [0.0, 1.0)
+
Expression Interval nextFloat(false, false) (0.0f, 1.0f)
diff --git a/src/main/java/org/scijava/util/MetaInfCombiner.java b/src/main/java/org/scijava/util/MetaInfCombiner.java
index c34b3a236..3d03195f6 100644
--- a/src/main/java/org/scijava/util/MetaInfCombiner.java
+++ b/src/main/java/org/scijava/util/MetaInfCombiner.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/util/MirrorWebsite.java b/src/main/java/org/scijava/util/MirrorWebsite.java
index d6f5ff1c6..2b5bdf4f9 100644
--- a/src/main/java/org/scijava/util/MirrorWebsite.java
+++ b/src/main/java/org/scijava/util/MirrorWebsite.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -58,7 +58,7 @@
* This program mirrors a given website.
* nextFloat(true, false) [0.0f, 1.0f) addWildcardParameters(Map.class) returns a type representing
+ * Map<?,?>.
*
* @return
*
@@ -3230,27 +3288,27 @@ else if (isMissingTypeParameters(clazz)) {
}
/**
- * Finds the most specific supertype of type whose erasure is
- * searchClass. In other words, returns a type representing the
- * class searchClass plus its exact type parameters in
- * type.
+ * Finds the most specific supertype of
type whose erasure is
+ * searchClass. In other words, returns a type representing the
+ * class searchClass plus its exact type parameters in
+ * type.
*
*
* searchClass is a real class or interface and type has
* parameters for itsearchClass is an array type, and type has type
* parameters for ittype is a raw type,
+ * or has no type parameters for searchClasssearchClass is not a superclass of type.
* class StringList implements List<String>,
+ * getExactSuperType(StringList.class, Collection.class) returns a
+ * {@link ParameterizedType} representing Collection<String>.
* class StringList implements List<String>,
+ * getTypeParameter(StringList.class, Collection.class.getTypeParameters()[0])
+ * returns String.
*
* @param type The type to inspect.
* @param variable The type variable to find the value for.
@@ -3483,8 +3541,6 @@ else if (type instanceof GenericArrayType) {
}
private static Type[] getArrayExactDirectSuperTypes(final Type arrayType) {
- // see
- // http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.10.3
final Type typeComponent = getArrayComponentType(arrayType);
Type[] result;
@@ -3513,9 +3569,9 @@ private static Type[] getArrayExactDirectSuperTypes(final Type arrayType) {
/**
* Returns the exact return type of the given method in the given type. This
- * may be different from m.getGenericReturnType() when the method
- * was declared in a superclass, or type has a type parameter that
- * is used in the return type, or type is a raw type.
+ * may be different from m.getGenericReturnType() when the method
+ * was declared in a superclass, or type has a type parameter that
+ * is used in the return type, or type is a raw type.
*/
public static Type getExactReturnType(final Method m, final Type type) {
final Type returnType = m.getGenericReturnType();
@@ -3531,9 +3587,9 @@ public static Type getExactReturnType(final Method m, final Type type) {
/**
* Returns the exact type of the given field in the given type. This may be
- * different from f.getGenericType() when the field was declared in
- * a superclass, or type has a type parameter that is used in the
- * type of the field, or type is a raw type.
+ * different from f.getGenericType() when the field was declared in
+ * a superclass, or type has a type parameter that is used in the
+ * type of the field, or type is a raw type.
*/
public static Type getExactFieldType(final Field f, final Type type) {
final Type returnType = f.getGenericType();
@@ -3549,9 +3605,9 @@ public static Type getExactFieldType(final Field f, final Type type) {
/**
* Returns the exact parameter types of the given method in the given type.
- * This may be different from m.getGenericParameterTypes() when the
- * method was declared in a superclass, or type has a type
- * parameter that is used in one of the parameters, or type is a
+ * This may be different from m.getGenericParameterTypes() when the
+ * method was declared in a superclass, or type has a type
+ * parameter that is used in one of the parameters, or type is a
* raw type.
*/
public static Type[] getExactParameterTypes(final Method m,
@@ -3610,14 +3666,14 @@ public static Type capture(final Type type) {
/**
* Returns list of classes and interfaces that are supertypes of the given
* type. For example given this class:
- * class Foo<A extends Number & Iterable<A>, B extends A>
+ * class Foo<A extends Number & Iterable<A>, B extends A>
*
- * calling this method on type parameters B
- * (Foo.class.getTypeParameters()[1]) returns a list containing
- * Number and Iterable.
+ * calling this method on type parameters B
+ * (Foo.class.getTypeParameters()[1]) returns a list containing
+ * Number and Iterable.
* GenericTypeReflector, but you don't want to deal with all the
* different sorts of types, and you are only really interested in concrete
* classes and interfaces.
* Type objects representing the upper bound(s)
* of this capture. This includes both the upper bound of a
- * ? extends wildcard, and the bounds declared with the type
+ * ? extends wildcard, and the bounds declared with the type
* variable. References to other (or the same) type variables in bounds
* coming from the type variable are replaced by their matching capture.
*/
Type[] getUpperBounds();
/**
- * Returns an array of Type objects representing the lower bound(s)
- * of this type variable. This is the bound of a ? super wildcard.
+ * Returns an array of Type objects representing the lower bound(s)
+ * of this type variable. This is the bound of a ? super wildcard.
* This normally contains only one or no types; it is an array for
* consistency with {@link WildcardType#getLowerBounds()}.
*/
diff --git a/src/main/java/org/scijava/util/UnitUtils.java b/src/main/java/org/scijava/util/UnitUtils.java
index 315c69149..43c13f1cb 100644
--- a/src/main/java/org/scijava/util/UnitUtils.java
+++ b/src/main/java/org/scijava/util/UnitUtils.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/util/VersionUtils.java b/src/main/java/org/scijava/util/VersionUtils.java
index 0dac9702f..2d7e97a32 100644
--- a/src/main/java/org/scijava/util/VersionUtils.java
+++ b/src/main/java/org/scijava/util/VersionUtils.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/util/XML.java b/src/main/java/org/scijava/util/XML.java
index aee47bf36..0dcbb1126 100644
--- a/src/main/java/org/scijava/util/XML.java
+++ b/src/main/java/org/scijava/util/XML.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/welcome/DefaultWelcomeService.java b/src/main/java/org/scijava/welcome/DefaultWelcomeService.java
index 763fe8a08..8977b0001 100644
--- a/src/main/java/org/scijava/welcome/DefaultWelcomeService.java
+++ b/src/main/java/org/scijava/welcome/DefaultWelcomeService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/welcome/WelcomeService.java b/src/main/java/org/scijava/welcome/WelcomeService.java
index dfdc1ace1..8e2f77aec 100644
--- a/src/main/java/org/scijava/welcome/WelcomeService.java
+++ b/src/main/java/org/scijava/welcome/WelcomeService.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/welcome/event/WelcomeEvent.java b/src/main/java/org/scijava/welcome/event/WelcomeEvent.java
index 09e67f6ab..6f9c5946c 100644
--- a/src/main/java/org/scijava/welcome/event/WelcomeEvent.java
+++ b/src/main/java/org/scijava/welcome/event/WelcomeEvent.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/main/java/org/scijava/widget/AbstractInputHarvester.java b/src/main/java/org/scijava/widget/AbstractInputHarvester.java
index 582b72b77..0785fa157 100644
--- a/src/main/java/org/scijava/widget/AbstractInputHarvester.java
+++ b/src/main/java/org/scijava/widget/AbstractInputHarvester.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -30,9 +30,11 @@
package org.scijava.widget;
import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
import org.scijava.AbstractContextual;
import org.scijava.convert.ConvertService;
@@ -119,8 +121,15 @@ private "format:#0.00".
+ * @param target
+ * The target style attribute, e.g. "format".
+ * @return The modifier for the given target, e.g. "#0.00".
+ */
+ public static String getStyleModifier(String widgetStyle, String target) {
+ if (widgetStyle == null || target == null)
+ return null;
+ String[] styles = widgetStyle.split(",");
+ for (String s : styles) {
+ if (s.trim().toLowerCase().startsWith(target.toLowerCase())) {
+ return s.split(":")[1];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get an array of all modifying values for a given style attribute.
+ *
+ * "extensions:png/gif/bmp".
+ * @param target
+ * The target style attribute, e.g. "extensions".
+ * @return An array of modifiers for the given target, e.g. ["png", "gif", "bmp"].
+ */
+ public static String[] getStyleModifiers(String widgetStyle, String target) {
+ String suffix = getStyleModifier(widgetStyle, target);
+ if (suffix == null) return null;
+ return suffix.split("/");
+ }
+
+}
diff --git a/src/test/java/org/scijava/ContextCreationTest.java b/src/test/java/org/scijava/ContextCreationTest.java
index ccf76e426..4760fa340 100644
--- a/src/test/java/org/scijava/ContextCreationTest.java
+++ b/src/test/java/org/scijava/ContextCreationTest.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -42,6 +42,8 @@
import java.util.List;
import org.junit.Test;
+import org.scijava.event.ContextCreatedEvent;
+import org.scijava.event.EventHandler;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.PluginIndex;
import org.scijava.plugin.PluginInfo;
@@ -149,6 +151,14 @@ public void testSciJavaServices() {
}
}
+ /** Tests that {@link ContextCreatedEvent} is published as expected. */
+ @Test
+ public void testContextCreatedEvent() {
+ assertEquals(0, ServiceNoticingContextCreated.created);
+ final Context context = new Context(ServiceNoticingContextCreated.class);
+ assertEquals(1, ServiceNoticingContextCreated.created);
+ }
+
/**
* Tests that dependent {@link Service}s are automatically created and
* populated in downstream {@link Service} classes.
@@ -441,6 +451,18 @@ private PluginIndex pluginIndex(final Class>... plugins) {
// -- Helper classes --
+ /** A service that notices when {@link ContextCreatedEvent} is published. */
+ public static class ServiceNoticingContextCreated extends AbstractService {
+
+ public static int created = 0;
+
+ @EventHandler
+ public void onEvent(final ContextCreatedEvent evt) {
+ created++;
+ }
+
+ }
+
/** A service which requires a {@link BarService}. */
public static class FooService extends AbstractService {
diff --git a/src/test/java/org/scijava/ContextDisposalTest.java b/src/test/java/org/scijava/ContextDisposalTest.java
new file mode 100644
index 000000000..50f4a1369
--- /dev/null
+++ b/src/test/java/org/scijava/ContextDisposalTest.java
@@ -0,0 +1,51 @@
+/*
+ * #%L
+ * SciJava Common shared library for SciJava software.
+ * %%
+ * Copyright (C) 2009 - 2026 SciJava developers.
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+package org.scijava;
+
+import org.junit.Test;
+
+/**
+ * Tests disposal of {@link Context}s.
+ *
+ * @author Curtis Rueden
+ */
+public class ContextDisposalTest {
+
+ /**
+ * Tests that a {@link Context} can be disposed more than once without
+ * throwing an exception.
+ */
+ @Test
+ public void testDoubleDisposal() {
+ final Context context = new Context();
+ context.dispose();
+ context.dispose();
+ }
+}
diff --git a/src/test/java/org/scijava/ContextInjectionTest.java b/src/test/java/org/scijava/ContextInjectionTest.java
index f05706dfa..7393056fc 100644
--- a/src/test/java/org/scijava/ContextInjectionTest.java
+++ b/src/test/java/org/scijava/ContextInjectionTest.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/SciJavaTest.java b/src/test/java/org/scijava/SciJavaTest.java
index b66a0587f..2a34e6607 100644
--- a/src/test/java/org/scijava/SciJavaTest.java
+++ b/src/test/java/org/scijava/SciJavaTest.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/AnnotatedA.java b/src/test/java/org/scijava/annotations/AnnotatedA.java
index b30632b66..6bf9c3864 100644
--- a/src/test/java/org/scijava/annotations/AnnotatedA.java
+++ b/src/test/java/org/scijava/annotations/AnnotatedA.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/AnnotatedB.java b/src/test/java/org/scijava/annotations/AnnotatedB.java
index c6a2af6b1..d723d0e76 100644
--- a/src/test/java/org/scijava/annotations/AnnotatedB.java
+++ b/src/test/java/org/scijava/annotations/AnnotatedB.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/AnnotatedC.java b/src/test/java/org/scijava/annotations/AnnotatedC.java
index 098b4597a..a5e4977b4 100644
--- a/src/test/java/org/scijava/annotations/AnnotatedC.java
+++ b/src/test/java/org/scijava/annotations/AnnotatedC.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/AnnotatedD.java b/src/test/java/org/scijava/annotations/AnnotatedD.java
index 9baa28c3d..d1a42b98e 100644
--- a/src/test/java/org/scijava/annotations/AnnotatedD.java
+++ b/src/test/java/org/scijava/annotations/AnnotatedD.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/AnnotatedInnerClass.java b/src/test/java/org/scijava/annotations/AnnotatedInnerClass.java
index 8a7be9293..9f2304ff5 100644
--- a/src/test/java/org/scijava/annotations/AnnotatedInnerClass.java
+++ b/src/test/java/org/scijava/annotations/AnnotatedInnerClass.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/Complex.java b/src/test/java/org/scijava/annotations/Complex.java
index cf767f6b3..554afabee 100644
--- a/src/test/java/org/scijava/annotations/Complex.java
+++ b/src/test/java/org/scijava/annotations/Complex.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/DirectoryIndexerTest.java b/src/test/java/org/scijava/annotations/DirectoryIndexerTest.java
index 06ce50b5b..0046a6c7a 100644
--- a/src/test/java/org/scijava/annotations/DirectoryIndexerTest.java
+++ b/src/test/java/org/scijava/annotations/DirectoryIndexerTest.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/EclipseHelperTest.java b/src/test/java/org/scijava/annotations/EclipseHelperTest.java
index e47ee1d4d..9acf9e1d9 100644
--- a/src/test/java/org/scijava/annotations/EclipseHelperTest.java
+++ b/src/test/java/org/scijava/annotations/EclipseHelperTest.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/Fruit.java b/src/test/java/org/scijava/annotations/Fruit.java
index 657d3ecb5..59fd00b47 100644
--- a/src/test/java/org/scijava/annotations/Fruit.java
+++ b/src/test/java/org/scijava/annotations/Fruit.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/LegacyTest.java b/src/test/java/org/scijava/annotations/LegacyTest.java
index 7e7290c52..4734a4763 100644
--- a/src/test/java/org/scijava/annotations/LegacyTest.java
+++ b/src/test/java/org/scijava/annotations/LegacyTest.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/annotations/Simple.java b/src/test/java/org/scijava/annotations/Simple.java
index 5b713e1d9..87a63ddf7 100644
--- a/src/test/java/org/scijava/annotations/Simple.java
+++ b/src/test/java/org/scijava/annotations/Simple.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/app/StatusServiceTest.java b/src/test/java/org/scijava/app/StatusServiceTest.java
index 02b074e38..dbca89909 100644
--- a/src/test/java/org/scijava/app/StatusServiceTest.java
+++ b/src/test/java/org/scijava/app/StatusServiceTest.java
@@ -2,7 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2020 SciJava developers.
+ * Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
diff --git a/src/test/java/org/scijava/command/CommandArrayConverterTest.java b/src/test/java/org/scijava/command/CommandArrayConverterTest.java
new file mode 100644
index 000000000..7a9915a47
--- /dev/null
+++ b/src/test/java/org/scijava/command/CommandArrayConverterTest.java
@@ -0,0 +1,184 @@
+/*-
+ * #%L
+ * SciJava Common shared library for SciJava software.
+ * %%
+ * Copyright (C) 2009 - 2026 SciJava developers.
+ * %%
+ * 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 org.junit.Test;
+import org.scijava.Context;
+import org.scijava.ItemIO;
+import org.scijava.Priority;
+import org.scijava.convert.AbstractConverter;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+
+import java.util.concurrent.ExecutionException;
+
+import static org.junit.Assert.*;
+
+public class CommandArrayConverterTest {
+
+ @Test
+ public void testArrayCommandRaw() throws InterruptedException,
+ ExecutionException
+ {
+ final Context context = new Context(CommandService.class);
+ final CommandService commandService = context.service(CommandService.class);
+
+ UserClass[] userObjects = new UserClass[2];
+ userObjects[0] = new UserClass("User Object 0", new Object());
+ userObjects[1] = new UserClass("User Object 1", new Object());
+
+ final CommandModule module = //
+ commandService.run(CommandRawArrayInput.class, true, "userObjects", userObjects ).get(); //
+ assertEquals("User Object 0;User Object 1;", module.getOutput("result"));
+ }
+
+ @Test
+ public void testArrayConvertFromStringCommandRaw() throws InterruptedException,
+ ExecutionException
+ {
+ final Context context = new Context(CommandService.class);
+ final CommandService commandService = context.service(CommandService.class);
+
+ final CommandModule module = //
+ commandService.run(CommandRawArrayInput.class, true, "userObjects", "User Object 0,User Object 1" ).get(); //
+
+ assertEquals("User Object 0;User Object 1;", module.getOutput("result"));
+ }
+
+ @Test
+ public void testArrayCommandWildcardGenerics() throws InterruptedException,
+ ExecutionException
+ {
+ final Context context = new Context(CommandService.class);
+ final CommandService commandService = context.service(CommandService.class);
+
+ UserClass[] userObjects = new UserClass[2];
+ userObjects[0] = new UserClass("User Object 0", new Object());
+ userObjects[1] = new UserClass("User Object 1", new Object());
+
+ final CommandModule module = //
+ commandService.run(CommandGenericsWildcardArrayInput.class, true, "userObjects", userObjects ).get(); //
+ assertEquals("User Object 0;User Object 1;", module.getOutput("result"));
+ }
+
+ @Test
+ public void testArrayConvertFromStringCommandWildcardGenerics() throws InterruptedException,
+ ExecutionException
+ {
+ final Context context = new Context(CommandService.class);
+ final CommandService commandService = context.service(CommandService.class);
+
+ final CommandModule module = //
+ commandService.run(CommandGenericsWildcardArrayInput.class, true, "userObjects", "User Object 0,User Object 1" ).get(); //
+
+ assertEquals("User Object 0;User Object 1;", module.getOutput("result"));
+ }
+
+ /** A command which uses a UserClass Raw Array parameter. */
+ @Plugin(type = Command.class)
+ public static class CommandRawArrayInput implements Command {
+
+ @Parameter
+ private UserClass[] userObjects;
+
+ @Parameter(type = ItemIO.OUTPUT)
+ private String result = "default";
+
+ @Override
+ public void run() {
+ final StringBuilder sb = new StringBuilder();
+ for (UserClass obj : userObjects) {
+ sb.append(obj.toString()+";");
+ }
+ result = sb.toString();
+ }
+ }
+
+ /** A command which uses a UserClass Array with generics wildcard */
+ @Plugin(type = Command.class)
+ public static class CommandGenericsWildcardArrayInput implements Command {
+
+ @Parameter
+ private UserClass>[] userObjects;
+
+ @Parameter(type = ItemIO.OUTPUT)
+ private String result = "default";
+
+ @Override
+ public void run() {
+ final StringBuilder sb = new StringBuilder();
+ for (UserClass obj : userObjects) {
+ sb.append(obj.toString()+";");
+ }
+ result = sb.toString();
+ }
+ }
+
+ @Plugin(type = org.scijava.convert.Converter.class, priority = Priority.LOW)
+ public static class StringToUserClassConverterNoGenerics extends AbstractConverter