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/.mailmap b/.mailmap
index e5a0e733c..5157cbbe1 100644
--- a/.mailmap
+++ b/.mailmap
@@ -1,7 +1,16 @@
Barry DeZonia
-Christian Dietz
+Christian Dietz
+Christian Dietz
+Gabriel Einsdorf
+Gabriel Einsdorf
+Gabriel Selzer
+Gabriel Selzer
ImageJ Jenkins
+Jan Eglinger
Johannes Schindelin
Johannes Schindelin
Jonathan Hale
+Leon Yang
+Leon Yang
Mark Hiner
+Richard Domander
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 1570c3987..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-language: java
-jdk: oraclejdk8
-branches:
- only: master
-install: true
-script: ".travis/build.sh"
-after_success: ".travis/notify.sh Travis-Success"
-after_failure: ".travis/notify.sh Travis-Failure"
-env:
- global:
- - secure: l5k7gyNsC7cS1QcqPcSzxQeueZmakAvYRDBevIGA61yDjL/cK4Pyy/7yNz4CHo+mgHaHul9uLIQuZvQdeAs4FqHi37brY1//emVI4BiUow+XDvge7DttDJ+JjBlGsZrxX0YKtnn/V83WO23RtGGQUZ8PBwpNWYB52stDpizhqqg=
- - secure: Hs3V7J4BmasiXNWOBzFuxYcPeImFYo8Ze3UWqg85gRa1YxKNbnJLGPjQoIcUxXQU34Fa0qvvWcPUyEjne+nAbWtAVrq/xRSVj5Fnen+UmH3fjV1aR6Rsr56gkdqlaeIfcxaaeGTeguHQB1LSNCIvcF5s8f6J+37l65UUxGsL3+4=
diff --git a/.travis/build.sh b/.travis/build.sh
deleted file mode 100755
index 4c2f8d2e4..000000000
--- a/.travis/build.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-dir="$(dirname "$0")"
-test "$TRAVIS_SECURE_ENV_VARS" = true \
- -a "$TRAVIS_PULL_REQUEST" = false \
- -a "$TRAVIS_BRANCH" = master &&
- mvn -Pdeploy-to-imagej deploy --settings "$dir/settings.xml" ||
- mvn install
diff --git a/.travis/notify.sh b/.travis/notify.sh
deleted file mode 100755
index b3b239e46..000000000
--- a/.travis/notify.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-curl -fs "https://jenkins.imagej.net/job/$1/buildWithParameters?token=$TOKEN_NAME&repo=$TRAVIS_REPO_SLUG&commit=$TRAVIS_COMMIT&pr=$TRAVIS_PULL_REQUEST"
diff --git a/.travis/settings.xml b/.travis/settings.xml
deleted file mode 100644
index 71a56300a..000000000
--- a/.travis/settings.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- imagej.releases
- travis
- ${env.MAVEN_PASS}
-
-
- imagej.snapshots
- travis
- ${env.MAVEN_PASS}
-
-
-
diff --git a/LICENSE.txt b/LICENSE.txt
index aaa28e8b4..55024a7c5 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,4 @@
-Copyright (c) 2009 - 2017, Board of Regents of the University of
-Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
-Institute of Molecular Cell Biology and Genetics.
+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
new file mode 100644
index 000000000..5b605b01d
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1,207 @@
+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,
+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
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2012] [MOJO Codehaus]
+
+ 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
+
+ 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,
+ 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.
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 fd1a7ddea..771234c9d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,16 +1,16 @@
-
+4.0.0org.scijavapom-scijava
- 13.1.0
+ 44.0.0scijava-common
- 2.62.2-SNAPSHOT
+ 2.100.2-SNAPSHOTSciJava CommonSciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by downstream projects in the SciJava ecosystem, such as ImageJ and SCIFIO.
@@ -18,7 +18,7 @@
2009SciJava
- http://www.scijava.org/
+ https://scijava.org/
@@ -31,7 +31,7 @@
ctruedenCurtis Rueden
- http://imagej.net/User:Rueden
+ https://imagej.net/people/ctruedenfounderlead
@@ -46,53 +46,99 @@
Mark Hiner
- http://imagej.net/User:Hinerm
+ https://imagej.net/people/hinermfounderhinermJohannes Schindelin
- http://imagej.net/User:Schindelin
+ https://imagej.net/people/dschodscho
- Barry DeZonia
- http://imagej.net/User:Bdezonia
- bdezonia
+ Chris Allan
+ chris-allan
- Lee Kamentsky
- http://imagej.net/User:Leek
- LeeKamentsky
+ Nicolas Chiaruttini
+ https://imagej.net/people/NicoKiaru
+ NicoKiaru
+
+
+ Barry DeZonia
+ https://imagej.net/people/bdezonia
+ bdezoniaChristian Dietz
- http://imagej.net/User:Dietzc
+ https://imagej.net/people/dietzcdietzcRichard Domander
- http://imagej.net/User:Rdom
+ https://imagej.net/people/rimadomarimadomaGabriel Einsdorf
- http://imagej.net/User:Gab1one
+ https://imagej.net/people/gab1onegab1one
+
+ Aivar Grislis
+ https://imagej.net/people/grislis
+ grislis
+ Jonathan Hale
+ https://imagej.net/people/SquareysSquareys
+
+ Grant Harris
+ https://imagej.net/people/tnargsirrah
+ tnargsirrah
+
+
+ Lee Kamentsky
+ https://imagej.net/people/LeeKamentsky
+ LeeKamentsky
+
+
+ Rick Lentz
+ https://imagej.net/people/ricklentz
+ ricklentz
+
+
+ Melissa Linkert
+ https://imagej.net/people/melissalinkert
+ melissalinkert
+ Kevin Mader
- http://imagej.net/User:Ksmader
+ https://imagej.net/people/kmaderkmader
+
+ Hadrien Mary
+ https://imagej.net/people/hadim
+ hadim
+
+
+ Alison Walter
+ https://imagej.net/people/awalter17
+ awalter17
+ Jay Warrick
+ https://imagej.net/people/jaywarrickjaywarrick
+
+ Christian Tischer
+ https://imagej.net/people/tischi
+ tischi
+
@@ -106,7 +152,7 @@
- scm:git:git://github.com/scijava/scijava-common
+ scm:git:https://github.com/scijava/scijava-commonscm:git:git@github.com:scijava/scijava-commonHEADhttps://github.com/scijava/scijava-common
@@ -116,18 +162,22 @@
https://github.com/scijava/scijava-common/issues
- Travis CI
- https://travis-ci.org/scijava/scijava-common
+ GitHub Actions
+ https://github.com/scijava/scijava-common/actionsorg.scijava
+ 8
+ 11bsd_2SciJava Common shared library for SciJava software.
- Board of Regents of the University of
-Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
-Institute of Molecular Cell Biology and Genetics.
+ SciJava developers.
+ **/bushe/**
+
+
+ 2.22.2
@@ -137,39 +187,32 @@ Institute of Molecular Cell Biology and Genetics.
parsington
-
-
- com.googlecode.gentyref
- gentyref
- 1.1.0
-
-
- org.bushe
- eventbus
- 1.4
-
-
junitjunittest
+
+ org.mockito
+ mockito-core
+ test
+
-
+
org.apache.maven.pluginsmaven-compiler-plugin
@@ -182,11 +225,22 @@ Institute of Molecular Cell Biology and Genetics.
exec-maven-plugin
+ index-annotationsprocess-classesjava
+
+ index-test-annotations
+ process-test-classes
+
+ java
+
+
+ test
+
+ org.scijava.annotations.EclipseHelper
diff --git a/src/it/apt-test/pom.xml b/src/it/apt-test/pom.xml
index fba517e29..c416a3860 100644
--- a/src/it/apt-test/pom.xml
+++ b/src/it/apt-test/pom.xml
@@ -3,9 +3,7 @@
#%L
SciJava Common shared library for SciJava software.
%%
- Copyright (C) 2009 - 2017 Board of Regents of the University of
- Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- Institute of Molecular Cell Biology and Genetics.
+ 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:
@@ -33,7 +31,7 @@
+ https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0@project.groupId@
diff --git a/src/it/apt-test/setup.bsh b/src/it/apt-test/setup.bsh
index 86549ce6d..30380371c 100644
--- a/src/it/apt-test/setup.bsh
+++ b/src/it/apt-test/setup.bsh
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/it/apt-test/src/main/java/org/scijava/annotation/its/Annotated.java b/src/it/apt-test/src/main/java/org/scijava/annotation/its/Annotated.java
index 466bd78c3..2d9e5d420 100644
--- a/src/it/apt-test/src/main/java/org/scijava/annotation/its/Annotated.java
+++ b/src/it/apt-test/src/main/java/org/scijava/annotation/its/Annotated.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/it/apt-test/src/main/java/org/scijava/annotation/its/CustomAnnotation.java b/src/it/apt-test/src/main/java/org/scijava/annotation/its/CustomAnnotation.java
index a21cd5344..1b456f1c3 100644
--- a/src/it/apt-test/src/main/java/org/scijava/annotation/its/CustomAnnotation.java
+++ b/src/it/apt-test/src/main/java/org/scijava/annotation/its/CustomAnnotation.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/it/apt-test/verify.bsh b/src/it/apt-test/verify.bsh
index edc3dfab2..679bbf6ed 100644
--- a/src/it/apt-test/verify.bsh
+++ b/src/it/apt-test/verify.bsh
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/it/settings.xml b/src/it/settings.xml
index d4ee30ac3..b3042e9e1 100644
--- a/src/it/settings.xml
+++ b/src/it/settings.xml
@@ -3,9 +3,7 @@
#%L
SciJava Common shared library for SciJava software.
%%
- Copyright (C) 2009 - 2017 Board of Regents of the University of
- Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- Institute of Molecular Cell Biology and Genetics.
+ 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/AbstractBasicDetails.java b/src/main/java/org/scijava/AbstractBasicDetails.java
index df0bcba15..75e28c611 100644
--- a/src/main/java/org/scijava/AbstractBasicDetails.java
+++ b/src/main/java/org/scijava/AbstractBasicDetails.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/AbstractContextual.java b/src/main/java/org/scijava/AbstractContextual.java
index 768ce4986..6d47180bf 100644
--- a/src/main/java/org/scijava/AbstractContextual.java
+++ b/src/main/java/org/scijava/AbstractContextual.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/AbstractGateway.java b/src/main/java/org/scijava/AbstractGateway.java
index a4980d63d..14f066a7f 100644
--- a/src/main/java/org/scijava/AbstractGateway.java
+++ b/src/main/java/org/scijava/AbstractGateway.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -44,6 +42,7 @@
import org.scijava.input.InputService;
import org.scijava.io.IOService;
import org.scijava.io.RecentFileService;
+import org.scijava.io.location.LocationService;
import org.scijava.log.LogService;
import org.scijava.main.MainService;
import org.scijava.menu.MenuService;
@@ -53,10 +52,12 @@
import org.scijava.platform.AppEventService;
import org.scijava.platform.PlatformService;
import org.scijava.plugin.AbstractRichPlugin;
+import org.scijava.plugin.PluginInfo;
import org.scijava.plugin.PluginService;
import org.scijava.prefs.PrefService;
import org.scijava.script.ScriptService;
import org.scijava.service.Service;
+import org.scijava.startup.StartupService;
import org.scijava.text.TextService;
import org.scijava.thread.ThreadService;
import org.scijava.tool.IconService;
@@ -84,7 +85,15 @@ public AbstractGateway() {
public AbstractGateway(final String appName, final Context context) {
this.appName = appName;
- if (context != null) setContext(context);
+ if (context != null) {
+ setContext(context);
+
+ // NB: Make a best effort to inject plugin metadata.
+ final PluginInfo> info = PluginInfo.getOrCreate(getClass(),
+ Gateway.class, context.getPluginIndex());
+ info.inject(this);
+ Priority.inject(this, info.getPriority());
+ }
}
// -- Gateway methods --
@@ -101,6 +110,9 @@ public void launch(final String... args) {
// NB: When running headless, the HeadlessUI will be used.
if (mainCount == 0) ui().showUI();
+ // perform all pending startup operations
+ startup().executeOperations();
+
if (ui().isHeadless()) {
// now that CLI processing/execution is done, we can shut down
getContext().dispose();
@@ -109,6 +121,8 @@ public void launch(final String... args) {
@Override
public String getShortName() {
+ final String pluginName = getInfo() == null ? null : getInfo().getName();
+ if (pluginName != null && !pluginName.isEmpty()) return pluginName;
return getClass().getSimpleName().toLowerCase();
}
@@ -177,6 +191,11 @@ public InputService input() {
public IOService io() {
return get(IOService.class);
}
+
+ @Override
+ public LocationService location() {
+ return get(LocationService.class);
+ }
@Override
public LogService log() {
@@ -232,6 +251,11 @@ public ScriptService script() {
return get(ScriptService.class);
}
+ @Override
+ public StartupService startup() {
+ return get(StartupService.class);
+ }
+
@Override
public StatusService status() {
return get(StatusService.class);
diff --git a/src/main/java/org/scijava/AbstractUIDetails.java b/src/main/java/org/scijava/AbstractUIDetails.java
index 2c81d5f31..11476a03a 100644
--- a/src/main/java/org/scijava/AbstractUIDetails.java
+++ b/src/main/java/org/scijava/AbstractUIDetails.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -50,7 +48,7 @@ public abstract class AbstractUIDetails extends AbstractBasicDetails implements
private String iconPath;
/** Sort priority of the object. */
- private double priority = Priority.NORMAL_PRIORITY;
+ private double priority = Priority.NORMAL;
/** Whether the object can be selected in the user interface. */
private boolean selectable;
diff --git a/src/main/java/org/scijava/BasicDetails.java b/src/main/java/org/scijava/BasicDetails.java
index 4dd50a1a9..6c37807e6 100644
--- a/src/main/java/org/scijava/BasicDetails.java
+++ b/src/main/java/org/scijava/BasicDetails.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Cancelable.java b/src/main/java/org/scijava/Cancelable.java
index 030ba023a..5bc0fe61c 100644
--- a/src/main/java/org/scijava/Cancelable.java
+++ b/src/main/java/org/scijava/Cancelable.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Context.java b/src/main/java/org/scijava/Context.java
index 4813fdeab..bfb118099 100644
--- a/src/main/java/org/scijava/Context.java
+++ b/src/main/java/org/scijava/Context.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -35,11 +33,15 @@
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.scijava.event.ContextCreatedEvent;
import org.scijava.event.ContextDisposingEvent;
import org.scijava.event.EventHandler;
import org.scijava.event.EventService;
@@ -51,6 +53,7 @@
import org.scijava.service.ServiceIndex;
import org.scijava.util.ClassUtils;
import org.scijava.util.Query;
+import org.scijava.util.Types;
/**
* Top-level SciJava application context, which initializes and maintains a list
@@ -59,7 +62,7 @@
* @author Curtis Rueden
* @see Service
*/
-public class Context implements Disposable {
+public class Context implements Disposable, AutoCloseable {
// -- Constants --
@@ -73,6 +76,14 @@ public class Context implements Disposable {
*/
public static final String STRICT_PROPERTY = "scijava.context.strict";
+ /** Set of currently active (not disposed) application contexts. */
+ private static final Map CONTEXTS =
+ new ConcurrentHashMap<>(); // NB: ConcurrentHashMap disallows nulls.
+
+ // -- Static fields --
+
+ private static Thread shutdownThread = null;
+
// -- Fields --
/** Index of the application context's services. */
@@ -98,6 +109,12 @@ public class Context implements Disposable {
*/
private boolean strict;
+ /**
+ * False if the context is currently active; true if the context
+ * has already been disposed, or is in the process of being disposed.
+ */
+ private boolean disposed;
+
/**
* Creates a new SciJava application context with all available services.
*
@@ -141,8 +158,9 @@ public Context(final boolean noServices, final boolean noPlugins) {
* {@code new Context(DisplayService.class)}) yield the potentially confusing
* warning:
*
- *
Type safety: A generic array of Class extends Service> is
- * created for a varargs parameter
+ *
Type safety: A generic array of
+ * {@code Class extends Service>} is created for a varargs
+ * parameter
*
* To avoid this, we have opted to use raw types and suppress the relevant
* warnings here instead.
@@ -248,6 +266,13 @@ public Context(final Collection> serviceClasses,
* those of lower priority). See {@link ServiceHelper#loadServices()} for more
* information.
*
+ *
+ * NB: Instiantiation of a Context has an implied requirement of a
+ * corresponding call to {@link Context#dispose()} at the end of the SciJava
+ * applicaton's lifecycle. This cleans up any remaining resources and allows
+ * the JVM to exit gracefully. This is called automatically when constructed as
+ * an {@link AutoCloseable}.
+ *
*
* @param serviceClasses A collection of types that implement the
* {@link Service} interface (e.g., {@code DisplayService.class}).
@@ -277,6 +302,26 @@ public Context(final Collection> serviceClasses,
new ServiceHelper(this, serviceClasses, strict);
serviceHelper.loadServices();
}
+
+ // If JVM shuts down with context still active, clean up after ourselves.
+ if (shutdownThread == null) {
+ synchronized (Context.class) {
+ if (shutdownThread == null) {
+ shutdownThread = new Thread(() -> {
+ final List contexts = new ArrayList<>(CONTEXTS.keySet());
+ for (final Context context : contexts) {
+ context.doDispose(false);
+ }
+ });
+ Runtime.getRuntime().addShutdownHook(shutdownThread);
+ }
+ }
+ }
+ CONTEXTS.put(this, true);
+
+ // Publish an event to indicate that context initialization is complete.
+ final EventService eventService = getService(EventService.class);
+ if (eventService != null) eventService.publish(new ContextCreatedEvent());
}
// -- Context methods --
@@ -321,7 +366,7 @@ public S service(final Class c) {
* service.
*/
public Service service(final String className) {
- final Class> c = ClassUtils.loadClass(className, false);
+ final Class> c = Types.load(className, false);
if (!Service.class.isAssignableFrom(c)) {
throw new IllegalArgumentException("Not a service class: " + c.getName());
}
@@ -340,7 +385,7 @@ public S getService(final Class c) {
/** Gets the service of the given class name (useful for scripts). */
public Service getService(final String className) {
- final Class> c = ClassUtils.loadClass(className);
+ final Class> c = Types.load(className);
if (c == null) return null;
if (!Service.class.isAssignableFrom(c)) return null; // not a service class
@SuppressWarnings("unchecked")
@@ -351,7 +396,7 @@ public Service getService(final String className) {
/**
* Injects the application context into the given object. This does three
* distinct things:
- *
+ *
*
If the given object has any non-final {@link Context} fields annotated
* with @{@link Parameter}, sets the value of those fields to this context.
*
@@ -360,8 +405,8 @@ public Service getService(final String className) {
* corresponding service available from this context.
*
Calls {@link EventService#subscribe(Object)} with the object to
* register any @{@link EventHandler} annotated methods as event subscribers.
- *
.
- *
+ *
+ *
*
* @param o The object to which the context should be assigned.
* @throws IllegalStateException If the object already has a context.
@@ -411,16 +456,14 @@ public boolean isInjectable(final Class> type) {
@Override
public void dispose() {
- final EventService eventService = getService(EventService.class);
- if (eventService != null) eventService.publish(new ContextDisposingEvent());
+ doDispose(true);
+ }
- // NB: Dispose services in reverse order.
- // This may or may not actually be necessary, but seems safer, since
- // dependent services will be disposed *before* their dependencies.
- final List services = serviceIndex.getAll();
- for (int s = services.size() - 1; s >= 0; s--) {
- services.get(s).dispose();
- }
+ // -- AutoCloseable methods --
+
+ @Override
+ public void close() {
+ dispose();
}
// -- Utility methods --
@@ -437,6 +480,19 @@ public static List> serviceClassList(
Arrays.asList(serviceClasses) : Arrays.asList(Service.class);
}
+ /**
+ * Gets the class loader to use. This will be the current thread's context
+ * class loader if non-null; otherwise it will be the system class loader.
+ *
+ * @see Thread#getContextClassLoader()
+ * @see ClassLoader#getSystemClassLoader()
+ */
+ public static ClassLoader getClassLoader() {
+ final ClassLoader contextCL = Thread.currentThread()
+ .getContextClassLoader();
+ return contextCL != null ? contextCL : ClassLoader.getSystemClassLoader();
+ }
+
// -- Helper methods --
private List getParameterFields(final Object o) {
@@ -527,8 +583,7 @@ private String createMissingServiceMessage(
final Class extends Service> serviceType)
{
final String nl = System.getProperty("line.separator");
- final ClassLoader classLoader = //
- Thread.currentThread().getContextClassLoader();
+ final ClassLoader classLoader = getClassLoader();
final StringBuilder msg = new StringBuilder(
"Required service is missing: " + serviceType.getName() + nl);
msg.append("Context: " + this + nl);
@@ -554,17 +609,34 @@ private String createMissingServiceMessage(
return msg.toString();
}
+ private synchronized void doDispose(final boolean announce) {
+ if (disposed) return;
+ disposed = true;
+ CONTEXTS.remove(this);
+ if (announce) {
+ final EventService eventService = getService(EventService.class);
+ if (eventService != null) eventService.publish(new ContextDisposingEvent());
+ }
+
+ // NB: Dispose services in reverse order.
+ // This may or may not actually be necessary, but seems safer, since
+ // dependent services will be disposed *before* their dependencies.
+ final List services = serviceIndex.getAll();
+ for (int s = services.size() - 1; s >= 0; s--) {
+ services.get(s).dispose();
+ }
+ }
+
private static PluginIndex plugins(final boolean empty) {
return empty ? new PluginIndex(null) : null;
}
private static List> services(final boolean empty) {
- if (empty) return Collections.>emptyList();
- return Arrays.>asList(Service.class);
+ if (empty) return Collections.> emptyList();
+ return Arrays.> asList(Service.class);
}
private static boolean strict() {
return !"false".equals(System.getProperty(STRICT_PROPERTY));
}
-
}
diff --git a/src/main/java/org/scijava/Contextual.java b/src/main/java/org/scijava/Contextual.java
index 9821c0dd7..f76c7fc0d 100644
--- a/src/main/java/org/scijava/Contextual.java
+++ b/src/main/java/org/scijava/Contextual.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Disposable.java b/src/main/java/org/scijava/Disposable.java
index 0a86dfbce..ac72c90b4 100644
--- a/src/main/java/org/scijava/Disposable.java
+++ b/src/main/java/org/scijava/Disposable.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Gateway.java b/src/main/java/org/scijava/Gateway.java
index e74b7188e..7cd98413e 100644
--- a/src/main/java/org/scijava/Gateway.java
+++ b/src/main/java/org/scijava/Gateway.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 +40,7 @@
import org.scijava.input.InputService;
import org.scijava.io.IOService;
import org.scijava.io.RecentFileService;
+import org.scijava.io.location.LocationService;
import org.scijava.log.LogService;
import org.scijava.main.MainService;
import org.scijava.menu.MenuService;
@@ -55,6 +54,7 @@
import org.scijava.plugin.RichPlugin;
import org.scijava.script.ScriptService;
import org.scijava.service.Service;
+import org.scijava.startup.StartupService;
import org.scijava.text.TextService;
import org.scijava.thread.ThreadService;
import org.scijava.tool.IconService;
@@ -118,7 +118,7 @@
* @author Mark Hiner
* @author Curtis Rueden
*/
-public interface Gateway extends RichPlugin {
+public interface Gateway extends RichPlugin, Disposable {
/**
* Perform launch operations associated with this gateway.
@@ -239,6 +239,13 @@ public interface Gateway extends RichPlugin {
*/
IOService io();
+ /**
+ * Gets this application context's {@link LocationService}.
+ *
+ * @return The {@link LocationService} of this application context.
+ */
+ LocationService location();
+
/**
* Gets this application context's {@link LogService}.
*
@@ -310,6 +317,13 @@ public interface Gateway extends RichPlugin {
*/
ScriptService script();
+ /**
+ * Gets this application context's {@link StartupService}.
+ *
+ * @return The {@link StartupService} of this application context.
+ */
+ StartupService startup();
+
/**
* Gets this application context's {@link StatusService}.
*
@@ -363,4 +377,8 @@ public interface Gateway extends RichPlugin {
/** @see org.scijava.app.App#getInfo(boolean) */
String getInfo(boolean mem);
+ @Override
+ default void dispose() {
+ context().dispose();
+ }
}
diff --git a/src/main/java/org/scijava/Identifiable.java b/src/main/java/org/scijava/Identifiable.java
index e04fbb682..53db16039 100644
--- a/src/main/java/org/scijava/Identifiable.java
+++ b/src/main/java/org/scijava/Identifiable.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -49,7 +47,7 @@
*
* In this way, objects themselves do not need to be {@link Serializable}, nor
* do multiple potentially equivalent objects need to be synthesized and then
- * compared using {@link #equals}.
+ * compared using {@link Object#equals}.
*
*
* @author Curtis Rueden
diff --git a/src/main/java/org/scijava/Initializable.java b/src/main/java/org/scijava/Initializable.java
index ce331d0df..56aa725ae 100644
--- a/src/main/java/org/scijava/Initializable.java
+++ b/src/main/java/org/scijava/Initializable.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Instantiable.java b/src/main/java/org/scijava/Instantiable.java
index 041ffa413..3edca09a4 100644
--- a/src/main/java/org/scijava/Instantiable.java
+++ b/src/main/java/org/scijava/Instantiable.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/InstantiableException.java b/src/main/java/org/scijava/InstantiableException.java
index 1af1cef0c..0ad031f7b 100644
--- a/src/main/java/org/scijava/InstantiableException.java
+++ b/src/main/java/org/scijava/InstantiableException.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/ItemIO.java b/src/main/java/org/scijava/ItemIO.java
index 96b7b49da..fa8b68684 100644
--- a/src/main/java/org/scijava/ItemIO.java
+++ b/src/main/java/org/scijava/ItemIO.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/ItemVisibility.java b/src/main/java/org/scijava/ItemVisibility.java
index db5e7db88..47897a359 100644
--- a/src/main/java/org/scijava/ItemVisibility.java
+++ b/src/main/java/org/scijava/ItemVisibility.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Locatable.java b/src/main/java/org/scijava/Locatable.java
index aff02fe28..a7da30625 100644
--- a/src/main/java/org/scijava/Locatable.java
+++ b/src/main/java/org/scijava/Locatable.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -33,7 +31,7 @@
import java.net.URL;
-import org.scijava.util.ClassUtils;
+import org.scijava.util.Types;
/**
* An object whose location is defined by a URL string.
@@ -44,7 +42,7 @@ public interface Locatable {
/** Gets the URL string defining the object's location. */
default String getLocation() {
- final URL location = ClassUtils.getLocation(getClass());
+ final URL location = Types.location(getClass());
return location == null ? null : location.toExternalForm();
}
diff --git a/src/main/java/org/scijava/MenuEntry.java b/src/main/java/org/scijava/MenuEntry.java
index f2fc2b8f1..4a2b33bb9 100644
--- a/src/main/java/org/scijava/MenuEntry.java
+++ b/src/main/java/org/scijava/MenuEntry.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/MenuPath.java b/src/main/java/org/scijava/MenuPath.java
index 1771a7e8b..533fd2461 100644
--- a/src/main/java/org/scijava/MenuPath.java
+++ b/src/main/java/org/scijava/MenuPath.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -61,7 +59,7 @@ public MenuPath(final Collection extends MenuEntry> menuEntries) {
/**
* Creates a menu path with entries parsed from the given string. Assumes
- * ">" as the separator (e.g., "File>New>Image").
+ * {@code >} as the separator (e.g., {@code File>New>Image}).
*
* @see #PATH_SEPARATOR
*/
diff --git a/src/main/java/org/scijava/Named.java b/src/main/java/org/scijava/Named.java
index 654fce4ab..9a381ee3a 100644
--- a/src/main/java/org/scijava/Named.java
+++ b/src/main/java/org/scijava/Named.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/NoSuchServiceException.java b/src/main/java/org/scijava/NoSuchServiceException.java
index b291f021d..d97d95647 100644
--- a/src/main/java/org/scijava/NoSuchServiceException.java
+++ b/src/main/java/org/scijava/NoSuchServiceException.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/NullContextException.java b/src/main/java/org/scijava/NullContextException.java
index ce05a776b..794926da9 100644
--- a/src/main/java/org/scijava/NullContextException.java
+++ b/src/main/java/org/scijava/NullContextException.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Optional.java b/src/main/java/org/scijava/Optional.java
index 1dddc3441..2ab55b3c9 100644
--- a/src/main/java/org/scijava/Optional.java
+++ b/src/main/java/org/scijava/Optional.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Prioritized.java b/src/main/java/org/scijava/Prioritized.java
index 416ae93f6..86bd0691a 100644
--- a/src/main/java/org/scijava/Prioritized.java
+++ b/src/main/java/org/scijava/Prioritized.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Priority.java b/src/main/java/org/scijava/Priority.java
index 019ae17bf..69da7767e 100644
--- a/src/main/java/org/scijava/Priority.java
+++ b/src/main/java/org/scijava/Priority.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -45,26 +43,45 @@ private Priority() {
// prevent instantiation of utility class
}
- /** Priority for items that must be sorted first. */
- public static final double FIRST_PRIORITY = Double.POSITIVE_INFINITY;
+ /**
+ * Priority for items that must be sorted first.
+ *
+ * Note that it is still possible to prioritize something earlier
+ * than this value (e.g., for testing purposes), although doing so strongly
+ * discouraged in production.
+ *
+ */
+ public static final double FIRST = +1e300;
+
+ /** Priority for items that very strongly prefer to be sorted early. */
+ public static final double EXTREMELY_HIGH = +1000000;
/** Priority for items that strongly prefer to be sorted early. */
- public static final double VERY_HIGH_PRIORITY = +10000;
+ public static final double VERY_HIGH = +10000;
/** Priority for items that prefer to be sorted earlier. */
- public static final double HIGH_PRIORITY = +100;
+ public static final double HIGH = +100;
/** Default priority for items. */
- public static final double NORMAL_PRIORITY = 0;
+ public static final double NORMAL = 0;
/** Priority for items that prefer to be sorted later. */
- public static final double LOW_PRIORITY = -100;
+ public static final double LOW = -100;
/** Priority for items that strongly prefer to be sorted late. */
- public static final double VERY_LOW_PRIORITY = -10000;
+ public static final double VERY_LOW = -10000;
- /** Priority for items that must be sorted last. */
- public static final double LAST_PRIORITY = Double.NEGATIVE_INFINITY;
+ /** Priority for items that very strongly prefer to be sorted late. */
+ public static final double EXTREMELY_LOW = -1000000;
+
+ /** Priority for items that must be sorted last.
+ *
+ * Note that it is still possible to prioritize something later
+ * than this value (e.g., for testing purposes), although doing so strongly
+ * discouraged in production.
+ *
+ */
+ public static final double LAST = -1e300;
/**
* Compares two {@link Prioritized} objects.
@@ -108,4 +125,33 @@ public static boolean inject(final Object o, final double priority) {
return true;
}
+ // -- Deprecated --
+
+ /** @deprecated Use {@link #FIRST} instead. */
+ @Deprecated
+ public static final double FIRST_PRIORITY = Double.POSITIVE_INFINITY;
+
+ /** @deprecated Use {@link #VERY_HIGH} instead. */
+ @Deprecated
+ public static final double VERY_HIGH_PRIORITY = +10000;
+
+ /** @deprecated Use {@link #HIGH} instead. */
+ @Deprecated
+ public static final double HIGH_PRIORITY = +100;
+
+ /** @deprecated Use {@link #NORMAL} instead. */
+ @Deprecated
+ public static final double NORMAL_PRIORITY = 0;
+
+ /** @deprecated Use {@link #LOW} instead. */
+ @Deprecated
+ public static final double LOW_PRIORITY = -100;
+
+ /** @deprecated Use {@link #VERY_LOW} instead. */
+ @Deprecated
+ public static final double VERY_LOW_PRIORITY = -10000;
+
+ /** @deprecated Use {@link #LAST} instead. */
+ @Deprecated
+ public static final double LAST_PRIORITY = Double.NEGATIVE_INFINITY;
}
diff --git a/src/main/java/org/scijava/SciJava.java b/src/main/java/org/scijava/SciJava.java
index 462b92f72..81375dcd6 100644
--- a/src/main/java/org/scijava/SciJava.java
+++ b/src/main/java/org/scijava/SciJava.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -44,7 +42,7 @@
*
* @author Curtis Rueden
*/
-@Plugin(type = Gateway.class)
+@Plugin(type = Gateway.class, name = "sj")
public class SciJava extends AbstractGateway {
// -- Constructors --
@@ -74,8 +72,9 @@ public SciJava(final boolean empty) {
* {@code new SciJava(LogService.class)}) yield the potentially confusing
* warning:
*
- *
Type safety: A generic array of Class extends Service> is
- * created for a varargs parameter
+ *
Type safety: A generic array of
+ * {@code Class extends Service>} is created for a varargs
+ * parameter
*
* To avoid this, we have opted to use raw types and suppress the relevant
* warning here instead.
@@ -111,12 +110,4 @@ public SciJava(final Collection> serviceClasses) {
public SciJava(final Context context) {
super(SciJavaApp.NAME, context);
}
-
- // -- Gateway methods --
-
- @Override
- public String getShortName() {
- return "sj";
- }
-
}
diff --git a/src/main/java/org/scijava/Typed.java b/src/main/java/org/scijava/Typed.java
index e76d0fc93..cfb55d690 100644
--- a/src/main/java/org/scijava/Typed.java
+++ b/src/main/java/org/scijava/Typed.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,13 +40,17 @@ public interface Typed {
/**
* Gets whether this object is compatible with the given data object.
*
- * By default, this method will return {@code true} always, since the type is
- * known to be compatible. But individual implementations may have other
- * requirements beyond class assignability.
+ * By default, this method will return {@code true} iff the data is assignable
+ * to the associated type given by {@link #getType()}. But individual
+ * implementations may have other requirements beyond class assignability.
*
*/
- default boolean supports(@SuppressWarnings("unused") T data) {
- return true;
+ default boolean supports(final T data) {
+ // NB: Even though the compiler will often guarantee that only data
+ // of type T is provided here, we still need the runtime check
+ // for cases where the exact type is not known to compiler --
+ // e.g., if the object was manufactured by reflection.
+ return getType().isInstance(data);
}
/** Gets the type associated with the object. */
diff --git a/src/main/java/org/scijava/UIDetails.java b/src/main/java/org/scijava/UIDetails.java
index 3545d6ce8..ccf12c261 100644
--- a/src/main/java/org/scijava/UIDetails.java
+++ b/src/main/java/org/scijava/UIDetails.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Validated.java b/src/main/java/org/scijava/Validated.java
index 7b006ffed..11835ba5c 100644
--- a/src/main/java/org/scijava/Validated.java
+++ b/src/main/java/org/scijava/Validated.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/ValidityProblem.java b/src/main/java/org/scijava/ValidityProblem.java
index ea8b3b121..782bcc642 100644
--- a/src/main/java/org/scijava/ValidityProblem.java
+++ b/src/main/java/org/scijava/ValidityProblem.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/Versioned.java b/src/main/java/org/scijava/Versioned.java
index 233510e35..6e746dad4 100644
--- a/src/main/java/org/scijava/Versioned.java
+++ b/src/main/java/org/scijava/Versioned.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/annotations/AbstractIndexWriter.java b/src/main/java/org/scijava/annotations/AbstractIndexWriter.java
index 0f8a06221..28fe310a5 100644
--- a/src/main/java/org/scijava/annotations/AbstractIndexWriter.java
+++ b/src/main/java/org/scijava/annotations/AbstractIndexWriter.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/annotations/AnnotationCombiner.java b/src/main/java/org/scijava/annotations/AnnotationCombiner.java
index a4f02935f..9b152b924 100644
--- a/src/main/java/org/scijava/annotations/AnnotationCombiner.java
+++ b/src/main/java/org/scijava/annotations/AnnotationCombiner.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 +40,7 @@
import java.util.HashSet;
import java.util.Set;
+import org.scijava.Context;
import org.scijava.util.Combiner;
import org.scijava.util.FileUtils;
@@ -64,7 +63,7 @@ public void combine(File outputDirectory) throws Exception {
}
final Set annotationFiles = getAnnotationFiles();
- final ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ final ClassLoader loader = Context.getClassLoader();
log("");
log("Writing annotations to " + outputDirectory.getAbsolutePath());
@@ -91,7 +90,7 @@ public Set getAnnotationFiles() throws IOException {
for (final String prefix : new String[] { PREFIX, LEGACY_PREFIX }) {
final Enumeration directories =
- Thread.currentThread().getContextClassLoader().getResources(prefix);
+ Context.getClassLoader().getResources(prefix);
while (directories.hasMoreElements()) {
final URL url = directories.nextElement();
for (final URL annotationIndexURL : FileUtils.listContents(url)) {
diff --git a/src/main/java/org/scijava/annotations/AnnotationProcessor.java b/src/main/java/org/scijava/annotations/AnnotationProcessor.java
index 0f96bab4f..e17d707a9 100644
--- a/src/main/java/org/scijava/annotations/AnnotationProcessor.java
+++ b/src/main/java/org/scijava/annotations/AnnotationProcessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 +37,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
+import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -51,7 +50,6 @@
import javax.annotation.processing.Filer;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
@@ -70,16 +68,38 @@
import org.scijava.annotations.AbstractIndexWriter.StreamFactory;
/**
- * The annotation processor for use with Java 6 and above.
+ * An annotation processor for indexing {@link Indexable} annotations at
+ * compile time. The design is inspired by
+ * Sezpoz, a compile-time
+ * indexing library by Jesse Glick.
+ *
+ * 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.
+ *
*
* @author Johannes Schindelin
*/
-@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes("*")
public class AnnotationProcessor extends AbstractProcessor {
private RoundEnvironment roundEnv;
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latestSupported();
+ }
+
@Override
public boolean process(final Set extends TypeElement> elements,
final RoundEnvironment env)
@@ -233,7 +253,7 @@ public InputStream openInput(final String annotationName)
return filer.getResource(StandardLocation.CLASS_OUTPUT, "",
Index.INDEX_PREFIX + annotationName).openInputStream();
}
- catch (final FileNotFoundException e) {
+ catch (final FileNotFoundException | NoSuchFileException e) {
return null;
}
}
diff --git a/src/main/java/org/scijava/annotations/ByteCodeAnalyzer.java b/src/main/java/org/scijava/annotations/ByteCodeAnalyzer.java
index 933ce8197..915d9148f 100644
--- a/src/main/java/org/scijava/annotations/ByteCodeAnalyzer.java
+++ b/src/main/java/org/scijava/annotations/ByteCodeAnalyzer.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/annotations/DirectoryIndexer.java b/src/main/java/org/scijava/annotations/DirectoryIndexer.java
index b292d9831..ac6be8fcd 100644
--- a/src/main/java/org/scijava/annotations/DirectoryIndexer.java
+++ b/src/main/java/org/scijava/annotations/DirectoryIndexer.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/annotations/EclipseHelper.java b/src/main/java/org/scijava/annotations/EclipseHelper.java
index 80cad4334..67f17ff7d 100644
--- a/src/main/java/org/scijava/annotations/EclipseHelper.java
+++ b/src/main/java/org/scijava/annotations/EclipseHelper.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 +40,7 @@
import java.util.jar.JarFile;
import java.util.jar.Manifest;
+import org.scijava.Context;
import org.scijava.util.FileUtils;
/**
@@ -296,7 +295,7 @@ else if (file.isDirectory()) {
*/
public static void main(final String... args) {
System.setProperty(FORCE_ANNOTATION_INDEX_PROPERTY, "true");
- updateAnnotationIndex(Thread.currentThread().getContextClassLoader());
+ updateAnnotationIndex(Context.getClassLoader());
}
}
diff --git a/src/main/java/org/scijava/annotations/Index.java b/src/main/java/org/scijava/annotations/Index.java
index 13d8e2d88..244b00d37 100644
--- a/src/main/java/org/scijava/annotations/Index.java
+++ b/src/main/java/org/scijava/annotations/Index.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -45,14 +43,15 @@
/**
* Makes the annotation indexes accessible.
*
- * You would call it like this:
- *
- * for (IndexItem item : Index.load(MyAnnotation.class)) {
- * // do something with item.annotation() and/or item.className()
- * }
- *
+ * You would call it like this:
*
*
+ *
+ * for (IndexItem<MyAnnotation> item : Index.load(MyAnnotation.class)) {
+ * // do something with item.annotation() and/or item.className()
+ * }
+ *
*
diff --git a/src/main/java/org/scijava/app/AppService.java b/src/main/java/org/scijava/app/AppService.java
index 884b0aea5..2d9c7e3de 100644
--- a/src/main/java/org/scijava/app/AppService.java
+++ b/src/main/java/org/scijava/app/AppService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/app/DefaultAppService.java b/src/main/java/org/scijava/app/DefaultAppService.java
index 52bb33ca4..9c8afc0c3 100644
--- a/src/main/java/org/scijava/app/DefaultAppService.java
+++ b/src/main/java/org/scijava/app/DefaultAppService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/app/DefaultStatusService.java b/src/main/java/org/scijava/app/DefaultStatusService.java
index 43a5e58b0..1b68a27ba 100644
--- a/src/main/java/org/scijava/app/DefaultStatusService.java
+++ b/src/main/java/org/scijava/app/DefaultStatusService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/app/SciJavaApp.java b/src/main/java/org/scijava/app/SciJavaApp.java
index ef758592f..b7604ca14 100644
--- a/src/main/java/org/scijava/app/SciJavaApp.java
+++ b/src/main/java/org/scijava/app/SciJavaApp.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -40,8 +38,7 @@
* @author Curtis Rueden
* @see AppService
*/
-@Plugin(type = App.class, name = SciJavaApp.NAME,
- priority = Priority.LOW_PRIORITY)
+@Plugin(type = App.class, name = SciJavaApp.NAME, priority = Priority.LOW)
public class SciJavaApp extends AbstractApp {
public static final String NAME = "SciJava";
diff --git a/src/main/java/org/scijava/app/StatusService.java b/src/main/java/org/scijava/app/StatusService.java
index 67cccf86d..70d0edf75 100644
--- a/src/main/java/org/scijava/app/StatusService.java
+++ b/src/main/java/org/scijava/app/StatusService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/app/event/StatusEvent.java b/src/main/java/org/scijava/app/event/StatusEvent.java
index d8a3005e2..653aefb8e 100644
--- a/src/main/java/org/scijava/app/event/StatusEvent.java
+++ b/src/main/java/org/scijava/app/event/StatusEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/cache/CacheService.java b/src/main/java/org/scijava/cache/CacheService.java
index c0ab8850b..ca5808918 100644
--- a/src/main/java/org/scijava/cache/CacheService.java
+++ b/src/main/java/org/scijava/cache/CacheService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/cache/DefaultCacheService.java b/src/main/java/org/scijava/cache/DefaultCacheService.java
index 87bcc909a..667ffdb30 100644
--- a/src/main/java/org/scijava/cache/DefaultCacheService.java
+++ b/src/main/java/org/scijava/cache/DefaultCacheService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,7 +40,7 @@
/**
* Trivial {@link CacheService} implementation. Wraps a {@link WeakHashMap}
*/
-@Plugin(type = Service.class, priority = Priority.VERY_LOW_PRIORITY)
+@Plugin(type = Service.class, priority = Priority.VERY_LOW)
public class DefaultCacheService extends AbstractService implements
CacheService
{
diff --git a/src/main/java/org/scijava/command/Command.java b/src/main/java/org/scijava/command/Command.java
index 19f5f1de4..17a37ebb1 100644
--- a/src/main/java/org/scijava/command/Command.java
+++ b/src/main/java/org/scijava/command/Command.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/command/CommandInfo.java b/src/main/java/org/scijava/command/CommandInfo.java
index 222237422..f25c213db 100644
--- a/src/main/java/org/scijava/command/CommandInfo.java
+++ b/src/main/java/org/scijava/command/CommandInfo.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -54,8 +52,10 @@
import org.scijava.plugin.Parameter;
import org.scijava.plugin.Plugin;
import org.scijava.plugin.PluginInfo;
+import org.scijava.service.Service;
import org.scijava.util.ClassUtils;
import org.scijava.util.StringMaker;
+import org.scijava.util.Types;
/**
* A collection of metadata about a particular {@link Command}.
@@ -299,9 +299,7 @@ public Class> loadDelegateClass() throws ClassNotFoundException {
return loadClass();
}
catch (final InstantiableException exc) {
- final ClassNotFoundException cnfe = new ClassNotFoundException();
- cnfe.initCause(exc);
- throw cnfe;
+ throw new ClassNotFoundException(null, exc);
}
}
@@ -463,7 +461,8 @@ private void checkFields(final Class> type) {
}
final String name = f.getName();
- if (inputMap.containsKey(name) || outputMap.containsKey(name)) {
+ if ((inputMap.containsKey(name) || outputMap.containsKey(name))
+ && !Service.class.isAssignableFrom(f.getType())) {
// NB: Shadowed parameters are bad because they are ambiguous.
final String error = "Invalid duplicate parameter: " + f;
problems.add(new ValidityProblem(error));
@@ -503,8 +502,7 @@ private void checkFields(final Class> type) {
private boolean isImmutable(final Class> type) {
// NB: All eight primitive types, as well as the boxed primitive
// wrapper classes, as well as strings, are immutable objects.
- return ClassUtils.isNumber(type) || ClassUtils.isText(type) ||
- ClassUtils.isBoolean(type);
+ return Types.isNumber(type) || Types.isText(type) || Types.isBoolean(type);
}
private Class> loadCommandClass() {
diff --git a/src/main/java/org/scijava/command/CommandModule.java b/src/main/java/org/scijava/command/CommandModule.java
index 6542022b6..226edc678 100644
--- a/src/main/java/org/scijava/command/CommandModule.java
+++ b/src/main/java/org/scijava/command/CommandModule.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/command/CommandModuleItem.java b/src/main/java/org/scijava/command/CommandModuleItem.java
index 10c0c7b78..079193311 100644
--- a/src/main/java/org/scijava/command/CommandModuleItem.java
+++ b/src/main/java/org/scijava/command/CommandModuleItem.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -45,7 +43,7 @@
import org.scijava.plugin.Attr;
import org.scijava.plugin.Parameter;
import org.scijava.util.ConversionUtils;
-import org.scijava.util.GenericUtils;
+import org.scijava.util.Types;
/**
* {@link ModuleItem} implementation describing an input or output of a command.
@@ -75,8 +73,7 @@ public Parameter getParameter() {
@Override
public Class getType() {
- final Class> type =
- GenericUtils.getFieldClasses(field, getDelegateClass()).get(0);
+ final Class> type = Types.raw(Types.fieldType(field, getDelegateClass()));
@SuppressWarnings("unchecked")
final Class typedType = (Class) type;
return typedType;
@@ -84,7 +81,7 @@ public Class getType() {
@Override
public Type getGenericType() {
- return GenericUtils.getFieldType(field, getDelegateClass());
+ return Types.fieldType(field, getDelegateClass());
}
@Override
@@ -253,7 +250,7 @@ private T tValue(final String value) {
private D tValue(final String value, final Class type) {
if (value == null || value.isEmpty()) return null;
- final Class saneType = ConversionUtils.getNonprimitiveType(type);
+ final Class saneType = Types.box(type);
return ConversionUtils.convert(value, saneType);
}
diff --git a/src/main/java/org/scijava/command/CommandService.java b/src/main/java/org/scijava/command/CommandService.java
index 7a4feb704..c1f78460c 100644
--- a/src/main/java/org/scijava/command/CommandService.java
+++ b/src/main/java/org/scijava/command/CommandService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 +50,7 @@
* A command is a particular type of plugin that is also a
* {@link Module}; i.e., it is {@link Runnable}, with typed inputs and outputs.
*
- * 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}.
*
diff --git a/src/main/java/org/scijava/command/ContextCommand.java b/src/main/java/org/scijava/command/ContextCommand.java
index 3ce3ec96d..c40649d53 100644
--- a/src/main/java/org/scijava/command/ContextCommand.java
+++ b/src/main/java/org/scijava/command/ContextCommand.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/command/DefaultCommandService.java b/src/main/java/org/scijava/command/DefaultCommandService.java
index 2e344365a..29756debb 100644
--- a/src/main/java/org/scijava/command/DefaultCommandService.java
+++ b/src/main/java/org/scijava/command/DefaultCommandService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -40,6 +38,7 @@
import org.scijava.event.EventHandler;
import org.scijava.event.EventService;
import org.scijava.log.LogService;
+import org.scijava.module.Module;
import org.scijava.module.ModuleService;
import org.scijava.plugin.AbstractPTService;
import org.scijava.plugin.Parameter;
@@ -181,20 +180,16 @@ public Future run(
public Future run(final CommandInfo info,
final boolean process, final Object... inputs)
{
- @SuppressWarnings({ "rawtypes", "unchecked" })
- final Future future =
- (Future) moduleService.run(info, process, inputs);
- return future;
+ final Future future = moduleService.run(info, process, inputs);
+ return validateFuture(future, info);
}
@Override
public Future run(final CommandInfo info,
final boolean process, final Map inputMap)
{
- @SuppressWarnings({ "rawtypes", "unchecked" })
- final Future future =
- (Future) moduleService.run(info, process, inputMap);
- return future;
+ final Future future = moduleService.run(info, process, inputMap);
+ return validateFuture(future, info);
}
// -- PTService methods --
@@ -347,4 +342,33 @@ private List> downcast(
return typedPlugins;
}
+ /**
+ * A HACK to avoid {@link ClassCastException} when calling run when the
+ * resultant module will not be a {@link CommandModule}. This is an API design
+ * flaw in CommandService currently, but for now we work around it rather than
+ * breaking backwards API compatibility.
+ */
+ private Future validateFuture(final Future future,
+ final CommandInfo info)
+ {
+ try {
+ final Class> commandClass = info.loadDelegateClass();
+ if (Module.class.isAssignableFrom(commandClass)) {
+ log.debug("The command '" + info.getIdentifier() +
+ "' extends Module directly. Due to a design flaw in the " +
+ "CommandService API, the result cannot be coerced to a " +
+ "Future, so null will be returned instead. " +
+ "If you need the resulting module, please instead call " +
+ "moduleService.run(commandService.getCommand(commandClass), ...).");
+ return null;
+ }
+ }
+ catch (final ClassNotFoundException exc) {
+ throw new IllegalStateException("Command class unavailable: " + //
+ info.getDelegateClassName(), exc);
+ }
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ final Future result = (Future) future;
+ return result;
+ }
}
diff --git a/src/main/java/org/scijava/command/DynamicCommand.java b/src/main/java/org/scijava/command/DynamicCommand.java
index aa81671c0..5cc3ab4b1 100644
--- a/src/main/java/org/scijava/command/DynamicCommand.java
+++ b/src/main/java/org/scijava/command/DynamicCommand.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -38,7 +36,9 @@
import org.scijava.Contextual;
import org.scijava.NullContextException;
import org.scijava.module.DefaultMutableModule;
+import org.scijava.module.ModuleService;
import org.scijava.plugin.Parameter;
+import org.scijava.plugin.PluginService;
import org.scijava.util.ClassUtils;
/**
@@ -59,6 +59,12 @@ public abstract class DynamicCommand extends DefaultMutableModule implements
@Parameter
private CommandService commandService;
+ @Parameter
+ protected PluginService pluginService;
+
+ @Parameter
+ protected ModuleService moduleService;
+
private DynamicCommandInfo info;
/** Reason for cancelation, or null if not canceled. */
@@ -70,7 +76,8 @@ public abstract class DynamicCommand extends DefaultMutableModule implements
public DynamicCommandInfo getInfo() {
if (info == null) {
// NB: Create dynamic metadata lazily.
- final CommandInfo commandInfo = commandService.getCommand(getClass());
+ CommandInfo commandInfo = commandService.getCommand(getClass());
+ if (commandInfo == null) commandInfo = new CommandInfo(getClass());
info = new DynamicCommandInfo(commandInfo, getClass());
}
return info;
@@ -86,7 +93,7 @@ public Object getInput(final String name) {
@Override
public Object getOutput(final String name) {
final Field field = getInfo().getOutputField(name);
- if (field == null) return super.getInput(name);
+ if (field == null) return super.getOutput(name);
return ClassUtils.getValue(field, this);
}
@@ -139,4 +146,19 @@ public String getCancelReason() {
return cancelReason;
}
+ // HACK: For OptionsPlugin.
+ public void uncancel() {
+ cancelReason = null;
+ }
+
+ // -- Internal methods --
+
+ /**
+ * Persists current input values. Use e.g. for {@link InteractiveCommand}s
+ * that want to persist values as they change, since interactive commands do
+ * not complete the module execution lifecycle normally.
+ */
+ protected void saveInputs() {
+ moduleService.saveInputs(this);
+ }
}
diff --git a/src/main/java/org/scijava/command/DynamicCommandInfo.java b/src/main/java/org/scijava/command/DynamicCommandInfo.java
index b47e64d91..06c4a16e7 100644
--- a/src/main/java/org/scijava/command/DynamicCommandInfo.java
+++ b/src/main/java/org/scijava/command/DynamicCommandInfo.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -50,9 +48,9 @@
* Helper class for maintaining a {@link DynamicCommand}'s associated
* {@link ModuleInfo}.
*
- * The {@link CommandService} has a plain {@link CommandInfo} object in its
- * index, populated from the {@link DynamicCommand}'s @{@link Plugin}
- * annotation. So this class adapts that object, delegating to it for the
+ * This class wraps a plain {@link CommandInfo} object (e.g. from the
+ * {@link CommandService}'s index, present due to an @{@link Plugin} annotation
+ * on the {@link DynamicCommand} class), delegating to it for the
* {@link UIDetails} methods. The plain {@link CommandInfo} cannot be used
* as-is, however, because we need to override the {@link ModuleInfo} methods as
* well as provide metadata manipulation functionality such as
diff --git a/src/main/java/org/scijava/command/Inputs.java b/src/main/java/org/scijava/command/Inputs.java
new file mode 100644
index 000000000..b2f5e4217
--- /dev/null
+++ b/src/main/java/org/scijava/command/Inputs.java
@@ -0,0 +1,102 @@
+/*
+ * #%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 java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import org.scijava.Context;
+import org.scijava.module.process.PreprocessorPlugin;
+
+/**
+ * A way to build a dynamic set of inputs, whose values are then harvested by
+ * the preprocessing framework.
+ *
+ * The {@link #run()} method of this command does nothing. If you want something
+ * custom to happen during execution, use a normal {@link Command} instead:
+ * either implement {@link Command directly}, or extend {@link ContextCommand}
+ * or {@link DynamicCommand}.
+ *
+ *
+ * @author Curtis Rueden
+ */
+public final class Inputs extends DynamicCommand {
+
+ public Inputs(final Context context) {
+ context.inject(this);
+ }
+
+ public Map harvest() {
+ try {
+ final List pre = //
+ pluginService.createInstancesOfType(PreprocessorPlugin.class);
+ return moduleService.run(this, pre, null).get().getInputs();
+ }
+ catch (final InterruptedException | ExecutionException exc) {
+ throw new RuntimeException(exc);
+ }
+ }
+}
diff --git a/src/main/java/org/scijava/command/Interactive.java b/src/main/java/org/scijava/command/Interactive.java
index bb41ca906..9714f4a6e 100644
--- a/src/main/java/org/scijava/command/Interactive.java
+++ b/src/main/java/org/scijava/command/Interactive.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/command/InteractiveCommand.java b/src/main/java/org/scijava/command/InteractiveCommand.java
index 920ad4047..b45e3f21f 100644
--- a/src/main/java/org/scijava/command/InteractiveCommand.java
+++ b/src/main/java/org/scijava/command/InteractiveCommand.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -95,6 +93,7 @@ public InteractiveCommand(final String... listenerNames) {
public void preview() {
// NB: Interactive commands call run upon any parameter change.
run();
+ saveInputs();
}
@Override
diff --git a/src/main/java/org/scijava/command/ModuleCommand.java b/src/main/java/org/scijava/command/ModuleCommand.java
index 14bdb641a..c09ad922c 100644
--- a/src/main/java/org/scijava/command/ModuleCommand.java
+++ b/src/main/java/org/scijava/command/ModuleCommand.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/command/Previewable.java b/src/main/java/org/scijava/command/Previewable.java
index 6d062edf4..4fbeaebb9 100644
--- a/src/main/java/org/scijava/command/Previewable.java
+++ b/src/main/java/org/scijava/command/Previewable.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/command/UnimplementedCommand.java b/src/main/java/org/scijava/command/UnimplementedCommand.java
index 8bc9448f5..f1103751a 100644
--- a/src/main/java/org/scijava/command/UnimplementedCommand.java
+++ b/src/main/java/org/scijava/command/UnimplementedCommand.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/command/console/RunArgument.java b/src/main/java/org/scijava/command/console/RunArgument.java
index c2a8ce8bb..b2529d279 100644
--- a/src/main/java/org/scijava/command/console/RunArgument.java
+++ b/src/main/java/org/scijava/command/console/RunArgument.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/command/run/CommandCodeRunner.java b/src/main/java/org/scijava/command/run/CommandCodeRunner.java
index f0ecfcb9b..bc8550275 100644
--- a/src/main/java/org/scijava/command/run/CommandCodeRunner.java
+++ b/src/main/java/org/scijava/command/run/CommandCodeRunner.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/console/AbstractConsoleArgument.java b/src/main/java/org/scijava/console/AbstractConsoleArgument.java
index 4711e5d18..76040544d 100644
--- a/src/main/java/org/scijava/console/AbstractConsoleArgument.java
+++ b/src/main/java/org/scijava/console/AbstractConsoleArgument.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/console/ConsoleArgument.java b/src/main/java/org/scijava/console/ConsoleArgument.java
index 49bd88590..44d058211 100644
--- a/src/main/java/org/scijava/console/ConsoleArgument.java
+++ b/src/main/java/org/scijava/console/ConsoleArgument.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/console/ConsoleService.java b/src/main/java/org/scijava/console/ConsoleService.java
index 7e9a9be58..3003892a4 100644
--- a/src/main/java/org/scijava/console/ConsoleService.java
+++ b/src/main/java/org/scijava/console/ConsoleService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -48,6 +46,7 @@
*/
public interface ConsoleService extends
HandlerService, ConsoleArgument>, SciJavaService
+ // TODO: SJC3: Extend Listenable
{
/** Handles arguments from an external source such as the command line. */
diff --git a/src/main/java/org/scijava/console/ConsoleUtils.java b/src/main/java/org/scijava/console/ConsoleUtils.java
index 47c712b62..2f62f0c14 100644
--- a/src/main/java/org/scijava/console/ConsoleUtils.java
+++ b/src/main/java/org/scijava/console/ConsoleUtils.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/console/DefaultConsoleService.java b/src/main/java/org/scijava/console/DefaultConsoleService.java
index 99b9867b0..b3a1ef64b 100644
--- a/src/main/java/org/scijava/console/DefaultConsoleService.java
+++ b/src/main/java/org/scijava/console/DefaultConsoleService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -36,6 +34,7 @@
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
import org.scijava.Context;
import org.scijava.console.OutputEvent.Source;
@@ -68,9 +67,7 @@ public class DefaultConsoleService extends
private OutputStreamReporter out, err;
/** List of listeners for {@code stdout} and {@code stderr} output. */
- private ArrayList listeners;
-
- private OutputListener[] cachedListeners;
+ private List listeners;
// -- ConsoleService methods --
@@ -115,26 +112,19 @@ public void processArgs(final String... args) {
@Override
public void addOutputListener(final OutputListener l) {
if (listeners == null) initListeners();
- synchronized (listeners) {
- listeners.add(l);
- cacheListeners();
- }
+ listeners.add(l);
}
@Override
public void removeOutputListener(final OutputListener l) {
if (listeners == null) initListeners();
- synchronized (listeners) {
- listeners.remove(l);
- cacheListeners();
- }
+ listeners.remove(l);
}
@Override
public void notifyListeners(final OutputEvent event) {
if (listeners == null) initListeners();
- final OutputListener[] toNotify = cachedListeners;
- for (final OutputListener l : toNotify)
+ for (final OutputListener l : listeners)
l.outputOccurred(event);
}
@@ -162,16 +152,11 @@ private synchronized void initListeners() {
err = new OutputStreamReporter(Source.STDERR);
syserr.getParent().addOutputStream(err);
- listeners = new ArrayList<>();
- cachedListeners = listeners.toArray(new OutputListener[0]);
+ listeners = new CopyOnWriteArrayList<>();
}
// -- Helper methods --
- private void cacheListeners() {
- cachedListeners = listeners.toArray(new OutputListener[listeners.size()]);
- }
-
private MultiPrintStream multiPrintStream(final PrintStream ps) {
if (ps instanceof MultiPrintStream) return (MultiPrintStream) ps;
return new MultiPrintStream(ps);
diff --git a/src/main/java/org/scijava/console/MultiOutputStream.java b/src/main/java/org/scijava/console/MultiOutputStream.java
index 855947bdf..a8d8238fd 100644
--- a/src/main/java/org/scijava/console/MultiOutputStream.java
+++ b/src/main/java/org/scijava/console/MultiOutputStream.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,6 +32,8 @@
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* A {@code MultiOutputStream} is a collection of constituent
@@ -48,9 +48,7 @@
*/
public class MultiOutputStream extends OutputStream {
- private final ArrayList streams;
-
- private OutputStream[] cachedStreams;
+ private final List streams;
/**
* Forwards output to a list of output streams.
@@ -58,37 +56,26 @@ public class MultiOutputStream extends OutputStream {
* @param os Output streams which will receive this stream's output.
*/
public MultiOutputStream(final OutputStream... os) {
- streams = new ArrayList<>(os.length);
- for (int i = 0; i < os.length; i++) {
- streams.add(os[i]);
- }
- cacheStreams();
+ streams = new CopyOnWriteArrayList<>(os);
}
// -- MultiOutputStream methods --
/** Adds an output stream to those receiving this stream's output. */
public void addOutputStream(final OutputStream os) {
- synchronized (streams) {
- streams.add(os);
- cacheStreams();
- }
+ streams.add(os);
}
/** Removes an output stream from those receiving this stream's output. */
public void removeOutputStream(final OutputStream os) {
- synchronized (streams) {
- streams.remove(os);
- cacheStreams();
- }
+ streams.remove(os);
}
// -- OutputStream methods --
@Override
public void write(final int b) throws IOException {
- final OutputStream[] toWrite = cachedStreams;
- for (final OutputStream stream : toWrite)
+ for (final OutputStream stream : streams)
stream.write(b);
}
@@ -96,8 +83,7 @@ public void write(final int b) throws IOException {
public void write(final byte[] buf, final int off, final int len)
throws IOException
{
- final OutputStream[] toWrite = cachedStreams;
- for (final OutputStream stream : toWrite)
+ for (final OutputStream stream : streams)
stream.write(buf, off, len);
}
@@ -105,8 +91,7 @@ public void write(final byte[] buf, final int off, final int len)
@Override
public void close() throws IOException {
- final OutputStream[] toClose = cachedStreams;
- for (final OutputStream stream : toClose)
+ for (final OutputStream stream : streams)
stream.close();
}
@@ -114,15 +99,8 @@ public void close() throws IOException {
@Override
public void flush() throws IOException {
- final OutputStream[] toFlush = cachedStreams;
- for (final OutputStream stream : toFlush)
+ for (final OutputStream stream : streams)
stream.flush();
}
- // -- Helper methods --
-
- private void cacheStreams() {
- cachedStreams = streams.toArray(new OutputStream[streams.size()]);
- }
-
}
diff --git a/src/main/java/org/scijava/console/MultiPrintStream.java b/src/main/java/org/scijava/console/MultiPrintStream.java
index 72fae4aea..c27df7e9b 100644
--- a/src/main/java/org/scijava/console/MultiPrintStream.java
+++ b/src/main/java/org/scijava/console/MultiPrintStream.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/console/OutputEvent.java b/src/main/java/org/scijava/console/OutputEvent.java
index d3bc2c5e7..c408bcaa3 100644
--- a/src/main/java/org/scijava/console/OutputEvent.java
+++ b/src/main/java/org/scijava/console/OutputEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/console/OutputListener.java b/src/main/java/org/scijava/console/OutputListener.java
index 1641542f0..b1543d339 100644
--- a/src/main/java/org/scijava/console/OutputListener.java
+++ b/src/main/java/org/scijava/console/OutputListener.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/console/SystemPropertyArgument.java b/src/main/java/org/scijava/console/SystemPropertyArgument.java
index ef812d1ef..33ba2771f 100644
--- a/src/main/java/org/scijava/console/SystemPropertyArgument.java
+++ b/src/main/java/org/scijava/console/SystemPropertyArgument.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -46,7 +44,7 @@
@Plugin(type = ConsoleArgument.class)
public class SystemPropertyArgument extends AbstractConsoleArgument {
- private static final String SYS_PROP_REGEX = "-D([\\w\\._-]+)(=(.*))?";
+ private static final String SYS_PROP_REGEX = "-D([^=]+)(=(.*))?";
private static final Pattern SYS_PROP_PAT = Pattern.compile(SYS_PROP_REGEX);
// -- Constructor --
diff --git a/src/main/java/org/scijava/convert/AbstractConvertService.java b/src/main/java/org/scijava/convert/AbstractConvertService.java
index 3938415f3..c3a56ae47 100644
--- a/src/main/java/org/scijava/convert/AbstractConvertService.java
+++ b/src/main/java/org/scijava/convert/AbstractConvertService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -31,145 +29,16 @@
package org.scijava.convert;
-import java.lang.reflect.Type;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
import org.scijava.plugin.AbstractHandlerService;
-import org.scijava.util.ConversionUtils;
/**
- * Abstract superclass for {@link ConvertService} implementations. Sets this
- * service as the active delegate service in {@link ConversionUtils}.
+ * Abstract superclass for {@link ConvertService} implementations.
*
* @author Mark Hiner
*/
-public abstract class AbstractConvertService extends AbstractHandlerService>
- implements ConvertService {
-
- // -- ConversionService methods --
- @SuppressWarnings({ "unchecked", "rawtypes" })
- @Override
- public Class> getPluginType() {
- return (Class) Converter.class;
- }
-
- @Override
- public Class getType() {
- return ConversionRequest.class;
- }
-
- @Override
- public Converter, ?> getHandler(final Object src, final Class> dest) {
- return getHandler(new ConversionRequest(src, dest));
- }
-
- @Override
- public Converter, ?> getHandler(final Class> src, final Class> dest) {
- return getHandler(new ConversionRequest(src, dest));
- }
-
- @Override
- public Converter, ?> getHandler(final Object src, final Type dest) {
- return getHandler(new ConversionRequest(src, dest));
- }
-
- @Override
- public Converter, ?> getHandler(final Class> src, final Type dest) {
- return getHandler(new ConversionRequest(src, dest));
- }
-
- @Override
- public boolean supports(final Object src, final Class> dest) {
- return supports(new ConversionRequest(src, dest));
- }
-
- @Override
- public boolean supports(final Class> src, final Class> dest) {
- return supports(new ConversionRequest(src, dest));
- }
-
- @Override
- public boolean supports(final Object src, final Type dest) {
- return supports(new ConversionRequest(src, dest));
- }
-
- @Override
- public boolean supports(final Class> src, final Type dest) {
- return supports(new ConversionRequest(src, dest));
- }
-
- @Override
- public Collection
*/
- public static final double PRIORITY = 10 * Priority.VERY_HIGH_PRIORITY;
+ public static final double PRIORITY = 10 * Priority.VERY_HIGH;
@Parameter
private LogService log;
@@ -141,6 +138,11 @@ public List> subscribe(final Object o) {
return subscribers;
}
+ @Override
+ public void subscribe(final EventSubscriber> subscriber) {
+ eventBus.subscribe(subscriber.getEventClass(), subscriber);
+ }
+
@Override
public void unsubscribe(final Collection> subscribers) {
for (final EventSubscriber> subscriber : subscribers) {
@@ -264,9 +266,10 @@ private synchronized void keepIt(final Object o, final ProxySubscriber> subscr
/**
* Helper class used by {@link #subscribe(Object)}.
*
- * Recapitulates some logic from {@link BaseProxySubscriber}, because that
- * class implements {@link org.bushe.swing.event.EventSubscriber} as a raw
- * type, which is incompatible with this class implementing SciJava's
+ * Recapitulates some logic from
+ * {@code org.scijava.event.bushe.BaseProxySubscriber}, because that class
+ * implements {@link org.scijava.event.bushe.EventSubscriber} as a raw type,
+ * which is incompatible with this class implementing SciJava's
* {@link EventSubscriber} as a typed interface; it becomes impossible to
* implement both {@code onEvent(Object)} and {@code onEvent(E)}.
*
diff --git a/src/main/java/org/scijava/event/EventDetails.java b/src/main/java/org/scijava/event/EventDetails.java
index 3336f4da9..5e9344b24 100644
--- a/src/main/java/org/scijava/event/EventDetails.java
+++ b/src/main/java/org/scijava/event/EventDetails.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/EventHandler.java b/src/main/java/org/scijava/event/EventHandler.java
index fc549034e..fa59a2a50 100644
--- a/src/main/java/org/scijava/event/EventHandler.java
+++ b/src/main/java/org/scijava/event/EventHandler.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,15 +40,15 @@
* handling methods and annotating each with @{@link EventHandler}.
*
* Note to developers: This annotation serves exactly the same purpose as
- * EventBus's {@link org.bushe.swing.event.annotation.EventSubscriber}
- * annotation, recapitulating a subset of the same functionality. We do this to
- * avoid third party code depending directly on EventBus. That is, we do not
- * wish to require SciJava developers to {@code import org.bushe.swing.event.*}
- * or similar. In this way, EventBus is isolated as only a transitive dependency
- * of downstream code, rather than a direct dependency. Unfortunately, because
- * Java annotation interfaces cannot utilize inheritance, we have to
- * recapitulate the functionality rather than extend it (as we are able to do
- * with {@link EventSubscriber}).
+ * EventBus's {@code org.scijava.event.bushe.EventSubscriber} annotation,
+ * recapitulating a subset of the same functionality. We do this to avoid third
+ * party code depending directly on EventBus. That is, we do not wish to require
+ * SciJava developers to {@code import org.scijava.event.bushe.*} or similar. In
+ * this way, EventBus is isolated as only a transitive dependency of downstream
+ * code, rather than a direct dependency. Unfortunately, because Java annotation
+ * interfaces cannot utilize inheritance, we have to recapitulate the
+ * functionality rather than extend it (as we are able to do with
+ * {@link EventSubscriber}).
*
*
* @author Curtis Rueden
diff --git a/src/main/java/org/scijava/event/EventHistory.java b/src/main/java/org/scijava/event/EventHistory.java
index 706ac59f3..7caaaf022 100644
--- a/src/main/java/org/scijava/event/EventHistory.java
+++ b/src/main/java/org/scijava/event/EventHistory.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/EventHistoryListener.java b/src/main/java/org/scijava/event/EventHistoryListener.java
index a5426bb76..30a305c83 100644
--- a/src/main/java/org/scijava/event/EventHistoryListener.java
+++ b/src/main/java/org/scijava/event/EventHistoryListener.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/EventService.java b/src/main/java/org/scijava/event/EventService.java
index efe3b1179..1b0907f7a 100644
--- a/src/main/java/org/scijava/event/EventService.java
+++ b/src/main/java/org/scijava/event/EventService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -127,6 +125,24 @@ public interface EventService extends SciJavaService {
*/
List> subscribe(Object o);
+ /**
+ * Subscribes the given {@link EventSubscriber} to its associated event class.
+ * Its {@link EventSubscriber#onEvent} method will be called whenever an event
+ * of the matching type is published.
+ *
+ * Important note: The event service does not keep a
+ * strong reference to the subscriber! If you use this method, you are also
+ * responsible for keeping a reference to the subscriber, or else it is likely
+ * to be garbage collected, and thus no longer respond to events as intended.
+ * One simple way to force a strong reference to exist is to add it to
+ * SciJava's {@link org.scijava.object.ObjectService} via
+ * {@link org.scijava.object.ObjectService#addObject}.
+ *
+ *
+ * @param subscriber the event subscriber to register
+ */
+ void subscribe(EventSubscriber> subscriber);
+
/**
* Removes all the given subscribers; they will no longer be notified when
* events are published.
diff --git a/src/main/java/org/scijava/event/EventSubscriber.java b/src/main/java/org/scijava/event/EventSubscriber.java
index 79c328cf6..9dcd25d94 100644
--- a/src/main/java/org/scijava/event/EventSubscriber.java
+++ b/src/main/java/org/scijava/event/EventSubscriber.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -46,7 +44,7 @@
* @param Type of event for which to listen
*/
public interface EventSubscriber extends
- org.bushe.swing.event.EventSubscriber
+ org.scijava.event.bushe.EventSubscriber
{
@Override
diff --git a/src/main/java/org/scijava/event/SciJavaEvent.java b/src/main/java/org/scijava/event/SciJavaEvent.java
index e644763e8..4e51d775f 100644
--- a/src/main/java/org/scijava/event/SciJavaEvent.java
+++ b/src/main/java/org/scijava/event/SciJavaEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -32,6 +30,7 @@
package org.scijava.event;
import org.scijava.AbstractContextual;
+import org.scijava.util.DebugUtils;
/**
* Base class for all SciJava events.
@@ -82,6 +81,14 @@ public StackTraceElement[] getStackTrace() {
return stackTrace;
}
+ /**
+ * Gets a stack trace for the calling thread when the event was published.
+ * This method is useful for debugging what triggered an event.
+ */
+ public String dumpStack() {
+ return DebugUtils.getStackDump(getCallingThread(), getStackTrace());
+ }
+
// Object methods --
@Override
diff --git a/src/main/java/org/scijava/event/bushe/AbstractProxySubscriber.java b/src/main/java/org/scijava/event/bushe/AbstractProxySubscriber.java
new file mode 100644
index 000000000..bd745a92c
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/AbstractProxySubscriber.java
@@ -0,0 +1,173 @@
+package org.scijava.event.bushe;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Common base class for EventService Proxies.
+ *
+ * Implementing Prioritized even when Priority is not used is always OK. The default
+ * value of 0 retains the FIFO order.
+ */
+public abstract class AbstractProxySubscriber implements ProxySubscriber, Prioritized {
+ private Object proxiedSubscriber;
+ private Method subscriptionMethod;
+ private ReferenceStrength referenceStrength;
+ private EventService eventService;
+ private int priority;
+ protected boolean veto;
+
+ protected AbstractProxySubscriber(Object proxiedSubscriber, Method subscriptionMethod,
+ ReferenceStrength referenceStrength, EventService es, boolean veto) {
+ this(proxiedSubscriber, subscriptionMethod, referenceStrength, 0, es, veto);
+ }
+
+ protected AbstractProxySubscriber(Object proxiedSubscriber, Method subscriptionMethod,
+ ReferenceStrength referenceStrength, int priority, EventService es, boolean veto) {
+ this.referenceStrength = referenceStrength;
+ this.priority = priority;
+ eventService = es;
+ this.veto = veto;
+ if (proxiedSubscriber == null) {
+ throw new IllegalArgumentException("The realSubscriber cannot be null when constructing a proxy subscriber.");
+ }
+ if (subscriptionMethod == null) {
+ throw new IllegalArgumentException("The subscriptionMethod cannot be null when constructing a proxy subscriber.");
+ }
+ Class> returnType = subscriptionMethod.getReturnType();
+ if (veto && returnType != Boolean.TYPE) {
+ throw new IllegalArgumentException("The subscriptionMethod must have the two parameters, the first one must be a String and the second a non-primitive (Object or derivative).");
+ }
+ if (ReferenceStrength.WEAK.equals(referenceStrength)) {
+ this.proxiedSubscriber = new WeakReference(proxiedSubscriber);
+ } else {
+ this.proxiedSubscriber = proxiedSubscriber;
+ }
+ this.subscriptionMethod = subscriptionMethod;
+ }
+
+ /** @return the object this proxy is subscribed on behalf of */
+ public Object getProxiedSubscriber() {
+ if (proxiedSubscriber instanceof WeakReference) {
+ return ((WeakReference)proxiedSubscriber).get();
+ }
+ return proxiedSubscriber;
+ }
+
+ /** @return the subscriptionMethod passed in the constructor */
+ public Method getSubscriptionMethod() {
+ return subscriptionMethod;
+ }
+
+ /** @return the EventService passed in the constructor */
+ public EventService getEventService() {
+ return eventService;
+ }
+
+ /** @return the ReferenceStrength passed in the constructor */
+ public ReferenceStrength getReferenceStrength() {
+ return referenceStrength;
+ }
+
+ /**
+ * @return the priority, no effect if priority is 0 (the default value)
+ */
+ public int getPriority() {
+ return priority;
+ }
+
+ /**
+ * Called by EventServices to inform the proxy that it is unsubscribed.
+ * The ProxySubscriber should perform any necessary cleanup.
+ *
+ * Overriding classes must call super.proxyUnsubscribed() or risk
+ * things not being cleanup up properly.
+ */
+ public void proxyUnsubscribed() {
+ proxiedSubscriber = null;
+ }
+
+ @Override
+ public final int hashCode() {
+ throw new RuntimeException("Proxy subscribers are not allowed in Hash " +
+ "Maps, since the underlying values use Weak References that" +
+ "may disappear, the calculations may not be the same in" +
+ "successive calls as required by hashCode.");
+ }
+
+ protected boolean retryReflectiveCallUsingAccessibleObject(Object[] args, Method subscriptionMethod, Object obj,
+ IllegalAccessException e, String message) {
+ boolean accessibleTriedAndFailed = false;
+ if (subscriptionMethod != null) {
+ AccessibleObject[] accessibleMethod = {subscriptionMethod};
+ try {
+ AccessibleObject.setAccessible(accessibleMethod, true);
+ } catch (SecurityException ex) {
+ // SecurityManager (Java 8 and earlier) denied setAccessible
+ accessibleTriedAndFailed = true;
+ } catch (RuntimeException ex) {
+ // InaccessibleObjectException (Java 9+) or similar denied setAccessible
+ accessibleTriedAndFailed = true;
+ }
+ if (!accessibleTriedAndFailed) {
+ try {
+ Object returnValue = subscriptionMethod.invoke(obj, args);
+ return Boolean.valueOf(returnValue+"");
+ } catch (InvocationTargetException e1) {
+ throw new RuntimeException(message, e);
+ } catch (IllegalAccessException e1) {
+ throw new RuntimeException(message, e);
+ }
+ }
+ }
+ if (accessibleTriedAndFailed) {
+ message = message + ". An attempt was made to make the method accessible, but access was denied.";
+ }
+ throw new RuntimeException(message, e);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof AbstractProxySubscriber) {
+ AbstractProxySubscriber bps = (AbstractProxySubscriber) obj;
+ if (referenceStrength != bps.referenceStrength) {
+ return false;
+ }
+ if (subscriptionMethod != bps.subscriptionMethod) {
+ return false;
+ }
+ if (ReferenceStrength.WEAK == referenceStrength) {
+ if (((WeakReference)proxiedSubscriber).get() != ((WeakReference)bps.proxiedSubscriber).get()) {
+ return false;
+ }
+ } else {
+ if (proxiedSubscriber != bps.proxiedSubscriber) {
+ return false;
+ }
+ }
+ if (veto != bps.veto) {
+ return false;
+ }
+ if (eventService != bps.eventService) {
+ return false;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "AbstractProxySubscriber{" +
+ "realSubscriber=" + (proxiedSubscriber instanceof WeakReference?
+ ((WeakReference)proxiedSubscriber).get():proxiedSubscriber) +
+ ", subscriptionMethod=" + subscriptionMethod +
+ ", veto=" + veto +
+ ", referenceStrength=" + referenceStrength +
+ ", eventService=" + eventService +
+ '}';
+ }
+}
diff --git a/src/main/java/org/scijava/event/bushe/EventService.java b/src/main/java/org/scijava/event/bushe/EventService.java
new file mode 100644
index 000000000..c2d41e9bb
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/EventService.java
@@ -0,0 +1,988 @@
+/**
+ * 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.util.List;
+import java.util.regex.Pattern;
+import java.lang.reflect.Type;
+
+/**
+ * The core interface. An EventService provides publish/subscribe services to a single JVM using Class-based and
+ * String-based (i.e. "topic") publications and subscriptions.
+ *
+ * In class-based pub/sub, {@link EventSubscriber}s subscribe to a type on an {@link EventService}, such
+ * as the {@link org.scijava.event.bushe.EventBus}, by providing a class, interface or generic type. The EventService
+ * notifies subscribers when objects are published on the EventService with a matching type. Full class semantics are
+ * respected. That is, if a subscriber subscribes to a class, the subscriber is notified if an object of
+ * that class is publish or if an object of a subclass of that class is published. Likewise if a subscriber subscribes
+ * to an interface, it will be notified if any object that implements that interface is published. Subscribers can
+ * subscribe "exactly" using {@link #subscribeExactly(Class, EventSubscriber)} so that they are notified only if an
+ * object of the exact class is published (and will not be notified if subclasses are published, since this would not
+ * be "exact")
+ *
+ *
+ * In topic-based pub/sub, an object "payload" is published on a topic name (String). {@link EventTopicSubscriber}s subscribe
+ * to either the exact name of the topic or they may subscribe using a Regular Expression that is used to match topic
+ * names.
+ *
+ * A single subscriber cannot subscribe more than once to an event or topic name. EventService implementations should
+ * handle double-subscription requests by returning false on subscribe(). A single EventSubscriber can subscribe to more
+ * than one event class, and a single EventTopicSubscriber can subscribe to more than one topic name or pattern. A
+ * single object may implement both EventSubscriber and EventTopicSubscriber interfaces. Subscribers are guaranteed to
+ * only be called for the classes and/or topic names they subscribe to. If a subscriber subscribes to a topic and to a
+ * regular expression that matches the topic name, this is considered two different subscriptions and the subscriber
+ * will be called twice for the publication on the topic. Similarly, if a subscriber subscribes to a class and its
+ * subclasses using subscribe() and again to a class of the same type using subscribeExactly(), this is considered two
+ * different subscriptions and the subscriber will be called twice for the publication for a single event of the exact
+ * type.
+ *
+ *
+ * By default the EventService only holds WeakReferences to subscribers. If a subscriber has no references to it, then
+ * it can be garbage collected. This avoids memory leaks in exchange for the risk of accidentally adding a listener and
+ * have it disappear unexpectedly. If you want to subscribe a subscriber that will have no other reference to it, then
+ * use one of the subscribeStrongly() methods, which will prevent garbage collection.
+ *
+ *
+ * Unless garbage collected, EventSubscribers will remain subscribed until they are passed to one of the unsubscribe()
+ * methods with the event class or topic name to which there are subscribed.
+ *
+ *
+ * Subscribers are called in the order in which they are subscribed by default (FIFO), unless subscribers implement
+ * {@link Prioritized}. Those subscribers that implement Prioritized and return a negative priority are moved to the
+ * front of the list (the more negative, the more to the front). Those subscribers that implement Prioritized and return
+ * a positive priority are moved to the end of the list (the more positive, the more to the back). The FIFO guarantee
+ * is only valid for the same subscribe() call. That is, the order of two subscribers, one to List.class and the other
+ * to ArrayList.class is not guaranteed to be in the order of subscription when an ArrayList is published. The same is
+ * true for topic subscribers when using RegEx expressions - when "Foo" is published, the order of subscribers that are
+ * subscribed to "Foo", "Fo*" and "F*" are not guaranteed, though the second "Fo*" subscriber will never be called
+ * before the first "Fo*" subscriber (ditto List and ArrayList). Prioritized subscribers are always guaranteed to be in
+ * the order of priority, no matter the call or the resulting mix of subscribers. All ordering rules apply to all
+ * types subscribers: class, topic, pattern, veto, etc. For Swing users, note that FIFO is
+ * the opposite of Swing, where event listeners are called in the reverse order of when they were subscribed (FILO).
+ *
+ *
+ * Publication on a class or topic name can be vetoed by a {@link VetoEventListener}. All VetoEventListeners are checked
+ * before any EventSubscribers or EventTopicSubscribers are called. This is unlike the JavaBean's
+ * VetoPropertyEventListener which can leave side effects and half-propogated events. VetoEventListeners are subscribed
+ * in the same manner as EventSubscribers and EventTopicSubscribers.
+ *
+ *
+ * The state of a published event can be tracked if an event or a topic's payload object implements the
+ * {@link org.scijava.event.bushe.PublicationStatus} interface. EventServices are required to set such objects'
+ * {@link org.scijava.event.bushe.PublicationStatus} at the appropriate times during publication.
+ *
+*
+ * This simple example prints "Hello World"
+ *
+ * EventService eventService = new ThreadSafeEventService();
+ * //Create a subscriber
+ * EventTopicSubscriber subscriber = new EventTopicSubscriber() {
+ * public void onEvent(String topic, Object event) {
+ * System.out.println(topic+" "+event);
+ * }
+ * });
+ * eventService.subscribe("Hello", subscriber);
+ * eventService.publish("Hello", "World");
+ * System.out.println(subscriber + " Since the reference is used after it is subscribed, it doesn't get garbage collected, this is not necessary if you use subscribeStrongly()");
+ *
+ *
+ *
+ * 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 List getSubscribers(Class eventClass);
+
+ /**
+ * Gets subscribers that subscribed with the given a class, but not those subscribed exactly to the class.
+ * @param eventClass the eventClass of interest
+ *
+ * @return the subscribers that are subscribed to match to a class and its supertypes, but not those subscribed by
+ * exact class
+ */
+ public List getSubscribersToClass(Class eventClass);
+
+ /**
+ * Gets subscribers that are subscribed exactly to a class, but not those subscribed non-exactly to a class.
+ * @param eventClass the eventClass of interest
+ *
+ * @return the subscribers that are subscribed by exact class but not those subscribed to match to a class and its
+ * supertypes
+ */
+ public List getSubscribersToExactClass(Class eventClass);
+
+ /**
+ * Gets the subscribers that subscribed to a generic type.
+ *
+ * @param type the type 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 List getSubscribers(Type type);
+
+ /**
+ * Union of getSubscribersByPattern(String) and geSubscribersToTopic(String)
+ *
+ * @param topic the topic of interest
+ *
+ * @return the subscribers that will be called when an event is published on the topic. This includes subscribers
+ * subscribed to match the exact topic name and those subscribed by a RegEx Pattern that matches the topic
+ * name.
+ */
+ public List getSubscribers(String topic);
+
+ /**
+ * Get the subscribers that subscribed to a topic.
+ * @param topic the topic of interest
+ *
+ * @return the subscribers that subscribed to the exact topic name.
+ */
+ public List getSubscribersToTopic(String topic);
+
+ /**
+ * Gets the subscribers that subscribed to a regular expression.
+ * @param pattern the RegEx pattern that was subscribed to
+ *
+ * @return the subscribers that were subscribed to this pattern.
+ */
+ public List getSubscribers(Pattern pattern);
+
+ /**
+ * Gets the subscribers that subscribed with a Pattern that matches the given topic.
+ * @param topic a topic to match Patterns against
+ *
+ * @return the subscribers that subscribed by a RegEx Pattern that matches the topic name.
+ */
+ public List getSubscribersByPattern(String topic);
+
+ /**
+ * Gets veto subscribers that subscribed to a given class.
+ * @param eventClass the eventClass of interest
+ *
+ * @return the veto subscribers that will be called when an event of eventClass or its subclasses is published.
+ */
+ public List getVetoSubscribers(Class eventClass);
+
+ /**
+ * Get veto subscribers that subscribed to a given class exactly.
+ * @param eventClass the eventClass of interest
+ *
+ * @return the veto subscribers that will be called when an event of eventClass (but not its subclasses) is
+ * published.
+ */
+ public List getVetoSubscribersToExactClass(Class eventClass);
+
+ /**
+ * Gets the veto subscribers that subscribed to a class.
+ * @param eventClass the eventClass of interest
+ *
+ * @return the veto subscribers that are subscribed to the eventClass and its subclasses
+ */
+ public List getVetoSubscribersToClass(Class eventClass);
+
+ /**
+ * Union of {@link #getVetoSubscribersToTopic(String)} and {@link #getVetoSubscribersByPattern(String)}
+ * Misnamed method, should be called {@link #getVetoSubscribers(String)}. Will be deprecated in 1.5.
+ *
+ * @param topicOrPattern the topic or pattern of interest
+ *
+ * @return the veto subscribers that will be called when an event is published on the topic.
+ */
+ public List getVetoEventListeners(String topicOrPattern);
+
+ /**
+ * Gets the veto subscribers that subscribed to a topic.
+ * @param topic the topic of interest
+ *
+ * @return the veto subscribers that will be called when an event is published on the topic.
+ */
+ public List getVetoSubscribersToTopic(String topic);
+
+ /**
+ * Gets the veto subscribers that subscribed to a regular expression.
+ * @param pattern the RegEx pattern for the topic of interest
+ *
+ * @return the veto subscribers that were subscribed to this pattern.
+ */
+ public List getVetoSubscribers(Pattern pattern);
+
+ /**
+ * Gets the veto subscribers that are subscribed by pattern that match the topic.
+ * @param topic the topic to match the pattern string subscribed to
+ *
+ * @return the veto subscribers that subscribed by pattern that will be called when an event is published on the topic.
+ */
+ public List getVetoSubscribersByPattern(String topic);
+
+ /**
+ * Misnamed method for backwards compatibility.
+ * Duplicate of {@link #getVetoSubscribersToTopic(String)}.
+ * Out of sync with {@link #getSubscribers(String)}.
+ * @param topic the topic exactly subscribed to
+ *
+ * @return the veto subscribers that are subscribed to the topic.
+ * @deprecated use getVetoSubscribersToTopic instead for direct replacement,
+ * or use getVetoEventListeners to get topic and pattern matchers.
+ * In EventBus 2.0 this name will replace getVetoEventListeners()
+ * and have it's union functionality
+ */
+ public List getVetoSubscribers(String topic);
+
+ /** Clears all current subscribers and veto subscribers */
+ public void clearAllSubscribers();
+
+ /**
+ * Sets the default cache size for each kind of event, default is 0 (no caching).
+ *
+ * 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 T getLastEvent(Class eventClass);
+
+ /**
+ * When caching, returns the last set of event published for the type supplied.
+ * @param eventClass an index into the cache
+ *
+ * @return the last events published for this event class, or null if caching is turned off (the default)
+ */
+ public List getCachedEvents(Class eventClass);
+
+ /**
+ * When caching, returns the last payload published on the topic name supplied.
+ * @param topic an index into the cache
+ *
+ * @return the last data Object published on this topic, or null if caching is turned off (the default)
+ */
+ public Object getLastTopicData(String topic);
+
+ /**
+ * When caching, returns the last set of payload objects published on the topic name supplied.
+ * @param topic an index into the cache
+ *
+ * @return the last data Objects published on this topic, or null if caching is turned off (the default)
+ */
+ public List getCachedTopicData(String topic);
+
+ /**
+ * Clears the event cache for a specific event class or interface and it's any of it's subclasses or implementing
+ * classes.
+ *
+ * @param eventClass the event class to clear the cache for
+ */
+ public void clearCache(Class eventClass);
+
+ /**
+ * Clears the topic data cache for a specific topic name.
+ *
+ * @param topic the topic name to clear the cache for
+ */
+ public void clearCache(String topic);
+
+ /**
+ * Clears the topic data cache for all topics that match a particular pattern.
+ *
+ * @param pattern the pattern to match topic caches to
+ */
+ public void clearCache(Pattern pattern);
+
+ /** Clear all event caches for all topics and event. */
+ public void clearCache();
+
+ /**
+ * Stop a subscription for 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 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 EventSubscriber {
+
+ /**
+ * Handle a published event.
The 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 {
+
+ /**
+ * Handle an event published on a topic.
+ *
+ * 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)}.
+ *
+ *
+ * @param topic the name of the topic published on
+ * @param data the data object published on the topic
+ */
+ public void onEvent(String topic, T data);
+}
diff --git a/src/main/java/org/scijava/event/bushe/Logger.java b/src/main/java/org/scijava/event/bushe/Logger.java
new file mode 100644
index 000000000..f2de3981b
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/Logger.java
@@ -0,0 +1,221 @@
+package org.scijava.event.bushe;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Central Logging class. Shields code from Logging implementation.
+ *
+ * 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.
+ *
+ */
+class Logger {
+ private java.util.logging.Logger utilLogger;
+ private /*Untyped to avoid java.lang.NoClassDefFoundError
+ org.apache.commons.logging.Log*/ Object commonsLogger;
+ private Map METHOD_CACHE_NO_PARAMS;
+ private Map METHOD_CACHE_ONE_PARAM;
+ private Map METHOD_CACHE_TWO_PARAMS;
+ private static Class> logFactoryClass;
+ private static Class> logClass;
+ private static Method getLogMethod;
+ private static final Object[] EMPTY_ARGS = new Object[0];
+ private static final Class[] CLASS_ARGS_EMPTY = new Class[0];
+ private static final Class[] CLASS_ARGS_ONE = new Class[]{Object.class};
+ private static final Class[] CLASS_ARGS_TWO = new Class[]{Object.class, Throwable.class};
+
+ /** Allows switching between Java and Commons logging.*/
+ public static enum LoggerType {
+ /*java.util.logging*/
+ JAVA,
+ /*org.apache.commons.logging*/
+ COMMONS
+ }
+
+ /** Standardized logging levels. */
+ public static enum Level {
+ FATAL,
+ ERROR,
+ WARN,
+ INFO,
+ DEBUG,
+ TRACE
+ }
+
+ public static LoggerType LOGGER_TYPE= null;
+
+ public static Logger getLogger(String name) {
+ if (LOGGER_TYPE == null) {
+ LOGGER_TYPE = getLoggerType();
+ }
+ if (LOGGER_TYPE == LoggerType.COMMONS) {
+ try {
+ Object logger = getLogMethod.invoke(null, name);
+ return new Logger(logger);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ return new Logger(java.util.logging.Logger.getLogger(name));
+ }
+
+ /**
+ * This method should only be called once in a JVM run.
+ * @return
+ */
+ private static LoggerType getLoggerType() {
+ LoggerType result = null;
+ //See if apache commons is available
+ try {
+ logFactoryClass = Class.forName("org.apache.commons.logging.LogFactory");
+ getLogMethod = logFactoryClass.getMethod("getLog", new Class[]{String.class});
+ logClass = Class.forName("org.apache.commons.logging.Log");
+ return LoggerType.COMMONS;
+ } catch (Throwable e) {
+ }
+ return LoggerType.JAVA;
+ }
+
+ public Logger(java.util.logging.Logger utilLogger) {
+ this.utilLogger = utilLogger;
+ }
+
+ public Logger(Object commonsLogger) {
+ this.commonsLogger = commonsLogger;
+ }
+
+ /**
+ * Returns whether this level is loggable. If there is
+ * a misconfiguration, this will always return false.
+ * @param level the EventBus Logger level
+ * @return whether this level is loggable.
+ */
+ public boolean isLoggable(Level level) {
+ if (utilLogger != null) {
+ java.util.logging.Level javaLevel = getJavaLevelFor(level);
+ return javaLevel != null && utilLogger.isLoggable(javaLevel);
+ } else if (commonsLogger != null) {
+ switch (level) {
+ case ERROR: return (Boolean)callCommonsLogger("isErrorEnabled");
+ case FATAL: return (Boolean)callCommonsLogger("isFatalEnabled");
+ case WARN: return (Boolean)callCommonsLogger("isWarnEnabled");
+ case INFO: return (Boolean)callCommonsLogger("isInfoEnabled");
+ case DEBUG: return (Boolean)callCommonsLogger("isDebugEnabled");
+ case TRACE: return (Boolean)callCommonsLogger("isTraceEnabled");
+ }
+ }
+ return false;
+ }
+
+ private java.util.logging.Level getJavaLevelFor(Level level) {
+ switch (level) {
+ case FATAL: return java.util.logging.Level.SEVERE;
+ case ERROR: return java.util.logging.Level.SEVERE;
+ case WARN: return java.util.logging.Level.WARNING;
+ case INFO: return java.util.logging.Level.INFO;
+ case DEBUG: return java.util.logging.Level.FINE;
+ case TRACE: return java.util.logging.Level.FINEST;
+ }
+ return null;
+ }
+
+ public void debug(String message) {
+ log(Level.DEBUG, message);
+ }
+
+ public void log(Level level, String message) {
+ log(level, message, null);
+ }
+
+ public void log(Level level, String message, Throwable throwable) {
+ if (!isLoggable(level)) {
+ return;
+ }
+ if (utilLogger != null) {
+ java.util.logging.Level javaLevel = getJavaLevelFor(level);
+ if (throwable == null) {
+ utilLogger.log(javaLevel, message);
+ } else {
+ utilLogger.log(javaLevel, message, throwable);
+ }
+ } else if (commonsLogger != null) {
+ if (throwable == null) {
+ switch (level) {
+ case ERROR: callCommonsLogger("error", message); break;
+ case FATAL: callCommonsLogger("fatal", message); break;
+ case WARN: callCommonsLogger("warn", message); break;
+ case INFO: callCommonsLogger("info", message); break;
+ case DEBUG: callCommonsLogger("debug", message); break;
+ case TRACE: callCommonsLogger("trace", message); break;
+ }
+ } else {
+ switch (level) {
+ case ERROR: callCommonsLogger("error", message, throwable); break;
+ case FATAL: callCommonsLogger("fatal", message, throwable); break;
+ case WARN: callCommonsLogger("warn", message, throwable); break;
+ case INFO: callCommonsLogger("info", message, throwable); break;
+ case DEBUG: callCommonsLogger("debug", message, throwable); break;
+ case TRACE: callCommonsLogger("trace", message, throwable); break;
+ }
+ }
+ }
+ }
+
+ private Object callCommonsLogger(String methodName) {
+ if (METHOD_CACHE_NO_PARAMS == null) {
+ METHOD_CACHE_NO_PARAMS = new HashMap();
+ }
+ return callCommonsLogger(METHOD_CACHE_NO_PARAMS, methodName, CLASS_ARGS_EMPTY, EMPTY_ARGS);
+ }
+
+ private Object callCommonsLogger(String methodName, String message) {
+ if (METHOD_CACHE_ONE_PARAM == null) {
+ METHOD_CACHE_ONE_PARAM = new HashMap();
+ }
+ return callCommonsLogger(METHOD_CACHE_ONE_PARAM, methodName, CLASS_ARGS_ONE, new Object[]{message});
+ }
+
+ private Object callCommonsLogger(String methodName, String message, Throwable throwable) {
+ if (METHOD_CACHE_TWO_PARAMS == null) {
+ METHOD_CACHE_TWO_PARAMS = new HashMap();
+ }
+ return callCommonsLogger(METHOD_CACHE_TWO_PARAMS, methodName, CLASS_ARGS_TWO, new Object[]{message, throwable});
+ }
+
+ private Object callCommonsLogger(Map cache, String methodName, Class[] classOfArgs, Object[] args) {
+ Method method = cache.get(methodName);
+ if (method == null) {
+ try {
+ method = logClass.getMethod(methodName, classOfArgs);
+ cache.put(methodName, method);
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ }
+ }
+ if (method == null) {
+ return null;
+ }
+ try {
+ return method.invoke(commonsLogger, args);
+ } catch (IllegalAccessException e) {
+ return null;
+ } catch (InvocationTargetException e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/scijava/event/bushe/Prioritized.java b/src/main/java/org/scijava/event/bushe/Prioritized.java
new file mode 100644
index 000000000..7a3187055
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/Prioritized.java
@@ -0,0 +1,14 @@
+package org.scijava.event.bushe;
+
+/**
+ * Subscribers can implement this interface in order to affect the order in which they are called.
+ *
+ * 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.
+ *
+ *
+ * @author Michael Bushe michael@bushe.com
+ */
+class SwingException extends Exception {
+ protected StackTraceElement[] callingStackTrace;
+
+ /** Default constructor */
+ public SwingException() {
+ super();
+ }
+
+ /**
+ * Constructor for compatibility with Exception. Use ClientException(String, Throwable, StackTraceElement[])
+ * instead
+ */
+ public SwingException(String message) {
+ super(message);
+ }
+
+ /** Constructor for compatibility with Exception Use ClientException(String, Throwable, StackTraceElement[]) instead */
+ public SwingException(Throwable cause) {
+ super(cause);
+ }
+
+ /** Constructor for compatibility with Exception Use ClientException(String, Throwable, StackTraceElement[]) instead */
+ public SwingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Preferred constructor.
+ *
+ * @param message The message of exception
+ * @param cause The cause of the exception in the same call stack
+ * @param callingStack the stack trace that the client used to call the exception to occur.
+ */
+ public SwingException(String message, Throwable cause, StackTraceElement[] callingStack) {
+ super(message, cause);
+ setCallingStack(callingStack);
+ }
+
+ /**
+ * Swing exceptions often have two stacks - one thread causes the posting of an action on another thread - usually
+ * the Swing EDT thread. The other is the stack of the actual thread the exception occurred on, the exception occurs
+ * after the post.
+ *
+ * @param swingCallingStack the stack trace that the client used to cause the exception to occur.
+ */
+ public void setCallingStack(StackTraceElement[] swingCallingStack) {
+ this.callingStackTrace = swingCallingStack;
+ }
+
+ /**
+ * Client exceptions often have two stacks - one thread causes the posting of an action on another thread - usually
+ * the Swing EDT thread. The other is the stack of the actual thread the exception occurred on.
+ *
+ * @return the stack trace that the client used to cause the exception to occur.
+ */
+ public StackTraceElement[] getCallingStack() {
+ return callingStackTrace;
+ }
+
+ /**
+ * Calls printWriter(ps, true)
+ *
+ * @param ps the print stream
+ */
+ public void printStackTrace(PrintStream ps) {
+ PrintWriter pw = new PrintWriter(ps, true);
+ printStackTrace(pw);
+ }
+
+ /**
+ * Prints the calling stack and the exception stack trace.
+ *
+ * @param pw
+ */
+ public void printStackTrace(PrintWriter pw) {
+ pw.println(this);
+ if (callingStackTrace != null) {
+ pw.println("Calling stack:");
+ for (int i = 0; i < callingStackTrace.length; i++) {
+ pw.println("\tat " + callingStackTrace[i]);
+ }
+ pw.println("Stack after call:");
+ }
+ super.printStackTrace(pw);
+ }
+}
+
diff --git a/src/main/java/org/scijava/event/bushe/ThreadSafeEventService.java b/src/main/java/org/scijava/event/bushe/ThreadSafeEventService.java
new file mode 100644
index 000000000..6863f6466
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/ThreadSafeEventService.java
@@ -0,0 +1,2180 @@
+/**
+ * Copyright 2005-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;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Type;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.WildcardType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.regex.Pattern;
+
+import org.scijava.event.bushe.Logger.Level;
+
+/**
+ * A thread-safe EventService implementation.
+ *
Multithreading
+ *
+ * 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).
+ *
+ *
Logging
+ *
+ * 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.
+ *
+ *
Cleanup of Stale WeakReferences and Stale Annotation Proxies
+ *
+ * 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.
+ *
+ *
+ *
On every publish, subscribe and unsubscribe, every subscriber and veto subscriber to a class or topic is checked to see
+ * if it is a stale WeakReference or a stale ProxySubscriber (one whose getProxySubscriber() returns null). If the subscriber
+ * is stale, it is unsubscribed from the EventService immediately. If it is a ProxySubscriber, it's proxyUnsubscribed()
+ * method is called after it is unsubscribed. (This isn't as expensive as it sounds, since checks to avoid double subscription is
+ * necessary anyway).
+ *
Another cleanup thread may get started to clean up remaining stale subscribers. This cleanup thread only comes into
+ * play for subscribers to topic or classes that haven't been used (published/subscribed/unsibscribed to). A detailed description
+ * of the cleanup thread follows.
+ *
+ *
The Cleanup Thread
+ *
+ * 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 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.
+ *
+ *
+ * Once the cleanup start threshold is exceeded, a 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.
+ *
+ *
+ * After a cleanup cycle completes, if the number of stale subscribers falls at or below the 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * @author Michael Bushe michael@bushe.com
+ * @see EventService for a complete description of the API
+ */
+@SuppressWarnings({"unchecked"})
+public class ThreadSafeEventService implements EventService {
+ public static final Integer CLEANUP_START_THRESHOLD_DEFAULT = 250;
+ public static final Integer CLEANUP_STOP_THRESHOLD_DEFAULT = 100;
+ public static final Long CLEANUP_PERIOD_MS_DEFAULT = 20L*60L*1000L;
+
+ protected static final Logger LOG = Logger.getLogger(EventService.class.getName());
+
+ //Making these generic collections is a bad idea, it doesn't compile since it's better to have all the maps
+ //go through the same set of code to do all the real publish and subscribe work
+ private Map subscribersByEventType = new HashMap();
+ private Map subscribersByEventClass = new HashMap();
+ private Map subscribersByExactEventClass = new HashMap();
+ private Map subscribersByTopic = new HashMap();
+ private Map subscribersByTopicPattern = new HashMap();
+ private Map vetoListenersByClass = new HashMap();
+ private Map vetoListenersByExactClass = new HashMap();
+ private Map vetoListenersByTopic = new HashMap();
+ private Map vetoListenersByTopicPattern = new HashMap();
+ private final Object listenerLock = new Object();
+ private final Object cacheLock = new Object();
+ private Long timeThresholdForEventTimingEventPublication;
+ private Map cacheByEvent = new HashMap();
+ private int defaultCacheSizePerClassOrTopic = 0;
+ private Map cacheSizesForEventClass;
+ private Map rawCacheSizesForEventClass;
+ private boolean rawCacheSizesForEventClassChanged;
+ private Map cacheByTopic = new HashMap();
+ private Map cacheSizesForTopic;
+ private Map rawCacheSizesForTopic;
+ private boolean rawCacheSizesForTopicChanged;
+ private Map rawCacheSizesForPattern;
+ private boolean rawCacheSizesForPatternChanged;
+ private Integer cleanupStartThreshhold;
+ private Integer cleanupStopThreshold;
+ private Long cleanupPeriodMS;
+ private int weakRefPlusProxySubscriberCount;
+ private Timer cleanupTimer;
+ private TimerTask cleanupTimerTask;
+ private static final Comparator PRIORITIZED_SUBSCRIBER_COMPARATOR = new PrioritizedSubscriberComparator();
+ private boolean hasEverUsedPrioritized;
+
+ /** Creates a ThreadSafeEventService that does not monitor timing of handlers. */
+ public ThreadSafeEventService() {
+ this(null, null, null, null);
+ }
+
+ /**
+ * Creates a ThreadSafeEventService while providing time monitoring options.
+ *
+ * @param timeThresholdForEventTimingEventPublication the longest time a subscriber should spend handling an event,
+ * The service will publish an SubscriberTimingEvent after listener processing if the time was exceeded. If null, no
+ * EventSubscriberTimingEvent will be issued.
+ */
+ public ThreadSafeEventService(Long timeThresholdForEventTimingEventPublication) {
+ this(timeThresholdForEventTimingEventPublication, null, null, null);
+ }
+
+ /**
+ * Creates a ThreadSafeEventService while providing proxy cleanup customization.
+ * Proxies are used with Annotations.
+ *
+ * @param cleanupStartThreshold see class javadoc.
+ * @param cleanupStopThreshold see class javadoc.
+ * @param cleanupPeriodMS see class javadoc.
+ */
+ public ThreadSafeEventService(Integer cleanupStartThreshold,
+ Integer cleanupStopThreshold, Long cleanupPeriodMS) {
+ this(null, cleanupStartThreshold, cleanupStopThreshold, cleanupPeriodMS);
+ }
+
+ /**
+ * Creates a ThreadSafeEventService while providing time monitoring options.
+ *
+ * @param timeThresholdForEventTimingEventPublication the longest time a subscriber should spend handling an event.
+ * The service will publish an SubscriberTimingEvent after listener processing if the time was exceeded. If null, no
+ * SubscriberTimingEvent will be issued.
+ * @param cleanupStartThreshold see class javadoc.
+ * @param cleanupStopThreshold see class javadoc.
+ * @param cleanupPeriodMS see class javadoc.
+ */
+ public ThreadSafeEventService(Long timeThresholdForEventTimingEventPublication,
+ Integer cleanupStartThreshold, Integer cleanupStopThreshold, Long cleanupPeriodMS) {
+ this.timeThresholdForEventTimingEventPublication = timeThresholdForEventTimingEventPublication;
+ if (cleanupStartThreshold == null) {
+ this.cleanupStartThreshhold = CLEANUP_START_THRESHOLD_DEFAULT;
+ } else {
+ this.cleanupStartThreshhold = cleanupStartThreshold;
+ }
+ if (cleanupStopThreshold == null) {
+ this.cleanupStopThreshold = CLEANUP_STOP_THRESHOLD_DEFAULT;
+ } else {
+ this.cleanupStopThreshold = cleanupStopThreshold;
+ }
+ if (cleanupPeriodMS == null) {
+ this.cleanupPeriodMS = CLEANUP_PERIOD_MS_DEFAULT;
+ } else {
+ this.cleanupPeriodMS = cleanupPeriodMS;
+ }
+ }
+
+ /**
+ * Gets the threshold above which cleanup starts. See the class javadoc on cleanup.
+ * @return the threshold at which cleanup starts
+ */
+ public Integer getCleanupStartThreshhold() {
+ synchronized (listenerLock) {
+ return cleanupStartThreshhold;
+ }
+ }
+
+ /**
+ * Sets the threshold above which cleanup starts. See the class javadoc on cleanup.
+ * @param cleanupStartThreshhold threshold at which cleanup starts
+ */
+ public void setCleanupStartThreshhold(Integer cleanupStartThreshhold) {
+ synchronized (listenerLock) {
+ this.cleanupStartThreshhold = cleanupStartThreshhold;
+ }
+ }
+
+ /**
+ * Gets the threshold below which cleanup stops. See the class javadoc on cleanup.
+ * @return threshold at which cleanup stops (it may start again)
+ */
+ public Integer getCleanupStopThreshold() {
+ synchronized (listenerLock) {
+ return cleanupStopThreshold;
+ }
+ }
+
+ /**
+ * Sets the threshold below which cleanup stops. See the class javadoc on cleanup.
+ * @param cleanupStopThreshold threshold at which cleanup stops (it may start again).
+ */
+ public void setCleanupStopThreshold(Integer cleanupStopThreshold) {
+ synchronized (listenerLock) {
+ this.cleanupStopThreshold = cleanupStopThreshold;
+ }
+ }
+
+ /**
+ * Get the cleanup interval. See the class javadoc on cleanup.
+ * @return interval in milliseconds between cleanup runs.
+ */
+ public Long getCleanupPeriodMS() {
+ synchronized (listenerLock) {
+ return cleanupPeriodMS;
+ }
+ }
+
+ /**
+ * Sets the cleanup interval. See the class javadoc on cleanup.
+ * @param cleanupPeriodMS interval in milliseconds between cleanup runs. Passing null
+ * stops cleanup.
+ */
+ public void setCleanupPeriodMS(Long cleanupPeriodMS) {
+ synchronized (listenerLock) {
+ this.cleanupPeriodMS = cleanupPeriodMS;
+ }
+ }
+
+ /** @see EventService#subscribe(Class,EventSubscriber) */
+ public boolean subscribe(Class cl, EventSubscriber eh) {
+ if (cl == null) {
+ throw new IllegalArgumentException("Event class must not be null");
+ }
+ if (eh == null) {
+ throw new IllegalArgumentException("Event subscriber must not be null");
+ }
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Subscribing by class, class:" + cl + ", subscriber:" + eh);
+ }
+ return subscribe(cl, subscribersByEventClass, new WeakReference(eh));
+ }
+
+ /** @see EventService#subscribe(java.lang.reflect.Type, EventSubscriber) */
+ public boolean subscribe(Type type, EventSubscriber eh) {
+ return subscribe(type, subscribersByEventType, new WeakReference(eh));
+ }
+
+ /** @see EventService#subscribeExactly(Class,EventSubscriber) */
+ public boolean subscribeExactly(Class cl, EventSubscriber eh) {
+ if (cl == null) {
+ throw new IllegalArgumentException("Event class must not be null");
+ }
+ if (eh == null) {
+ throw new IllegalArgumentException("Event subscriber must not be null");
+ }
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Subscribing by class, class:" + cl + ", subscriber:" + eh);
+ }
+ return subscribe(cl, subscribersByExactEventClass, new WeakReference(eh));
+ }
+
+ /** @see EventService#subscribe(String,EventTopicSubscriber) */
+ public boolean subscribe(String topic, EventTopicSubscriber eh) {
+ if (topic == null) {
+ throw new IllegalArgumentException("Topic must not be null");
+ }
+ if (eh == null) {
+ throw new IllegalArgumentException("Event topic subscriber must not be null");
+ }
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Subscribing by topic name, name:" + topic + ", subscriber:" + eh);
+ }
+ return subscribe(topic, subscribersByTopic, new WeakReference(eh));
+ }
+
+ /** @see EventService#subscribe(Pattern,EventTopicSubscriber) */
+ public boolean subscribe(Pattern pat, EventTopicSubscriber eh) {
+ if (pat == null) {
+ throw new IllegalArgumentException("Pattern must not be null");
+ }
+ if (eh == null) {
+ throw new IllegalArgumentException("Event subscriber must not be null");
+ }
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Subscribing by pattern, pattern:" + pat + ", subscriber:" + eh);
+ }
+ PatternWrapper patternWrapper = new PatternWrapper(pat);
+ return subscribe(patternWrapper, subscribersByTopicPattern, new WeakReference(eh));
+ }
+
+ /** @see EventService#subscribeStrongly(Class,EventSubscriber) */
+ public boolean subscribeStrongly(Class cl, EventSubscriber eh) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Subscribing weakly by class, class:" + cl + ", subscriber:" + eh);
+ }
+ if (eh == null) {
+ throw new IllegalArgumentException("Subscriber cannot be null.");
+ }
+ return subscribe(cl, subscribersByEventClass, eh);
+ }
+
+ /** @see EventService#subscribeExactlyStrongly(Class,EventSubscriber) */
+ public boolean subscribeExactlyStrongly(Class cl, EventSubscriber eh) {
+ if (cl == null) {
+ throw new IllegalArgumentException("Event class must not be null");
+ }
+ if (eh == null) {
+ throw new IllegalArgumentException("Event subscriber must not be null");
+ }
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Subscribing by class, class:" + cl + ", subscriber:" + eh);
+ }
+ return subscribe(cl, subscribersByExactEventClass, eh);
+ }
+
+ /** @see EventService#subscribeStrongly(String,EventTopicSubscriber) */
+ public boolean subscribeStrongly(String name, EventTopicSubscriber eh) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Subscribing weakly by topic name, name:" + name + ", subscriber:" + eh);
+ }
+ if (eh == null) {
+ throw new IllegalArgumentException("Subscriber cannot be null.");
+ }
+ return subscribe(name, subscribersByTopic, eh);
+ }
+
+ /** @see EventService#subscribeStrongly(Pattern,EventTopicSubscriber) */
+ public boolean subscribeStrongly(Pattern pat, EventTopicSubscriber eh) {
+ if (pat == null) {
+ throw new IllegalArgumentException("Pattern must not be null");
+ }
+ if (eh == null) {
+ throw new IllegalArgumentException("Event subscriber must not be null");
+ }
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Subscribing by pattern, pattern:" + pat + ", subscriber:" + eh);
+ }
+ PatternWrapper patternWrapper = new PatternWrapper(pat);
+ return subscribe(patternWrapper, subscribersByTopicPattern, eh);
+ }
+
+
+ /** @see org.scijava.event.bushe.EventService#clearAllSubscribers() */
+ public void clearAllSubscribers() {
+ synchronized (listenerLock) {
+ unsubscribeAllInMap(subscribersByEventType);
+ unsubscribeAllInMap(subscribersByEventClass);
+ unsubscribeAllInMap(subscribersByExactEventClass);
+ unsubscribeAllInMap(subscribersByTopic);
+ unsubscribeAllInMap(subscribersByTopicPattern);
+ unsubscribeAllInMap(vetoListenersByClass);
+ unsubscribeAllInMap(vetoListenersByExactClass);
+ unsubscribeAllInMap(vetoListenersByTopic);
+ unsubscribeAllInMap(vetoListenersByTopicPattern);
+ }
+ }
+
+ private void unsubscribeAllInMap(Map subscriberMap) {
+ synchronized (listenerLock) {
+ Set subscriptionKeys = subscriberMap.keySet();
+ for (Object key : subscriptionKeys) {
+ List subscribers = (List) subscriberMap.get(key);
+ while (!subscribers.isEmpty()) {
+ unsubscribe(key, subscriberMap, subscribers.get(0));
+ }
+ }
+ }
+ }
+
+ /** @see EventService#subscribeVetoListener(Class,VetoEventListener) */
+ public boolean subscribeVetoListener(Class eventClass, VetoEventListener vetoListener) {
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("VetoEventListener cannot be null.");
+ }
+ if (eventClass == null) {
+ throw new IllegalArgumentException("eventClass cannot be null.");
+ }
+ return subscribeVetoListener(eventClass, vetoListenersByClass, new WeakReference(vetoListener));
+ }
+
+ /** @see EventService#subscribeVetoListenerExactly(Class,VetoEventListener) */
+ public boolean subscribeVetoListenerExactly(Class eventClass, VetoEventListener vetoListener) {
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("VetoEventListener cannot be null.");
+ }
+ if (eventClass == null) {
+ throw new IllegalArgumentException("eventClass cannot be null.");
+ }
+ return subscribeVetoListener(eventClass, vetoListenersByExactClass, new WeakReference(vetoListener));
+ }
+
+ /** @see EventService#subscribeVetoListener(String,VetoTopicEventListener) */
+ public boolean subscribeVetoListener(String topic, VetoTopicEventListener vetoListener) {
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("VetoEventListener cannot be null.");
+ }
+ if (topic == null) {
+ throw new IllegalArgumentException("topic cannot be null.");
+ }
+ return subscribeVetoListener(topic, vetoListenersByTopic, new WeakReference(vetoListener));
+ }
+
+ /** @see EventService#subscribeVetoListener(Pattern,VetoTopicEventListener) */
+ public boolean subscribeVetoListener(Pattern topicPattern, VetoTopicEventListener vetoListener) {
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("VetoEventListener cannot be null.");
+ }
+ if (topicPattern == null) {
+ throw new IllegalArgumentException("topicPattern cannot be null.");
+ }
+ PatternWrapper patternWrapper = new PatternWrapper(topicPattern);
+ return subscribeVetoListener(patternWrapper, vetoListenersByTopicPattern, new WeakReference(vetoListener));
+ }
+
+ /** @see EventService#subscribeVetoListenerStrongly(Class,VetoEventListener) */
+ public boolean subscribeVetoListenerStrongly(Class eventClass, VetoEventListener vetoListener) {
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("VetoEventListener cannot be null.");
+ }
+ if (eventClass == null) {
+ throw new IllegalArgumentException("eventClass cannot be null.");
+ }
+ return subscribeVetoListener(eventClass, vetoListenersByClass, vetoListener);
+ }
+
+ /** @see EventService#subscribeVetoListenerExactlyStrongly(Class,VetoEventListener) */
+ public boolean subscribeVetoListenerExactlyStrongly(Class eventClass, VetoEventListener vetoListener) {
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("VetoEventListener cannot be null.");
+ }
+ if (eventClass == null) {
+ throw new IllegalArgumentException("eventClass cannot be null.");
+ }
+ return subscribeVetoListener(eventClass, vetoListenersByExactClass, vetoListener);
+ }
+
+ /** @see EventService#subscribeVetoListenerStrongly(String,VetoTopicEventListener) */
+ public boolean subscribeVetoListenerStrongly(String topic, VetoTopicEventListener vetoListener) {
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("VetoListener cannot be null.");
+ }
+ if (topic == null) {
+ throw new IllegalArgumentException("topic cannot be null.");
+ }
+ return subscribeVetoListener(topic, vetoListenersByTopic, vetoListener);
+ }
+
+ /** @see EventService#subscribeVetoListenerStrongly(Pattern,VetoTopicEventListener) */
+ public boolean subscribeVetoListenerStrongly(Pattern topicPattern, VetoTopicEventListener vetoListener) {
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("VetoTopicEventListener cannot be null.");
+ }
+ if (topicPattern == null) {
+ throw new IllegalArgumentException("topicPattern cannot be null.");
+ }
+ PatternWrapper patternWrapper = new PatternWrapper(topicPattern);
+ return subscribeVetoListener(patternWrapper, vetoListenersByTopicPattern, vetoListener);
+ }
+
+ /**
+ * All veto subscriptions methods call this method. Extending classes only have to override this method to subscribe
+ * all veto subscriptions.
+ *
+ * @param subscription the topic, Pattern, or event class to subscribe to
+ * @param vetoListenerMap the internal map of veto listeners to use (by topic of class)
+ * @param vetoListener the veto listener to subscribe, may be a VetoEventListener or a WeakReference to one
+ *
+ * @return boolean if the veto listener is subscribed (was not subscribed).
+ *
+ * @throws IllegalArgumentException if vl or o is null
+ */
+ protected boolean subscribeVetoListener(final Object subscription, final Map vetoListenerMap, final Object vetoListener) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("subscribeVetoListener(" + subscription + "," + vetoListener + ")");
+ }
+ if (vetoListener == null) {
+ throw new IllegalArgumentException("Can't subscribe null veto listener to " + subscription);
+ }
+ if (subscription == null) {
+ throw new IllegalArgumentException("Can't subscribe veto listener to null.");
+ }
+ return subscribe(subscription, vetoListenerMap, vetoListener);
+ }
+
+ /**
+ * All subscribe methods call this method, including veto subscriptions.
+ * Extending classes only have to override this method to subscribe all
+ * subscriber subscriptions.
+ *
+ * Overriding this method is only for the adventurous. This basically gives you just enough rope to hang yourself.
+ *
+ *
+ * @param classTopicOrPatternWrapper the topic String, event Class, or PatternWrapper to subscribe to
+ * @param subscriberMap the internal map of subscribers to use (by topic or class)
+ * @param subscriber the EventSubscriber or EventTopicSubscriber to subscribe, or a WeakReference to either
+ *
+ * @return boolean if the subscriber is subscribed (was not subscribed).
+ *
+ * @throws IllegalArgumentException if subscriber or topicOrClass is null
+ */
+ protected boolean subscribe(final Object classTopicOrPatternWrapper, final Map subscriberMap, final Object subscriber) {
+ if (classTopicOrPatternWrapper == null) {
+ throw new IllegalArgumentException("Can't subscribe to null.");
+ }
+ if (subscriber == null) {
+ throw new IllegalArgumentException("Can't subscribe null subscriber to " + classTopicOrPatternWrapper);
+ }
+ boolean alreadyExists = false;
+
+ //Find the real subscriber underlying weak refs and proxies
+ Object realSubscriber = subscriber;
+ boolean isWeakRef = subscriber instanceof WeakReference;
+ if (isWeakRef) {
+ realSubscriber = ((WeakReference) subscriber).get();
+ }
+ if (realSubscriber instanceof Prioritized) {
+ hasEverUsedPrioritized = true;
+ }
+ boolean isWeakProxySubscriber = false;
+ if (subscriber instanceof ProxySubscriber) {
+ ProxySubscriber proxySubscriber = (ProxySubscriber) subscriber;
+ if (proxySubscriber instanceof Prioritized) {
+ hasEverUsedPrioritized = true;
+ }
+ isWeakProxySubscriber = proxySubscriber.getReferenceStrength() == ReferenceStrength.WEAK;
+ if (isWeakProxySubscriber) {
+ realSubscriber = ((ProxySubscriber) subscriber).getProxiedSubscriber();
+ }
+ }
+ if (isWeakRef && isWeakProxySubscriber) {
+ throw new IllegalArgumentException("ProxySubscribers should always be subscribed strongly.");
+ }
+ if (realSubscriber == null) {
+ return false;//already garbage collected? Weird.
+ }
+ synchronized (listenerLock) {
+ List currentSubscribers = (List) subscriberMap.get(classTopicOrPatternWrapper);
+ if (currentSubscribers == null) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Creating new subscriber map for:" + classTopicOrPatternWrapper);
+ }
+ currentSubscribers = new ArrayList();
+ subscriberMap.put(classTopicOrPatternWrapper, currentSubscribers);
+ } else {
+ //Double subscription check and stale subscriber cleanup
+ //Need to compare the underlying referents for WeakReferences and ProxySubscribers
+ //to make sure a weak ref and a hard ref aren't both subscribed
+ //to the same topic and object.
+ //Use the proxied subscriber for comparison if a ProxySubscribers is used
+ //Subscribing the same object by proxy and subscribing explicitly should
+ //not subscribe the same object twice
+ for (Iterator iterator = currentSubscribers.iterator(); iterator.hasNext();) {
+ Object currentSubscriber = iterator.next();
+ Object realCurrentSubscriber = getRealSubscriberAndCleanStaleSubscriberIfNecessary(iterator, currentSubscriber);
+ if (realSubscriber.equals(realCurrentSubscriber)) {
+ //Already subscribed.
+ //Remove temporarily, to add to the end of the calling list
+ iterator.remove();
+ alreadyExists = true;
+ }
+ }
+ }
+ currentSubscribers.add(subscriber);
+ if (isWeakProxySubscriber || isWeakRef) {
+ incWeakRefPlusProxySubscriberCount();
+ }
+ return !alreadyExists;
+ }
+ }
+
+ /** @see EventService#unsubscribe(Class,EventSubscriber) */
+ public boolean unsubscribe(Class cl, EventSubscriber eh) {
+ return unsubscribe(cl, subscribersByEventClass, eh);
+ }
+
+ /** @see EventService#unsubscribeExactly(Class,EventSubscriber) */
+ public boolean unsubscribeExactly(Class cl, EventSubscriber eh) {
+ return unsubscribe(cl, subscribersByExactEventClass, eh);
+ }
+
+ /** @see EventService#unsubscribe(String,EventTopicSubscriber) */
+ public boolean unsubscribe(String name, EventTopicSubscriber eh) {
+ return unsubscribe(name, subscribersByTopic, eh);
+ }
+
+ /** @see EventService#unsubscribe(String,EventTopicSubscriber) */
+ public boolean unsubscribe(Pattern topicPattern, EventTopicSubscriber eh) {
+ PatternWrapper patternWrapper = new PatternWrapper(topicPattern);
+ return unsubscribe(patternWrapper, subscribersByTopicPattern, eh);
+ }
+
+ /** @see EventService#unsubscribe(Class,Object) */
+ public boolean unsubscribe(Class eventClass, Object subscribedByProxy) {
+ EventSubscriber subscriber = (EventSubscriber) getProxySubscriber(eventClass, subscribedByProxy);
+ if (subscriber == null) {
+ return false;
+ } else {
+ return unsubscribe(eventClass, subscriber);
+ }
+ }
+
+ /** @see EventService#unsubscribeExactly(Class,Object) */
+ public boolean unsubscribeExactly(Class eventClass, Object subscribedByProxy) {
+ EventSubscriber subscriber = (EventSubscriber) getProxySubscriber(eventClass, subscribedByProxy);
+ if (subscriber == null) {
+ return false;
+ } else {
+ return unsubscribeExactly(eventClass, subscriber);
+ }
+ }
+
+ /** @see EventService#unsubscribe(String,Object) */
+ public boolean unsubscribe(String topic, Object subscribedByProxy) {
+ EventTopicSubscriber subscriber = (EventTopicSubscriber) getProxySubscriber(topic, subscribedByProxy);
+ if (subscriber == null) {
+ return false;
+ } else {
+ return unsubscribe(topic, subscriber);
+ }
+ }
+
+ /** @see EventService#unsubscribe(java.util.regex.Pattern,Object) */
+ public boolean unsubscribe(Pattern pattern, Object subscribedByProxy) {
+ EventTopicSubscriber subscriber = (EventTopicSubscriber) getProxySubscriber(pattern, subscribedByProxy);
+ if (subscriber == null) {
+ return false;
+ } else {
+ return unsubscribe(pattern, subscriber);
+ }
+ }
+
+ /**
+ * All event subscriber unsubscriptions call this method. Extending classes only have to override this method to
+ * subscribe all subscriber unsubscriptions.
+ *
+ * @param o the topic or event class to unsubscribe from
+ * @param subscriberMap the map of subscribers to use (by topic of class)
+ * @param subscriber the subscriber to unsubscribe, either an EventSubscriber or an EventTopicSubscriber, or a WeakReference
+ * to either
+ *
+ * @return boolean if the subscriber is unsubscribed (was subscribed).
+ */
+ protected boolean unsubscribe(Object o, Map subscriberMap, Object subscriber) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("unsubscribe(" + o + "," + subscriber + ")");
+ }
+ if (o == null) {
+ throw new IllegalArgumentException("Can't unsubscribe to null.");
+ }
+ if (subscriber == null) {
+ throw new IllegalArgumentException("Can't unsubscribe null subscriber to " + o);
+ }
+ synchronized (listenerLock) {
+ return removeFromSetResolveWeakReferences(subscriberMap, o, subscriber);
+ }
+ }
+
+ /** @see EventService#unsubscribeVeto(Class,Object) */
+ public boolean unsubscribeVeto(Class eventClass, Object subscribedByProxy) {
+ VetoEventListener subscriber = (VetoEventListener) getVetoProxySubscriber(eventClass, subscribedByProxy);
+ if (subscriber == null) {
+ return false;
+ } else {
+ return unsubscribeVetoListener(eventClass, subscriber);
+ }
+ }
+
+ /** @see EventService#unsubscribeVetoExactly(Class,Object) */
+ public boolean unsubscribeVetoExactly(Class eventClass, Object subscribedByProxy) {
+ VetoEventListener subscriber = (VetoEventListener) getVetoProxySubscriber(eventClass, subscribedByProxy);
+ if (subscriber == null) {
+ return false;
+ } else {
+ return unsubscribeVetoListenerExactly(eventClass, subscriber);
+ }
+ }
+
+ /** @see EventService#unsubscribeVeto(String,Object) */
+ public boolean unsubscribeVeto(String topic, Object subscribedByProxy) {
+ VetoTopicEventListener subscriber = (VetoTopicEventListener) getVetoProxySubscriber(topic, subscribedByProxy);
+ if (subscriber == null) {
+ return false;
+ } else {
+ return unsubscribeVetoListener(topic, subscriber);
+ }
+ }
+
+ /** @see EventService#unsubscribeVeto(java.util.regex.Pattern,Object) */
+ public boolean unsubscribeVeto(Pattern pattern, Object subscribedByProxy) {
+ VetoTopicEventListener subscriber = (VetoTopicEventListener) getVetoProxySubscriber(pattern, subscribedByProxy);
+ if (subscriber == null) {
+ return false;
+ } else {
+ return unsubscribeVetoListener(pattern, subscriber);
+ }
+ }
+
+ /** @see EventService#unsubscribeVetoListener(Class,VetoEventListener) */
+ public boolean unsubscribeVetoListener(Class eventClass, VetoEventListener vetoListener) {
+ return unsubscribeVetoListener(eventClass, vetoListenersByClass, vetoListener);
+ }
+
+ /** @see EventService#unsubscribeVetoListenerExactly(Class,VetoEventListener) */
+ public boolean unsubscribeVetoListenerExactly(Class eventClass, VetoEventListener vetoListener) {
+ return unsubscribeVetoListener(eventClass, vetoListenersByExactClass, vetoListener);
+ }
+
+ /** @see EventService#unsubscribeVetoListener(String,VetoTopicEventListener) */
+ public boolean unsubscribeVetoListener(String topic, VetoTopicEventListener vetoListener) {
+ return unsubscribeVetoListener(topic, vetoListenersByTopic, vetoListener);
+ }
+
+ /** @see EventService#unsubscribeVetoListener(Pattern,VetoTopicEventListener) */
+ public boolean unsubscribeVetoListener(Pattern topicPattern, VetoTopicEventListener vetoListener) {
+ PatternWrapper patternWrapper = new PatternWrapper(topicPattern);
+ return unsubscribeVetoListener(patternWrapper, vetoListenersByTopicPattern, vetoListener);
+ }
+
+ /**
+ * All veto unsubscriptions methods call this method. Extending classes only have to override this method to
+ * subscribe all veto unsubscriptions.
+ *
+ * @param o the topic or event class to unsubscribe from
+ * @param vetoListenerMap the map of veto listeners to use (by topic or class)
+ * @param vl the veto listener to unsubscribe, or a WeakReference to one
+ *
+ * @return boolean if the veto listener is unsubscribed (was subscribed).
+ */
+ protected boolean unsubscribeVetoListener(Object o, Map vetoListenerMap, Object vl) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("unsubscribeVetoListener(" + o + "," + vl + ")");
+ }
+ if (o == null) {
+ throw new IllegalArgumentException("Can't unsubscribe veto listener to null.");
+ }
+ if (vl == null) {
+ throw new IllegalArgumentException("Can't unsubscribe null veto listener to " + o);
+ }
+ synchronized (listenerLock) {
+ return removeFromSetResolveWeakReferences(vetoListenerMap, o, vl);
+ }
+ }
+
+ private ProxySubscriber getProxySubscriber(Class eventClass, Object subscribedByProxy) {
+ List subscribers = getSubscribers(eventClass);
+ return getProxySubscriber(subscribers, subscribedByProxy);
+ }
+
+ private ProxySubscriber getProxySubscriber(String topic, Object subscribedByProxy) {
+ List subscribers = getSubscribers(topic);
+ return getProxySubscriber(subscribers, subscribedByProxy);
+ }
+
+ private ProxySubscriber getProxySubscriber(Pattern pattern, Object subscribedByProxy) {
+ List subscribers = getSubscribersToPattern(pattern);
+ return getProxySubscriber(subscribers, subscribedByProxy);
+ }
+
+ private ProxySubscriber getVetoProxySubscriber(Class eventClass, Object subscribedByProxy) {
+ List subscribers = getVetoSubscribers(eventClass);
+ return getProxySubscriber(subscribers, subscribedByProxy);
+ }
+
+ private ProxySubscriber getVetoProxySubscriber(String topic, Object subscribedByProxy) {
+ List subscribers = getVetoSubscribers(topic);
+ return getProxySubscriber(subscribers, subscribedByProxy);
+ }
+
+ private ProxySubscriber getVetoProxySubscriber(Pattern pattern, Object subscribedByProxy) {
+ List subscribers = getVetoSubscribers(pattern);
+ return getProxySubscriber(subscribers, subscribedByProxy);
+ }
+
+ private ProxySubscriber getProxySubscriber(List subscribers, Object subscribedByProxy) {
+ for (Iterator iter = subscribers.iterator(); iter.hasNext();) {
+ Object subscriber = iter.next();
+ if (subscriber instanceof WeakReference) {
+ WeakReference wr = (WeakReference) subscriber;
+ subscriber = wr.get();
+ }
+ if (subscriber instanceof ProxySubscriber) {
+ ProxySubscriber proxy = (ProxySubscriber) subscriber;
+ subscriber = proxy.getProxiedSubscriber();
+ if (subscriber == subscribedByProxy) {
+ return proxy;
+ }
+ }
+ }
+ return null;
+ }
+
+ /** @see EventService#publish(Object) */
+ public void publish(Object event) {
+ if (event == null) {
+ throw new IllegalArgumentException("Cannot publish null event.");
+ }
+ publish(event, null, null, getSubscribers(event.getClass()), getVetoSubscribers(event.getClass()), null);
+ }
+
+ /** @see EventService#publish(java.lang.reflect.Type, Object) */
+ public void publish(Type genericType, Object event) {
+ if (genericType == null) {
+ throw new IllegalArgumentException("genericType must not be null.");
+ }
+ if (event == null) {
+ throw new IllegalArgumentException("Cannot publish null event.");
+ }
+ publish(event, null, null, getSubscribers(genericType), null/*getVetoSubscribers(genericType)*/, null);
+ }
+
+ /** @see EventService#publish(String,Object) */
+ public void publish(String topicName, Object eventObj) {
+ publish(null, topicName, eventObj, getSubscribers(topicName), getVetoEventListeners(topicName), null);
+ }
+
+ /**
+ * All publish methods call this method. Extending classes only have to override this method to handle all
+ * publishing cases.
+ *
+ * @param event the event to publish, null if publishing on a topic
+ * @param topic if publishing on a topic, the topic to publish on, else null
+ * @param eventObj if publishing on a topic, the eventObj to publish, else null
+ * @param subscribers the subscribers to publish to - must be a snapshot copy
+ * @param vetoSubscribers the veto subscribers to publish to - must be a snapshot copy.
+ * @param callingStack the stack that called this publication, helpful for reporting errors on other threads
+ * @throws IllegalArgumentException if eh or o is null
+ */
+ protected void publish(final Object event, final String topic, final Object eventObj,
+ final List subscribers, final List vetoSubscribers, StackTraceElement[] callingStack) {
+
+ if (event == null && topic == null) {
+ throw new IllegalArgumentException("Can't publish to null topic/event.");
+ }
+
+ setStatus(PublicationStatus.Initiated, event, topic, eventObj);
+ //topic or event
+ logEvent(event, topic, eventObj);
+
+ //Check all veto subscribers, if any veto, then don't publish or cache
+ if (checkVetoSubscribers(event, topic, eventObj, vetoSubscribers, callingStack)) {
+ setStatus(PublicationStatus.Vetoed, event, topic, eventObj);
+ return;
+ } else {
+ setStatus(PublicationStatus.Queued, event, topic, eventObj);
+ }
+
+ addEventToCache(event, topic, eventObj);
+
+ if (subscribers == null || subscribers.isEmpty()) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("No subscribers for event or topic. Event:" + event + ", Topic:" + topic);
+ }
+ } else {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Publishing to subscribers:" + subscribers);
+ }
+ setStatus(PublicationStatus.Publishing, event, topic, eventObj);
+ for (int i = 0; i < subscribers.size(); i++) {
+ Object eh = subscribers.get(i);
+ if (event != null) {
+ EventSubscriber eventSubscriber = (EventSubscriber) eh;
+ long start = System.currentTimeMillis();
+ try {
+ eventSubscriber.onEvent(event);
+ } catch (Throwable e) {
+ handleException(event, e, callingStack, eventSubscriber);
+ }
+ } else {
+ EventTopicSubscriber eventTopicSubscriber = (EventTopicSubscriber) eh;
+ try {
+ eventTopicSubscriber.onEvent(topic, eventObj);
+ } catch (Throwable e) {
+ onEventException(topic, eventObj, e, callingStack, eventTopicSubscriber);
+ }
+ }
+ }
+ }
+ setStatus(PublicationStatus.Completed, event, topic, eventObj);
+ }
+
+ /**
+ * Called during publication to set the status on an event. Can be used by subclasses
+ * to be notified when an event transitions from one state to another. Implementers
+ * are required to call setPublicationStatus
+ * @param status the status to set on the object
+ * @param event the event being published, will be null if topic is not null
+ * @param topic the topic eventObj is being published on, will be null if event is not null
+ * @param eventObj the payload being published on the topic , will be null if event is not null
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ protected void setStatus(PublicationStatus status, Object event, String topic, Object eventObj) {
+ if (event instanceof PublicationStatusTracker) {
+ ((PublicationStatusTracker)event).setPublicationStatus(status);
+ }
+ if (eventObj instanceof PublicationStatusTracker) {
+ ((PublicationStatusTracker)eventObj).setPublicationStatus(status);
+ }
+ }
+
+ /**
+ * Handles subscribers that are Prioritized by putting the most negative prioritized subscribers
+ * first, the most positive prioritized subscribers last, and leaving non-Prioritized in their
+ * original FIFO order.
+ * @param subscribers the subscribers to sort
+ * @return the same list if there are no prioritized subscribers in the list, otherwise a new sorted result
+ */
+ private List sortSubscribers(List subscribers) {
+ if (subscribers == null) {
+ return null;
+ }
+ List prioritizedSubscribers = null;
+ Iterator iterator = subscribers.iterator();
+ while (iterator.hasNext()) {
+ Object subscriber = iterator.next();
+ if (subscriber instanceof Prioritized) {
+ Prioritized prioritized = ((Prioritized)subscriber);
+ if (prioritized.getPriority() != 0) {
+ iterator.remove();
+ if (prioritizedSubscribers == null) {
+ prioritizedSubscribers = new ArrayList();
+ }
+ prioritizedSubscribers.add(prioritized);
+ }
+ }
+ }
+ if (prioritizedSubscribers == null) {
+ return subscribers;
+ } else {
+ List result = new ArrayList(prioritizedSubscribers.size()+subscribers.size());
+ Collections.sort(prioritizedSubscribers, PRIORITIZED_SUBSCRIBER_COMPARATOR);
+ boolean haveAddedFIFOSubscribers = false;
+ for (Prioritized prioritizedSubscriber : prioritizedSubscribers) {
+ if (prioritizedSubscriber.getPriority() > 0 && !haveAddedFIFOSubscribers) {
+ for (Object subscriber : subscribers) {
+ result.add(subscriber);
+ }
+ haveAddedFIFOSubscribers = true;
+ }
+ result.add(prioritizedSubscriber);
+ }
+ //Issue 26 - of all priorities are negative, then add the FIFO after processing all of them
+ if (!haveAddedFIFOSubscribers) {
+ for (Object subscriber : subscribers) {
+ result.add(subscriber);
+ }
+ }
+ return result;
+ }
+ }
+
+ private boolean checkVetoSubscribers(Object event, String topic, Object eventObj, List vetoSubscribers,
+ StackTraceElement[] callingStack) {
+ if (vetoSubscribers != null && !vetoSubscribers.isEmpty()) {
+ for (Iterator vlIter = vetoSubscribers.iterator(); vlIter.hasNext();) {
+ Object vetoer = vlIter.next();
+ VetoEventListener vl = null;
+ VetoTopicEventListener vtl = null;
+ if (event == null) {
+ vtl = (VetoTopicEventListener) vetoer;
+ } else {
+ vl = (VetoEventListener) vetoer;
+ }
+ long start = System.currentTimeMillis();
+ try {
+ boolean shouldVeto = false;
+ if (event == null) {
+ shouldVeto = vtl.shouldVeto(topic, eventObj);
+ } else {
+ shouldVeto = vl.shouldVeto(event);
+ }
+ if (shouldVeto) {
+ handleVeto(vl, event, vtl, topic, eventObj);
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Publication vetoed. Event:" + event + ", Topic:" + topic + ", veto subscriber:" + vl);
+ }
+ return true;
+ }
+ } catch (Throwable ex) {
+ subscribeVetoException(event, topic, eventObj, ex, callingStack, vl);
+ }
+ }
+ }
+ return false;
+ }
+
+ private void logEvent(Object event, String topic, Object eventObj) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ if (event != null) {
+ LOG.debug("Publishing event: class=" + event.getClass() + ", event=" + event);
+ } else if (topic != null) {
+ LOG.debug("Publishing event: topic=" + topic + ", eventObj=" + eventObj);
+ }
+ }
+ }
+
+ /**
+ * Adds an event to the event cache, if appropriate. This method is called just before publication to listeners,
+ * after the event passes any veto listeners.
+ *
+ * Using protected visibility to open the caching to other implementations.
+ *
+ *
+ * @param event the event about to be published, null if topic is non-null
+ * @param topic the topic about to be published to, null if the event is non-null
+ * @param eventObj the eventObj about to be published on a topic, null if the event is non-null
+ */
+ protected void addEventToCache(Object event, String topic, Object eventObj) {
+ //Taking the listener lock here, since a listener that is now subscribing will want
+ //this event since they are not in this subscriber list.
+ synchronized (listenerLock) {
+ if (event != null) {
+ int cacheSizeForEventClass = getCacheSizeForEventClass(event.getClass());
+ List eventClassCache = (List) cacheByEvent.get(event.getClass());
+ if (cacheSizeForEventClass <= 0) {
+ if (eventClassCache != null) {
+ //the cache threshold was lowered to 0
+ cacheByEvent.remove(event.getClass());
+ }
+ } else {
+ if (eventClassCache == null) {
+ eventClassCache = new LinkedList();
+ cacheByEvent.put(event.getClass(), eventClassCache);
+ }
+ eventClassCache.add(0, event);
+ while (eventClassCache.size() > cacheSizeForEventClass) {
+ eventClassCache.remove(eventClassCache.size() - 1);
+ }
+ }
+ } else {
+ //topic
+ int cacheSizeForTopic = getCacheSizeForTopic(topic);
+ List topicCache = (List) cacheByTopic.get(topic);
+ if (cacheSizeForTopic <= 0) {
+ if (topicCache != null) {
+ //the cache threshold was lowered to 0
+ topicCache.remove(topic);
+ }
+ } else {
+ if (topicCache == null) {
+ topicCache = new LinkedList();
+ cacheByTopic.put(topic, topicCache);
+ }
+ topicCache.add(0, eventObj);
+ while (topicCache.size() > cacheSizeForTopic) {
+ topicCache.remove(topicCache.size() - 1);
+ }
+ }
+ }
+ }
+ }
+
+ /** @see EventService#getSubscribers(Class) */
+ public List getSubscribers(Class eventClass) {
+ List hierarchyMatches;
+ List exactMatches;
+ synchronized (listenerLock) {
+ hierarchyMatches = getSubscribersToClass(eventClass);
+ exactMatches = getSubscribersToExactClass(eventClass);
+ }
+ List result = new ArrayList();
+ if (exactMatches != null) {
+ result.addAll(exactMatches);
+ }
+ if (hierarchyMatches != null) {
+ result.addAll(hierarchyMatches);
+ }
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+
+ }
+
+ /** @see EventService#getSubscribersToClass(Class) */
+ public List getSubscribersToClass(Class eventClass) {
+ synchronized (listenerLock) {
+ Map classMap = subscribersByEventClass;
+ List result = getEventOrVetoSubscribersToClass(classMap, eventClass);
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+ }
+ }
+
+ /** @see EventService#getSubscribersToExactClass(Class) */
+ public List getSubscribersToExactClass(Class eventClass) {
+ synchronized (listenerLock) {
+ return getSubscribers(eventClass, subscribersByExactEventClass);
+ }
+ }
+
+ /** @see EventService#getSubscribers(Type) */
+ public List getSubscribers(Type eventType) {
+ List result;
+ synchronized (listenerLock) {
+ result = getEventOrVetoSubscribersToType(subscribersByEventType, eventType);
+ }
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+ }
+
+ /** @see EventService#getSubscribers(String) */
+ public List getSubscribers(String topic) {
+ List result = new ArrayList();
+ List exactMatches;
+ List patternMatches;
+ synchronized (listenerLock) {
+ exactMatches = getSubscribersToTopic(topic);
+ patternMatches = getSubscribersByPattern(topic);
+ }
+ if (exactMatches != null) {
+ result.addAll(exactMatches);
+ }
+ if (patternMatches != null) {
+ result.addAll(patternMatches);
+ }
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+ }
+
+ /** @see EventService#getSubscribersToTopic(String) */
+ public List getSubscribersToTopic(String topic) {
+ synchronized (listenerLock) {
+ return getSubscribers(topic, subscribersByTopic);
+ }
+ }
+
+ /** @see EventService#getSubscribers(Pattern) */
+ public List getSubscribers(Pattern pattern) {
+ synchronized (listenerLock) {
+ return getSubscribers(pattern, subscribersByTopicPattern);
+ }
+ }
+
+ /** @see EventService#getSubscribersByPattern(String) */
+ public List getSubscribersByPattern(String topic) {
+ return getSubscribersByPattern(topic, subscribersByTopicPattern);
+ }
+
+ /** @see EventService#getVetoSubscribers(Class) */
+ public List getVetoSubscribers(Class eventClass) {
+ List result = new ArrayList();
+ List exactMatches;
+ List hierarchyMatches;
+ synchronized (listenerLock) {
+ exactMatches = getVetoSubscribersToClass(eventClass);
+ hierarchyMatches = getVetoSubscribersToExactClass(eventClass);
+ }
+ if (exactMatches != null) {
+ result.addAll(exactMatches);
+ }
+ if (hierarchyMatches != null) {
+ result.addAll(hierarchyMatches);
+ }
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+ }
+
+ /** @see EventService#getVetoSubscribersToClass(Class) */
+ public List getVetoSubscribersToClass(Class eventClass) {
+ List result;
+ synchronized (listenerLock) {
+ Map classMap = vetoListenersByClass;
+ result = getEventOrVetoSubscribersToClass(classMap, eventClass);
+ }
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+ }
+
+ /** @see EventService#getVetoSubscribersToExactClass(Class) */
+ public List getVetoSubscribersToExactClass(Class eventClass) {
+ synchronized (listenerLock) {
+ return getSubscribers(eventClass, vetoListenersByExactClass);
+ }
+ }
+
+ /** @see EventService#getVetoEventListeners(String) */
+ public List getVetoEventListeners(String topicOrPattern) {
+ List result = new ArrayList();
+ List exactMatches;
+ List patternMatches;
+ synchronized (listenerLock) {
+ exactMatches = getVetoSubscribersToTopic(topicOrPattern);
+ patternMatches = getVetoSubscribersByPattern(topicOrPattern);
+ }
+ if (exactMatches != null) {
+ result.addAll(exactMatches);
+ }
+ if (patternMatches != null) {
+ result.addAll(patternMatches);
+ }
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+ }
+
+ /** @see EventService#getVetoSubscribersToTopic(String) */
+ public List getVetoSubscribersToTopic(String topic) {
+ synchronized (listenerLock) {
+ return getSubscribers(topic, vetoListenersByTopic);
+ }
+ }
+
+ /**
+ * Note: this is inconsistent with getSubscribers(String)
+ * @see EventService#getVetoSubscribersToTopic(String)
+ * @deprecated use getVetoSubscribersToTopic instead for direct replacement,
+ * or use getVetoEventListeners to get topic and pattern matchers.
+ * In EventBus 2.0 this name will replace getVetoEventListeners()
+ * and have it's union functionality
+ */
+ public List getVetoSubscribers(String topic) {
+ synchronized (listenerLock) {
+ return getVetoSubscribersToTopic(topic);
+ }
+ }
+
+ /** @see EventService#getVetoSubscribers(Pattern) */
+ public List getVetoSubscribers(Pattern topicPattern) {
+ synchronized (listenerLock) {
+ PatternWrapper patternWrapper = new PatternWrapper(topicPattern);
+ return getSubscribers(patternWrapper, vetoListenersByTopicPattern);
+ }
+ }
+
+ /** @see EventService#getVetoSubscribersByPattern(String) */
+ public List getVetoSubscribersByPattern(String pattern) {
+ return getSubscribersByPattern(pattern, vetoListenersByTopicPattern);
+ }
+
+ /** Used for subscribers and veto subscribers */
+ private List getSubscribersByPattern(String topic, Map subscribersByTopicPattern) {
+ List result = new ArrayList();
+ synchronized (listenerLock) {
+ Set keys = subscribersByTopicPattern.keySet();
+ for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
+ PatternWrapper patternKey = (PatternWrapper) iterator.next();
+ if (patternKey.matches(topic)) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Pattern " + patternKey + " matched topic name " + topic);
+ }
+ Collection subscribers = (Collection) subscribersByTopicPattern.get(patternKey);
+ result.addAll(createCopyOfContentsRemoveWeakRefs(subscribers));
+ }
+ }
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+ }
+ }
+
+ protected List getSubscribersToPattern(Pattern topicPattern) {
+ synchronized (listenerLock) {
+ PatternWrapper patternWrapper = new PatternWrapper(topicPattern);
+ return getSubscribers(patternWrapper, subscribersByTopicPattern);
+ }
+ }
+
+ private List getSubscribers(Object classOrTopic, Map subscriberMap) {
+ List result;
+ synchronized (listenerLock) {
+ List subscribers = (List) subscriberMap.get(classOrTopic);
+ //Make a defensive copy of subscribers and veto listeners so listeners
+ //can change the listener list while the listeners are being called
+ //Resolve WeakReferences and unsubscribe if necessary.
+ result = createCopyOfContentsRemoveWeakRefs(subscribers);
+ }
+ if (hasEverUsedPrioritized) {
+ result = sortSubscribers(result);
+ }
+ return result;
+ }
+
+ private List getEventOrVetoSubscribersToClass(Map classMap, Class eventClass) {
+ List result = new ArrayList();
+ Set keys = classMap.keySet();
+ for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
+ Class cl = (Class) iterator.next();
+ if (cl.isAssignableFrom(eventClass)) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Hierarchical match " + cl + " matched event of class " + eventClass);
+ }
+ Collection subscribers = (Collection) classMap.get(cl);
+ result.addAll(createCopyOfContentsRemoveWeakRefs(subscribers));
+ }
+ }
+ return result;
+ }
+
+ private List getEventOrVetoSubscribersToType(Map typeMap, Type eventType) {
+ List result = new ArrayList();
+ Set mapKeySet = typeMap.keySet();
+ for (Object mapKey : mapKeySet) {
+ Type subscriberType = (Type) mapKey;
+ if (eventType instanceof ParameterizedType && subscriberType instanceof ParameterizedType) {
+ ParameterizedType subscriberPT = (ParameterizedType) subscriberType;
+ ParameterizedType eventPT = (ParameterizedType) eventType;
+ if (eventPT.getRawType().equals(subscriberPT.getRawType())) {
+ Type[] mapTypeArgs = subscriberPT.getActualTypeArguments();
+ Type[] eventTypeArgs = eventPT.getActualTypeArguments();
+ if (mapTypeArgs == null || eventTypeArgs == null || mapTypeArgs.length != eventTypeArgs.length) {
+ continue;
+ }
+ boolean parameterArgsMatch = true;
+ for (int argCount = 0; argCount < mapTypeArgs.length; argCount++) {
+ Type eventTypeArg = eventTypeArgs[argCount];
+ if (eventTypeArg instanceof WildcardType) {
+ throw new IllegalArgumentException("Only simple Class parameterized types can be published, not wildcards, etc. Published attempt made for:"+eventTypeArg);
+ }
+ Type subscriberTypeArg = mapTypeArgs[argCount];
+ if (subscriberTypeArg instanceof WildcardType) {
+ WildcardType wildcardSubscriberTypeArg = (WildcardType) subscriberTypeArg;
+ Type[] upperBound = wildcardSubscriberTypeArg.getUpperBounds();
+ Type[] lowerBound = wildcardSubscriberTypeArg.getLowerBounds();
+ if (upperBound != null && upperBound.length > 0) {
+ if (upperBound[0] instanceof Class) {
+ Class upper = (Class) upperBound[0];
+ if (eventTypeArg instanceof Class) {
+ if (!upper.isAssignableFrom((Class) eventTypeArg)) {
+ parameterArgsMatch = false;
+ break;
+ }
+ } else {
+ parameterArgsMatch = false;
+ break;
+ }
+ } else {
+ throw new IllegalArgumentException("Only Class and Interface types are supported as types of wildcard subscriptions. Type:"+upperBound[0]);
+ }
+ }
+ if (lowerBound != null && lowerBound.length > 0) {
+ if (lowerBound[0] instanceof Class) {
+ Class lower = (Class) lowerBound[0];
+ if (eventTypeArg instanceof Class) {
+ if (!((Class)eventTypeArg).isAssignableFrom(lower)) {
+ parameterArgsMatch = false;
+ break;
+ }
+ } else {
+ parameterArgsMatch = false;
+ break;
+ }
+ } else {
+ throw new IllegalArgumentException("Only Class and Interface types are supported as types of wildcard subscriptions. Type:"+upperBound[0]);
+ }
+ }
+ } else if (!subscriberTypeArg.equals(eventTypeArg)) {
+ parameterArgsMatch = false;
+ break;
+ }
+ }
+ if (parameterArgsMatch) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ LOG.debug("Exact parameterized subscriberType match for event subscriberType " + eventType);
+ }
+ Collection subscribers = (Collection) typeMap.get(subscriberType);
+ if (subscribers != null) {
+ result.addAll(createCopyOfContentsRemoveWeakRefs(subscribers));
+ }
+ }
+ }
+ }
+ }
+ return result;
+// Type o = p.getOwnerType();
+// if (o != null) {
+//
+// }
+// p.getActualTypeArguments();
+// }
+ /*
+ } else if (type instanceof TypeVariable>) {
+ TypeVariable> v = (TypeVariable>)type;
+ out.print(v.getName());
+ } else if (type instanceof GenericArrayType) {
+ GenericArrayType a = (GenericArrayType)type;
+ printType(a.getGenericComponentType());
+ out.print("[]");
+ } else if (type instanceof WildcardType) {
+ WildcardType w = (WildcardType)type;
+ Type[] upper = w.getUpperBounds();
+ Type[] lower = w.getLowerBounds();
+ if (upper.length==1 && lower.length==0) {
+ out.print("? extends ");
+ printType(upper[0]);
+ } else if (upper.length==0 && lower.length==1) {
+ out.print("? super ");
+ printType(lower[0]);
+ } else assert false;
+ }
+ */
+ }
+
+ /**
+ * Handle vetos of an event or topic, by default logs finely.
+ *
+ * @param vl the veto listener for an event
+ * @param event the event, can be null if topic is not
+ * @param vtl the veto listener for a topic
+ * @param topic can be null if event is not
+ * @param eventObj the object published with the topic
+ */
+ protected void handleVeto(VetoEventListener vl, Object event,
+ VetoTopicEventListener vtl, String topic, Object eventObj) {
+ if (LOG.isLoggable(Level.DEBUG)) {
+ if (event != null) {
+ LOG.debug("Vetoing event: class=" + event.getClass() + ", event=" + event + ", vetoer:" + vl);
+ } else {
+ LOG.debug("Vetoing event: topic=" + topic + ", eventObj=" + eventObj + ", vetoer:" + vtl);
+ }
+ }
+ }
+
+ /**
+ * Given a Map (of Lists of subscribers or veto listeners), removes the toRemove element from the List in the map for
+ * the given key. The entire map is checked for WeakReferences and ProxySubscribers and they are all unsubscribed
+ * if stale.
+ *
+ * @param map map of lists
+ * @param key key for a List in the map
+ * @param toRemove the object to remove form the list with the key of the map
+ *
+ * @return true if toRemove was unsubscribed
+ */
+ private boolean removeFromSetResolveWeakReferences(Map map, Object key, Object toRemove) {
+ List subscribers = (List) map.get(key);
+ if (subscribers == null) {
+ return false;
+ }
+ if (subscribers.remove(toRemove)) {
+ if (toRemove instanceof WeakReference) {
+ decWeakRefPlusProxySubscriberCount();
+ }
+ if (toRemove instanceof ProxySubscriber) {
+ ((ProxySubscriber)toRemove).proxyUnsubscribed();
+ decWeakRefPlusProxySubscriberCount();
+ }
+ return true;
+ }
+
+ //search for WeakReferences and ProxySubscribers
+ for (Iterator iter = subscribers.iterator(); iter.hasNext();) {
+ Object existingSubscriber = iter.next();
+ if (existingSubscriber instanceof ProxySubscriber) {
+ ProxySubscriber proxy = (ProxySubscriber) existingSubscriber;
+ existingSubscriber = proxy.getProxiedSubscriber();
+ if (existingSubscriber == toRemove) {
+ removeProxySubscriber(proxy, iter);
+ return true;
+ }
+ }
+ if (existingSubscriber instanceof WeakReference) {
+ WeakReference wr = (WeakReference) existingSubscriber;
+ Object realRef = wr.get();
+ if (realRef == null) {
+ //clean up a garbage collected reference
+ iter.remove();
+ decWeakRefPlusProxySubscriberCount();
+ return true;
+ } else if (realRef == toRemove) {
+ iter.remove();
+ decWeakRefPlusProxySubscriberCount();
+ return true;
+ } else if (realRef instanceof ProxySubscriber) {
+ ProxySubscriber proxy = (ProxySubscriber) realRef;
+ existingSubscriber = proxy.getProxiedSubscriber();
+ if (existingSubscriber == toRemove) {
+ removeProxySubscriber(proxy, iter);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Given a set (or subscribers or veto listeners), makes a copy of the set, resolving WeakReferences to hard
+ * references, and removing garbage collected references from the original set.
+ *
+ * @param subscribersOrVetoListeners
+ *
+ * @return a copy of the set
+ */
+ private List createCopyOfContentsRemoveWeakRefs(Collection subscribersOrVetoListeners) {
+ if (subscribersOrVetoListeners == null) {
+ return null;
+ }
+ List copyOfSubscribersOrVetolisteners = new ArrayList(subscribersOrVetoListeners.size());
+ for (Iterator iter = subscribersOrVetoListeners.iterator(); iter.hasNext();) {
+ Object elem = iter.next();
+ if (elem instanceof ProxySubscriber) {
+ ProxySubscriber proxy = (ProxySubscriber)elem;
+ elem = proxy.getProxiedSubscriber();
+ if (elem == null) {
+ removeProxySubscriber(proxy, iter);
+ } else {
+ copyOfSubscribersOrVetolisteners.add(proxy);
+ }
+ } else if (elem instanceof WeakReference) {
+ Object hardRef = ((WeakReference) elem).get();
+ if (hardRef == null) {
+ //Was reclaimed, unsubscribe
+ iter.remove();
+ decWeakRefPlusProxySubscriberCount();
+ } else {
+ copyOfSubscribersOrVetolisteners.add(hardRef);
+ }
+ } else {
+ copyOfSubscribersOrVetolisteners.add(elem);
+ }
+ }
+ return copyOfSubscribersOrVetolisteners;
+ }
+
+ /**
+ * Sets the default cache size for each kind of event, default is 0 (no caching).
+ *
+ * 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
+ */
+ public void setDefaultCacheSizePerClassOrTopic(int defaultCacheSizePerClassOrTopic) {
+ synchronized (cacheLock) {
+ this.defaultCacheSizePerClassOrTopic = defaultCacheSizePerClassOrTopic;
+ }
+ }
+
+ /** @return the default number of event payloads kept per event class or topic */
+ public int getDefaultCacheSizePerClassOrTopic() {
+ synchronized (cacheLock) {
+ return defaultCacheSizePerClassOrTopic;
+ }
+ }
+
+ /**
+ * 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) {
+ synchronized (cacheLock) {
+ if (rawCacheSizesForEventClass == null) {
+ rawCacheSizesForEventClass = new HashMap();
+ }
+ rawCacheSizesForEventClass.put(eventClass, new Integer(cacheSize));
+ rawCacheSizesForEventClassChanged = true;
+ }
+ }
+
+ /**
+ * 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) {
+ if (eventClass == null) {
+ throw new IllegalArgumentException("eventClass must not be null.");
+ }
+ synchronized (cacheLock) {
+ if (rawCacheSizesForEventClass == null || rawCacheSizesForEventClass.size() == 0) {
+ return getDefaultCacheSizePerClassOrTopic();
+ }
+ if (cacheSizesForEventClass == null) {
+ cacheSizesForEventClass = new HashMap();
+ }
+ if (rawCacheSizesForEventClassChanged) {
+ cacheSizesForEventClass.clear();
+ cacheSizesForEventClass.putAll(rawCacheSizesForEventClass);
+ rawCacheSizesForEventClassChanged = false;
+ }
+
+ //Has this been computed yet or set directly?
+ Integer size = (Integer) cacheSizesForEventClass.get(eventClass);
+ if (size != null) {
+ return size.intValue();
+ } else {
+ //must be computed
+ Class parent = eventClass.getSuperclass();
+ while (parent != null) {
+ Integer parentSize = (Integer) cacheSizesForEventClass.get(parent);
+ if (parentSize != null) {
+ cacheSizesForEventClass.put(eventClass, parentSize);
+ return parentSize.intValue();
+ }
+ parent = parent.getSuperclass();
+ }
+ //try interfaces
+ Class[] interfaces = eventClass.getInterfaces();
+ for (int i = 0; i < interfaces.length; i++) {
+ Class anInterface = interfaces[i];
+ Integer interfaceSize = (Integer) cacheSizesForEventClass.get(anInterface);
+ if (interfaceSize != null) {
+ cacheSizesForEventClass.put(eventClass, interfaceSize);
+ return interfaceSize.intValue();
+ }
+ }
+ }
+ return getDefaultCacheSizePerClassOrTopic();
+ }
+ }
+
+ /**
+ * Set the number of published data objects cached for a particular event topic. By default, no caching is done.
+ *
+ * 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.
+ *
+ *
+ * @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) {
+ synchronized (cacheLock) {
+ if (rawCacheSizesForTopic == null) {
+ rawCacheSizesForTopic = new HashMap();
+ }
+ rawCacheSizesForTopic.put(topicName, new Integer(cacheSize));
+ rawCacheSizesForTopicChanged = true;
+ }
+ }
+
+ /**
+ * Set the number of published data objects cached for topics matching a pattern. By default, caching is done.
+ *
+ * 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.
+ *
+ *
+ * @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) {
+ synchronized (cacheLock) {
+ if (rawCacheSizesForPattern == null) {
+ rawCacheSizesForPattern = new HashMap();
+ }
+ PatternWrapper patternWrapper = new PatternWrapper(pattern);
+ rawCacheSizesForPattern.put(patternWrapper, new Integer(cacheSize));
+ rawCacheSizesForPatternChanged = true;
+ }
+ }
+
+ /**
+ * Returns the number of cached data objects published on a particular topic. By default, no caching is performed.
+ *
+ * This result is computed for a particular topic from the values passed to #setCacheSizeForTopic(String, int) and
+ * #setCacheSizeForTopic(Pattern, int).
+ *
+ *
+ * @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) {
+ if (topic == null) {
+ throw new IllegalArgumentException("topic must not be null.");
+ }
+ synchronized (cacheLock) {
+ if ((rawCacheSizesForTopic == null || (rawCacheSizesForTopic != null && rawCacheSizesForTopic.size() == 0)) &&
+ (rawCacheSizesForPattern == null || (rawCacheSizesForPattern != null && rawCacheSizesForPattern.size() == 0))) {
+ return getDefaultCacheSizePerClassOrTopic();
+ }
+ if (cacheSizesForTopic == null) {
+ cacheSizesForTopic = new HashMap();
+ }
+ if (rawCacheSizesForTopicChanged || rawCacheSizesForPatternChanged) {
+ cacheSizesForTopic.clear();
+ cacheSizesForTopic.putAll(rawCacheSizesForTopic);
+ rawCacheSizesForTopicChanged = false;
+ rawCacheSizesForPatternChanged = false;
+ }
+
+ //Is this an exact match or has it been matched to a pattern yet?
+ Integer size = cacheSizesForTopic.get(topic);
+ if (size != null) {
+ return size;
+ } else {
+ //try matching patterns
+ if (rawCacheSizesForPattern != null) {
+ Set patterns = rawCacheSizesForPattern.keySet();
+ for (Iterator iterator = patterns.iterator(); iterator.hasNext();) {
+ PatternWrapper pattern = (PatternWrapper) iterator.next();
+ if (pattern.matches(topic)) {
+ size = rawCacheSizesForPattern.get(pattern);
+ cacheSizesForTopic.put(topic, size);
+ return size;
+ }
+ }
+ }
+ }
+ return getDefaultCacheSizePerClassOrTopic();
+ }
+ }
+
+ /**
+ * @param eventClass an index into the cache, cannot be an interface
+ *
+ * @return the last event published for this event class, or null if caching is turned off (the default)
+ */
+ public Object getLastEvent(Class eventClass) {
+ if (eventClass.isInterface()) {
+ throw new IllegalArgumentException("Interfaces are not accepted in get last event, use a specific event class.");
+ }
+ synchronized (cacheLock) {
+ List eventCache = cacheByEvent.get(eventClass);
+ if (eventCache == null || eventCache.size() == 0) {
+ return null;
+ }
+ return eventCache.get(0);
+ }
+ }
+
+ /**
+ * @param eventClass an index into the cache, cannot be an interface
+ *
+ * @return the last events published for this event class, or null if caching is turned off (the default)
+ */
+ public List getCachedEvents(Class eventClass) {
+ if (eventClass.isInterface()) {
+ throw new IllegalArgumentException("Interfaces are not accepted in get last event, use a specific event class.");
+ }
+ synchronized (cacheLock) {
+ List eventCache = cacheByEvent.get(eventClass);
+ if (eventCache == null || eventCache.size() == 0) {
+ return null;
+ }
+ return eventCache;
+ }
+ }
+
+ /**
+ * @param topic an index into the cache
+ *
+ * @return the last data Object published on this topic, or null if caching is turned off (the default)
+ */
+ public Object getLastTopicData(String topic) {
+ synchronized (cacheLock) {
+ List topicCache = cacheByTopic.get(topic);
+ if (topicCache == null || topicCache.size() == 0) {
+ return null;
+ }
+ return topicCache.get(0);
+ }
+ }
+
+ /**
+ * @param topic an index into the cache
+ *
+ * @return the last data Objects published on this topic, or null if caching is turned off (the default)
+ */
+ public List getCachedTopicData(String topic) {
+ synchronized (cacheLock) {
+ List topicCache = cacheByTopic.get(topic);
+ if (topicCache == null || topicCache.size() == 0) {
+ return null;
+ }
+ return topicCache;
+ }
+ }
+
+ /**
+ * Clears the event cache for a specific event class or interface and it's any of it's subclasses or implementing
+ * classes.
+ *
+ * @param eventClassToClear the event class to clear the cache for
+ */
+ public void clearCache(Class eventClassToClear) {
+ synchronized (cacheLock) {
+ Set classes = cacheByEvent.keySet();
+ for (Iterator iterator = classes.iterator(); iterator.hasNext();) {
+ Class cachedClass = (Class) iterator.next();
+ if (eventClassToClear.isAssignableFrom(cachedClass)) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * Clears the topic data cache for a specific topic name.
+ *
+ * @param topic the topic name to clear the cache for
+ */
+ public void clearCache(String topic) {
+ synchronized (cacheLock) {
+ cacheByTopic.remove(topic);
+ }
+ }
+
+ /**
+ * Clears the topic data cache for all topics that match a particular pattern.
+ *
+ * @param pattern the pattern to match topic caches to
+ */
+ public void clearCache(Pattern pattern) {
+ synchronized (cacheLock) {
+ Set classes = cacheByTopic.keySet();
+ for (Iterator iterator = classes.iterator(); iterator.hasNext();) {
+ String cachedTopic = (String) iterator.next();
+ if (pattern.matcher(cachedTopic).matches()) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ /** Clear all event caches for all topics and event. */
+ public void clearCache() {
+ synchronized (cacheLock) {
+ cacheByEvent.clear();
+ cacheByTopic.clear();
+ }
+ }
+
+ /** Called during veto exceptions, calls handleException */
+ protected void subscribeVetoException(final Object event, final String topic, final Object eventObj,
+ Throwable e, StackTraceElement[] callingStack, VetoEventListener vetoer) {
+ String str = "EventService veto event listener r:" + vetoer;
+ if (vetoer != null) {
+ str = str + ". Vetoer class:" + vetoer.getClass();
+ }
+ handleException("vetoing", event, topic, eventObj, e, callingStack, str);
+ }
+
+ /** Called during event handling exceptions, calls handleException */
+ protected void onEventException(final String topic, final Object eventObj, Throwable e,
+ StackTraceElement[] callingStack, EventTopicSubscriber eventTopicSubscriber) {
+ String str = "EventService topic subscriber:" + eventTopicSubscriber;
+ if (eventTopicSubscriber != null) {
+ str = str + ". Subscriber class:" + eventTopicSubscriber.getClass();
+ }
+ handleException("handling event", null, topic, eventObj, e, callingStack, str);
+ }
+
+ /** Called during event handling exceptions, calls handleException */
+ protected void handleException(final Object event, Throwable e,
+ StackTraceElement[] callingStack, EventSubscriber eventSubscriber) {
+ String str = "EventService subscriber:" + eventSubscriber;
+ if (eventSubscriber != null) {
+ str = str + ". Subscriber class:" + eventSubscriber.getClass();
+ }
+ handleException("handling event topic", event, null, null, e, callingStack, str);
+ }
+
+ /**
+ * All exception handling goes through this method. Logs a warning by default.
+ */
+ protected void handleException(final String action, final Object event, final String topic,
+ final Object eventObj, Throwable e, StackTraceElement[] callingStack, String sourceString) {
+ String eventClassString = (event == null ? "none" : event.getClass().getName());
+ String eventString = event + "";
+ String contextMsg = "Exception " + action + " event class=" + eventClassString
+ + ", event=" + eventString + ", topic=" + topic + ", eventObj=" + eventObj;
+ SwingException clientEx = new SwingException(contextMsg, e, callingStack);
+ String msg = "Exception thrown by;" + sourceString;
+ LOG.log(Level.WARN, msg, clientEx);
+ }
+
+ /**
+ * Unsubscribe a subscriber if it is a stale ProxySubscriber. Used during subscribe() and
+ * in the cleanup Timer. See the class javadoc.
+ *
+ * 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?).
+ *
+ * @param iterator current iterator
+ * @param existingSubscriber the current value of the iterator
+ * @return the real value of the param, or the proxied subscriber of the param if
+ * the param is a a ProxySubscriber
+ */
+ protected Object getRealSubscriberAndCleanStaleSubscriberIfNecessary(Iterator iterator, Object existingSubscriber) {
+ ProxySubscriber existingProxySubscriber = null;
+ if (existingSubscriber instanceof WeakReference) {
+ existingSubscriber = ((WeakReference) existingSubscriber).get();
+ if (existingSubscriber == null) {
+ iterator.remove();
+ decWeakRefPlusProxySubscriberCount();
+ }
+ }
+ if (existingSubscriber instanceof ProxySubscriber) {
+ existingProxySubscriber = (ProxySubscriber) existingSubscriber;
+ existingSubscriber = existingProxySubscriber.getProxiedSubscriber();
+ if (existingProxySubscriber == null) {
+ removeProxySubscriber(existingProxySubscriber, iterator);
+ }
+ }
+ return existingSubscriber;
+ }
+
+ protected void removeProxySubscriber(ProxySubscriber proxy, Iterator iter) {
+ iter.remove();
+ proxy.proxyUnsubscribed();
+ decWeakRefPlusProxySubscriberCount();
+ }
+
+ /**
+ * Increment the count of stale proxies and start a cleanup task if necessary
+ */
+ protected void incWeakRefPlusProxySubscriberCount() {
+ synchronized(listenerLock) {
+ weakRefPlusProxySubscriberCount++;
+ if (cleanupStartThreshhold == null || cleanupPeriodMS == null) {
+ return;
+ }
+ if (weakRefPlusProxySubscriberCount >= cleanupStartThreshhold) {
+ startCleanup();
+ }
+ }
+ }
+
+ /**
+ * Decrement the count of stale proxies
+ */
+ protected void decWeakRefPlusProxySubscriberCount() {
+ synchronized(listenerLock) {
+ weakRefPlusProxySubscriberCount--;
+ if (weakRefPlusProxySubscriberCount < 0) {
+ weakRefPlusProxySubscriberCount = 0;
+ }
+ }
+ }
+
+ private void startCleanup() {
+ synchronized(listenerLock) {
+ if (cleanupTimer == null) {
+ cleanupTimer = new Timer(true);
+ }
+ if (cleanupTimerTask == null) {
+ cleanupTimerTask = new CleanupTimerTask();
+ cleanupTimer.schedule(cleanupTimerTask, 0L, cleanupPeriodMS);
+ }
+ }
+ }
+
+ class CleanupTimerTask extends TimerTask {
+ @Override
+ public void run() {
+ synchronized(listenerLock) {
+ if (weakRefPlusProxySubscriberCount <= cleanupStopThreshold) {
+ this.cancel();
+ cleanupTimer = null;
+ cleanupTimerTask = null;
+ LOG.debug("Cancelled scheduled weak reference and proxy cleanup.");
+ return;
+ }
+ LOG.debug("Starting a weak reference and proxy cleanup.");
+ List
+ *
+ * @param event The event object to veto or allow to be published.
+ *
+ * @return true if the event should be vetoed and not published, false if the event should be published.
+ */
+ public boolean shouldVeto(T event);
+}
diff --git a/src/main/java/org/scijava/event/bushe/VetoTopicEventListener.java b/src/main/java/org/scijava/event/bushe/VetoTopicEventListener.java
new file mode 100644
index 000000000..f44d9693a
--- /dev/null
+++ b/src/main/java/org/scijava/event/bushe/VetoTopicEventListener.java
@@ -0,0 +1,26 @@
+package org.scijava.event.bushe;
+
+/**
+ * Interface for classes that can veto publication on topic names from the {@link org.scijava.event.bushe.EventService}.
+ *
+ * @author Michael Bushe michael@bushe.com
+ */
+interface VetoTopicEventListener {
+
+ /**
+ * Determine whether a topic publication should be vetoed or allowed.
+ *
+ * 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 c3976e9b8..73878ca15 100644
--- a/src/main/java/org/scijava/input/Accelerator.java
+++ b/src/main/java/org/scijava/input/Accelerator.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 5c485330c..392dc250d 100644
--- a/src/main/java/org/scijava/input/DefaultInputService.java
+++ b/src/main/java/org/scijava/input/DefaultInputService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 91fce4bc5..50dba7820 100644
--- a/src/main/java/org/scijava/input/InputModifiers.java
+++ b/src/main/java/org/scijava/input/InputModifiers.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 215256789..0c7adae95 100644
--- a/src/main/java/org/scijava/input/InputService.java
+++ b/src/main/java/org/scijava/input/InputService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 f6401761f..a8eeccaad 100644
--- a/src/main/java/org/scijava/input/KeyCode.java
+++ b/src/main/java/org/scijava/input/KeyCode.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -54,7 +52,7 @@ public enum KeyCode {
CANCEL(0x03),
/** Clear. */
- CLEAR(0x0C),
+ CLEAR(0x0c),
/** Shift (left or right). */
SHIFT(0x10),
@@ -72,7 +70,7 @@ public enum KeyCode {
CAPS_LOCK(0x14),
/** Escape. */
- ESCAPE(0x1B),
+ ESCAPE(0x1b),
/** Space (' '). */
SPACE(0x20),
@@ -102,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),
@@ -144,10 +142,10 @@ public enum KeyCode {
NUM9(0x39),
/** Semicolon (';'). */
- SEMICOLON(0x3B),
+ SEMICOLON(0x3b),
/** Equals ('='). */
- EQUALS(0x3D),
+ EQUALS(0x3d),
/** The letter A. */
A(0x41),
@@ -177,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),
@@ -225,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),
@@ -267,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),
@@ -323,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),
@@ -442,7 +440,7 @@ public enum KeyCode {
/** TODO. */
DEAD_SEMIVOICED_SOUND(0x8f),
- /** Ampersand ('&'). */
+ /** Ampersand ('&'). */
AMPERSAND(0x96),
/** Asterisk ('*'). */
@@ -494,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),
@@ -565,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);
@@ -634,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 cc87247e6..15bb0ccc8 100644
--- a/src/main/java/org/scijava/input/MouseCursor.java
+++ b/src/main/java/org/scijava/input/MouseCursor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 1574e109c..3c965739a 100644
--- a/src/main/java/org/scijava/io/AbstractIOPlugin.java
+++ b/src/main/java/org/scijava/io/AbstractIOPlugin.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -31,15 +29,60 @@
package org.scijava.io;
+import org.scijava.io.location.Location;
+import org.scijava.io.location.LocationService;
import org.scijava.plugin.AbstractHandlerPlugin;
+import org.scijava.plugin.Parameter;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
/**
* Abstract base class for {@link IOPlugin}s.
*
* @author Curtis Rueden
*/
-public abstract class AbstractIOPlugin extends AbstractHandlerPlugin
- implements IOPlugin
+public abstract class AbstractIOPlugin extends
+ AbstractHandlerPlugin implements IOPlugin
{
- // NB: No implementation needed.
+
+ @Parameter
+ private LocationService locationService;
+
+ @Override
+ public boolean supportsOpen(final String source) {
+ try {
+ return supportsOpen(locationService.resolve(source));
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean supportsSave(final String destination) {
+ try {
+ return supportsSave(locationService.resolve(destination));
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void save(final D data, final String destination) throws IOException {
+ try {
+ save(data, locationService.resolve(destination));
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public D open(final String destination) throws IOException {
+ try {
+ return open(locationService.resolve(destination));
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
+
}
diff --git a/src/main/java/org/scijava/io/AbstractTypedIOService.java b/src/main/java/org/scijava/io/AbstractTypedIOService.java
new file mode 100644
index 000000000..fd7effc63
--- /dev/null
+++ b/src/main/java/org/scijava/io/AbstractTypedIOService.java
@@ -0,0 +1,148 @@
+/*
+ * #%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.io;
+
+import org.scijava.io.location.Location;
+import org.scijava.io.location.LocationService;
+import org.scijava.plugin.AbstractHandlerService;
+import org.scijava.plugin.Parameter;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+/**
+ * Abstract base class for typed {@link IOPlugin}s.
+ *
+ * @author Curtis Rueden
+ * @author Deborah Schmidt
+ */
+public abstract class AbstractTypedIOService extends AbstractHandlerService> implements TypedIOService
+{
+
+ @Parameter
+ private LocationService locationService;
+
+ @Parameter
+ private IOService ioService;
+
+ @Override
+ public D open(String source) throws IOException {
+ try {
+ return open(locationService.resolve(source));
+ }
+ catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public D open(Location source) throws IOException {
+ IOPlugin> opener = ioService().getOpener(source);
+ if (opener == null) {
+ throw new UnsupportedOperationException("No compatible opener found.");
+ }
+ try {
+ return (D) opener.open(source);
+ }
+ catch (ClassCastException e) {
+ throw new UnsupportedOperationException(
+ "Opened data does not conform to requested type.", e);
+ }
+ }
+
+ @Override
+ public void save(D data, String destination) throws IOException {
+ try {
+ save(data, locationService.resolve(destination));
+ }
+ catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void save(D data, Location destination) throws IOException {
+ IOPlugin saver = ioService().getSaver(data, destination);
+ if (saver != null) {
+ saver.save(data, destination);
+ }
+ else {
+ throw new UnsupportedOperationException("No compatible saver found.");
+ }
+ }
+
+ @Override
+ public boolean canOpen(String source) {
+ try {
+ return canOpen(locationService.resolve(source));
+ }
+ catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean canOpen(Location source) {
+ IOPlugin> opener = ioService().getOpener(source);
+ if (opener == null) return false;
+ try {
+ Class ignored = (Class) (opener.getDataType());
+ return true;
+ }
+ catch(ClassCastException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean canSave(D data, String source) {
+ try {
+ return canSave(data, locationService.resolve(source));
+ }
+ catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean canSave(D data, Location destination) {
+ IOPlugin saver = ioService.getSaver(data, destination);
+ if (saver == null) return false;
+ return saver.supportsSave(destination);
+ }
+
+ protected LocationService locationService() {
+ return locationService;
+ }
+
+ protected IOService ioService() {
+ return ioService;
+ }
+}
diff --git a/src/main/java/org/scijava/io/ByteArrayByteBank.java b/src/main/java/org/scijava/io/ByteArrayByteBank.java
new file mode 100644
index 000000000..f0183f2d8
--- /dev/null
+++ b/src/main/java/org/scijava/io/ByteArrayByteBank.java
@@ -0,0 +1,154 @@
+/*
+ * #%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.io;
+
+import org.scijava.util.ByteArray;
+
+/**
+ * {@link ByteBank} implementation backed by a {@link ByteArray}. Self-growing
+ * up to a maximum capacity of {@link Integer#MAX_VALUE}.
+ *
+ * @author Gabriel Einsdorf
+ */
+public class ByteArrayByteBank implements ByteBank {
+
+ private final ByteArray buffer;
+ private long size;
+
+ /**
+ * Creates a {@link ByteArrayByteBank}.
+ */
+ public ByteArrayByteBank() {
+ this(new ByteArray());
+ }
+
+ /**
+ * Creates a {@link ByteArrayByteBank} with the specified initial capacity.
+ *
+ * @param initialCapacity the initial capacity of this {@link ByteBank}
+ */
+ public ByteArrayByteBank(final int initialCapacity) {
+ this(emptyByteArrayOfCapacity(initialCapacity));
+ }
+
+ /**
+ * Creates a {@link ByteArrayByteBank} that wraps the provided byte array.
+ *
+ * @param bytes the bytes to wrap
+ */
+ public ByteArrayByteBank(final byte[] bytes) {
+ this(new ByteArray(bytes));
+ }
+
+ /**
+ * Creates a {@link ByteArrayByteBank} that wraps the specified
+ * {@link ByteArray}.
+ *
+ * @param bytes the {@link ByteArray} to wrap
+ */
+ public ByteArrayByteBank(final ByteArray bytes) {
+ buffer = bytes;
+ size = bytes.size();
+ }
+
+ @Override
+ public long getMaxBufferSize() {
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public void setBytes(final long startpos, final byte[] bytes,
+ final int offset, final int length)
+ {
+ // ensure we have space
+ checkWritePos(startpos, startpos + length);
+ final int neededCapacity = (int) (size + length);
+ buffer.ensureCapacity(neededCapacity);
+
+ // copy the data
+ System.arraycopy(bytes, offset, buffer.getArray(), (int) startpos, length);
+ buffer.setSize(neededCapacity);
+ updateSize(startpos + length);
+ }
+
+ @Override
+ public void setByte(final long pos, final byte b) {
+ checkWritePos(pos, pos);
+ buffer.ensureCapacity((int) pos);
+ // NB: update the size of the underlying buffer before appending to it
+ if (pos == buffer.size()) {
+ buffer.setSize((int) (pos + 1));
+ }
+ buffer.setValue((int) pos, b);
+ updateSize(pos + 1);
+ }
+
+ @Override
+ public void clear() {
+ buffer.clear();
+ size = 0;
+ }
+
+ @Override
+ public byte getByte(final long pos) {
+ checkReadPos(pos, pos);
+ // the buffer might contain bytes with negative value
+ // we need to flip the sign to positive to satisfy the method contract
+ return buffer.getValue((int) pos);
+ }
+
+ @Override
+ public int getBytes(final long startPos, final byte[] b, final int offset,
+ final int length)
+ {
+ checkReadPos(startPos, startPos + length);
+ // ensure we don't try to read data which is not in the buffer
+ final int readLength = (int) Math.min(size() - startPos, length);
+ System.arraycopy(buffer.getArray(), (int) startPos, b, offset, readLength);
+ return readLength;
+ }
+
+ @Override
+ public long size() {
+ return size;
+ }
+
+ // -- Helper methods --
+
+ private void updateSize(final long newSize) {
+ size = newSize > size ? newSize : size;
+ }
+
+ private static ByteArray emptyByteArrayOfCapacity(final int capacity) {
+ final ByteArray byteArray = new ByteArray(new byte[capacity]);
+ byteArray.setSize(0);
+ return byteArray;
+ }
+}
diff --git a/src/main/java/org/scijava/io/ByteBank.java b/src/main/java/org/scijava/io/ByteBank.java
new file mode 100644
index 000000000..8d466e8ce
--- /dev/null
+++ b/src/main/java/org/scijava/io/ByteBank.java
@@ -0,0 +1,209 @@
+/*
+ * #%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.io;
+
+/**
+ * A {@link ByteBank} is a self-growing buffer over arbitrary bytes.
+ *
+ * @author Gabriel Einsdorf
+ * @author Curtis Rueden
+ */
+public interface ByteBank {
+
+ /**
+ * @param pos the position to read from
+ * @return the byte at the given position
+ */
+ public byte getByte(long pos);
+
+ /**
+ * @param startPos the position in the buffer to start reading from
+ * @param bytes the byte array to read into
+ * @return the number of bytes read
+ */
+ default int getBytes(long startPos, byte[] bytes) {
+ return getBytes(startPos, bytes, 0, bytes.length);
+ }
+
+ /**
+ * @param startPos the position in the buffer to start reading from
+ * @param bytes the byte array to read into
+ * @param offset the offset in the bytes array
+ * @param length the number of elements to read into the bytes array
+ * @return number of bytes read
+ */
+ int getBytes(long startPos, byte[] bytes, int offset, int length);
+
+ /**
+ * Copies part of this buffer into a newly allocated byte array.
+ *
+ * @param offset the initial position in the buffer
+ * @param len the number of bytes to copy
+ * @return The newly allocated byte array containing the data.
+ */
+ default byte[] toByteArray(final long offset, final int len) {
+ if (offset < 0 || len < 0 || offset + len > size()) {
+ throw new IllegalArgumentException("Invalid range");
+ }
+ final byte[] bytes = new byte[len];
+ getBytes(offset, bytes);
+ return bytes;
+ }
+
+ /**
+ * Copies this entire buffer into a newly allocated byte array.
+ *
+ * @return The newly allocated byte array containing the data.
+ */
+ default byte[] toByteArray() {
+ long max = size();
+ if (max > Integer.MAX_VALUE) {
+ throw new IllegalStateException(
+ "Byte bank is too large to store into a single byte[]");
+ }
+ return toByteArray(0, (int) max);
+ }
+
+ /**
+ * Sets the bytes starting form the given position to the values form the
+ * provided array.
+ *
+ * @param startPos the position in the buffer to start writing from
+ * @param bytes the byte array to write
+ * @param offset the offset in the bytes array
+ * @param length the number of bytes to read
+ */
+ void setBytes(long startPos, byte[] bytes, int offset, int length);
+
+ /**
+ * Appends the given bytes to the buffer
+ *
+ * @param bytes the array containing the bytes to append to the buffer
+ * @param length the number of elements to append from the bytes array
+ */
+ default void appendBytes(byte[] bytes, int length) {
+ appendBytes(bytes, 0, length);
+ }
+
+ /**
+ * Appends the given bytes to the buffer
+ *
+ * @param bytes the array containing the bytes to append to the buffer
+ * @param offset the offset in the bytes array
+ * @param length the number of elements to append from the bytes array
+ */
+ default void appendBytes(byte[] bytes, int offset, int length) {
+ setBytes(size(), bytes, offset, length);
+ }
+
+ /**
+ * Check if we can read from the specified range
+ *
+ * @param start the start position of the range
+ * @param end the end position of the range
+ */
+ default void checkReadPos(final long start, final long end) {
+ basicRangeCheck(start, end);
+ if (start > size()) {
+ throw new IndexOutOfBoundsException("Requested position: " + start +
+ " is outside the buffer: " + size());
+ }
+ }
+
+ /**
+ * Check if we can write to the specified range
+ *
+ * @param start the start position of the range
+ * @param end the end position of the range
+ * @throws IndexOutOfBoundsException if
+ */
+ default void checkWritePos(final long start, final long end) {
+ if (start > size() + 1) { // we can't have holes in the buffer
+ throw new IndexOutOfBoundsException("Requested start position: " + start +
+ " would leave a hole in the buffer, largest legal position is: " +
+ size());
+ }
+ if (end < start) {
+ throw new IllegalArgumentException(
+ "Invalid range, end is smaller than start!");
+ }
+ if (end > getMaxBufferSize()) {
+ throw new IndexOutOfBoundsException("Requested position " + end +
+ " is larger than the maximal buffer size: " + getMaxBufferSize());
+ }
+ }
+
+ /**
+ * Ensures that the requested range satisfies basic sanity criteria.
+ *
+ * @param start the start of the range
+ * @param end the end of the range
+ */
+ default void basicRangeCheck(final long start, final long end) {
+ if (start > size()) {
+ throw new IndexOutOfBoundsException("Requested position: " + start +
+ " is outside the buffer: " + size());
+ }
+ if (end < start) {
+ throw new IllegalArgumentException(
+ "Invalid range, end is smaller than start!");
+ }
+ }
+
+ /**
+ * Clears the buffer
+ */
+ void clear();
+
+ /**
+ * @return the offset which follows the last byte stored in this ByteBank
+ */
+ long size();
+
+ /**
+ * Sets the byte at the given position
+ *
+ * @param pos the position
+ * @param b the value to set
+ */
+ void setByte(long pos, byte b);
+
+ /**
+ * @return the maximal size of the buffer
+ */
+ long getMaxBufferSize();
+
+ /**
+ * @return True iff the buffer is read-only.
+ */
+ default boolean isReadOnly() {
+ return false;
+ }
+}
diff --git a/src/main/java/org/scijava/io/DataHandle.java b/src/main/java/org/scijava/io/DataHandle.java
deleted file mode 100644
index f058d9241..000000000
--- a/src/main/java/org/scijava/io/DataHandle.java
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * #%L
- * SciJava Common shared library for SciJava software.
- * %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
- * %%
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- * #L%
- */
-
-package org.scijava.io;
-
-import java.io.Closeable;
-import java.io.DataInput;
-import java.io.DataOutput;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-import org.scijava.plugin.WrapperPlugin;
-
-/**
- * A data handle is a plugin which provides access to bytes in a data
- * stream (e.g., files or arrays), identified by a {@link Location}.
- *
- * @author Curtis Rueden
- * @see DataHandleInputStream
- * @see DataHandleOutputStream
- */
-public interface DataHandle extends WrapperPlugin,
- DataInput, DataOutput, Closeable
-{
-
- /** Default block size to use when searching through the stream. */
- int DEFAULT_BLOCK_SIZE = 256 * 1024; // 256 KB
-
- /** Default bound on bytes to search when searching through the stream. */
- int MAX_SEARCH_SIZE = 512 * 1024 * 1024; // 512 MB
-
- /** Returns the current offset in the stream. */
- long offset() throws IOException;
-
- /** Returns the length of the stream. */
- long length() throws IOException;
-
- /**
- * Returns the current order of the stream.
- *
- * @return See above.
- */
- ByteOrder getOrder();
-
- /** Gets the endianness of the stream. */
- default boolean isLittleEndian() {
- return getOrder() == ByteOrder.LITTLE_ENDIAN;
- }
-
- /**
- * Sets the byte order of the stream.
- *
- * @param order Order to set.
- */
- void setOrder(ByteOrder order);
-
- /** Sets the endianness of the stream. */
- default void setOrder(final boolean little) {
- setOrder(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
- }
-
- /** Gets the native encoding of the stream. */
- String getEncoding();
-
- /** Sets the native encoding of the stream. */
- void setEncoding(String encoding);
-
- /**
- * Reads up to {@code buf.remaining()} bytes of data from the stream into a
- * {@link ByteBuffer}.
- */
- default int read(final ByteBuffer buf) throws IOException {
- return read(buf, buf.remaining());
- }
-
- /**
- * Reads up to {@code len} bytes of data from the stream into a
- * {@link ByteBuffer}.
- *
- * @return the total number of bytes read into the buffer.
- */
- default int read(final ByteBuffer buf, final int len) throws IOException {
- final int n;
- if (buf.hasArray()) {
- // read directly into the array
- n = read(buf.array(), buf.arrayOffset(), len);
- }
- else {
- // read into a temporary array, then copy
- final byte[] b = new byte[len];
- n = read(b);
- buf.put(b, 0, n);
- }
- return n;
- }
-
- /**
- * Sets the stream pointer offset, measured from the beginning of the stream,
- * at which the next read or write occurs.
- */
- void seek(long pos) throws IOException;
-
- /**
- * Writes up to {@code buf.remaining()} bytes of data from the given
- * {@link ByteBuffer} to the stream.
- */
- default void write(final ByteBuffer buf) throws IOException {
- write(buf, buf.remaining());
- }
-
- /**
- * Writes up to len bytes of data from the given ByteBuffer to the stream.
- */
- default void write(final ByteBuffer buf, final int len)
- throws IOException
- {
- if (buf.hasArray()) {
- // write directly from the buffer's array
- write(buf.array(), buf.arrayOffset(), len);
- }
- else {
- // copy into a temporary array, then write
- final byte[] b = new byte[len];
- buf.get(b);
- write(b);
- }
- }
-
-
- /** Reads a string of arbitrary length, terminated by a null char. */
- default String readCString() throws IOException {
- final String line = findString("\0");
- return line.length() == 0 ? null : line;
- }
-
- /** Reads a string of up to length n. */
- default String readString(int n) throws IOException {
- final long avail = length() - offset();
- if (n > avail) n = (int) avail;
- final byte[] b = new byte[n];
- readFully(b);
- return new String(b, getEncoding());
- }
-
- /**
- * Reads a string ending with one of the characters in the given string.
- *
- * @see #findString(String...)
- */
- default String readString(final String lastChars) throws IOException {
- if (lastChars.length() == 1) return findString(lastChars);
- final String[] terminators = new String[lastChars.length()];
- for (int i = 0; i < terminators.length; i++) {
- terminators[i] = lastChars.substring(i, i + 1);
- }
- return findString(terminators);
- }
-
- /**
- * Reads a string ending with one of the given terminating substrings.
- *
- * @param terminators The strings for which to search.
- * @return The string from the initial position through the end of the
- * terminating sequence, or through the end of the stream if no
- * terminating sequence is found.
- */
- default String findString(final String... terminators) throws IOException {
- return findString(true, DEFAULT_BLOCK_SIZE, terminators);
- }
-
- /**
- * Reads or skips a string ending with one of the given terminating
- * substrings.
- *
- * @param saveString Whether to collect the string from the current file
- * pointer to the terminating bytes, and return it. If false, returns
- * null.
- * @param terminators The strings for which to search.
- * @throws IOException If saveString flag is set and the maximum search length
- * (512 MB) is exceeded.
- * @return The string from the initial position through the end of the
- * terminating sequence, or through the end of the stream if no
- * terminating sequence is found, or null if saveString flag is unset.
- */
- default String findString(final boolean saveString,
- final String... terminators) throws IOException
- {
- return findString(saveString, DEFAULT_BLOCK_SIZE, terminators);
- }
-
- /**
- * Reads a string ending with one of the given terminating substrings, using
- * the specified block size for buffering.
- *
- * @param blockSize The block size to use when reading bytes in chunks.
- * @param terminators The strings for which to search.
- * @return The string from the initial position through the end of the
- * terminating sequence, or through the end of the stream if no
- * terminating sequence is found.
- */
- default String findString(final int blockSize, final String... terminators)
- throws IOException
- {
- return findString(true, blockSize, terminators);
- }
-
- /**
- * Reads or skips a string ending with one of the given terminating
- * substrings, using the specified block size for buffering.
- *
- * @param saveString Whether to collect the string from the current file
- * pointer to the terminating bytes, and return it. If false, returns
- * null.
- * @param blockSize The block size to use when reading bytes in chunks.
- * @param terminators The strings for which to search.
- * @throws IOException If saveString flag is set and the maximum search length
- * (512 MB) is exceeded.
- * @return The string from the initial position through the end of the
- * terminating sequence, or through the end of the stream if no
- * terminating sequence is found, or null if saveString flag is unset.
- */
- default String findString(final boolean saveString, final int blockSize,
- final String... terminators) throws IOException
- {
- final StringBuilder out = new StringBuilder();
- final long startPos = offset();
- long bytesDropped = 0;
- final long inputLen = length();
- long maxLen = inputLen - startPos;
- final boolean tooLong = saveString && maxLen > MAX_SEARCH_SIZE;
- if (tooLong) maxLen = MAX_SEARCH_SIZE;
- boolean match = false;
- int maxTermLen = 0;
- for (final String term : terminators) {
- final int len = term.length();
- if (len > maxTermLen) maxTermLen = len;
- }
-
- @SuppressWarnings("resource")
- final InputStreamReader in =
- new InputStreamReader(new DataHandleInputStream<>(this), getEncoding());
- final char[] buf = new char[blockSize];
- long loc = 0;
- while (loc < maxLen && offset() < length() - 1) {
- // if we're not saving the string, drop any old, unnecessary output
- if (!saveString) {
- final int outLen = out.length();
- if (outLen >= maxTermLen) {
- final int dropIndex = outLen - maxTermLen + 1;
- final String last = out.substring(dropIndex, outLen);
- out.setLength(0);
- out.append(last);
- bytesDropped += dropIndex;
- }
- }
-
- // read block from stream
- final int r = in.read(buf, 0, blockSize);
- if (r <= 0) throw new IOException("Cannot read from stream: " + r);
-
- // append block to output
- out.append(buf, 0, r);
-
- // check output, returning smallest possible string
- int min = Integer.MAX_VALUE, tagLen = 0;
- for (final String t : terminators) {
- final int len = t.length();
- final int start = (int) (loc - bytesDropped - len);
- final int value = out.indexOf(t, start < 0 ? 0 : start);
- if (value >= 0 && value < min) {
- match = true;
- min = value;
- tagLen = len;
- }
- }
-
- if (match) {
- // reset stream to proper location
- seek(startPos + bytesDropped + min + tagLen);
-
- // trim output string
- if (saveString) {
- out.setLength(min + tagLen);
- return out.toString();
- }
- return null;
- }
-
- loc += r;
- }
-
- // no match
- if (tooLong) throw new IOException("Maximum search length reached.");
- return saveString ? out.toString() : null;
- }
-
- // -- InputStream look-alikes --
-
- /**
- * Reads the next byte of data from the stream.
- *
- * @return the next byte of data, or -1 if the end of the stream is reached.
- * @throws IOException - if an I/O error occurs.
- */
- int read() throws IOException;
-
- /**
- * Reads up to b.length bytes of data from the stream into an array of bytes.
- *
- * @return the total number of bytes read into the buffer.
- */
- default int read(byte[] b) throws IOException {
- return read(b, 0, b.length);
- }
-
- /**
- * Reads up to len bytes of data from the stream into an array of bytes.
- *
- * @return the total number of bytes read into the buffer.
- */
- int read(byte[] b, int off, int len) throws IOException;
-
- /**
- * Skips over and discards {@code n} bytes of data from the stream. The
- * {@code skip} method may, for a variety of reasons, end up skipping over
- * some smaller number of bytes, possibly {@code 0}. This may result from any
- * of a number of conditions; reaching end of file before {@code n} bytes have
- * been skipped is only one possibility. The actual number of bytes skipped is
- * returned. If {@code n} is negative, no bytes are skipped.
- *
- * @param n - the number of bytes to be skipped.
- * @return the actual number of bytes skipped.
- * @throws IOException - if an I/O error occurs.
- */
- default long skip(final long n) throws IOException {
- if (n < 0) return 0;
- final long remain = length() - offset();
- final long num = n < remain ? n : remain;
- seek(offset() + num);
- return num;
- }
-
-}
diff --git a/src/main/java/org/scijava/io/DefaultIOService.java b/src/main/java/org/scijava/io/DefaultIOService.java
index 2e6cdc63b..0b67ce690 100644
--- a/src/main/java/org/scijava/io/DefaultIOService.java
+++ b/src/main/java/org/scijava/io/DefaultIOService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -32,10 +30,13 @@
package org.scijava.io;
import java.io.IOException;
+import java.net.URISyntaxException;
import org.scijava.event.EventService;
import org.scijava.io.event.DataOpenedEvent;
import org.scijava.io.event.DataSavedEvent;
+import org.scijava.io.location.Location;
+import org.scijava.io.location.LocationService;
import org.scijava.log.LogService;
import org.scijava.plugin.AbstractHandlerService;
import org.scijava.plugin.Parameter;
@@ -49,7 +50,7 @@
*/
@Plugin(type = Service.class)
public final class DefaultIOService
- extends AbstractHandlerService> implements IOService
+ extends AbstractHandlerService> implements IOService
{
@Parameter
@@ -58,28 +59,76 @@ public final class DefaultIOService
@Parameter
private EventService eventService;
- // -- IOService methods --
+ @Parameter
+ private LocationService locationService;
+
+ @Override
+ public IOPlugin> getOpener(final String source) throws IOException {
+ try {
+ return getOpener(locationService.resolve(source));
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public IOPlugin getSaver(D data, String destination) throws IOException {
+ try {
+ return getSaver(data, locationService.resolve(destination));
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
@Override
public Object open(final String source) throws IOException {
+ try {
+ return open(locationService.resolve(source));
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void save(final Object data, final String destination)
+ throws IOException
+ {
+ try {
+ save(data, locationService.resolve(destination));
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public Object open(final Location source) throws IOException {
final IOPlugin> opener = getOpener(source);
- if (opener == null) return null; // no appropriate IOPlugin
+ if (opener == null) {
+ log.error("No opener IOPlugin found for " + source + ".");
+ return null;
+ }
final Object data = opener.open(source);
- if (data == null) return null; // IOPlugin returned no data; canceled?
+ if (data == null) {
+ log.warn("Opener IOPlugin " + opener + " returned no data. Canceled?");
+ return null; // IOPlugin returned no data; canceled?
+ }
eventService.publish(new DataOpenedEvent(source, data));
return data;
}
@Override
- public void save(final Object data, final String destination)
+ public void save(final Object data, final Location destination)
throws IOException
{
final IOPlugin saver = getSaver(data, destination);
if (saver != null) {
saver.save(data, destination);
eventService.publish(new DataSavedEvent(destination, data));
+ } else {
+ log.error("No Saver IOPlugin found for " + data.toString() + ".");
}
}
+
}
diff --git a/src/main/java/org/scijava/io/DefaultRecentFileService.java b/src/main/java/org/scijava/io/DefaultRecentFileService.java
index 62506bb26..3cef6fb1a 100644
--- a/src/main/java/org/scijava/io/DefaultRecentFileService.java
+++ b/src/main/java/org/scijava/io/DefaultRecentFileService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -43,6 +41,8 @@
import org.scijava.event.EventHandler;
import org.scijava.event.EventService;
import org.scijava.io.event.IOEvent;
+import org.scijava.io.location.FileLocation;
+import org.scijava.io.location.Location;
import org.scijava.menu.MenuConstants;
import org.scijava.module.ModuleInfo;
import org.scijava.module.ModuleService;
@@ -174,11 +174,19 @@ public void initialize() {
moduleService.addModules(recentModules.values());
}
+ @Override
+ public void dispose() {
+ clear();
+ }
+
// -- Event handlers --
@EventHandler
protected void onEvent(final IOEvent event) {
- add(event.getDescriptor());
+ final Location loc = event.getLocation();
+ if (!(loc instanceof FileLocation)) return;
+ final FileLocation fileLoc = (FileLocation) loc;
+ add(fileLoc.getFile().getPath());
}
// -- Helper methods --
diff --git a/src/main/java/org/scijava/io/IOPlugin.java b/src/main/java/org/scijava/io/IOPlugin.java
index 7de3cc315..b8c69cc18 100644
--- a/src/main/java/org/scijava/io/IOPlugin.java
+++ b/src/main/java/org/scijava/io/IOPlugin.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -32,7 +30,10 @@
package org.scijava.io;
import java.io.IOException;
+import java.net.URISyntaxException;
+import org.scijava.io.location.Location;
+import org.scijava.io.location.LocationService;
import org.scijava.plugin.HandlerPlugin;
import org.scijava.plugin.Plugin;
@@ -50,52 +51,91 @@
* @see Plugin
* @see IOService
*/
-public interface IOPlugin extends HandlerPlugin {
+public interface IOPlugin extends HandlerPlugin {
/** The type of data opened and/or saved by the plugin. */
Class getDataType();
/** Checks whether the I/O plugin can open data from the given source. */
- @SuppressWarnings("unused")
default boolean supportsOpen(final String source) {
+ try {
+ return supportsOpen(context().service(LocationService.class).resolve(source));
+ }
+ catch (final URISyntaxException exc) {
+ return false;
+ }
+ }
+
+ /** Checks whether the I/O plugin can open data from the given location. */
+ @SuppressWarnings("unused")
+ default boolean supportsOpen(final Location source) {
return false;
}
/** Checks whether the I/O plugin can save data to the given destination. */
- @SuppressWarnings("unused")
default boolean supportsSave(final String destination) {
+ try {
+ return supportsSave(context().service(LocationService.class).resolve(destination));
+ }
+ catch (final URISyntaxException exc) {
+ return false;
+ }
+ }
+
+ /** Checks whether the I/O plugin can save data to the given location. */
+ @SuppressWarnings("unused")
+ default boolean supportsSave(final Location destination) {
return false;
}
/**
* Checks whether the I/O plugin can save the given data to the specified
- * destination.
+ * location.
*/
default boolean supportsSave(final Object data, final String destination) {
return supportsSave(destination) && getDataType().isInstance(data);
}
+ default boolean supportsSave(final Object data, final Location destination) {
+ return supportsSave(destination) && getDataType().isInstance(data);
+ }
+
/** Opens data from the given source. */
@SuppressWarnings("unused")
default D open(final String source) throws IOException {
throw new UnsupportedOperationException();
}
- /** Saves the given data to the specified destination. */
+ /** Opens data from the given location. */
@SuppressWarnings("unused")
+ default D open(final Location source) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ /** Saves the given data to the specified destination. */
default void save(final D data, final String destination) throws IOException {
+ try {
+ save(data, context().service(LocationService.class).resolve(destination));
+ }
+ catch (final URISyntaxException exc) {
+ throw new UnsupportedOperationException(exc);
+ }
+ }
+
+ /** Saves the given data to the specified location. */
+ @SuppressWarnings("unused")
+ default void save(final D data, final Location destination) throws IOException {
throw new UnsupportedOperationException();
}
// -- Typed methods --
- @Override
default boolean supports(final String descriptor) {
return supportsOpen(descriptor) || supportsSave(descriptor);
}
@Override
- default Class getType() {
- return String.class;
+ default Class getType() {
+ return Location.class;
}
}
diff --git a/src/main/java/org/scijava/io/IOService.java b/src/main/java/org/scijava/io/IOService.java
index a4f284680..672ba9f9b 100644
--- a/src/main/java/org/scijava/io/IOService.java
+++ b/src/main/java/org/scijava/io/IOService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -33,6 +31,7 @@
import java.io.IOException;
+import org.scijava.io.location.Location;
import org.scijava.plugin.HandlerService;
import org.scijava.service.SciJavaService;
@@ -40,18 +39,22 @@
* Interface for high-level data I/O: opening and saving data.
*
* @author Curtis Rueden
- * @see DataHandleService
- * @see Location
*/
-public interface IOService extends HandlerService>,
+public interface IOService extends HandlerService>,
SciJavaService
{
/**
* Gets the most appropriate {@link IOPlugin} for opening data from the given
- * source.
+ * location.
*/
- default IOPlugin> getOpener(final String source) {
+ IOPlugin> getOpener(final String source) throws IOException;
+
+ /**
+ * Gets the most appropriate {@link IOPlugin} for opening data from the given
+ * location.
+ */
+ default IOPlugin> getOpener(Location source) {
for (final IOPlugin> handler : getInstances()) {
if (handler.supportsOpen(source)) return handler;
}
@@ -60,9 +63,15 @@ default IOPlugin> getOpener(final String source) {
/**
* Gets the most appropriate {@link IOPlugin} for saving data to the given
- * destination.
+ * location.
+ */
+ IOPlugin getSaver(final D data, final String destination) throws IOException;
+
+ /**
+ * Gets the most appropriate {@link IOPlugin} for saving data to the given
+ * location.
*/
- default IOPlugin getSaver(final D data, final String destination) {
+ default IOPlugin getSaver(D data, Location destination) {
for (final IOPlugin> handler : getInstances()) {
if (handler.supportsSave(data, destination)) {
@SuppressWarnings("unchecked")
@@ -81,7 +90,7 @@ default IOPlugin getSaver(final D data, final String destination) {
* The opener to use is automatically determined based on available
* {@link IOPlugin}s; see {@link #getOpener(String)}.
*
- *
+ *
* @param source The source (e.g., file path) from which to data should be
* loaded.
* @return An object representing the loaded data, or null if the source is
@@ -90,6 +99,22 @@ default IOPlugin getSaver(final D data, final String destination) {
*/
Object open(String source) throws IOException;
+ /**
+ * Loads data from the given location.
+ *
+ * The opener to use is automatically determined based on available
+ * {@link IOPlugin}s; see {@link #getOpener(Location)}.
+ *
+ *
+ * @param source The location from which to data should be loaded.
+ * @return An object representing the loaded data, or null if the source is
+ * not supported.
+ * @throws IOException if something goes wrong loading the data.
+ */
+ default Object open(Location source) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
/**
* Saves data to the given destination. The nature of the destination is left
* intentionally general, but the most common example is a file path.
@@ -97,7 +122,7 @@ default IOPlugin getSaver(final D data, final String destination) {
* The saver to use is automatically determined based on available
* {@link IOPlugin}s; see {@link #getSaver(Object, String)}.
*
- *
+ *
* @param data The data to be saved to the destination.
* @param destination The destination (e.g., file path) to which data should
* be saved.
@@ -105,6 +130,21 @@ default IOPlugin getSaver(final D data, final String destination) {
*/
void save(Object data, String destination) throws IOException;
+ /**
+ * Saves data to the given location.
+ *
+ * The saver to use is automatically determined based on available
+ * {@link IOPlugin}s; see {@link #getSaver(Object, Location)}.
+ *
+ *
+ * @param data The data to be saved to the destination.
+ * @param destination The destination location to which data should be saved.
+ * @throws IOException if something goes wrong saving the data.
+ */
+ default void save(Object data, Location destination) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
// -- HandlerService methods --
@Override
@@ -114,7 +154,7 @@ default Class> getPluginType() {
}
@Override
- default Class getType() {
- return String.class;
+ default Class getType() {
+ return Location.class;
}
}
diff --git a/src/main/java/org/scijava/io/RecentFileService.java b/src/main/java/org/scijava/io/RecentFileService.java
index 6eb0e68cd..20a6ad9c8 100644
--- a/src/main/java/org/scijava/io/RecentFileService.java
+++ b/src/main/java/org/scijava/io/RecentFileService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/TypedIOService.java b/src/main/java/org/scijava/io/TypedIOService.java
new file mode 100644
index 000000000..e6882fc95
--- /dev/null
+++ b/src/main/java/org/scijava/io/TypedIOService.java
@@ -0,0 +1,180 @@
+/*
+ * #%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.io;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import org.scijava.io.location.FileLocation;
+import org.scijava.io.location.Location;
+import org.scijava.io.location.LocationService;
+import org.scijava.plugin.HandlerService;
+import org.scijava.service.SciJavaService;
+
+/**
+ * Interface for high-level data I/O: opening and saving data of a specific type.
+ *
+ * @author Curtis Rueden
+ * @author Deborah Schmidt
+ */
+public interface TypedIOService extends HandlerService>,
+ SciJavaService
+{
+
+ /**
+ * Gets the most appropriate {@link IOPlugin} for opening data from the given
+ * location.
+ */
+ default IOPlugin getOpener(final String source) {
+ try {
+ return getOpener(context().service(LocationService.class).resolve(source));
+ }
+ catch (final URISyntaxException exc) {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the most appropriate {@link IOPlugin} for opening data from the given
+ * location.
+ */
+ default IOPlugin getOpener(Location source) {
+ for (final IOPlugin handler : getInstances()) {
+ if (handler.supportsOpen(source)) return handler;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the most appropriate {@link IOPlugin} for saving data to the given
+ * location.
+ */
+ default IOPlugin getSaver(final D data, final String destination) {
+ try {
+ return getSaver(data, context().service(LocationService.class).resolve(destination));
+ }
+ catch (final URISyntaxException exc) {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the most appropriate {@link IOPlugin} for saving data to the given
+ * location.
+ */
+ default IOPlugin getSaver(D data, Location destination) {
+ for (final IOPlugin> handler : getInstances()) {
+ if (handler.supportsSave(data, destination)) {
+ return (IOPlugin) handler;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Loads data from the given source. For extensibility, the nature of the
+ * source is left intentionally general, but two common examples include file
+ * paths and URLs.
+ *
+ * The opener to use is automatically determined based on available
+ * {@link IOPlugin}s; see {@link #getOpener(String)}.
+ *
+ *
+ * @param source The source (e.g., file path) from which to data should be
+ * loaded.
+ * @return An object representing the loaded data, or null if the source is
+ * not supported.
+ * @throws IOException if something goes wrong loading the data.
+ */
+ D open(String source) throws IOException;
+
+ /**
+ * Loads data from the given location.
+ *
+ * The opener to use is automatically determined based on available
+ * {@link IOPlugin}s; see {@link #getOpener(Location)}.
+ *
+ *
+ * @param source The location from which to data should be loaded.
+ * @return An object representing the loaded data, or null if the source is
+ * not supported.
+ * @throws IOException if something goes wrong loading the data.
+ */
+ D open(Location source) throws IOException;
+
+ /**
+ * Saves data to the given destination. The nature of the destination is left
+ * intentionally general, but the most common example is a file path.
+ *
+ * The saver to use is automatically determined based on available
+ * {@link IOPlugin}s; see {@link #getSaver(Object, String)}.
+ *
+ *
+ * @param data The data to be saved to the destination.
+ * @param destination The destination (e.g., file path) to which data should
+ * be saved.
+ * @throws IOException if something goes wrong saving the data.
+ */
+ void save(D data, String destination) throws IOException;
+
+ /**
+ * Saves data to the given location.
+ *
+ * The saver to use is automatically determined based on available
+ * {@link IOPlugin}s; see {@link #getSaver(Object, Location)}.
+ *
+ *
+ * @param data The data to be saved to the destination.
+ * @param destination The destination location to which data should be saved.
+ * @throws IOException if something goes wrong saving the data.
+ */
+ void save(D data, Location destination) throws IOException;
+
+ boolean canOpen(String source);
+
+ boolean canOpen(Location source);
+
+ boolean canSave(D data, String destination);
+
+ boolean canSave(D data, Location destination);
+
+ // -- HandlerService methods --
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ default Class> getPluginType() {
+ return (Class) IOPlugin.class;
+ }
+
+ @Override
+ default Class getType() {
+ return Location.class;
+ }
+}
diff --git a/src/main/java/org/scijava/io/console/OpenArgument.java b/src/main/java/org/scijava/io/console/OpenArgument.java
index 6f0c7947e..bfdc3dbdd 100644
--- a/src/main/java/org/scijava/io/console/OpenArgument.java
+++ b/src/main/java/org/scijava/io/console/OpenArgument.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -41,6 +39,7 @@
import org.scijava.log.LogService;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.Plugin;
+import org.scijava.startup.StartupService;
/**
* Handles the {@code --open} command line argument.
@@ -50,13 +49,16 @@
@Plugin(type = ConsoleArgument.class)
public class OpenArgument extends AbstractConsoleArgument {
- @Parameter
+ @Parameter(required = false)
private IOService ioService;
- @Parameter
+ @Parameter(required = false)
private DisplayService displayService;
- @Parameter
+ @Parameter(required = false)
+ private StartupService startupService;
+
+ @Parameter(required = false)
private LogService log;
// -- Constructor --
@@ -74,12 +76,23 @@ public void handle(final LinkedList args) {
args.removeFirst(); // --open
final String source = args.removeFirst();
- try {
- final Object o = ioService.open(source);
- displayService.createDisplay(o);
- }
- catch (IOException exc) {
- log.error(exc);
- }
+ // open the source after the UI is shown
+ startupService.addOperation(() -> {
+ try {
+ final Object o = ioService.open(source);
+ displayService.createDisplay(o);
+ }
+ catch (final IOException exc) {
+ if (log != null) log.error(exc);
+ }
+ });
+ }
+
+ // -- Typed methods --
+
+ @Override
+ public boolean supports(final LinkedList args) {
+ return startupService != null && ioService != null &&
+ displayService != null && super.supports(args);
}
}
diff --git a/src/main/java/org/scijava/io/event/DataOpenedEvent.java b/src/main/java/org/scijava/io/event/DataOpenedEvent.java
index 08df4c44e..c177c9195 100644
--- a/src/main/java/org/scijava/io/event/DataOpenedEvent.java
+++ b/src/main/java/org/scijava/io/event/DataOpenedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -31,22 +29,34 @@
package org.scijava.io.event;
+
+import org.scijava.io.location.FileLocation;
+import org.scijava.io.location.Location;
+
/**
- * An event indicating that data has been opened from a source.
+ * An event indicating that data has been opened from a location.
*
* @author Curtis Rueden
*/
public class DataOpenedEvent extends IOEvent {
+ public DataOpenedEvent(final Location location, final Object data) {
+ super(location, data);
+ }
+
+ /**
+ * @deprecated use {@link #DataOpenedEvent(Location, Object)} instead
+ */
+ @Deprecated
public DataOpenedEvent(final String source, final Object data) {
super(source, data);
}
- // -- DataOpenedEvent methods --
-
- /** Gets the source from which data was opened. */
+ /**
+ * @deprecated use {@link #getLocation} instead
+ */
+ @Deprecated
public String getSource() {
return getDescriptor();
}
-
}
diff --git a/src/main/java/org/scijava/io/event/DataSavedEvent.java b/src/main/java/org/scijava/io/event/DataSavedEvent.java
index 73e21c01a..ec75495c9 100644
--- a/src/main/java/org/scijava/io/event/DataSavedEvent.java
+++ b/src/main/java/org/scijava/io/event/DataSavedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -31,6 +29,10 @@
package org.scijava.io.event;
+
+import org.scijava.io.location.FileLocation;
+import org.scijava.io.location.Location;
+
/**
* An event indicating that data has been saved to a destination.
*
@@ -38,15 +40,23 @@
*/
public class DataSavedEvent extends IOEvent {
- public DataSavedEvent(final String destination, final Object data) {
+ public DataSavedEvent(final Location destination, final Object data) {
super(destination, data);
}
- // -- DataSavedEvent methods --
+ /**
+ * @deprecated use {@link #DataSavedEvent(Location, Object)} instead
+ */
+ @Deprecated
+ public DataSavedEvent(final String destination, final Object data) {
+ super(destination, data);
+ }
- /** Gets the destination to which data was saved. */
+ /**
+ * @deprecated use {@link #getLocation} instead
+ */
+ @Deprecated
public String getDestination() {
return getDescriptor();
}
-
}
diff --git a/src/main/java/org/scijava/io/event/IOEvent.java b/src/main/java/org/scijava/io/event/IOEvent.java
index 83bb16f7b..20a28a191 100644
--- a/src/main/java/org/scijava/io/event/IOEvent.java
+++ b/src/main/java/org/scijava/io/event/IOEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -31,7 +29,12 @@
package org.scijava.io.event;
+import java.net.URISyntaxException;
+
import org.scijava.event.SciJavaEvent;
+import org.scijava.io.location.FileLocation;
+import org.scijava.io.location.Location;
+import org.scijava.io.location.LocationService;
/**
* An event indicating that I/O (e.g., opening or saving) has occurred.
@@ -40,20 +43,41 @@
*/
public abstract class IOEvent extends SciJavaEvent {
- /** The data descriptor (source or destination). */
+ /** The data location (source or destination). */
+ private final Location location;
+
+ /** @deprecated use {@link #location} instead */
+ @Deprecated
private final String descriptor;
/** The data for which I/O took place. */
private final Object data;
+ /**
+ * @deprecated use {@link #IOEvent(Location, Object)} instead
+ */
+ @Deprecated
public IOEvent(final String descriptor, final Object data) {
+ this.location = null;
this.descriptor = descriptor;
this.data = data;
}
- /** Gets the data descriptor (source or destination). */
- public String getDescriptor() {
- return descriptor;
+ public IOEvent(final Location location, final Object data) {
+ this.location = location;
+ this.descriptor = null;
+ this.data = data;
+ }
+
+ /** Gets the data location (source or destination). */
+ public Location getLocation() {
+ if (location != null) return location;
+ try {
+ return context().service(LocationService.class).resolve(descriptor);
+ }
+ catch (final URISyntaxException exc) {
+ return null;
+ }
}
/** Gets the data for which I/O took place. */
@@ -65,7 +89,20 @@ public Object getData() {
@Override
public String toString() {
- return super.toString() + "\n\tdescriptor = " + data + "\n\tdata = " + data;
+ return super.toString() + "\n\tlocation = " + location + "\n\tdata = " +
+ data;
}
+ /**
+ * @deprecated use {@link #getLocation()} instead
+ */
+ @Deprecated
+ public String getDescriptor() {
+ if (descriptor != null) return descriptor;
+ if (location instanceof FileLocation) {
+ final FileLocation fileLocation = (FileLocation) location;
+ return fileLocation.getFile().getAbsolutePath();
+ }
+ return location.getURI().toString();
+ }
}
diff --git a/src/main/java/org/scijava/io/AbstractDataHandle.java b/src/main/java/org/scijava/io/handle/AbstractDataHandle.java
similarity index 89%
rename from src/main/java/org/scijava/io/AbstractDataHandle.java
rename to src/main/java/org/scijava/io/handle/AbstractDataHandle.java
index 7a30a6d3b..f2446f90a 100644
--- a/src/main/java/org/scijava/io/AbstractDataHandle.java
+++ b/src/main/java/org/scijava/io/handle/AbstractDataHandle.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,10 +27,9 @@
* #L%
*/
-package org.scijava.io;
-
-import java.nio.ByteOrder;
+package org.scijava.io.handle;
+import org.scijava.io.location.Location;
import org.scijava.plugin.AbstractWrapperPlugin;
/**
@@ -44,6 +41,13 @@ public abstract class AbstractDataHandle extends
AbstractWrapperPlugin implements DataHandle
{
+ private byte[] conversionBuffer = new byte[8];
+
+ @Override
+ public byte[] conversionBuffer() {
+ return conversionBuffer;
+ }
+
// -- Fields --
private ByteOrder order = ByteOrder.BIG_ENDIAN;
diff --git a/src/main/java/org/scijava/io/handle/AbstractHigherOrderHandle.java b/src/main/java/org/scijava/io/handle/AbstractHigherOrderHandle.java
new file mode 100644
index 000000000..7fa3d8df9
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/AbstractHigherOrderHandle.java
@@ -0,0 +1,112 @@
+/*
+ * #%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.io.handle;
+
+import java.io.IOException;
+
+import org.scijava.io.location.Location;
+
+/**
+ * Abstract superclass for {@link DataHandle}s that operate over other
+ * {@link DataHandle}s.
+ *
+ * @author Gabriel Einsdorf
+ */
+public abstract class AbstractHigherOrderHandle extends
+ AbstractDataHandle
+{
+
+ private DataHandle handle;
+ private boolean closed;
+
+ public AbstractHigherOrderHandle(final DataHandle handle) {
+ this.handle = handle;
+ set(handle.get()); // provides access to underlying location
+ }
+
+ @Override
+ public boolean isReadable() {
+ return !closed && handle.isReadable();
+ }
+
+ @Override
+ public boolean isWritable() {
+ return !closed && handle.isWritable();
+ }
+
+ @Override
+ public long length() throws IOException {
+ ensureOpen();
+ return handle.length();
+ }
+
+ @Override
+ public Class getType() {
+ return handle.getType();
+ }
+
+ @Override
+ public boolean exists() throws IOException {
+ return handle.exists();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ cleanup();
+ closed = true;
+ handle.close();
+ handle = null;
+ }
+ }
+
+ protected void ensureOpen() throws IOException {
+ if (closed) {
+ throw new IOException("This handle is closed!");
+ }
+ }
+
+ /**
+ * Clean up data structures after a handle has been closed in the
+ * {@link #close()} method.
+ *
+ * @throws IOException
+ */
+ protected abstract void cleanup() throws IOException;
+
+ /**
+ * @return the {@link DataHandle} wrapped by this
+ * {@link AbstractHigherOrderHandle}
+ */
+ protected DataHandle handle() {
+ return handle;
+ }
+
+}
diff --git a/src/main/java/org/scijava/io/handle/AbstractSeekableStreamHandle.java b/src/main/java/org/scijava/io/handle/AbstractSeekableStreamHandle.java
new file mode 100644
index 000000000..5b56c5e93
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/AbstractSeekableStreamHandle.java
@@ -0,0 +1,103 @@
+/*
+ * #%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.io.handle;
+
+import java.io.IOException;
+
+import org.scijava.io.location.Location;
+
+public abstract class AbstractSeekableStreamHandle extends
+ AbstractStreamHandle implements SeekableStreamHandle
+{
+
+ private long jumpCutoff = 10000;
+
+ @Override
+ public void seek(final long pos) throws IOException {
+
+ // how much and which direction we have to jump
+ final long delta = pos - offset();
+
+ if (delta == 0) {
+ return;
+ // nothing to do
+ }
+ else if (delta > 0) {
+ // offset position is "downstream"
+
+ // try to reconnect instead of linearly reading large chunks
+ if (recreatePossible() && delta > jumpCutoff) {
+ recreateStreamFromPos(pos);
+ }
+ else {
+ jump(delta);
+ }
+
+ }
+ else { // delta < 0
+ // need to recreate the stream
+ if (recreatePossible()) {
+ recreateStreamFromPos(pos);
+ }
+ else {
+ resetStream();
+ jump(pos);
+ }
+ }
+ setOffset(pos);
+ }
+
+ /**
+ * Recreates the internal input stream available through {@link #in()}, so
+ * that it starts from the specified position.
+ *
+ * @param pos
+ * @throws IOException
+ */
+ protected abstract void recreateStreamFromPos(long pos) throws IOException;
+
+ /**
+ * In some implementations of this class, the ability to recreate the stream
+ * depends on external factors (e.g. server support). This influences a
+ *
+ * @return if recreate is actually possible.
+ * @throws IOException
+ */
+ protected abstract boolean recreatePossible() throws IOException;
+
+ /**
+ * Sets the maximum of bytes which are read from the stream when seeking
+ * forward. Any larger number will result in a call to
+ * {@link #recreateStreamFromPos(long)}.
+ */
+ protected void setJumpCutoff(long jumpCutoff) {
+ this.jumpCutoff = jumpCutoff;
+ }
+}
diff --git a/src/main/java/org/scijava/io/FileLocation.java b/src/main/java/org/scijava/io/handle/AbstractStreamHandle.java
similarity index 67%
rename from src/main/java/org/scijava/io/FileLocation.java
rename to src/main/java/org/scijava/io/handle/AbstractStreamHandle.java
index 3d93ee12c..75a9f4939 100644
--- a/src/main/java/org/scijava/io/FileLocation.java
+++ b/src/main/java/org/scijava/io/handle/AbstractStreamHandle.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,40 +27,37 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.handle;
-import java.io.File;
-import java.net.URI;
+import org.scijava.io.location.Location;
/**
- * {@link Location} backed by a {@link File} on disk.
+ * Abstract base class for {@link StreamHandle} implementations.
*
* @author Curtis Rueden
+ * @author Melissa Linkert
*/
-public class FileLocation extends AbstractLocation {
+public abstract class AbstractStreamHandle extends
+ AbstractDataHandle implements StreamHandle
+{
- private final File file;
+ // -- Fields --
- public FileLocation(final File file) {
- this.file = file;
- }
-
- public FileLocation(final String path) {
- this(new File(path));
- }
+ /** Current position within the stream(s). */
+ private long offset;
- // -- FileLocation methods --
+ // -- StreamHandle methods --
- /** Gets the associated {@link File}. */
- public File getFile() {
- return file;
+ @Override
+ public void setOffset(final long offset) {
+ this.offset = offset;
}
- // -- Location methods --
+ // -- DataHandle methods --
@Override
- public URI getURI() {
- return getFile().toURI();
+ public long offset() {
+ return offset;
}
}
diff --git a/src/main/java/org/scijava/io/handle/BytesHandle.java b/src/main/java/org/scijava/io/handle/BytesHandle.java
new file mode 100644
index 000000000..b09228673
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/BytesHandle.java
@@ -0,0 +1,185 @@
+/*
+ * #%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.io.handle;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import org.scijava.io.ByteBank;
+import org.scijava.io.location.BytesLocation;
+import org.scijava.plugin.Plugin;
+
+/**
+ * {@link DataHandle} for a {@link BytesLocation}.
+ *
+ * @author Curtis Rueden
+ * @author Melissa Linkert
+ * @author Gabriel Einsdorf
+ */
+@Plugin(type = DataHandle.class)
+public class BytesHandle extends AbstractDataHandle {
+
+ private long offset = 0;
+
+ // -- Constructors --
+
+ public BytesHandle() { }
+
+ public BytesHandle(final BytesLocation location) {
+ set(location);
+ }
+
+ // -- DataHandle methods --
+
+ @Override
+ public boolean isReadable() {
+ return true;
+ }
+
+ @Override
+ public boolean isWritable() {
+ return !bytes().isReadOnly();
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ @Override
+ public long offset() {
+ return offset;
+ }
+
+ @Override
+ public long length() {
+ return bytes().size();
+ }
+
+ @Override
+ public void setLength(final long length) throws IOException {
+ // check if new length is legal
+ bytes().basicRangeCheck(0, length);
+ // TODO update the maxLength?
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, int len) throws IOException {
+ if(len == 0) return 0;
+ if (offset + len > length()) {
+ len = (int) (length() - offset);
+ }
+ if(len == 0) { // EOF
+ return -1;
+ }
+ bytes().getBytes(offset, b, off, len);
+ offset += len;
+ return len;
+ }
+
+ @Override
+ public void seek(final long pos) throws IOException {
+ if (pos > length()) setLength(pos);
+ offset = pos;
+ }
+
+ // -- DataInput methods --
+
+ @Override
+ public byte readByte() throws IOException {
+ ensureReadable(1);
+ try {
+ // we need to convert the bytes into the range 0-255
+ return bytes().getByte(offset++);
+ }
+ catch (final Exception e) {
+ throw eofException(e);
+ }
+ }
+
+ @Override
+ public void readFully(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ ensureReadable(len);
+ try {
+ bytes().getBytes(offset, b, off, len);
+ offset += len;
+ }
+ catch (final Exception e) {
+ throw eofException(e);
+ }
+ }
+
+ // -- DataOutput methods --
+
+ @Override
+ public void write(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ ensureWritable(len);
+ bytes().setBytes(offset, b, off, len);
+ offset += len;
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ ensureWritable(1);
+ bytes().setByte(offset, (byte) b);
+ offset++;
+ }
+
+ // -- Closeable methods --
+
+ @Override
+ public void close() {
+ // NB: No action needed.
+ }
+
+ // -- Typed methods --
+
+ @Override
+ public Class getType() {
+ return BytesLocation.class;
+ }
+
+ // -- Helper methods --
+
+ private ByteBank bytes() {
+ return get().getByteBank();
+ }
+
+ private EOFException eofException(final Throwable cause) {
+ final EOFException eof = new EOFException();
+ eof.initCause(cause);
+ return eof;
+ }
+
+}
diff --git a/src/main/java/org/scijava/io/handle/DataHandle.java b/src/main/java/org/scijava/io/handle/DataHandle.java
new file mode 100644
index 000000000..18cddadcb
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/DataHandle.java
@@ -0,0 +1,672 @@
+/*
+ * #%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.io.handle;
+
+import java.io.Closeable;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.DataOutput;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Date;
+
+import org.scijava.io.location.Location;
+import org.scijava.plugin.WrapperPlugin;
+import org.scijava.util.Bytes;
+
+/**
+ * A data handle is a plugin which provides both streaming and random
+ * access to bytes at a {@link Location} (e.g., files or arrays).
+ *
+ * @author Curtis Rueden
+ * @see DataHandleInputStream
+ * @see DataHandleOutputStream
+ */
+public interface DataHandle extends WrapperPlugin,
+ DataInput, DataOutput, Closeable
+{
+
+ public enum ByteOrder {
+ LITTLE_ENDIAN, BIG_ENDIAN
+ }
+
+ /** Default block size to use when searching through the stream. */
+ int DEFAULT_BLOCK_SIZE = 256 * 1024; // 256 KB
+
+ /** Default bound on bytes to search when searching through the stream. */
+ int MAX_SEARCH_SIZE = 512 * 1024 * 1024; // 512 MB
+
+ /** Gets whether reading from this handle is supported. */
+ boolean isReadable();
+
+ /** Gets whether writing to this handle is supported. */
+ boolean isWritable();
+
+ /**
+ * Tests whether this handle's location actually exists at the source.
+ *
+ * @return True if the location exists; false if not.
+ * @throws IOException If something goes wrong with the existence check.
+ */
+ boolean exists() throws IOException;
+
+ /**
+ * Gets the last modified timestamp of the location.
+ *
+ * @return The last modified timestamp, or null if the handle does not support
+ * this feature or if the location does not exist.
+ * @throws IOException If something goes wrong with the last modified check.
+ */
+ default Date lastModified() throws IOException {
+ return null;
+ }
+
+ /**
+ * Gets a "fast" checksum which succinctly represents the contents of the data
+ * stream. The term "fast" here refers to the idea that the checksum be
+ * retrievable quickly, without actually performing a thorough computation
+ * across the entire data stream. Typically, such a thing is feasible because
+ * the checksum was calculated a priori; e.g., artifacts deployed to remote
+ * Maven repositories are always deployed with corresponding checksum files.
+ *
+ * No guarantee is made about the exact nature of the checksum (e.g., SHA-1 or
+ * MD5), only that the value is deterministic for this particular location
+ * with its current contents. In other words: if a checksum differs from a
+ * previous inquiry, you can be sure the contents have changed; conversely, if
+ * the checksum is still the same, the contents are highly likely to be
+ * unchanged.
+ *
+ *
+ * @return The checksum, or null if the handle does not support this feature.
+ * @throws IOException If something goes wrong when accessing the checksum.
+ */
+ default String checksum() throws IOException {
+ return null;
+ }
+
+ /** Returns the current offset in the stream. */
+ long offset() throws IOException;
+
+ /**
+ * Sets the stream offset, measured from the beginning of the stream, at which
+ * the next read or write occurs.
+ */
+ void seek(long pos) throws IOException;
+
+ /**
+ * Returns the length of the data in bytes.
+ *
+ * @return The length, or -1 if the length is unknown.
+ */
+ long length() throws IOException;
+
+ /**
+ * Sets the new length of the handle.
+ *
+ * @param length New length.
+ * @throws IOException If there is an error changing the handle's length.
+ */
+ void setLength(long length) throws IOException;
+
+ /**
+ * Gets the number of bytes which can be read from, or written to, the
+ * data handle, bounded by the specified number of bytes.
+ *
+ * In the case of reading, attempting to read the returned number of bytes is
+ * guaranteed not to throw {@link EOFException}. However, be aware that the
+ * following methods might still process fewer bytes than indicated
+ * by this method:
+ *
+ *
+ *
{@link #read(byte[])}
+ *
{@link #read(byte[], int, int)}
+ *
{@link #skip(long)}
+ *
{@link #skipBytes(int)}
+ *
+ *
+ * In the case of writing, attempting to write the returned number of bytes is
+ * guaranteed not to expand the length of the handle; i.e., the write will
+ * only overwrite bytes already within the handle's bounds.
+ *
+ *
+ * @param count Desired number of bytes to read/write.
+ * @return The actual number of bytes which could be read/written,
+ * which might be less than the requested value.
+ * @throws IOException If something goes wrong with the check.
+ */
+ default long available(final long count) throws IOException {
+ final long remain = length() - offset();
+ return remain < count ? remain : count;
+ }
+
+ /**
+ * Ensures that the handle has sufficient bytes available to read.
+ *
+ * @param count Number of bytes to read.
+ * @see #available(long)
+ * @throws EOFException If there are insufficient bytes available.
+ * @throws IOException If the handle is write-only, or something goes wrong
+ * with the check.
+ */
+ default void ensureReadable(final long count) throws IOException {
+ if (!isReadable()) throw new IOException("This handle is write-only.");
+ if (available(count) < count) throw new EOFException();
+ }
+
+ /**
+ * Ensures that the handle has the correct length to be written to, and
+ * extends it as required.
+ *
+ * @param count Number of bytes to write.
+ * @return {@code true} if the handle's length was sufficient, or
+ * {@code false} if the handle's length required an extension.
+ * @throws IOException If the handle is read-only, or something goes wrong
+ * with the check, or there is an error changing the handle's
+ * length.
+ */
+ default boolean ensureWritable(final long count) throws IOException {
+ if (!isWritable()) throw new IOException("This handle is read-only.");
+ final long minLength = offset() + count;
+ if (length() < minLength) {
+ setLength(minLength);
+ return false;
+ }
+ return true;
+ }
+
+ /** Returns the byte order of the stream. */
+ ByteOrder getOrder();
+
+ /**
+ * Sets the byte order of the stream.
+ *
+ * @param order Order to set.
+ */
+ void setOrder(ByteOrder order);
+
+ /**
+ * Returns true iff the stream's order is {@link ByteOrder#BIG_ENDIAN}.
+ *
+ * @see #getOrder()
+ */
+ default boolean isBigEndian() {
+ return getOrder() == ByteOrder.BIG_ENDIAN;
+ }
+
+ /**
+ * Returns true iff the stream's order is {@link ByteOrder#LITTLE_ENDIAN}.
+ *
+ * @see #getOrder()
+ */
+ default boolean isLittleEndian() {
+ return getOrder() == ByteOrder.LITTLE_ENDIAN;
+ }
+
+ /**
+ * Sets the endianness of the stream.
+ *
+ * @param little If true, sets the order to {@link ByteOrder#LITTLE_ENDIAN};
+ * otherwise, sets the order to {@link ByteOrder#BIG_ENDIAN}.
+ * @see #setOrder(ByteOrder)
+ */
+ default void setLittleEndian(final boolean little) {
+ setOrder(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
+ }
+
+ /** Gets the native encoding of the stream. */
+ String getEncoding();
+
+ /** Sets the native encoding of the stream. */
+ void setEncoding(String encoding);
+
+ /**
+ * @return a 8 byte long buffer array used for type conversions
+ */
+ byte[] conversionBuffer();
+
+ /** Reads a string of arbitrary length, terminated by a null char. */
+ default String readCString() throws IOException {
+ final String line = findString("\0");
+ return line.length() == 0 ? null : line;
+ }
+
+ /** Reads a string of up to length n. */
+ default String readString(final int n) throws IOException {
+ final int r = (int) available(n);
+ final byte[] b = new byte[r];
+ readFully(b);
+ return new String(b, getEncoding());
+ }
+
+ /**
+ * Reads a string ending with one of the characters in the given string.
+ *
+ * @see #findString(String...)
+ */
+ default String readString(final String lastChars) throws IOException {
+ if (lastChars.length() == 1) return findString(lastChars);
+ final String[] terminators = new String[lastChars.length()];
+ for (int i = 0; i < terminators.length; i++) {
+ terminators[i] = lastChars.substring(i, i + 1);
+ }
+ return findString(terminators);
+ }
+
+ /**
+ * Reads a string ending with one of the given terminating substrings.
+ *
+ * @param terminators The strings for which to search.
+ * @return The string from the initial position through the end of the
+ * terminating sequence, or through the end of the stream if no
+ * terminating sequence is found.
+ */
+ default String findString(final String... terminators) throws IOException {
+ return findString(true, DEFAULT_BLOCK_SIZE, terminators);
+ }
+
+ /**
+ * Reads or skips a string ending with one of the given terminating
+ * substrings.
+ *
+ * @param saveString Whether to collect the string from the current offset to
+ * the terminating bytes, and return it. If false, returns null.
+ * @param terminators The strings for which to search.
+ * @throws IOException If saveString flag is set and the maximum search length
+ * (512 MB) is exceeded.
+ * @return The string from the initial position through the end of the
+ * terminating sequence, or through the end of the stream if no
+ * terminating sequence is found, or null if saveString flag is unset.
+ */
+ default String findString(final boolean saveString,
+ final String... terminators) throws IOException
+ {
+ return findString(saveString, DEFAULT_BLOCK_SIZE, terminators);
+ }
+
+ /**
+ * Reads a string ending with one of the given terminating substrings, using
+ * the specified block size for buffering.
+ *
+ * @param blockSize The block size to use when reading bytes in chunks.
+ * @param terminators The strings for which to search.
+ * @return The string from the initial position through the end of the
+ * terminating sequence, or through the end of the stream if no
+ * terminating sequence is found.
+ */
+ default String findString(final int blockSize, final String... terminators)
+ throws IOException
+ {
+ return findString(true, blockSize, terminators);
+ }
+
+ /**
+ * Reads or skips a string ending with one of the given terminating
+ * substrings, using the specified block size for buffering.
+ *
+ * @param saveString Whether to collect the string from the current offset to
+ * the terminating bytes, and return it. If false, returns null.
+ * @param blockSize The block size to use when reading bytes in chunks.
+ * @param terminators The strings for which to search.
+ * @throws IOException If saveString flag is set and the maximum search length
+ * (512 MB) is exceeded.
+ * @return The string from the initial position through the end of the
+ * terminating sequence, or through the end of the stream if no
+ * terminating sequence is found, or null if saveString flag is unset.
+ */
+ default String findString(final boolean saveString, final int blockSize,
+ final String... terminators) throws IOException
+ {
+ final StringBuilder out = new StringBuilder();
+ final long startPos = offset();
+ long bytesDropped = 0;
+ final long maxLen = saveString ? MAX_SEARCH_SIZE : Long.MAX_VALUE;
+ boolean match = false;
+ int maxTermLen = 0;
+ for (final String term : terminators) {
+ final int len = term.length();
+ if (len > maxTermLen) maxTermLen = len;
+ }
+
+ @SuppressWarnings("resource")
+ final InputStreamReader in = new InputStreamReader(
+ new DataHandleInputStream<>(this), getEncoding());
+ final char[] buf = new char[blockSize];
+ long loc = 0;
+ int r = 0;
+
+ // NB: we need at least 2 bytes to read a char
+ while (loc < maxLen && ((r = in.read(buf, 0, blockSize)) > 1)) {
+ // if we're not saving the string, drop any old, unnecessary output
+ if (!saveString) {
+ final int outLen = out.length();
+ if (outLen >= maxTermLen) {
+ final int dropIndex = outLen - maxTermLen + 1;
+ final String last = out.substring(dropIndex, outLen);
+ out.setLength(0);
+ out.append(last);
+ bytesDropped += dropIndex;
+ }
+ }
+ // append block to output
+ out.append(buf, 0, r);
+
+ // check output, returning smallest possible string
+ int min = Integer.MAX_VALUE;
+ int tagLen = 0;
+ for (final String t : terminators) {
+ final int len = t.length();
+ final int start = (int) (loc - bytesDropped - len);
+ final int value = out.indexOf(t, start < 0 ? 0 : start);
+ if (value >= 0 && value < min) {
+ match = true;
+ min = value;
+ tagLen = len;
+ }
+ }
+
+ if (match) {
+ // reset stream to proper location
+ seek(startPos + bytesDropped + min + tagLen);
+
+ // trim output string
+ if (saveString) {
+ out.setLength(min + tagLen);
+ return out.toString();
+ }
+ return null;
+ }
+
+ loc += r;
+ }
+
+ // no match
+ if (loc > MAX_SEARCH_SIZE) {
+ throw new IOException("Maximum search length reached.");
+ }
+ return saveString ? out.toString() : null;
+ }
+
+ /**
+ * Writes the provided string, followed by a newline character.
+ *
+ * @param string The string to write.
+ * @throws IOException If an I/O error occurs.
+ */
+ default void writeLine(final String string) throws IOException {
+ writeBytes(string);
+ writeBytes("\n");
+ }
+
+ // -- InputStream look-alikes --
+
+ /**
+ * Reads the next byte of data from the stream.
+ *
+ * @return the next byte of data, or -1 if the end of the stream is reached.
+ * @throws IOException - if an I/O error occurs.
+ */
+ default int read() throws IOException {
+ return offset() < length() ? readByte() & 0xff : -1;
+ }
+
+ /**
+ * Reads up to b.length bytes of data from the stream into an array of bytes.
+ *
+ * @return the total number of bytes read into the buffer.
+ */
+ default int read(final byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ /**
+ * Reads up to {@code len} bytes of data from the stream into an array of
+ * bytes.
+ *
+ * @return the total number of bytes read into the buffer.
+ */
+ int read(byte[] b, int off, int len) throws IOException;
+
+ /**
+ * Skips over and discards {@code n} bytes of data from the stream. The
+ * {@code skip} method may, for a variety of reasons, end up skipping over
+ * some smaller number of bytes, possibly {@code 0}. This may result from any
+ * of a number of conditions; reaching end of file before {@code n} bytes have
+ * been skipped is only one possibility. The actual number of bytes skipped is
+ * returned. If {@code n} is negative, no bytes are skipped.
+ *
+ * @param n - the number of bytes to be skipped.
+ * @return the actual number of bytes skipped.
+ * @throws IOException - if an I/O error occurs.
+ */
+ default long skip(final long n) throws IOException {
+ final long skip = available(n);
+ if (skip <= 0) return 0;
+ seek(offset() + skip);
+ return skip;
+ }
+
+ // -- DataInput methods --
+
+ @Override
+ default void readFully(final byte[] b) throws IOException {
+ readFully(b, 0, b.length);
+ }
+
+ @Override
+ default void readFully(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ // NB: Adapted from java.io.DataInputStream.readFully(byte[], int, int).
+ if (len < 0) throw new IndexOutOfBoundsException();
+ int n = 0;
+ while (n < len) {
+ final int count = read(b, off + n, len - n);
+ if (count < 0) throw new EOFException();
+ n += count;
+ }
+ }
+
+ @Override
+ default int skipBytes(final int n) throws IOException {
+ // NB: Cast here is safe since the value of n bounds the result to an int.
+ final int skip = (int) available(n);
+ if (skip < 0) return 0;
+ seek(offset() + skip);
+ return skip;
+ }
+
+ @Override
+ default boolean readBoolean() throws IOException {
+ return readByte() != 0;
+ }
+
+ @Override
+ default int readUnsignedByte() throws IOException {
+ return readByte() & 0xff;
+ }
+
+ @Override
+ default short readShort() throws IOException {
+ final byte[] buf = conversionBuffer();
+ final int read = read(buf, 0, 2);
+ if (read < 2) throw new EOFException();
+ return Bytes.toShort(buf, isLittleEndian());
+ }
+
+ @Override
+ default int readUnsignedShort() throws IOException {
+ return readShort() & 0xffff;
+ }
+
+ @Override
+ default char readChar() throws IOException {
+ return (char) readShort();
+ }
+
+ @Override
+ default int readInt() throws IOException {
+ final byte[] buf = conversionBuffer();
+ final int read = read(buf, 0, 4);
+ if (read < 4) throw new EOFException();
+ return Bytes.toInt(buf, isLittleEndian());
+ }
+
+ @Override
+ default long readLong() throws IOException {
+ final byte[] buf = conversionBuffer();
+ final int read = read(buf, 0, 8);
+ if (read < 8) {
+ throw new EOFException();
+ }
+ return Bytes.toLong(buf, isLittleEndian());
+ }
+
+ @Override
+ default float readFloat() throws IOException {
+ return Float.intBitsToFloat(readInt());
+ }
+
+ @Override
+ default double readDouble() throws IOException {
+ return Double.longBitsToDouble(readLong());
+ }
+
+ @Override
+ default String readLine() throws IOException {
+ // NB: Adapted from java.io.RandomAccessFile.readLine().
+
+ final StringBuilder input = new StringBuilder();
+ int c = -1;
+ boolean eol = false;
+
+ while (!eol) {
+ switch (c = read()) {
+ case -1:
+ case '\n':
+ eol = true;
+ break;
+ case '\r':
+ eol = true;
+ final long cur = offset();
+ if (read() != '\n') seek(cur);
+ break;
+ default:
+ input.append((char) c);
+ break;
+ }
+ }
+
+ if (c == -1 && input.length() == 0) {
+ return null;
+ }
+ return input.toString();
+ }
+
+ @Override
+ default String readUTF() throws IOException {
+ return DataInputStream.readUTF(this);
+ }
+
+ // -- DataOutput methods --
+
+ @Override
+ default void write(final byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ default void writeBoolean(final boolean v) throws IOException {
+ write(v ? 1 : 0);
+ }
+
+ @Override
+ default void writeByte(final int v) throws IOException {
+ write(v);
+ }
+
+ @Override
+ default void writeShort(final int v) throws IOException {
+ final byte[] buf = conversionBuffer();
+ Bytes.unpack(v, buf, 0, 2, isLittleEndian());
+ write(buf, 0, 2);
+ }
+
+ @Override
+ default void writeChar(final int v) throws IOException {
+ writeShort(v);
+ }
+
+ @Override
+ default void writeInt(final int v) throws IOException {
+ final byte[] buf = conversionBuffer();
+ Bytes.unpack(v, buf, 0, 4, isLittleEndian());
+ write(buf, 0, 4);
+ }
+
+ @Override
+ default void writeLong(final long v) throws IOException {
+ final byte[] buf = conversionBuffer();
+ Bytes.unpack(v, buf, 0, 8, isLittleEndian());
+ write(buf, 0, 8);
+ }
+
+ @Override
+ default void writeFloat(final float v) throws IOException {
+ final byte[] buf = conversionBuffer();
+ Bytes.unpack(Float.floatToIntBits(v), buf, 0, 4, isLittleEndian());
+ write(buf, 0, 4);
+ }
+
+ @Override
+ default void writeDouble(final double v) throws IOException {
+ final byte[] buf = conversionBuffer();
+ Bytes.unpack(Double.doubleToLongBits(v), buf, 0, 8, isLittleEndian());
+ write(buf, 0, 8);
+ }
+
+ @Override
+ default void writeBytes(final String s) throws IOException {
+ write(s.getBytes("UTF-8"));
+ }
+
+ @Override
+ default void writeChars(final String s) throws IOException {
+ final int len = s.length();
+ for (int i = 0; i < len; i++) {
+ writeChar(s.charAt(i));
+ }
+ }
+
+ @Override
+ default void writeUTF(final String str) throws IOException {
+ DataHandles.writeUTF(str, this);
+ }
+}
diff --git a/src/main/java/org/scijava/io/DataHandleInputStream.java b/src/main/java/org/scijava/io/handle/DataHandleInputStream.java
similarity index 93%
rename from src/main/java/org/scijava/io/DataHandleInputStream.java
rename to src/main/java/org/scijava/io/handle/DataHandleInputStream.java
index 6b2e21e4a..10e6b84e9 100644
--- a/src/main/java/org/scijava/io/DataHandleInputStream.java
+++ b/src/main/java/org/scijava/io/handle/DataHandleInputStream.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,11 +27,13 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.handle;
import java.io.IOException;
import java.io.InputStream;
+import org.scijava.io.location.Location;
+
/**
* {@link InputStream} backed by a {@link DataHandle}.
*
diff --git a/src/main/java/org/scijava/io/DataHandleOutputStream.java b/src/main/java/org/scijava/io/handle/DataHandleOutputStream.java
similarity index 91%
rename from src/main/java/org/scijava/io/DataHandleOutputStream.java
rename to src/main/java/org/scijava/io/handle/DataHandleOutputStream.java
index 6f876694d..3328a4e0a 100644
--- a/src/main/java/org/scijava/io/DataHandleOutputStream.java
+++ b/src/main/java/org/scijava/io/handle/DataHandleOutputStream.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,11 +27,13 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.handle;
import java.io.IOException;
import java.io.OutputStream;
+import org.scijava.io.location.Location;
+
/**
* {@link OutputStream} backed by a {@link DataHandle}.
*
diff --git a/src/main/java/org/scijava/io/handle/DataHandleService.java b/src/main/java/org/scijava/io/handle/DataHandleService.java
new file mode 100644
index 000000000..fae0e7f4a
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/DataHandleService.java
@@ -0,0 +1,121 @@
+/*
+ * #%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.io.handle;
+
+import java.io.IOException;
+
+import org.scijava.io.IOService;
+import org.scijava.io.location.Location;
+import org.scijava.plugin.WrapperService;
+import org.scijava.service.SciJavaService;
+
+/**
+ * Interface for low-level data I/O: reading and writing bytes using
+ * {@link DataHandle}s.
+ *
+ * @author Curtis Rueden
+ * @see IOService
+ * @see Location
+ */
+public interface DataHandleService extends
+ WrapperService>, SciJavaService
+{
+
+ // -- PTService methods --
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ default Class> getPluginType() {
+ return (Class) DataHandle.class;
+ }
+
+ // -- Typed methods --
+
+ @Override
+ default Class getType() {
+ return Location.class;
+ }
+
+ /**
+ * Convenience method to test whether it describes an existing file.
+ *
+ * @param location the location to test
+ * @return The result of {@link DataHandle#exists()} on a newly created handle
+ * on this location. Also returns {@code false} if the handle can not
+ * be created.
+ * @throws IOException if the creation of the handle fails exceptionally
+ */
+ default boolean exists(final Location location) throws IOException {
+ try (DataHandle handle = create(location)) {
+ return handle == null ? false : handle.exists();
+ }
+ }
+
+ /**
+ * Wraps the provided {@link DataHandle} in a read-only buffer for accelerated
+ * reading.
+ *
+ * @param handle the handle to wrap
+ * @return The handle wrapped in a read-only buffer, or {@code null} if the
+ * input handle is {@code null}
+ * @see ReadBufferDataHandle#ReadBufferDataHandle(DataHandle)
+ */
+ default DataHandle readBuffer(final DataHandle handle) {
+ return handle == null ? null : new ReadBufferDataHandle(handle);
+ }
+
+ /**
+ * Creates a {@link DataHandle} on the provided {@link Location} wrapped in a
+ * read-only buffer for accelerated reading.
+ *
+ * @param location the Location to create a buffered handle on.
+ * @return A {@link DataHandle} on the provided location wrapped in a
+ * read-only buffer, or {@code null} if no handle could be created for
+ * the location.
+ * @see ReadBufferDataHandle#ReadBufferDataHandle(DataHandle)
+ */
+ default DataHandle readBuffer(final Location location) {
+ final DataHandle handle = create(location);
+ return handle == null ? null : new ReadBufferDataHandle(handle);
+ }
+
+ /**
+ * Wraps the provided {@link DataHandle} in a write-only buffer for
+ * accelerated writing.
+ *
+ * @param handle the handle to wrap
+ * @return the handle wrapped in a write-only buffer or {@code null} if the
+ * provided handle is {@code null}
+ * @see WriteBufferDataHandle#WriteBufferDataHandle(DataHandle)
+ */
+ default DataHandle writeBuffer(final DataHandle handle) {
+ return handle == null ? null : new WriteBufferDataHandle(handle);
+ }
+}
diff --git a/src/main/java/org/scijava/io/handle/DataHandles.java b/src/main/java/org/scijava/io/handle/DataHandles.java
new file mode 100644
index 000000000..1464f637e
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/DataHandles.java
@@ -0,0 +1,252 @@
+/*
+ * #%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.io.handle;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.io.UTFDataFormatException;
+
+import org.scijava.io.location.Location;
+import org.scijava.task.Task;
+
+/**
+ * Utility methods for working with {@link DataHandle}s.
+ *
+ * @author Curtis Rueden
+ * @author Gabriel Einsdorf
+ */
+public final class DataHandles {
+
+ private DataHandles() {
+ // Prevent instantiation of utility class.
+ }
+
+ /**
+ * Writes a string to the specified DataOutput using modified UTF-8 encoding
+ * in a machine-independent manner.
+ *
+ * First, two bytes are written to out as if by the {@code writeShort} method
+ * giving the number of bytes to follow. This value is the number of bytes
+ * actually written out, not the length of the string. Following the length,
+ * each character of the string is output, in sequence, using the modified
+ * UTF-8 encoding for the character. If no exception is thrown, the counter
+ * {@code written} is incremented by the total number of bytes written to the
+ * output stream. This will be at least two plus the length of {@code str},
+ * and at most two plus thrice the length of {@code str}.
+ *
+ *
+ * @param str a string to be written.
+ * @param out destination to write to
+ * @return The number of bytes written out.
+ * @throws IOException if an I/O error occurs.
+ */
+ public static int writeUTF(final String str, final DataOutput out)
+ throws IOException
+ {
+ // Encode string as modified UTF-8 per java.io.DataOutput specification.
+ final int strlen = str.length();
+ int utflen = 0;
+ for (int i = 0; i < strlen; i++) {
+ final char c = str.charAt(i);
+ if (c >= '\u0001' && c <= '\u007F') utflen += 1;
+ else if (c <= '\u07FF') utflen += 2;
+ else utflen += 3;
+ }
+ if (utflen > 65535) throw new UTFDataFormatException(
+ "encoded string too long: " + utflen + " bytes");
+ final byte[] bytes = new byte[utflen + 2];
+ bytes[0] = (byte) ((utflen >>> 8) & 0xFF);
+ bytes[1] = (byte) (utflen & 0xFF);
+ int pos = 2;
+ for (int i = 0; i < strlen; i++) {
+ final char c = str.charAt(i);
+ if (c >= '\u0001' && c <= '\u007F') {
+ bytes[pos++] = (byte) c;
+ }
+ else if (c <= '\u07FF') {
+ bytes[pos++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
+ bytes[pos++] = (byte) (0x80 | (c & 0x3F));
+ }
+ else {
+ bytes[pos++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
+ bytes[pos++] = (byte) (0x80 | ((c >> 6) & 0x3F));
+ bytes[pos++] = (byte) (0x80 | (c & 0x3F));
+ }
+ }
+ out.write(bytes);
+ return utflen + 2;
+ }
+
+ protected static IOException readOnlyException() {
+ return new IOException("This handle is read-only!");
+ }
+
+ protected static IOException writeOnlyException() {
+ return new IOException("This handle is write-only!");
+ }
+
+
+ /**
+ * Copies all bytes from the input to the output handle. Reading and writing
+ * start at the current positions of the handles.
+ *
+ * @param in the input handle
+ * @param out the output handle
+ * @return the number of bytes copied
+ * @throws IOException if an I/O error occurs.
+ */
+ public static long copy(final DataHandle in,
+ final DataHandle out) throws IOException
+ {
+ return copy(in, out, 0l, null);
+ }
+
+ /**
+ * Copies all bytes from the input to the output handle, reporting the
+ * progress to the provided task. Reading and writing start at the current
+ * positions of the handles.
+ *
+ * @param in the input handle
+ * @param out the output handle
+ * @param task task to report progress to
+ * @return the number of bytes copied
+ * @throws IOException if an I/O error occurs.
+ */
+ public static long copy(final DataHandle in,
+ final DataHandle out, final Task task) throws IOException
+ {
+ return copy(in, out, 0l, task);
+ }
+
+ /**
+ * Copies up to length bytes from the input to the output handle.
+ * Reading and writing start at the current positions of the handles. Stops
+ * early if there are no more bytes available from the input handle.
+ *
+ * @param in the input handle
+ * @param out the output handle
+ * @param length maximum number of bytes to copy; will copy all bytes if set
+ * to 0
+ * @return the number of bytes copied
+ * @throws IOException if an I/O error occurs.
+ */
+ public static long copy(final DataHandle in,
+ final DataHandle out, final int length) throws IOException
+ {
+ return copy(in, out, length, null);
+ }
+
+ /**
+ * Copies up to length bytes from the input to the output handle,
+ * reporting the progress to the provided task. Reading and writing start at
+ * the current positions of the handles. Stops early if there are no more
+ * bytes available from the input handle.
+ *
+ * @param in input handle
+ * @param out the output handle
+ * @param length maximum number of bytes to copy; will copy all bytes if set
+ * to 0
+ * @param task a task object to use for reporting the status of the copy
+ * operation. Can be null if no reporting is needed.
+ * @return the number of bytes copied
+ * @throws IOException if an I/O error occurs.
+ */
+ public static long copy(final DataHandle in,
+ final DataHandle out, final long length, final Task task)
+ throws IOException
+ {
+ return copy(in, out, length, task, 64 * 1024);
+ }
+
+ /**
+ * Copies up to length bytes from the input to the output handle,
+ * reporting the progress to the provided task. Reading and writing start at
+ * the current positions of the handles. Stops early if there are no more
+ * bytes available from the input handle. Uses a buffer of the provided size,
+ * instead of using the default size.
+ *
+ * @param in input handle
+ * @param out the output handle
+ * @param length maximum number of bytes to copy, will copy all bytes if set
+ * to 0
+ * @param task a task object to use for reporting the status of the copy
+ * operation. Can be null if no reporting is needed.
+ * @return the number of bytes copied
+ * @throws IOException if an I/O error occurs.
+ */
+ public static long copy(final DataHandle in,
+ final DataHandle out, final long length, final Task task,
+ final int bufferSize) throws IOException
+ {
+
+ // get length of input
+ final long inputlength;
+ {
+ long i = 0;
+ try {
+ i = in.length();
+ }
+ catch (final IOException exc) {
+ // Assume unknown length.
+ i = 0;
+ }
+ inputlength = i;
+ }
+
+ if (task != null) {
+ if (length > 0) task.setProgressMaximum(length);
+ else if (inputlength > 0) task.setProgressMaximum(inputlength);
+ }
+
+ final byte[] buffer = new byte[bufferSize];
+ long totalRead = 0;
+
+ while (true) {
+ if (task != null && task.isCanceled()) break;
+ final int r;
+ // ensure we do not read more than required into the buffer
+ if (length > 0 && totalRead + bufferSize > length) {
+ int remaining = (int) (length - totalRead);
+ r = in.read(buffer, 0, remaining);
+ }
+ else {
+ r = in.read(buffer);
+ }
+ if (r <= 0) break; // EOF
+ if (task != null && task.isCanceled()) break;
+ out.write(buffer, 0, r);
+ totalRead += r;
+ if (task != null) {
+ task.setProgressValue(task.getProgressValue() + r);
+ }
+ }
+ return totalRead;
+ }
+}
diff --git a/src/main/java/org/scijava/io/DefaultDataHandleService.java b/src/main/java/org/scijava/io/handle/DefaultDataHandleService.java
similarity index 89%
rename from src/main/java/org/scijava/io/DefaultDataHandleService.java
rename to src/main/java/org/scijava/io/handle/DefaultDataHandleService.java
index dc98ce28f..1cdbcdf46 100644
--- a/src/main/java/org/scijava/io/DefaultDataHandleService.java
+++ b/src/main/java/org/scijava/io/handle/DefaultDataHandleService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,8 +27,9 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.handle;
+import org.scijava.io.location.Location;
import org.scijava.plugin.AbstractWrapperService;
import org.scijava.plugin.Plugin;
import org.scijava.service.Service;
diff --git a/src/main/java/org/scijava/io/handle/DummyHandle.java b/src/main/java/org/scijava/io/handle/DummyHandle.java
new file mode 100644
index 000000000..74b2d24ab
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/DummyHandle.java
@@ -0,0 +1,146 @@
+/*
+ * #%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.io.handle;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.scijava.io.location.DummyLocation;
+import org.scijava.plugin.Plugin;
+
+/**
+ * A {@link DataHandle} which reads all zeroes, and writes no actual data.
+ *
+ * @author Curtis Rueden
+ */
+@Plugin(type = DataHandle.class)
+public class DummyHandle extends AbstractDataHandle {
+
+ // -- Fields --
+
+ private long offset;
+ private long length;
+
+ // -- Constructors --
+
+ public DummyHandle() { }
+
+ public DummyHandle(final DummyLocation location) {
+ set(location);
+ }
+
+ // -- DataHandle methods --
+
+ @Override
+ public boolean isReadable() {
+ return true;
+ }
+
+ @Override
+ public boolean isWritable() {
+ return true;
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ @Override
+ public long offset() {
+ return offset;
+ }
+
+ @Override
+ public void seek(final long pos) {
+ if (pos > length()) setLength(pos);
+ offset = pos;
+ }
+
+ @Override
+ public long length() {
+ return length;
+ }
+
+ @Override
+ public void setLength(final long length) {
+ this.length = length;
+ }
+
+ // -- DataInput methods --
+
+ @Override
+ public byte readByte() throws IOException {
+ final long r = available(1);
+ if (r <= 0) return -1;
+ offset++;
+ return 0;
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ final int r = (int) available(len);
+ offset += r;
+ Arrays.fill(b, off, off + r, (byte) 0);
+ return r;
+ }
+
+ // -- DataOutput methods --
+
+ @Override
+ public void write(final int v) throws IOException {
+ ensureWritable(1);
+ offset++;
+ }
+
+ @Override
+ public void write(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ ensureWritable(len);
+ offset += len;
+ }
+
+ // -- Closeable methods --
+
+ @Override
+ public void close() {
+ // NB: No action needed.
+ }
+
+ // -- Typed methods --
+
+ @Override
+ public Class getType() {
+ return DummyLocation.class;
+ }
+}
diff --git a/src/main/java/org/scijava/io/FileHandle.java b/src/main/java/org/scijava/io/handle/FileHandle.java
similarity index 51%
rename from src/main/java/org/scijava/io/FileHandle.java
rename to src/main/java/org/scijava/io/handle/FileHandle.java
index f3ac1f422..3734f7ebb 100644
--- a/src/main/java/org/scijava/io/FileHandle.java
+++ b/src/main/java/org/scijava/io/handle/FileHandle.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,11 +27,16 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.handle;
+import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Date;
+import org.scijava.io.location.FileLocation;
import org.scijava.plugin.Plugin;
/**
@@ -50,13 +53,27 @@ public class FileHandle extends AbstractDataHandle {
private RandomAccessFile raf;
/** The mode of the {@link RandomAccessFile}. */
- private String mode = "rw";
+ private String mode;
+
+ /** True iff the {@link #close()} has already been called. */
+ private boolean closed;
+
+ // -- Constructors --
+
+ public FileHandle() { }
+
+ public FileHandle(final FileLocation location) {
+ set(location);
+ }
// -- FileHandle methods --
- /** Gets the random access file object backing this FileHandle. */
+ /**
+ * Gets the random access file object backing this FileHandle. If the
+ * underlying file does not exist yet, it will be created.
+ */
public RandomAccessFile getRandomAccessFile() throws IOException {
- return raf();
+ return writer();
}
public String getMode() {
@@ -72,196 +89,185 @@ public void setMode(final String mode) {
// -- DataHandle methods --
+ @Override
+ public boolean isReadable() {
+ return getMode().contains("r");
+ }
+
+ @Override
+ public boolean isWritable() {
+ return getMode().contains("w");
+ }
+
+ @Override
+ public boolean exists() {
+ return get().getFile().exists();
+ }
+
+ @Override
+ public Date lastModified() {
+ final long lastModified = get().getFile().lastModified();
+ return lastModified == 0 ? null : new Date(lastModified);
+ }
+
@Override
public long offset() throws IOException {
- return raf().getFilePointer();
+ return exists() ? reader().getFilePointer() : 0;
}
@Override
public long length() throws IOException {
- return raf().length();
+ return exists() ? reader().length() : -1;
+ }
+
+ @Override
+ public void setLength(final long length) throws IOException {
+ writer().setLength(length);
}
@Override
public int read() throws IOException {
- return raf().read();
+ return reader().read();
}
@Override
public int read(final byte[] b) throws IOException {
- return raf().read(b);
+ return reader().read(b);
}
@Override
public int read(final byte[] b, final int off, final int len)
throws IOException
{
- return raf().read(b, off, len);
+ return reader().read(b, off, len);
}
@Override
public void seek(final long pos) throws IOException {
- raf().seek(pos);
+ if (isWritable()) {
+ writer().seek(pos);
+ }
+ else {
+ reader().seek(pos);
+ }
}
// -- DataInput methods --
@Override
public boolean readBoolean() throws IOException {
- return raf().readBoolean();
+ return reader().readBoolean();
}
@Override
public byte readByte() throws IOException {
- return raf().readByte();
- }
-
- @Override
- public char readChar() throws IOException {
- return raf().readChar();
- }
-
- @Override
- public double readDouble() throws IOException {
- return raf().readDouble();
- }
-
- @Override
- public float readFloat() throws IOException {
- return raf().readFloat();
+ return reader().readByte();
}
@Override
public void readFully(final byte[] b) throws IOException {
- raf().readFully(b);
+ reader().readFully(b);
}
@Override
public void readFully(final byte[] b, final int off, final int len)
throws IOException
{
- raf().readFully(b, off, len);
- }
-
- @Override
- public int readInt() throws IOException {
- return raf().readInt();
+ reader().readFully(b, off, len);
}
@Override
public String readLine() throws IOException {
- return raf().readLine();
- }
-
- @Override
- public long readLong() throws IOException {
- return raf().readLong();
- }
-
- @Override
- public short readShort() throws IOException {
- return raf().readShort();
+ return reader().readLine();
}
@Override
public int readUnsignedByte() throws IOException {
- return raf().readUnsignedByte();
- }
-
- @Override
- public int readUnsignedShort() throws IOException {
- return raf().readUnsignedShort();
+ return reader().readUnsignedByte();
}
@Override
public String readUTF() throws IOException {
- return raf().readUTF();
+ return reader().readUTF();
}
@Override
public int skipBytes(final int n) throws IOException {
- return raf().skipBytes(n);
+ return reader().skipBytes(n);
}
// -- DataOutput methods --
@Override
public void write(final byte[] b) throws IOException {
- raf().write(b);
+ writer().write(b);
}
@Override
public void write(final byte[] b, final int off, final int len)
throws IOException
{
- raf().write(b, off, len);
+ writer().write(b, off, len);
}
@Override
public void write(final int b) throws IOException {
- raf().write(b);
+ writer().write(b);
}
@Override
public void writeBoolean(final boolean v) throws IOException {
- raf().writeBoolean(v);
+ writer().writeBoolean(v);
}
@Override
public void writeByte(final int v) throws IOException {
- raf().writeByte(v);
+ writer().writeByte(v);
}
@Override
public void writeBytes(final String s) throws IOException {
- raf().writeBytes(s);
- }
-
- @Override
- public void writeChar(final int v) throws IOException {
- raf().writeChar(v);
+ writer().writeBytes(s);
}
@Override
public void writeChars(final String s) throws IOException {
- raf().writeChars(s);
- }
-
- @Override
- public void writeDouble(final double v) throws IOException {
- raf().writeDouble(v);
+ writer().writeChars(s);
}
@Override
- public void writeFloat(final float v) throws IOException {
- raf().writeFloat(v);
+ public void writeUTF(final String str) throws IOException {
+ writer().writeUTF(str);
}
- @Override
- public void writeInt(final int v) throws IOException {
- raf().writeInt(v);
- }
+ // -- Closeable methods --
@Override
- public void writeLong(final long v) throws IOException {
- raf().writeLong(v);
+ public synchronized void close() throws IOException {
+ if (raf != null) raf.close();
+ closed = true;
}
- @Override
- public void writeShort(final int v) throws IOException {
- raf().writeShort(v);
- }
+ // -- WrapperPlugin methods --
@Override
- public void writeUTF(final String str) throws IOException {
- raf().writeUTF(str);
- }
-
- // -- Closeable methods --
+ public void set(FileLocation loc) {
+ super.set(loc);
- @Override
- public void close() throws IOException {
- raf().close();
+ // Infer the initial mode based on file existence + permissions.
+ final File file = loc.getFile();
+ String mode;
+ if (file.exists()) {
+ final Path path = loc.getFile().toPath();
+ mode = "";
+ if (Files.isReadable(path)) mode += "r";
+ if (Files.isWritable(path)) mode += "w";
+ }
+ else {
+ // Non-existent file; assume the intent is to create it.
+ mode = "rw";
+ }
+ setMode(mode);
}
// -- Typed methods --
@@ -273,13 +279,49 @@ public Class getType() {
// -- Helper methods --
- private RandomAccessFile raf() throws IOException {
- if (raf == null) initRAF();
+ /**
+ * Access method for the internal {@link RandomAccessFile}, that succeeds
+ * independently of the underlying file existing on disk. This allows us to
+ * create a new file for writing.
+ *
+ * @return the internal {@link RandomAccessFile} creating a new file on disk
+ * if needed.
+ * @throws IOException if the {@link RandomAccessFile} could not be created.
+ */
+ private RandomAccessFile writer() throws IOException {
+ if (raf == null) initRAF(true);
return raf;
}
- private synchronized void initRAF() throws IOException {
- raf = new RandomAccessFile(get().getFile(), getMode());
+ /**
+ * Access method for the internal {@link RandomAccessFile}, that only succeeds
+ * if the underlying file exists on disk. This prevents accidental creation of
+ * an empty file when calling read operations on a non-existent file.
+ *
+ * @return the internal {@link RandomAccessFile}.
+ * @throws IOException if the {@link RandomAccessFile} could not be created,
+ * or the backing file does not exists.
+ */
+ private RandomAccessFile reader() throws IOException {
+ if (raf == null) initRAF(false);
+ return raf;
}
+ /**
+ * Initializes the {@link RandomAccessFile}.
+ *
+ * @param create whether to create the {@link RandomAccessFile} if the
+ * underlying file does not exist yet.
+ * @throws IOException if the {@link RandomAccessFile} could not be created,
+ * or the backing file does not exist and the {@code create}
+ * parameter was set to {@code false}.
+ */
+ private synchronized void initRAF(final boolean create) throws IOException {
+ if (!create && !exists()) {
+ throw new IOException("Trying to read from non-existent file!");
+ }
+ if (closed) throw new IOException("Handle already closed");
+ if (raf != null) return;
+ raf = new RandomAccessFile(get().getFile(), getMode());
+ }
}
diff --git a/src/main/java/org/scijava/io/handle/ReadBufferDataHandle.java b/src/main/java/org/scijava/io/handle/ReadBufferDataHandle.java
new file mode 100644
index 000000000..fed7de110
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/ReadBufferDataHandle.java
@@ -0,0 +1,319 @@
+/*
+ * #%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.io.handle;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.scijava.io.location.Location;
+
+/**
+ * Read-only buffered {@link DataHandle}. It buffers the underlying handle into
+ * a fixed number of pages, swapping them out when necessary.
+ */
+public class ReadBufferDataHandle extends AbstractHigherOrderHandle {
+
+ private static final int DEFAULT_PAGE_SIZE = 10_000;
+ private static final int DEFAULT_NUM_PAGES = 10;
+
+ private final int pageSize;
+ private final List pages;
+ private final int[] slotToPage;
+ private final LRUReplacementStrategy replacementStrategy;
+ private final Map pageToSlot;
+
+ /**
+ * Cached length value, for performance. When reading data, length is not
+ * expected to change, but querying it (e.g. via native filesystem access)
+ * can be slow, and we need to query the length frequently.
+ */
+ private long length = -1;
+ private long offset = 0l;
+ private byte[] currentPage;
+ private int currentPageID = -1;
+
+ /**
+ * Creates a {@link ReadBufferDataHandle} wrapping the provided handle using the
+ * default values for the size of the pages ({@value #DEFAULT_PAGE_SIZE} byte)
+ * and number of pages ({@link #DEFAULT_NUM_PAGES}).
+ *
+ * @param handle
+ * the handle to wrap
+ */
+ public ReadBufferDataHandle(final DataHandle handle) {
+ this(handle, DEFAULT_PAGE_SIZE);
+ }
+
+ /**
+ * Creates a {@link ReadBufferDataHandle} wrapping the provided handle using the
+ * default value for the number of pages ({@link #DEFAULT_NUM_PAGES}).
+ *
+ * @param handle
+ * the handle to wrap
+ * @param pageSize
+ * the size of the used pages
+ */
+ public ReadBufferDataHandle(final DataHandle handle, final int pageSize) {
+ this(handle, pageSize, DEFAULT_NUM_PAGES);
+ }
+
+ /**
+ * Creates a {@link ReadBufferDataHandle} wrapping the provided handle.
+ *
+ * @param handle
+ * the handle to wrap
+ * @param pageSize
+ * the size of the used pages
+ * @param numPages
+ * the number of pages to use
+ */
+ public ReadBufferDataHandle(final DataHandle handle, final int pageSize, final int numPages) {
+ super(handle);
+ this.pageSize = pageSize;
+
+ // init maps
+ slotToPage = new int[numPages];
+ Arrays.fill(slotToPage, -1);
+
+ pages = new ArrayList<>(numPages);
+ for (int i = 0; i < numPages; i++) {
+ pages.add(null);
+ }
+
+ pageToSlot = new HashMap<>();
+ replacementStrategy = new LRUReplacementStrategy(numPages);
+ }
+
+ /**
+ * Ensures that the byte at the given offset is buffered, and sets the current
+ * page to be the one containing the specified location.
+ */
+ private void ensureBuffered(final long globalOffset) throws IOException {
+ ensureOpen();
+ final int pageID = (int) (globalOffset / pageSize);
+ if (pageID == currentPageID)
+ return;
+
+ final int slotID = pageToSlot.computeIfAbsent(pageID, replacementStrategy::pickVictim);
+ final int inSlotID = slotToPage[slotID];
+
+ if (inSlotID != pageID) { // desired page is not buffered
+ // update the mappings
+ slotToPage[slotID] = pageID;
+ pageToSlot.put(pageID, slotID);
+ pageToSlot.put(inSlotID, null);
+
+ // read the page
+ currentPage = readPage(pageID, slotID);
+ } else {
+ currentPage = pages.get(slotID);
+ }
+ replacementStrategy.accessed(slotID);
+ currentPageID = pageID;
+ }
+
+ /**
+ * Reads the page with the id pageID into the slot with the id
+ * slotID.
+ *
+ * @param pageID
+ * the id of the page to read
+ * @param slotID
+ * the id of the slot to read the page into
+ * @return the read page
+ * @throws IOException
+ * if the reading fails
+ */
+ private byte[] readPage(final int pageID, final int slotID) throws IOException {
+ replacementStrategy.accessed(slotID);
+ byte[] page = pages.get(slotID);
+ if (page == null) {
+ // lazy initialization
+ page = new byte[pageSize];
+ pages.set(slotID, page);
+ }
+
+ final long startOfPage = pageID * (long) pageSize;
+ if (handle().offset() != startOfPage) {
+ handle().seek(startOfPage);
+ }
+
+ // NB: we read repeatedly until the page is full or EOF is reached
+ // handle().read(..) might read less bytes than requested
+ int off = 0;
+ while (off < pageSize) {
+ final int read = handle().read(page, off, pageSize - off);
+ if (read == -1) { // EOF
+ break;
+ }
+ off += read;
+ }
+ return page;
+ }
+
+ /**
+ * Calculates the offset in the current page for the given global offset
+ */
+ private int globalToLocalOffset(final long off) {
+ return (int) (off % pageSize);
+ }
+
+ @Override
+ public void seek(final long pos) throws IOException {
+ this.offset = pos;
+ }
+
+ @Override
+ public long length() throws IOException {
+ if (length < 0) length = super.length();
+ return length;
+ }
+
+ @Override
+ public int read(final byte[] b, final int targetOffset, final int len)
+ throws IOException
+ {
+ if (len == 0) return 0;
+
+ // the last position we will read
+ final long endPos = offset + len;
+
+ // the number of bytes we plan to read
+ final int readLength = (int) (endPos < length() ? len : length() - offset);
+
+ int read = 0; // the number of bytes we have read
+ int localTargetOff = targetOffset;
+
+ while (read < readLength) {
+ ensureBuffered(offset);
+
+ // calculate local offsets
+ final int pageOffset = globalToLocalOffset(offset);
+ int localLength = pageSize - pageOffset;
+ localLength = Math.min(localLength, readLength - read);
+ localLength = Math.min(localLength, b.length - localTargetOff);
+ if (localLength == 0) break; // we've read all we can
+
+ // copy the data
+ System.arraycopy(currentPage, pageOffset, b, localTargetOff, localLength);
+
+ // update offsets
+ read += localLength;
+ offset += localLength;
+ localTargetOff += localLength;
+ }
+ // return -1 if we tried to read at least one byte but failed
+ return read != 0 ? read : -1;
+ }
+
+ @Override
+ public byte readByte() throws IOException {
+ ensureBuffered(offset);
+ return currentPage[globalToLocalOffset(offset++)];
+ }
+
+ @Override
+ public boolean isReadable() {
+ return true;
+ }
+
+ @Override
+ public long offset() throws IOException {
+ return offset;
+ }
+
+ @Override
+ protected void cleanup() {
+ pages.clear();
+ currentPage = null;
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ throw DataHandles.readOnlyException();
+ }
+
+ @Override
+ public void write(final byte[] b, final int off, final int len) throws IOException {
+ throw DataHandles.readOnlyException();
+ }
+
+ @Override
+ public void setLength(final long length) throws IOException {
+ throw DataHandles.readOnlyException();
+ }
+
+ /**
+ * Simple strategy to pick the slot that get's evicted from the cache. This
+ * strategy always picks the least recently used slot.
+ */
+ private class LRUReplacementStrategy {
+
+ private final Deque queue;
+
+ /**
+ * Creates a {@link LRUReplacementStrategy} with the specified number of slots.
+ *
+ * @param numSlots
+ * the number of slots to use
+ */
+ public LRUReplacementStrategy(final int numSlots) {
+ queue = new ArrayDeque<>(numSlots);
+
+ // fill the queue
+ for (int i = 0; i < numSlots; i++) {
+ queue.add(i);
+ }
+ }
+
+ /**
+ * Notifies this strategy that a slot has been accessed, pushing it to the end
+ * of the queue.
+ *
+ * @param slotID
+ * the id of the slot that has been accessed
+ */
+ public void accessed(final int slotID) {
+ // put accessed element to the end of the queue
+ queue.remove(slotID);
+ queue.add(slotID);
+ }
+
+ public int pickVictim(@SuppressWarnings("unused") final int pageID) {
+ return queue.peek();
+ }
+ }
+}
diff --git a/src/main/java/org/scijava/io/handle/ResettableStreamHandle.java b/src/main/java/org/scijava/io/handle/ResettableStreamHandle.java
new file mode 100644
index 000000000..b4a7310f8
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/ResettableStreamHandle.java
@@ -0,0 +1,70 @@
+/*
+ * #%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.io.handle;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.scijava.io.location.Location;
+
+/**
+ * A {@link DataHandle} backed by an {@link InputStream} and/or
+ * {@link OutputStream}. Supports resetting the handle to the start of the
+ * internal stream(s).
+ */
+public interface ResettableStreamHandle extends
+ StreamHandle
+{
+
+ @Override
+ default void seek(final long pos) throws IOException {
+ final long off = offset();
+ if (pos == off) return; // nothing to do
+ if (pos > off) {
+ // jump from the current offset
+ jump(pos - off);
+ }
+ else {
+ // jump from the beginning of the stream
+ resetStream();
+ jump(pos);
+ }
+ setOffset(pos);
+ }
+
+ /**
+ * Resets the stream to its start.
+ *
+ * @throws IOException If something goes wrong with the reset
+ */
+ @Override
+ void resetStream() throws IOException;
+}
diff --git a/src/main/java/org/scijava/io/handle/SeekableStreamHandle.java b/src/main/java/org/scijava/io/handle/SeekableStreamHandle.java
new file mode 100644
index 000000000..6b4746f56
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/SeekableStreamHandle.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.io.handle;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.scijava.io.location.Location;
+
+/**
+ * A {@link DataHandle} backed by an {@link InputStream} and/or
+ * {@link OutputStream}. Supports seeking to an arbitrary position within the
+ * stream.
+ *
+ * @author Gabriel Einsdorf
+ */
+public interface SeekableStreamHandle extends
+ ResettableStreamHandle
+{
+
+ @Override
+ void seek(long pos) throws IOException;
+}
diff --git a/src/main/java/org/scijava/io/handle/StreamHandle.java b/src/main/java/org/scijava/io/handle/StreamHandle.java
new file mode 100644
index 000000000..30a3e88ba
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/StreamHandle.java
@@ -0,0 +1,190 @@
+/*
+ * #%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.io.handle;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.scijava.io.location.Location;
+
+/**
+ * A {@link DataHandle} backed by an {@link InputStream} and/or
+ * {@link OutputStream}.
+ *
+ * @author Curtis Rueden
+ * @author Melissa Linkert
+ * @author Gabriel Einsdorf
+ */
+public interface StreamHandle extends DataHandle {
+
+ // -- StreamHandle methods --
+
+ /**
+ * Gets an input stream for reading data, positioned at the current offset.
+ *
+ * @return the appropriate input stream, or null if the handle is write-only.
+ * @throws IOException
+ */
+ InputStream in() throws IOException;
+
+ /**
+ * Gets an output stream for writing data, positioned at the current offset.
+ *
+ * @return the appropriate output stream, or null if the handle is read-only.
+ */
+ OutputStream out() throws IOException;
+
+ /**
+ * Sets the offset of the handle to the given position.
+ *
+ * This method is intended to be called only in conjunction with reading from
+ * the input stream, or writing to the output stream. Otherwise, the contents
+ * may get out of sync.
+ *
+ */
+ void setOffset(long offset);
+
+ /**
+ * Increments the handle's offset by the given amount.
+ *
+ * This method is intended to be called only in conjunction with reading from
+ * the input stream, or writing to the output stream. Otherwise, the contents
+ * may get out of sync.
+ *
+ */
+ default void advance(final long bytes) throws IOException {
+ setOffset(offset() + bytes);
+ }
+
+ // -- DataHandle methods --
+
+ @Override
+ default void seek(final long pos) throws IOException {
+ if (pos == offset()) return;
+ if (pos > offset()) {
+ jump(pos - offset());
+ }
+ else {
+ throw new UnsupportedOperationException(
+ "Can't seek backwards through this StreamHandle");
+ }
+ }
+
+ /**
+ * Resets the stream to its start.
+ *
+ * @throws IOException If something goes wrong with the reset
+ */
+ void resetStream() throws IOException;
+
+ default void jump(final long n) throws IOException, EOFException {
+ long remain = n;
+ while (remain > 0) {
+ final long r = in().skip(remain);
+ if (r < 0) throw new EOFException();
+ remain -= r;
+ }
+ }
+
+ @Override
+ default void ensureReadable(final long count) throws IOException {
+ if (in() == null) throw new IOException("This handle is write-only.");
+ DataHandle.super.ensureReadable(count);
+ }
+
+ @Override
+ default boolean ensureWritable(final long count) throws IOException {
+ if (out() == null) throw new IOException("This handle is read-only.");
+ return DataHandle.super.ensureWritable(count);
+ }
+
+ @Override
+ default int read() throws IOException {
+ ensureReadable(0);
+ final int v = in().read();
+ if (v >= 0) advance(1);
+ return v;
+ }
+
+ @Override
+ default byte readByte() throws IOException {
+ int ch = this.read();
+ if (ch < 0) throw new EOFException();
+ return (byte) (ch);
+ }
+
+ @Override
+ default int read(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ final int n = in().read(b, off, len);
+ if (n >= 0) advance(n);
+ return n;
+ }
+
+ // -- DataOutput methods --
+
+ @Override
+ default void write(final int v) throws IOException {
+ ensureWritable(1);
+ out().write(v);
+ advance(1);
+ }
+
+ @Override
+ default void writeByte(int v) throws IOException {
+ write(v);
+ }
+
+ @Override
+ default void write(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ ensureWritable(len);
+ out().write(b, off, len);
+ advance(len);
+ }
+
+ // -- Closeable methods --
+
+ @Override
+ default void close() throws IOException {
+ // TODO: Double check this logic.
+ try (final InputStream in = in()) {
+ if (in != null) in.close();
+ }
+ try (final OutputStream out = out()) {
+ if (out != null) out.close();
+ }
+ }
+
+}
diff --git a/src/main/java/org/scijava/io/handle/WriteBufferDataHandle.java b/src/main/java/org/scijava/io/handle/WriteBufferDataHandle.java
new file mode 100644
index 000000000..f1549aa30
--- /dev/null
+++ b/src/main/java/org/scijava/io/handle/WriteBufferDataHandle.java
@@ -0,0 +1,203 @@
+/*
+ * #%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.io.handle;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import org.scijava.io.location.Location;
+
+/**
+ * Wraps a {@link DataHandle} and acts as a write buffer.
+ *
+ * @author Gabriel Einsdorf
+ */
+public class WriteBufferDataHandle extends AbstractHigherOrderHandle {
+
+ private static final int DEFAULT_BUFFERSIZE = 10_000;
+ private long offset = 0;
+ private int nextPos = 0;
+
+ private byte[] buffer;
+ private final int bufferSize;
+
+ /**
+ * Creates a {@link WriteBufferDataHandle} that wraps the given
+ * {@link DataHandle}, the default size for the buffer is used
+ * ({@value #DEFAULT_BUFFERSIZE} bytes).
+ *
+ * @param handle the handle to wrap
+ */
+ public WriteBufferDataHandle(final DataHandle handle) {
+ this(handle, DEFAULT_BUFFERSIZE);
+ }
+
+ /**
+ * Creates a {@link WriteBufferDataHandle} that wraps the given
+ * {@link DataHandle}
+ *
+ * @param handle the handle to wrap
+ * @param bufferSize the size of the write buffer in bytes
+ */
+ public WriteBufferDataHandle(final DataHandle handle,
+ final int bufferSize)
+ {
+ super(handle);
+ this.bufferSize = bufferSize;
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ ensureOpen();
+ // if buffer is full flush
+ if (nextPos >= buffer.length) {
+ flush();
+ }
+ // buffer the byte
+ buffer[nextPos] = (byte) b;
+ nextPos++;
+ offset++;
+ }
+
+ @Override
+ public void write(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ ensureOpen();
+ // ensure the range is valid
+ if ((off < 0) || (off > b.length) || (len < 0) || ((off +
+ len) > b.length) || ((off + len) < 0))
+ {
+ throw new IndexOutOfBoundsException();
+ }
+ else if (len == 0) {
+ return; // nothing to do
+ }
+
+ // skip the buffering and write directly to the handle
+ if (len > buffer.length) {
+ flush();
+ handle().write(b, off, len);
+ offset += len;
+ return;
+ }
+
+ // copy to buffer / flush if necessary
+ int start = off;
+ final int total = off + len;
+ while (start < total) {
+ final int numItems = Math.min(buffer.length - nextPos, total - start);
+ System.arraycopy(b, start, buffer, nextPos, numItems);
+ start += numItems;
+ nextPos += numItems;
+ if (nextPos >= buffer.length) {
+ flush();
+ }
+ }
+ }
+
+ /**
+ * Write the buffer content to the underlying handle
+ */
+ private void flush() throws IOException {
+ ensureOpen();
+ if (nextPos == 0) return;
+
+ handle().write(buffer, 0, nextPos);
+ nextPos = 0;
+ }
+
+ @Override
+ public long length() throws IOException {
+ // data written out + data in the buffer
+ return handle().length() + nextPos - 1;
+ }
+
+ @Override
+ public void setLength(final long length) throws IOException {
+ ensureOpen();
+ handle().setLength(length);
+ }
+
+ @Override
+ public boolean isReadable() {
+ return false;
+ }
+
+ /**
+ * @throws IOException if this handle has been closed
+ */
+ @Override
+ protected void ensureOpen() throws IOException {
+ super.ensureOpen();
+ if (buffer == null) {
+ buffer = new byte[bufferSize];
+ }
+ }
+
+ @Override
+ public long offset() throws IOException {
+ return offset;
+ }
+
+ @Override
+ public void seek(final long pos) throws IOException {
+ ensureOpen();
+ if (pos >= length()) {
+ throw new EOFException();
+ }
+ flush();
+ offset = pos;
+ handle().seek(offset);
+ }
+
+ @Override
+ public long skip(final long n) throws IOException {
+ throw new IOException("Operation 'skip' is not supported!");
+ }
+
+ @Override
+ public byte readByte() throws IOException {
+ throw DataHandles.writeOnlyException();
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, final int len)
+ throws IOException
+ {
+ throw DataHandles.writeOnlyException();
+ }
+
+ @Override
+ protected void cleanup() throws IOException {
+ flush();
+ buffer = null;
+ }
+}
diff --git a/src/main/java/org/scijava/io/location/AbstractLocation.java b/src/main/java/org/scijava/io/location/AbstractLocation.java
new file mode 100644
index 000000000..007a1a8a9
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/AbstractLocation.java
@@ -0,0 +1,68 @@
+/*
+ * #%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.io.location;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * Abstract base class for {@link Location} implementations.
+ *
+ * @author Curtis Rueden
+ */
+public abstract class AbstractLocation implements Location {
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((getURI() == null) ? 0 : getURI().hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ final Location other = (Location) obj;
+ return Objects.equals(getURI(), other.getURI());
+ }
+
+ @Override
+ public String toString() {
+ final String prefix = getClass().getSimpleName() + ":";
+ final URI uri = getURI();
+ if (uri != null) return prefix + uri;
+ final String name = getName();
+ if (name != null) return prefix + name;
+ return prefix + defaultName();
+ }
+}
diff --git a/src/main/java/org/scijava/io/location/AbstractLocationResolver.java b/src/main/java/org/scijava/io/location/AbstractLocationResolver.java
new file mode 100644
index 000000000..b40f6c0d9
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/AbstractLocationResolver.java
@@ -0,0 +1,69 @@
+/*
+ * #%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.io.location;
+
+import java.net.URI;
+
+import org.scijava.plugin.AbstractHandlerPlugin;
+
+/**
+ * Abstract super class for {@link LocationResolver} plugins.
+ *
+ * @author Gabriel Einsdorf
+ */
+public abstract class AbstractLocationResolver extends
+ AbstractHandlerPlugin implements LocationResolver
+{
+
+ private final String[] schemes;
+
+ /**
+ * @param schemes the uri schmemes that the implementing sub-type supports
+ */
+ public AbstractLocationResolver(String... schemes) {
+ assert schemes.length > 0;
+ this.schemes = schemes;
+ }
+
+ @Override
+ public boolean supports(URI uri) {
+ boolean supports = false;
+ for (final String scheme : schemes) {
+ supports = supports || scheme.equals(uri.getScheme());
+ }
+ return supports;
+ }
+
+ @Override
+ public Class getType() {
+ return URI.class;
+ }
+
+}
diff --git a/src/main/java/org/scijava/io/location/AbstractRemoteLocation.java b/src/main/java/org/scijava/io/location/AbstractRemoteLocation.java
new file mode 100644
index 000000000..61b3f9735
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/AbstractRemoteLocation.java
@@ -0,0 +1,55 @@
+/*
+ * #%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.io.location;
+
+/**
+ * Abstract base class for {@link RemoteLocation} implementations.
+ *
+ * @author Gabriel Einsdorf
+ */
+public abstract class AbstractRemoteLocation extends AbstractLocation implements
+ RemoteLocation
+{
+
+ /**
+ * The {@link RemoteLocation}s timeout in milliseconds.
+ */
+ private long timeout = 15_000l;
+
+ @Override
+ public long getTimeout() {
+ return timeout;
+ }
+
+ @Override
+ public void setTimeout(final long timeout) {
+ this.timeout = timeout;
+ }
+}
diff --git a/src/main/java/org/scijava/io/location/BrowsableLocation.java b/src/main/java/org/scijava/io/location/BrowsableLocation.java
new file mode 100644
index 000000000..f6efc3b99
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/BrowsableLocation.java
@@ -0,0 +1,100 @@
+/*
+ * #%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.io.location;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A {@link Location} that offers methods to browse other locations relative to
+ * it.
+ *
+ * @author Gabriel Einsdorf
+ * @author Curtis Rueden
+ */
+public interface BrowsableLocation extends Location {
+
+ /**
+ * Obtains a location pointing to the parent directory of this one.
+ *
+ * @return the parent location of this one, or null if this
+ * location has no parent.
+ * @throws IOException if something goes wrong obtaining the parent.
+ */
+ BrowsableLocation parent() throws IOException;
+
+ /**
+ * Obtains a collection of locations for whom this location is the parent.
+ * Note that this will only succeed if calls to {@link #isDirectory()} on this
+ * location return true.
+ *
+ * @return A set containing the children of this location, or
+ * {@link Collections#EMPTY_SET} if this location has no children.
+ * @throws IOException if something goes wrong obtaining the children.
+ * @throws IllegalArgumentException if this location is not a directory (i.e.,
+ * {@link #isDirectory()} returns false).
+ */
+ Set children() throws IOException;
+
+ /**
+ * Obtains a location relative to this one, which will be configured
+ * like the current location, but point to a the file specified by the
+ * path parameter.
+ *
+ * @param path the relative path of the desired location.
+ * @return A location that points to the specified file location.
+ * @throws IOException if something goes wrong obtaining the sibling
+ */
+ BrowsableLocation sibling(String path) throws IOException;
+
+ /**
+ * Tests whether this location is a directory, meaning that it can have
+ * children. It is recommended to use this method before calling
+ * {@link #child(String)} or {@link #children()}, to ensure those calls
+ * succeed.
+ *
+ * @return True iff the location represents a directory.
+ */
+ boolean isDirectory();
+
+ /**
+ * Obtains a location with the given name, for whom this location is the
+ * parent. Note that this will only succeed if calls to {@link #isDirectory()}
+ * on this location return true.
+ *
+ * @param name the name of the child
+ * @return a location pointing to the child
+ * @throws IOException if something goes wrong obtaining the child.
+ * @throws IllegalArgumentException if this location is not a directory (i.e.,
+ * {@link #isDirectory()} returns false).
+ */
+ BrowsableLocation child(String name) throws IOException;
+}
diff --git a/src/main/java/org/scijava/io/location/BytesLocation.java b/src/main/java/org/scijava/io/location/BytesLocation.java
new file mode 100644
index 000000000..0b7f604ed
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/BytesLocation.java
@@ -0,0 +1,183 @@
+/*
+ * #%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.io.location;
+
+import org.scijava.io.ByteArrayByteBank;
+import org.scijava.io.ByteBank;
+import org.scijava.util.ByteArray;
+
+/**
+ * {@link Location} backed by a {@link ByteBank}.
+ *
+ * @author Curtis Rueden
+ * @author Gabriel Einsdorf
+ */
+public class BytesLocation extends AbstractLocation {
+
+ private final ByteBank bytes;
+
+ private final String name;
+
+ /**
+ * Creates a {@link BytesLocation} backed by the specified
+ * {@link ByteBank}.
+ *
+ * @param bytes the {@link ByteBank} that will back this {@link Location}
+ */
+ public BytesLocation(final ByteBank bytes) {
+ this(bytes, null);
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by the specified {@link ByteBank}.
+ *
+ * @param bytes the {@link ByteBank} that will back this {@link Location}
+ * @param name the name of this {@link Location}
+ */
+ public BytesLocation(final ByteBank bytes, final String name) {
+ this.bytes = bytes;
+ this.name = name;
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by a {@link ByteArrayByteBank} with
+ * the specified initial capacity, but with a reported size of 0. This method
+ * can be used to avoid needing to grow the underlying {@link ByteBank}.
+ */
+ public BytesLocation(final int initialCapacity) {
+ this(initialCapacity, null);
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by a {@link ByteArrayByteBank} with
+ * the specified initial capacity, but with a reported size of 0. This method
+ * can be used to avoid needing to grow the underlying {@link ByteBank}.
+ *
+ * @param name the name of this {@link Location}
+ */
+ public BytesLocation(final int initialCapacity, final String name) {
+ this.bytes = new ByteArrayByteBank(initialCapacity);
+ this.name = name;
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by a {@link ByteArrayByteBank}
+ * that wraps the specified {@link ByteArray}.
+ */
+ public BytesLocation(final ByteArray bytes) {
+ this(bytes, null);
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by a {@link ByteArrayByteBank} that
+ * wraps the specified {@link ByteArray}.
+ *
+ * @param name the name of this Location.
+ */
+ public BytesLocation(final ByteArray bytes, final String name) {
+ this.bytes = new ByteArrayByteBank(bytes);
+ this.name = name;
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by a {@link ByteArrayByteBank}
+ * which wraps the specified array.
+ *
+ * @param bytes the array to wrap
+ */
+ public BytesLocation(final byte[] bytes) {
+ this(bytes, null);
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by a {@link ByteArrayByteBank} which
+ * wraps the specified array.
+ *
+ * @param bytes the array to wrap
+ * @param name the name of this Location.
+ */
+ public BytesLocation(final byte[] bytes, final String name) {
+ this.bytes = new ByteArrayByteBank(bytes);
+ this.name = name;
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by a {@link ByteArrayByteBank} with
+ * the specified initial capacity and the provided data.
+ *
+ * @param bytes the bytes to copy into the new {@link BytesLocation}
+ * @param offset the offset in the bytes array to start copying from
+ * @param length the number of bytes to copy, starting from the offset
+ */
+ public BytesLocation(final byte[] bytes, final int offset, final int length) {
+ this(bytes, offset, length, null);
+ }
+
+ /**
+ * Creates a {@link BytesLocation} backed by a {@link ByteArrayByteBank} with
+ * the specified initial capacity and the provided data.
+ *
+ * @param bytes the bytes to copy into the new {@link BytesLocation}
+ * @param offset the offset in the bytes array to start copying from
+ * @param length the number of bytes to copy, starting from the offset
+ * @param name the name of this Location.
+ */
+ public BytesLocation(final byte[] bytes, final int offset, final int length,
+ final String name)
+ {
+ this.bytes = new ByteArrayByteBank(length);
+ this.bytes.setBytes(0l, bytes, offset, length);
+ this.name = name;
+ }
+
+ // -- BytesLocation methods --
+
+ /** Gets the backing {@link ByteBank}. */
+ public ByteBank getByteBank() {
+ return bytes;
+ }
+
+ @Override
+ public String getName() {
+ return name != null ? name : defaultName();
+ }
+
+ // -- Object methods --
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ return obj == this;
+ }
+}
diff --git a/src/main/java/org/scijava/io/location/DefaultLocationService.java b/src/main/java/org/scijava/io/location/DefaultLocationService.java
new file mode 100644
index 000000000..028dd807e
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/DefaultLocationService.java
@@ -0,0 +1,49 @@
+/*
+ * #%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.io.location;
+
+import java.net.URI;
+
+import org.scijava.plugin.AbstractHandlerService;
+import org.scijava.plugin.Plugin;
+import org.scijava.service.Service;
+
+/**
+ * Default {@link LocationService} implementation.
+ *
+ * @author Gabriel Einsdorf
+ */
+@Plugin(type = Service.class)
+public class DefaultLocationService extends
+ AbstractHandlerService implements
+ LocationService
+{
+ // NB: No implementation needed.
+}
diff --git a/src/main/java/org/scijava/io/AbstractLocation.java b/src/main/java/org/scijava/io/location/DummyLocation.java
similarity index 81%
rename from src/main/java/org/scijava/io/AbstractLocation.java
rename to src/main/java/org/scijava/io/location/DummyLocation.java
index 8937d3847..0a9dd546f 100644
--- a/src/main/java/org/scijava/io/AbstractLocation.java
+++ b/src/main/java/org/scijava/io/location/DummyLocation.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,13 +27,13 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.location;
/**
- * Abstract base class for {@link Location} implementations.
- *
+ * {@link Location} backed by nothing whatsoever.
+ *
* @author Curtis Rueden
*/
-public abstract class AbstractLocation implements Location {
+public class DummyLocation extends AbstractLocation {
// NB: No implementation needed.
}
diff --git a/src/main/java/org/scijava/io/location/FileLocation.java b/src/main/java/org/scijava/io/location/FileLocation.java
new file mode 100644
index 000000000..4cf688d3b
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/FileLocation.java
@@ -0,0 +1,127 @@
+/*
+ * #%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.io.location;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * {@link Location} backed by a {@link File} on disk.
+ *
+ * @author Curtis Rueden
+ * @author Gabriel Einsdorf
+ */
+public class FileLocation extends AbstractLocation implements
+ BrowsableLocation
+{
+
+ private final File file;
+
+ public FileLocation(final File file) {
+ Objects.requireNonNull(file);
+ this.file = file;
+ }
+
+ public FileLocation(final String path) {
+ this(new File(path));
+ }
+
+ public FileLocation(final URI path) {
+ this(new File(path));
+ }
+
+ // -- FileLocation methods --
+
+ /** Gets the associated {@link File}. */
+ public File getFile() {
+ return file;
+ }
+
+ // -- Location methods --
+
+ @Override
+ public URI getURI() {
+ return getFile().toURI();
+ }
+
+ @Override
+ public String getName() {
+ return file.getName();
+ }
+
+ // -- BrowsableLocation methods --
+
+ @Override
+ public FileLocation parent() throws IOException {
+ return new FileLocation(file.getParentFile());
+ }
+
+ @Override
+ public Set children() throws IOException {
+ validateDirectory();
+ final File[] files = file.listFiles();
+ if (files == null) return Collections.emptySet();
+
+ final Set out = new HashSet<>(files.length);
+ for (final File child : files) {
+ out.add(new FileLocation(child));
+ }
+ return out;
+ }
+
+ @Override
+ public FileLocation sibling(final String path) {
+ return new FileLocation(new File(file.getParentFile(), path));
+ }
+
+ @Override
+ public FileLocation child(final String name) {
+ validateDirectory();
+ return new FileLocation(new File(file, name));
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return file.isDirectory();
+ }
+
+ // -- Helper methods --
+
+ private void validateDirectory() {
+ if (isDirectory()) return;
+ throw new IllegalArgumentException(
+ "This location does not point to a directory!");
+ }
+}
diff --git a/src/main/java/org/scijava/io/location/FileLocationResolver.java b/src/main/java/org/scijava/io/location/FileLocationResolver.java
new file mode 100644
index 000000000..5e6b19df0
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/FileLocationResolver.java
@@ -0,0 +1,52 @@
+/*
+ * #%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.io.location;
+
+import java.net.URI;
+
+import org.scijava.plugin.Plugin;
+
+/**
+ * Implementation of {@link LocationResolver} for {@link FileLocation}.
+ *
+ * @author Gabriel Einsdorf
+ */
+@Plugin(type = LocationResolver.class)
+public class FileLocationResolver extends AbstractLocationResolver {
+
+ public FileLocationResolver() {
+ super("file");
+ }
+
+ @Override
+ public Location resolve(URI uri) {
+ return new FileLocation(uri);
+ }
+}
diff --git a/src/main/java/org/scijava/io/Location.java b/src/main/java/org/scijava/io/location/Location.java
similarity index 65%
rename from src/main/java/org/scijava/io/Location.java
rename to src/main/java/org/scijava/io/location/Location.java
index 34b79f63b..2c579ad7e 100644
--- a/src/main/java/org/scijava/io/Location.java
+++ b/src/main/java/org/scijava/io/location/Location.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,10 +27,12 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.location;
import java.net.URI;
+import org.scijava.io.handle.DataHandle;
+
/**
* A location is a data descriptor, such as a file on disk, a remote
* URL, or a database connection.
@@ -42,10 +42,11 @@
* resource identifier ({@link URI}), a location identifies where
* the data resides, without necessarily specifying how to access that
* data. The {@link DataHandle} interface defines a plugin that knows how to
- * provide a stream of bytes for a particular kind of location.
+ * read and/or write bytes for a particular kind of location.
*
*
* @author Curtis Rueden
+ * @author Gabriel Einsdorf
*/
public interface Location {
@@ -57,4 +58,30 @@ default URI getURI() {
return null;
}
+ /**
+ * Gets a (typically short) name expressing this location. This string is not
+ * intended to unambiguously identify the location, but rather act as a
+ * friendly, human-readable name. The precise behavior will depend on the
+ * implementation, but as an example, a file-based location could return the
+ * name of the associated file without its full path.
+ *
+ * @return The name, or an empty string if no name is available.
+ */
+ default String getName() {
+ return defaultName();
+ }
+
+ /**
+ * Gets the default name used when no explicit name is assigned.
+ *
+ * Note: this is mostly intended for debugging, since most kinds of
+ * {@code Location} will assign some non-default name. But in cases where that
+ * does not occur, this value can be useful to detect the situation.
+ *
+ *
+ * @return The default name string.
+ */
+ default String defaultName() {
+ return "Location.defaultName";
+ }
}
diff --git a/src/main/java/org/scijava/io/location/LocationResolver.java b/src/main/java/org/scijava/io/location/LocationResolver.java
new file mode 100644
index 000000000..e4c60eada
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/LocationResolver.java
@@ -0,0 +1,53 @@
+/*
+ * #%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.io.location;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.scijava.plugin.HandlerPlugin;
+
+/**
+ * {@link LocationResolver} plugins allow resolving an {@link URI} to a
+ * {@link Location}. Extending {@link AbstractLocationResolver} is recommended
+ * for easy implementation.
+ *
+ * @author Gabriel Einsdorf
+ */
+public interface LocationResolver extends HandlerPlugin {
+
+ /**
+ * Resolves the given {@link URI} to a {@link Location}
+ *
+ * @return the resolved Location
+ * @throws URISyntaxException
+ */
+ Location resolve(URI uri) throws URISyntaxException;
+}
diff --git a/src/main/java/org/scijava/io/location/LocationService.java b/src/main/java/org/scijava/io/location/LocationService.java
new file mode 100644
index 000000000..078eca44c
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/LocationService.java
@@ -0,0 +1,106 @@
+/*
+ * #%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.io.location;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.scijava.plugin.HandlerService;
+import org.scijava.service.SciJavaService;
+
+/**
+ * A service that allows resolving of URIs to Locations, using
+ * {@link LocationResolver} plugins for translation.
+ *
+ * @author Gabriel Einsdorf
+ */
+public interface LocationService extends HandlerService,
+ SciJavaService
+{
+
+ /**
+ * Turns the given string into a {@link URI}, then resolves it to a
+ * {@link Location}.
+ *
+ * @param uriString the uri to resolve
+ * @return the resolved {@link Location}
+ * @throws URISyntaxException if the URI is malformed
+ */
+ default Location resolve(final String uriString) throws URISyntaxException {
+ try {
+ Location loc = resolve(new URI(uriString));
+ if (loc != null) return loc;
+ }
+ catch (final URISyntaxException exc) {
+ // In general, filenames are not valid URI strings.
+ // Particularly on Windows, there are backslashes, which are invalid in URIs.
+ // So we explicitly turn this string into a file if an error happens above.
+ }
+ return resolve(new File(uriString).toURI());
+ }
+
+ /**
+ * Resolves the given {@link URI} to a location. If the {@code scheme} part of
+ * the URI is {@code null} the path component is resolved as a local file.
+ *
+ * @param uri the uri to resolve
+ * @return the resolved {@link Location} or null if no resolver
+ * could be found.
+ * @throws URISyntaxException if the URI is malformed
+ */
+ default Location resolve(URI uri) throws URISyntaxException {
+ if (uri.getScheme() == null) { // Fallback for local files
+ uri = new File(uri.getPath()).toURI();
+ }
+ final LocationResolver resolver = getResolver(uri);
+ return resolver != null ? resolver.resolve(uri) : null;
+ }
+
+ /** @deprecated Use {@link #getHandler} instead. */
+ @Deprecated
+ default LocationResolver getResolver(URI uri) {
+ return getHandler(uri);
+ }
+
+ // -- PTService methods --
+
+ @Override
+ default Class getPluginType() {
+ return LocationResolver.class;
+ }
+
+ // -- Typed methods --
+
+ @Override
+ default Class getType() {
+ return URI.class;
+ }
+}
diff --git a/src/main/java/org/scijava/io/location/RemoteLocation.java b/src/main/java/org/scijava/io/location/RemoteLocation.java
new file mode 100644
index 000000000..20fd65fef
--- /dev/null
+++ b/src/main/java/org/scijava/io/location/RemoteLocation.java
@@ -0,0 +1,53 @@
+/*
+ * #%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.io.location;
+
+/**
+ * A {@link Location} which resides on a remote machine.
+ *
+ * @author Gabriel Einsdorf
+ */
+public interface RemoteLocation extends Location {
+
+ /**
+ * Sets the connection timeout for this location.
+ *
+ * @param timeout The timeout in milliseconds.
+ */
+ void setTimeout(long timeout);
+
+ /**
+ * Gets the connection timeout for this location.
+ *
+ * @return The connection timeout in milliseconds.
+ */
+ long getTimeout();
+
+}
diff --git a/src/main/java/org/scijava/io/URILocation.java b/src/main/java/org/scijava/io/location/URILocation.java
similarity index 93%
rename from src/main/java/org/scijava/io/URILocation.java
rename to src/main/java/org/scijava/io/location/URILocation.java
index 819fb2282..52621ea91 100644
--- a/src/main/java/org/scijava/io/URILocation.java
+++ b/src/main/java/org/scijava/io/location/URILocation.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,7 +27,7 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.location;
import java.io.UnsupportedEncodingException;
import java.net.URI;
@@ -114,7 +112,7 @@ private Map decodeQuery(final String query) {
* @see URLDecoder
*/
private String decode(final String s) {
- // http://stackoverflow.com/a/6926987
+ // https://stackoverflow.com/a/6926987
try {
return URLDecoder.decode(s.replace("+", "%2B"), "UTF-8");
}
diff --git a/src/main/java/org/scijava/io/URLLocation.java b/src/main/java/org/scijava/io/location/URLLocation.java
similarity index 87%
rename from src/main/java/org/scijava/io/URLLocation.java
rename to src/main/java/org/scijava/io/location/URLLocation.java
index 1b7490894..396dedeae 100644
--- a/src/main/java/org/scijava/io/URLLocation.java
+++ b/src/main/java/org/scijava/io/location/URLLocation.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -29,7 +27,7 @@
* #L%
*/
-package org.scijava.io;
+package org.scijava.io.location;
import java.net.URI;
import java.net.URISyntaxException;
@@ -60,7 +58,7 @@ public URL getURL() {
/**
* Gets the associated {@link URI}, or null if this URL is not formatted
- * strictly according to to RFC2396 and cannot be converted to a URI.
+ * strictly according to RFC2396 and cannot be converted to a URI.
*/
@Override
public URI getURI() {
diff --git a/src/main/java/org/scijava/io/nio/ByteBufferByteBank.java b/src/main/java/org/scijava/io/nio/ByteBufferByteBank.java
new file mode 100644
index 000000000..6f9ec504d
--- /dev/null
+++ b/src/main/java/org/scijava/io/nio/ByteBufferByteBank.java
@@ -0,0 +1,180 @@
+/*
+ * #%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.io.nio;
+
+import java.nio.ByteBuffer;
+import java.util.function.Function;
+
+import org.scijava.io.ByteBank;
+
+/**
+ * A {@link ByteBank} backed by a {@link ByteBuffer}. Self-growing up to a
+ * maximal capacity of {@link Integer#MAX_VALUE}
+ *
+ * @author Gabriel Einsdorf
+ */
+public class ByteBufferByteBank implements ByteBank {
+
+ private static final int DEFAULT_CAPACITY = 10_000;
+
+ private ByteBuffer buffer;
+
+ private int size;
+
+ private Function provider;
+
+ public ByteBufferByteBank() {
+ provider = ByteBuffer::allocate;
+ buffer = provider.apply(DEFAULT_CAPACITY);
+ }
+
+ public ByteBufferByteBank(final Function provider) {
+ this.provider = provider;
+ buffer = provider.apply(DEFAULT_CAPACITY);
+ }
+
+ public ByteBufferByteBank(final Function provider,
+ final int initialCapacity)
+ {
+ this.provider = provider;
+ buffer = provider.apply(initialCapacity);
+ }
+
+ public ByteBufferByteBank(final int initialCapacity) {
+ provider = ByteBuffer::allocate;
+ buffer = provider.apply(initialCapacity);
+ }
+
+ @Override
+ public long getMaxBufferSize() {
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public void setBytes(final long startpos, final byte[] bytes,
+ final int offset, final int length)
+ {
+ // ensure we have space
+ checkWritePos(startpos, startpos + length);
+ final int neededCapacity = size + length;
+ ensureCapacity(neededCapacity);
+
+ // copy the data
+ buffer.position((int) startpos);
+ buffer.put(bytes, offset, length);
+
+ // update the maxpos
+ updateSize(startpos + length);
+ }
+
+ @Override
+ public void setByte(final long pos, final byte b) {
+ checkWritePos(pos, pos);
+ if (pos == buffer.capacity()) {
+ ensureCapacity((int) pos + 1);
+ }
+ buffer.put((int) pos, b);
+ updateSize(pos + 1);
+ }
+
+ @Override
+ public void clear() {
+ buffer.clear();
+ size = 0;
+ }
+
+ @Override
+ public byte getByte(final long pos) {
+ checkReadPos(pos, pos);
+ // the buffer might contain bytes with negative value
+ // we need to flip the sign to positive to satisfy the contract of this
+ // method
+ return buffer.get((int) pos);
+ }
+
+ @Override
+ public int getBytes(final long startPos, final byte[] b, final int offset,
+ final int length)
+ {
+ checkReadPos(startPos, startPos + length);
+ // ensure we don't try to read data which is not in the buffer
+ final int readLength = (int) Math.min(size() - startPos, length);
+ buffer.position((int) startPos);
+ buffer.get(b, offset, readLength);
+
+ return readLength;
+ }
+
+ @Override
+ public long size() {
+ return size;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ // NB: Some ByteBuffers are read-only. But there is no API to check it.
+ // Therefore, we make a "best effort" guess based on known read-only types.
+ // Since these read-only types are not public, we compare class names rather
+ // than checking for type equality or using instanceof.
+ final String className = buffer.getClass().getName();
+ return className.equals("java.nio.HeapByteBufferR") ||
+ className.equals("java.nio.DirectByteBufferR");
+ }
+
+ // -- Helper methods --
+
+ private void ensureCapacity(final int minCapacity) {
+ final int oldCapacity = buffer.capacity();
+ if (minCapacity <= oldCapacity) return; // no need to grow
+
+ // grow the array by up to 50% (plus a small constant)
+ final int growth = Math.min(oldCapacity / 2 + 16, Integer.MAX_VALUE);
+ final int newCapacity;
+ if (growth > Integer.MAX_VALUE - oldCapacity) {
+ // growth would push array over the maximum array size
+ newCapacity = Integer.MAX_VALUE;
+ }
+ else newCapacity = oldCapacity + growth;
+ // ensure the array grows by at least the requested minimum capacity
+ final int newLength = Math.max(minCapacity, newCapacity);
+
+ // copy the data into a new array
+ buffer.position(0);
+ final ByteBuffer newBuffer = ByteBuffer.allocate(newLength);
+ newBuffer.order(buffer.order());
+ newBuffer.put(buffer);
+ buffer = newBuffer;
+ }
+
+ private void updateSize(final long newSize) {
+ size = (int) (newSize > size ? newSize : size);
+ }
+
+}
diff --git a/src/main/java/org/scijava/io/nio/DefaultNIOService.java b/src/main/java/org/scijava/io/nio/DefaultNIOService.java
new file mode 100644
index 000000000..2550caab8
--- /dev/null
+++ b/src/main/java/org/scijava/io/nio/DefaultNIOService.java
@@ -0,0 +1,117 @@
+/*
+ * #%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.io.nio;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+
+import org.scijava.log.LogService;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+import org.scijava.service.AbstractService;
+import org.scijava.service.Service;
+
+/**
+ * Default service for working with the {@link java.nio} package, particularly
+ * NIO {@link ByteBuffer} objects.
+ *
+ * @author Chris Allan
+ * @author Curtis Rueden
+ */
+@Plugin(type = Service.class)
+public class DefaultNIOService extends AbstractService implements NIOService {
+
+ // -- Fields --
+
+ @Parameter
+ private LogService log;
+
+ /** Whether or not we are to use memory mapped I/O. */
+ private final boolean useMappedByteBuffer = Boolean.parseBoolean(System
+ .getProperty("mappedBuffers"));
+
+ // -- NIOService API methods --
+
+ @Override
+ public ByteBuffer allocate(final FileChannel channel, final MapMode mapMode,
+ final long bufferStartPosition, final int newSize) throws IOException
+ {
+ log.debug("NIO: allocate: mapped=" + useMappedByteBuffer + ", start=" +
+ bufferStartPosition + ", size=" + newSize);
+ if (useMappedByteBuffer) {
+ return allocateMappedByteBuffer(channel, mapMode, bufferStartPosition,
+ newSize);
+ }
+ return allocateDirect(channel, bufferStartPosition, newSize);
+ }
+
+ // -- Helper methods --
+
+ /**
+ * Allocates memory and copies the desired file data into it.
+ *
+ * @param channel File channel to allocate or map byte buffers from.
+ * @param bufferStartPosition The absolute position of the start of the
+ * buffer.
+ * @param newSize The buffer size.
+ * @return A newly allocated NIO byte buffer.
+ * @throws IOException If there is an issue aligning or allocating the buffer.
+ */
+ private ByteBuffer allocateDirect(final FileChannel channel,
+ final long bufferStartPosition, final int newSize) throws IOException
+ {
+ final ByteBuffer buffer = ByteBuffer.allocate(newSize);
+ channel.read(buffer, bufferStartPosition);
+ return buffer;
+ }
+
+ /**
+ * Memory maps the desired file data into memory.
+ *
+ * @param channel File channel to allocate or map byte buffers from.
+ * @param mapMode The map mode. Required but only used if memory mapped I/O is
+ * to occur.
+ * @param bufferStartPosition The absolute position of the start of the
+ * buffer.
+ * @param newSize The buffer size.
+ * @return A newly mapped NIO byte buffer.
+ * @throws IOException If there is an issue mapping, aligning or allocating
+ * the buffer.
+ */
+ private ByteBuffer allocateMappedByteBuffer(final FileChannel channel,
+ final MapMode mapMode, final long bufferStartPosition, final int newSize)
+ throws IOException
+ {
+ return channel.map(mapMode, bufferStartPosition, newSize);
+ }
+
+}
diff --git a/src/main/java/org/scijava/io/nio/NIOService.java b/src/main/java/org/scijava/io/nio/NIOService.java
new file mode 100644
index 000000000..5e90ced72
--- /dev/null
+++ b/src/main/java/org/scijava/io/nio/NIOService.java
@@ -0,0 +1,71 @@
+/*
+ * #%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.io.nio;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+
+import org.scijava.service.SciJavaService;
+
+/**
+ * Interface for services that work with the {@link java.nio} package,
+ * particularly NIO {@link ByteBuffer} objects.
+ *
+ * @author Chris Allan
+ * @author Curtis Rueden
+ */
+public interface NIOService extends SciJavaService {
+
+ /**
+ * Allocates or maps the desired file data into memory.
+ *
+ * This method provides a facade to byte buffer allocation that enables
+ * FileChannel.map() usage on platforms where it's unlikely to
+ * give us problems and heap allocation where it is.
+ *
+ *
+ * @param channel File channel to allocate or map byte buffers from.
+ * @param mapMode The map mode. Required but only used if memory mapped I/O is
+ * to occur.
+ * @param bufferStartPosition The absolute position of the start of the
+ * buffer.
+ * @param newSize The buffer size.
+ * @return A newly allocated or mapped NIO byte buffer.
+ * @see "https://bugs.java.com/bugdatabase/view_bug.do?bug_id=5092131"
+ * @see "https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6417205"
+ * @throws IOException If there is an issue mapping, aligning or allocating
+ * the buffer.
+ */
+ ByteBuffer allocate(FileChannel channel, MapMode mapMode,
+ long bufferStartPosition, int newSize) throws IOException;
+
+}
diff --git a/src/main/java/org/scijava/log/AbstractLogService.java b/src/main/java/org/scijava/log/AbstractLogService.java
index 69eddd8c0..099750f95 100644
--- a/src/main/java/org/scijava/log/AbstractLogService.java
+++ b/src/main/java/org/scijava/log/AbstractLogService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -38,263 +36,160 @@
import org.scijava.service.AbstractService;
/**
- * Base class for {@link LogService} implementationst.
+ * Base class for {@link LogService} implementations.
*
* @author Johannes Schindelin
+ * @author Curtis Rueden
+ * @author Matthias Arzt
*/
+@IgnoreAsCallingClass
public abstract class AbstractLogService extends AbstractService implements
LogService
{
- private int currentLevel = System.getenv("DEBUG") == null ? INFO : DEBUG;
+ private int currentLevel = levelFromEnvironment();
- private final Map classAndPackageLevels =
- new HashMap<>();
+ private final Map classAndPackageLevels;
- // -- abstract methods --
-
- /**
- * Displays a message.
- *
- * @param msg the message to display.
- */
- protected abstract void log(final String msg);
-
- /**
- * Displays an exception.
- *
- * @param t the exception to display.
- */
- protected abstract void log(final Throwable t);
+ private final Logger rootLogger;
// -- constructor --
public AbstractLogService() {
- // check SciJava log level system properties for initial logging levels
-
- // global log level property
- final String logProp = System.getProperty(LOG_LEVEL_PROPERTY);
- final int level = level(logProp);
- if (level >= 0) setLevel(level);
-
- if (getLevel() == 0) {
- // use the default, which is WARN unless the DEBUG env. variable is set
- setLevel(System.getenv("DEBUG") == null ? INFO : DEBUG);
- }
-
- // populate custom class- and package-specific log level properties
- final String logLevelPrefix = LOG_LEVEL_PROPERTY + ":";
- final Properties props = System.getProperties();
- for (final Object propKey : props.keySet()) {
- if (!(propKey instanceof String)) continue;
- final String propName = (String) propKey;
- if (!propName.startsWith(logLevelPrefix)) continue;
- final String classOrPackageName =
- propName.substring(logLevelPrefix.length());
- setLevel(classOrPackageName, level(props.getProperty(propName)));
- }
-
+ this(System.getProperties());
}
- // -- helper methods --
-
- protected void log(final int level, final Object msg, final Throwable t) {
- if (level > getLevel()) return;
-
- if (msg != null || t == null) {
- log(level, msg);
- }
- if (t != null) log(t);
- }
-
- protected void log(final int level, final Object msg) {
- final String prefix = getPrefix(level);
- log((prefix == null ? "" : prefix + " ") + msg);
- }
-
- protected String getPrefix(final int level) {
- switch (level) {
- case ERROR:
- return "[ERROR]";
- case WARN:
- return "[WARNING]";
- case INFO:
- return "[INFO]";
- case DEBUG:
- return "[DEBUG]";
- case TRACE:
- return "[TRACE]";
- default:
- return null;
- }
+ public AbstractLogService(final Properties properties) {
+ rootLogger = new RootLogger();
+ // provide this constructor to enable unit tests
+ final int level = LogLevel.value(properties.getProperty(
+ LogService.LOG_LEVEL_PROPERTY));
+ if (level >= 0) currentLevel = level;
+ classAndPackageLevels = setupMapFromProperties(properties,
+ LogService.LOG_LEVEL_PROPERTY + ":");
+ initLogSourceLevels(properties);
}
- // -- LogService methods --
+ // -- AbstractLogService methods --
@Override
- public void debug(final Object msg) {
- log(DEBUG, msg, null);
+ public void setLevel(final int level) {
+ currentLevel = level;
}
@Override
- public void debug(final Throwable t) {
- log(DEBUG, null, t);
+ public void setLevel(final String classOrPackageName, final int level) {
+ classAndPackageLevels.put(classOrPackageName, level);
}
@Override
- public void debug(final Object msg, final Throwable t) {
- log(DEBUG, msg, t);
+ public void setLevelForLogger(final String source, final int level) {
+ rootLogger.getSource().subSource(source).setLogLevel(level);
}
- @Override
- public void error(final Object msg) {
- log(ERROR, msg, null);
- }
+ abstract protected void messageLogged(LogMessage message);
- @Override
- public void error(final Throwable t) {
- log(ERROR, null, t);
- }
+ // -- Logger methods --
@Override
- public void error(final Object msg, final Throwable t) {
- log(ERROR, msg, t);
+ public void alwaysLog(final int level, final Object msg, final Throwable t) {
+ rootLogger.alwaysLog(level, msg, t);
}
@Override
- public void info(final Object msg) {
- log(INFO, msg, null);
+ public LogSource getSource() {
+ return rootLogger.getSource();
}
@Override
- public void info(final Throwable t) {
- log(INFO, null, t);
- }
-
- @Override
- public void info(final Object msg, final Throwable t) {
- log(INFO, msg, t);
+ public int getLevel() {
+ if (classAndPackageLevels.isEmpty()) return currentLevel;
+ return getLevelForClass(CallingClassUtils.getCallingClassName(),
+ currentLevel);
}
@Override
- public void trace(final Object msg) {
- log(TRACE, msg, null);
+ public Logger subLogger(String name, int level) {
+ return rootLogger.subLogger(name, level);
}
@Override
- public void trace(final Throwable t) {
- log(TRACE, null, t);
+ public void addLogListener(final LogListener listener) {
+ rootLogger.addLogListener(listener);
}
@Override
- public void trace(final Object msg, final Throwable t) {
- log(TRACE, msg, t);
+ public void removeLogListener(final LogListener listener) {
+ rootLogger.removeLogListener(listener);
}
@Override
- public void warn(final Object msg) {
- log(WARN, msg, null);
+ public void notifyListeners(final LogMessage event) {
+ rootLogger.notifyListeners(event);
}
- @Override
- public void warn(final Throwable t) {
- log(WARN, null, t);
- }
+ // -- Deprecated --
- @Override
- public void warn(final Object msg, final Throwable t) {
- log(WARN, msg, t);
+ /** @deprecated Use {@link LogLevel#prefix(int)} instead. */
+ @Deprecated
+ protected String getPrefix(final int level) {
+ return "[" + LogLevel.prefix(level) + "]";
}
- @Override
- public boolean isDebug() {
- return getLevel() >= DEBUG;
- }
+ // -- Helper methods --
- @Override
- public boolean isError() {
- return getLevel() >= ERROR;
+ private void initLogSourceLevels(Properties properties) {
+ Map nameLevels = setupMapFromProperties(properties,
+ LOG_LEVEL_BY_SOURCE_PROPERTY + ":");
+ nameLevels.forEach(this::setLevelForLogger);
}
- @Override
- public boolean isInfo() {
- return getLevel() >= INFO;
+ private int getLevelForClass(String classOrPackageName, int defaultLevel) {
+ // check for a custom log level for calling class or its parent packages
+ while (classOrPackageName != null) {
+ final Integer level = classAndPackageLevels.get(classOrPackageName);
+ if (level != null) return level;
+ classOrPackageName = parentPackage(classOrPackageName);
+ }
+ return defaultLevel;
}
- @Override
- public boolean isTrace() {
- return getLevel() >= TRACE;
+ private String parentPackage(final String classOrPackageName) {
+ final int dot = classOrPackageName.lastIndexOf(".");
+ if (dot < 0) return null;
+ return classOrPackageName.substring(0, dot);
}
- @Override
- public boolean isWarn() {
- return getLevel() >= WARN;
+ private int levelFromEnvironment() {
+ // use the default, which is INFO unless the DEBUG env. variable is set
+ return System.getenv("DEBUG") == null ? LogLevel.INFO : LogLevel.DEBUG;
}
- @Override
- public int getLevel() {
- if (!classAndPackageLevels.isEmpty()) {
- // check for a custom log level for calling class or its parent packages
- String classOrPackageName = callingClass();
- while (classOrPackageName != null) {
- final Integer level = classAndPackageLevels.get(classOrPackageName);
- if (level != null) return level;
- classOrPackageName = parentPackage(classOrPackageName);
+ private Map setupMapFromProperties(Properties properties,
+ String prefix)
+ {
+ final HashMap map = new HashMap<>();
+ for (final String propName : properties.stringPropertyNames())
+ if (propName.startsWith(prefix)) {
+ final String key = propName.substring(prefix.length());
+ map.put(key, LogLevel.value(properties.getProperty(propName)));
}
- }
- // no custom log level; return the global log level
- return currentLevel;
+ return map;
}
- @Override
- public void setLevel(final int level) {
- currentLevel = level;
- }
-
- @Override
- public void setLevel(final String classOrPackageName, final int level) {
- classAndPackageLevels.put(classOrPackageName, level);
- }
+ // -- Helper classes --
- // -- Helper methods --
+ @IgnoreAsCallingClass
+ private class RootLogger extends DefaultLogger {
- /** Extracts the log level value from a string. */
- private int level(final String logProp) {
- if (logProp == null) return -1;
-
- // check whether it's a string label (e.g., "debug")
- final String log = logProp.trim().toLowerCase();
- if (log.startsWith("n")) return NONE;
- if (log.startsWith("e")) return ERROR;
- if (log.startsWith("w")) return WARN;
- if (log.startsWith("i")) return INFO;
- if (log.startsWith("d")) return DEBUG;
- if (log.startsWith("t")) return TRACE;
-
- // check whether it's a numerical value (e.g., 5)
- try {
- return Integer.parseInt(log);
- }
- catch (final NumberFormatException exc) {
- // nope!
+ public RootLogger() {
+ super(AbstractLogService.this::messageLogged, LogSource.newRoot(),
+ LogLevel.NONE);
}
- return -1;
- }
- private String callingClass() {
- final String thisClass = AbstractLogService.class.getName();
- for (final StackTraceElement element : new Exception().getStackTrace()) {
- final String className = element.getClassName();
- // NB: Skip stack trace elements from other methods of this class.
- if (!thisClass.equals(className)) return className;
+ @Override
+ public int getLevel() {
+ return AbstractLogService.this.getLevel();
}
- return null;
- }
-
- private String parentPackage(final String classOrPackageName) {
- final int dot = classOrPackageName.lastIndexOf(".");
- if (dot < 0) return null;
- return classOrPackageName.substring(0, dot);
}
-
}
diff --git a/src/main/java/org/scijava/log/CallingClassUtils.java b/src/main/java/org/scijava/log/CallingClassUtils.java
new file mode 100644
index 000000000..3a615a5b2
--- /dev/null
+++ b/src/main/java/org/scijava/log/CallingClassUtils.java
@@ -0,0 +1,99 @@
+/*
+ * #%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.log;
+
+import org.scijava.Context;
+
+/**
+ * Utility class for getting the calling class of a method.
+ *
+ * @author Matthias Arzt
+ */
+
+@IgnoreAsCallingClass
+public final class CallingClassUtils {
+
+ private CallingClassUtils() {
+ // prevent instantiation of utility class
+ }
+
+ /**
+ * Inspects the stack trace to return the name of the class that calls
+ * this method, but ignores every class annotated with @IgnoreAsCallingClass.
+ *
+ * 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();
+ for (int i = 1; i < stackTrace.length - 1; i++) {
+ Class> clazz = Class.forName(stackTrace[i].getClassName());
+ if (!clazz.isAnnotationPresent(IgnoreAsCallingClass.class))
+ return clazz;
+ }
+ }
+ catch (ClassNotFoundException ignore) {}
+ throw new IllegalStateException();
+ }
+
+}
diff --git a/src/main/java/org/scijava/log/DefaultLogger.java b/src/main/java/org/scijava/log/DefaultLogger.java
new file mode 100644
index 000000000..a5522aa50
--- /dev/null
+++ b/src/main/java/org/scijava/log/DefaultLogger.java
@@ -0,0 +1,107 @@
+/*
+ * #%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.log;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Default implementation of {@link Logger}.
+ *
+ * @author Matthias Arzt
+ * @author Curtis Rueden
+ */
+@IgnoreAsCallingClass
+public class DefaultLogger implements Logger, LogListener {
+
+ private final LogListener destination;
+
+ private final LogSource source;
+
+ private final int level;
+
+ private final List listeners = new CopyOnWriteArrayList<>();
+
+ public DefaultLogger(final LogListener destination,
+ final LogSource source, final int level)
+ {
+ this.destination = destination;
+ this.source = source;
+ this.level = level;
+ }
+
+ // -- Logger methods --
+
+ @Override
+ public LogSource getSource() {
+ return source;
+ }
+
+ @Override
+ public int getLevel() {
+ return level;
+ }
+
+ @Override
+ public void alwaysLog(final int level, final Object msg, final Throwable t) {
+ messageLogged(new LogMessage(source, level, msg, t));
+ }
+
+ @Override
+ public Logger subLogger(final String name, final int level) {
+ LogSource source = getSource().subSource(name);
+ int actualLevel = source.hasLogLevel() ? source.logLevel() : level;
+ return new DefaultLogger(this, source, actualLevel);
+ }
+
+ @Override
+ public void addLogListener(final LogListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeLogListener(final LogListener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public void notifyListeners(final LogMessage message) {
+ for (LogListener listener : listeners)
+ listener.messageLogged(message);
+ }
+
+ // -- LogListener methods --
+
+ @Override
+ public void messageLogged(final LogMessage message) {
+ notifyListeners(message);
+ destination.messageLogged(message);
+ }
+}
diff --git a/src/main/java/org/scijava/log/DefaultUncaughtExceptionHandler.java b/src/main/java/org/scijava/log/DefaultUncaughtExceptionHandler.java
index ab28a4515..d562921e5 100644
--- a/src/main/java/org/scijava/log/DefaultUncaughtExceptionHandler.java
+++ b/src/main/java/org/scijava/log/DefaultUncaughtExceptionHandler.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -68,10 +66,6 @@ public static void install(final LogService log) {
final UncaughtExceptionHandler handler =
new DefaultUncaughtExceptionHandler(log);
Thread.setDefaultUncaughtExceptionHandler(handler);
-
- // Needed for modal dialog handling before Java7:
- System.setProperty("sun.awt.exception.handler",
- DefaultUncaughtExceptionHandler.class.getName());
}
}
diff --git a/src/main/java/org/scijava/log/IgnoreAsCallingClass.java b/src/main/java/org/scijava/log/IgnoreAsCallingClass.java
new file mode 100644
index 000000000..5aa4a690a
--- /dev/null
+++ b/src/main/java/org/scijava/log/IgnoreAsCallingClass.java
@@ -0,0 +1,42 @@
+/*
+ * #%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.log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Classes annotated with {@link IgnoreAsCallingClass} are ignored by
+ * {@link CallingClassUtils#getCallingClassName()}.
+ *
+ * @author Matthias Arzt
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface IgnoreAsCallingClass {}
diff --git a/src/main/java/org/scijava/log/LogLevel.java b/src/main/java/org/scijava/log/LogLevel.java
new file mode 100644
index 000000000..aabab92aa
--- /dev/null
+++ b/src/main/java/org/scijava/log/LogLevel.java
@@ -0,0 +1,94 @@
+/*
+ * #%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.log;
+
+/**
+ * Constants for specifying a logger's level of verbosity.
+ *
+ * @author Curtis Rueden
+ */
+public final class LogLevel {
+
+ private LogLevel() {
+ // prevent instantiation of utility class
+ }
+
+ public static final int NONE = 0;
+ public static final int ERROR = 1;
+ public static final int WARN = 2;
+ public static final int INFO = 3;
+ public static final int DEBUG = 4;
+ public static final int TRACE = 5;
+
+ public static String prefix(final int level) {
+ switch (level) {
+ case ERROR:
+ return "ERROR";
+ case WARN:
+ return "WARNING";
+ case INFO:
+ return "INFO";
+ case DEBUG:
+ return "DEBUG";
+ case TRACE:
+ return "TRACE";
+ default:
+ return "LEVEL" + level;
+ }
+ }
+
+ /**
+ * Extracts the log level value from a string.
+ *
+ * @return The log level, or -1 if the level cannot be parsed.
+ */
+ public static int value(final String s) {
+ if (s == null) return -1;
+
+ // check whether it's a string label (e.g., "debug")
+ final String log = s.trim().toLowerCase();
+ if (log.startsWith("n")) return LogLevel.NONE;
+ if (log.startsWith("e")) return LogLevel.ERROR;
+ if (log.startsWith("w")) return LogLevel.WARN;
+ if (log.startsWith("i")) return LogLevel.INFO;
+ if (log.startsWith("d")) return LogLevel.DEBUG;
+ if (log.startsWith("t")) return LogLevel.TRACE;
+
+ // check whether it's a numerical value (e.g., 5)
+ try {
+ return Integer.parseInt(log);
+ }
+ catch (final NumberFormatException exc) {
+ // nope!
+ }
+ return -1;
+ }
+
+}
diff --git a/src/main/java/org/scijava/log/LogListener.java b/src/main/java/org/scijava/log/LogListener.java
new file mode 100644
index 000000000..19c03c71f
--- /dev/null
+++ b/src/main/java/org/scijava/log/LogListener.java
@@ -0,0 +1,47 @@
+/*
+ * #%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.log;
+
+/**
+ * Callback function used by {@link Logger}.
+ *
+ * @author Matthias Arzt
+ * @see Logger
+ * @see LogMessage
+ */
+public interface LogListener {
+
+ /**
+ * This method is normally called from many threads in parallel. It must be
+ * implemented highly thread safe and must not use any kind of locks.
+ */
+ void messageLogged(LogMessage message);
+
+}
diff --git a/src/main/java/org/scijava/log/LogMessage.java b/src/main/java/org/scijava/log/LogMessage.java
new file mode 100644
index 000000000..dce591878
--- /dev/null
+++ b/src/main/java/org/scijava/log/LogMessage.java
@@ -0,0 +1,139 @@
+/*
+ * #%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.log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedList;
+
+import org.scijava.event.EventService;
+
+/**
+ * A log message broadcast by a {@link Logger}.
+ *
+ * NB: The message is published on the calling thread by
+ * {@link Logger#notifyListeners}, not on a dedicated event dispatch
+ * thread by the {@link EventService}. This is done to avoid the overhead of the
+ * event service's synchronized pub/sub implementation, as well as to avoid
+ * potential infinite loops caused by debugging log messages surrounding event
+ * publication.
+ *
+ *
+ * @author Matthias Arzt
+ */
+public class LogMessage {
+
+ private final LogSource source;
+ private final int level;
+ private final String message;
+ private final Throwable throwable;
+ private final Date time;
+
+ private Collection attachments;
+
+ public LogMessage(LogSource source, int level, Object message,
+ Throwable throwable)
+ {
+ this.source = source;
+ this.attachments = null;
+ this.level = level;
+ this.message = message == null ? null : message.toString();
+ this.throwable = throwable;
+ this.time = new Date();
+ }
+
+ public LogMessage(LogSource source, int level, Object msg) {
+ this(source, level, msg, null);
+ }
+
+ /** Represents the source of the message. */
+ public LogSource source() {
+ return source;
+ }
+
+ /**
+ * Log level of the message.
+ *
+ * @see LogLevel
+ */
+ public int level() {
+ return level;
+ }
+
+ /** The content of this log message. */
+ public String text() {
+ return message;
+ }
+
+ /** Exception associated with the log message. */
+ public Throwable throwable() {
+ return throwable;
+ }
+
+ /** Time of the creation of the log message. */
+ public Date time() {
+ return time;
+ }
+
+ /**
+ * Collection of objects that have been attached to this message with
+ * {@link #attach(Object)}.
+ */
+ public Collection attachments() {
+ if (attachments == null) return Collections.emptyList();
+ return Collections.unmodifiableCollection(attachments);
+ }
+
+ /**
+ * Attach object to this log message. This can be used to attach additional
+ * information to the log message.
+ */
+ public void attach(Object value) {
+ if (attachments == null) attachments = new LinkedList<>();
+ attachments.add(value);
+ }
+
+ // -- Object methods --
+
+ @Override
+ public String toString() {
+ final StringWriter sw = new StringWriter();
+ final PrintWriter printer = new PrintWriter(sw);
+ printer.print("[" + LogLevel.prefix(level()) + "] ");
+ if(text() != null) printer.println(text());
+ if (throwable() != null) {
+ throwable().printStackTrace(printer);
+ }
+ return sw.toString();
+ }
+}
diff --git a/src/main/java/org/scijava/log/LogService.java b/src/main/java/org/scijava/log/LogService.java
index c86563871..ad9cdb40b 100644
--- a/src/main/java/org/scijava/log/LogService.java
+++ b/src/main/java/org/scijava/log/LogService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -37,69 +35,61 @@
* Interface for the logging service.
*
* The service supports five common logging levels: {@link #ERROR},
- * {@link #WARN}, {@link #INFO}, {@link #TRACE} and {@link #DEBUG}. It provides
- * methods for logging messages, exception stack traces and combinations of the
- * two.
+ * {@link #WARN}, {@link #INFO}, {@link #TRACE} and {@link #DEBUG}. It is
+ * extensible to additional levels as needed. It provides methods for logging
+ * messages, exception stack traces and combinations of the two.
*
*
* @author Curtis Rueden
+ * @author Matthias Arzt
*/
-public interface LogService extends SciJavaService {
+public interface LogService extends SciJavaService, Logger {
/** System property to set for overriding the default logging level. */
String LOG_LEVEL_PROPERTY = "scijava.log.level";
- int NONE = 0;
- int ERROR = 1;
- int WARN = 2;
- int INFO = 3;
- int DEBUG = 4;
- int TRACE = 5;
-
- void debug(Object msg);
-
- void debug(Throwable t);
-
- void debug(Object msg, Throwable t);
-
- void error(Object msg);
-
- void error(Throwable t);
-
- void error(Object msg, Throwable t);
-
- void info(Object msg);
-
- void info(Throwable t);
-
- void info(Object msg, Throwable t);
-
- void trace(Object msg);
-
- void trace(Throwable t);
-
- void trace(Object msg, Throwable t);
-
- void warn(Object msg);
-
- void warn(Throwable t);
-
- void warn(Object msg, Throwable t);
-
- boolean isDebug();
-
- boolean isError();
-
- boolean isInfo();
-
- boolean isTrace();
-
- boolean isWarn();
-
- int getLevel();
+ String LOG_LEVEL_BY_SOURCE_PROPERTY = "scijava.log.level.source";
+ /** Changes the log level of the root logger. */
void setLevel(int level);
+ /**
+ * For messages that are logged directly to the LogService. The log level can
+ * be set depending on the class that makes the log.
+ *
+ * @param classOrPackageName If this is the name of a class. Messages logged
+ * directly by this class are logged, if the message's level is less
+ * or equal to the given level. If this is a package, the same holds
+ * for all classes in this package.
+ * @param level Given level.
+ */
void setLevel(String classOrPackageName, int level);
+ /**
+ * Setting the log level for loggers depending on their {@link LogSource}.
+ * This will only affect loggers that are created after this method has been
+ * called.
+ */
+ void setLevelForLogger(String source, int level);
+
+ // -- Deprecated --
+
+ /** @deprecated Use {@link LogLevel#NONE}. */
+ @Deprecated
+ int NONE = LogLevel.NONE;
+ /** @deprecated Use {@link LogLevel#ERROR}. */
+ @Deprecated
+ int ERROR = LogLevel.ERROR;
+ /** @deprecated Use {@link LogLevel#WARN}. */
+ @Deprecated
+ int WARN = LogLevel.WARN;
+ /** @deprecated Use {@link LogLevel#INFO}. */
+ @Deprecated
+ int INFO = LogLevel.INFO;
+ /** @deprecated Use {@link LogLevel#DEBUG}. */
+ @Deprecated
+ int DEBUG = LogLevel.DEBUG;
+ /** @deprecated Use {@link LogLevel#TRACE}. */
+ @Deprecated
+ int TRACE = LogLevel.TRACE;
}
diff --git a/src/main/java/org/scijava/log/LogSource.java b/src/main/java/org/scijava/log/LogSource.java
new file mode 100644
index 000000000..5f03b1870
--- /dev/null
+++ b/src/main/java/org/scijava/log/LogSource.java
@@ -0,0 +1,143 @@
+/*
+ * #%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.log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringJoiner;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+/**
+ * Identifies where a {@link LogMessage} came from.
+ *
+ * @author Matthias Arzt
+ */
+public class LogSource {
+
+ public static final String SEPARATOR = ":";
+
+ private final LogSource parent;
+
+ private final List path;
+
+ private final ConcurrentMap children =
+ new ConcurrentSkipListMap<>();
+
+ private String formatted = null;
+
+ private Integer logLevel;
+
+ private LogSource(LogSource parent, String name) {
+ this.parent = parent;
+ List parentPath = parent.path();
+ List list = new ArrayList<>(parentPath.size() + 1);
+ list.addAll(parentPath);
+ list.add(name);
+ this.path = Collections.unmodifiableList(list);
+ }
+
+ private LogSource() {
+ this.parent = null;
+ this.path = Collections.emptyList();
+ }
+
+ /** Returns the root log source. This LogSource represents the empty list. */
+ public static LogSource newRoot() {
+ return new LogSource();
+ }
+
+ /**
+ * Returns a log source with the given path.
+ *
+ * @param subPath Relative path to the source, divided by
+ * {@link LogSource#SEPARATOR}.
+ */
+ public LogSource subSource(final String subPath) {
+ LogSource result = this;
+ for (final String name : subPath.split(SEPARATOR))
+ result = result.child(name);
+ return result;
+ }
+
+ /** Returns the list of strings which is represented by this LogSource. */
+ public List path() {
+ return path;
+ }
+
+ /** Returns the last entry in the list of strings. */
+ public String name() {
+ if (path.isEmpty()) return "";
+ return path.get(path.size() - 1);
+ }
+
+ @Override
+ public String toString() {
+ if (formatted != null) return formatted;
+ StringJoiner joiner = new StringJoiner(SEPARATOR);
+ path.forEach(s -> joiner.add(s));
+ formatted = joiner.toString();
+ return formatted;
+ }
+
+ public boolean isRoot() {
+ return parent == null;
+ }
+
+ /** Gets the parent of this source, or null if the source is a root. */
+ public LogSource parent() {
+ return parent;
+ }
+
+ public void setLogLevel(int logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ public boolean hasLogLevel() {
+ return logLevel != null;
+ }
+
+ public int logLevel() {
+ if (!hasLogLevel()) throw new IllegalStateException();
+ return logLevel;
+ }
+
+ // -- Helper methods --
+
+ private LogSource child(final String name) {
+ if (name.isEmpty()) return this;
+ LogSource child = children.get(name);
+ if (child != null) return child;
+ child = new LogSource(this, name);
+ children.putIfAbsent(name, child);
+ return children.get(name);
+ }
+}
diff --git a/src/main/java/org/scijava/log/Logged.java b/src/main/java/org/scijava/log/Logged.java
index cb73be8c5..bee37e0fa 100644
--- a/src/main/java/org/scijava/log/Logged.java
+++ b/src/main/java/org/scijava/log/Logged.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,5 +37,6 @@
public interface Logged {
/** Gets the {@link LogService} to use when logging activities. */
+ // TODO: SJC3: Generalize to Logger instead of LogService.
LogService log();
}
diff --git a/src/main/java/org/scijava/log/Logger.java b/src/main/java/org/scijava/log/Logger.java
new file mode 100644
index 000000000..c2657b0b0
--- /dev/null
+++ b/src/main/java/org/scijava/log/Logger.java
@@ -0,0 +1,218 @@
+/*
+ * #%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.log;
+
+import static org.scijava.log.LogLevel.DEBUG;
+import static org.scijava.log.LogLevel.ERROR;
+import static org.scijava.log.LogLevel.INFO;
+import static org.scijava.log.LogLevel.TRACE;
+import static org.scijava.log.LogLevel.WARN;
+
+/**
+ * Interface for objects which can produce log messages.
+ *
+ * It provides methods for logging messages, exception stack traces and
+ * combinations of the two.
+ *
+ *
+ * @author Curtis Rueden
+ * @see LogLevel
+ * @see LogService
+ */
+@IgnoreAsCallingClass
+public interface Logger {
+
+ default void debug(final Object msg) {
+ log(DEBUG, msg);
+ }
+
+ default void debug(final Throwable t) {
+ log(DEBUG, t);
+ }
+
+ default void debug(final Object msg, final Throwable t) {
+ log(DEBUG, msg, t);
+ }
+
+ default void error(final Object msg) {
+ log(ERROR, msg);
+ }
+
+ default void error(final Throwable t) {
+ log(ERROR, t);
+ }
+
+ default void error(final Object msg, final Throwable t) {
+ log(ERROR, msg, t);
+ }
+
+ default void info(final Object msg) {
+ log(INFO, msg);
+ }
+
+ default void info(final Throwable t) {
+ log(INFO, t);
+ }
+
+ default void info(final Object msg, final Throwable t) {
+ log(INFO, msg, t);
+ }
+
+ default void trace(final Object msg) {
+ log(TRACE, msg);
+ }
+
+ default void trace(final Throwable t) {
+ log(TRACE, t);
+ }
+
+ default void trace(final Object msg, final Throwable t) {
+ log(TRACE, msg, t);
+ }
+
+ default void warn(final Object msg) {
+ log(WARN, msg);
+ }
+
+ default void warn(final Throwable t) {
+ log(WARN, t);
+ }
+
+ default void warn(final Object msg, final Throwable t) {
+ log(WARN, msg, t);
+ }
+
+ default boolean isDebug() {
+ return isLevel(DEBUG);
+ }
+
+ default boolean isError() {
+ return isLevel(ERROR);
+ }
+
+ default boolean isInfo() {
+ return isLevel(INFO);
+ }
+
+ default boolean isTrace() {
+ return isLevel(TRACE);
+ }
+
+ default boolean isWarn() {
+ return isLevel(WARN);
+ }
+
+ default boolean isLevel(final int level) {
+ return getLevel() >= level;
+ }
+
+ /**
+ * Logs a message.
+ *
+ * @param level The level at which the message will be logged. If the current
+ * level (given by {@link #getLevel()} is below this one, no logging
+ * is performed.
+ * @param msg The message to log.
+ */
+ default void log(final int level, final Object msg) {
+ log(level, msg, null);
+ }
+
+ /**
+ * Logs an exception.
+ *
+ * @param level The level at which the exception will be logged. If the
+ * current level (given by {@link #getLevel()} is below this one, no
+ * logging is performed.
+ * @param t The exception to log.
+ */
+ default void log(final int level, final Throwable t) {
+ log(level, null, t);
+ }
+
+ /**
+ * Logs a message with an exception.
+ *
+ * @param level The level at which the information will be logged. If the
+ * current level (given by {@link #getLevel()} is below this one, no
+ * logging is performed.
+ * @param msg The message to log.
+ * @param t The exception to log.
+ */
+ default void log(final int level, final Object msg, final Throwable t) {
+ if (isLevel(level)) alwaysLog(level, msg, t);
+ }
+
+ /**
+ * Logs a message with an exception. This message will always be logged even
+ * if its level is above the current level (given by {@link #getLevel()}).
+ *
+ * @param level The level at which the information will be logged.
+ * @param msg The message to log.
+ * @param t The exception to log.
+ */
+ void alwaysLog(int level, Object msg, Throwable t);
+
+ /** Returns the name of this logger. */
+ default String getName() {
+ return getSource().name();
+ }
+
+ /** Returns the {@link LogSource} associated with this logger. */
+ LogSource getSource();
+
+ /** Returns the log level of this logger. see {@link LogLevel} */
+ int getLevel();
+
+ /**
+ * Creates a sub logger, that forwards the message it gets to this logger. The
+ * sub logger will have the same log level as this logger.
+ */
+ default Logger subLogger(String name) {
+ return subLogger(name, getLevel());
+ }
+
+ /**
+ * Creates a sub logger, that forwards the message it gets to this logger.
+ *
+ * @param name The name of the sub logger.
+ * @param level The log level of the sub logger.
+ */
+ Logger subLogger(String name, int level);
+
+ /** Adds an item to the list of registered listeners. */
+ void addLogListener(LogListener listener);
+
+ /** Removes an item from the list of registered listeners. */
+ void removeLogListener(LogListener listener);
+
+ /** Broadcasts the given log message to the registered listeners. */
+ void notifyListeners(final LogMessage message);
+}
diff --git a/src/main/java/org/scijava/log/StderrLogService.java b/src/main/java/org/scijava/log/StderrLogService.java
index f79baab50..8c67b082b 100644
--- a/src/main/java/org/scijava/log/StderrLogService.java
+++ b/src/main/java/org/scijava/log/StderrLogService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -31,6 +29,9 @@
package org.scijava.log;
+import java.io.PrintStream;
+import java.util.function.Function;
+
import org.scijava.Priority;
import org.scijava.plugin.Plugin;
import org.scijava.service.Service;
@@ -46,36 +47,19 @@
* @author Johannes Schindelin
* @author Curtis Rueden
*/
-@Plugin(type = Service.class, priority = Priority.LOW_PRIORITY)
+@Plugin(type = Service.class, priority = Priority.LOW)
public class StderrLogService extends AbstractLogService {
- @Override
- protected void log(final int level, final Object msg) {
- final String prefix = getPrefix(level);
- final String message = (prefix == null ? "" : prefix + " ") + msg;
- // NB: Emit severe messages to stderr, and less severe ones to stdout.
- if (level <= WARN) System.err.println(message);
- else System.out.println(message);
- }
+ private Function levelToStream =
+ level -> (level <= LogLevel.WARN) ? System.err : System.out;
- /**
- * Prints a message to stderr.
- *
- * @param message the message
- */
- @Override
- protected void log(final String message) {
- System.err.println(message);
+ public void setPrintStreams(Function levelToStream) {
+ this.levelToStream = levelToStream;
}
- /**
- * Prints an exception to stderr.
- *
- * @param t the exception
- */
@Override
- protected void log(final Throwable t) {
- t.printStackTrace();
+ protected void messageLogged(LogMessage message) {
+ final PrintStream out = levelToStream.apply(message.level());
+ out.print(message);
}
-
}
diff --git a/src/main/java/org/scijava/main/DefaultMainService.java b/src/main/java/org/scijava/main/DefaultMainService.java
index f8fcfc99d..699a78132 100644
--- a/src/main/java/org/scijava/main/DefaultMainService.java
+++ b/src/main/java/org/scijava/main/DefaultMainService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -41,7 +39,7 @@
import org.scijava.plugin.Plugin;
import org.scijava.service.AbstractService;
import org.scijava.service.Service;
-import org.scijava.util.ClassUtils;
+import org.scijava.util.Types;
/**
* Default implementation of {@link MainService}.
@@ -101,7 +99,7 @@ public String[] args() {
@Override
public void exec() {
try {
- final Class> mainClass = ClassUtils.loadClass(className, false);
+ final Class> mainClass = Types.load(className, false);
final Method main = mainClass.getMethod("main", String[].class);
main.invoke(null, new Object[] { args });
}
diff --git a/src/main/java/org/scijava/main/MainService.java b/src/main/java/org/scijava/main/MainService.java
index d865ec95c..36b92b89b 100644
--- a/src/main/java/org/scijava/main/MainService.java
+++ b/src/main/java/org/scijava/main/MainService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 d33c64121..0e0ebe29d 100644
--- a/src/main/java/org/scijava/main/console/MainArgument.java
+++ b/src/main/java/org/scijava/main/console/MainArgument.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 153d5303d..a18ae6b3b 100644
--- a/src/main/java/org/scijava/main/run/MainCodeRunner.java
+++ b/src/main/java/org/scijava/main/run/MainCodeRunner.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -41,14 +39,14 @@
import org.scijava.plugin.Plugin;
import org.scijava.run.AbstractCodeRunner;
import org.scijava.run.CodeRunner;
-import org.scijava.util.ClassUtils;
+import org.scijava.util.Types;
/**
* Executes the given class's {@code main} method.
*
* @author Curtis Rueden
*/
-@Plugin(type = CodeRunner.class, priority = Priority.LOW_PRIORITY)
+@Plugin(type = CodeRunner.class, priority = Priority.LOW)
public class MainCodeRunner extends AbstractCodeRunner {
@Parameter(required = false)
@@ -107,7 +105,7 @@ private Method getMain(final Object code) {
private Class> getClass(final Object code) {
if (code instanceof Class) return (Class>) code;
- if (code instanceof String) return ClassUtils.loadClass((String) code);
+ if (code instanceof String) return Types.load((String) code);
return null;
}
diff --git a/src/main/java/org/scijava/menu/AbstractMenuCreator.java b/src/main/java/org/scijava/menu/AbstractMenuCreator.java
index d3835cdca..726639ae8 100644
--- a/src/main/java/org/scijava/menu/AbstractMenuCreator.java
+++ b/src/main/java/org/scijava/menu/AbstractMenuCreator.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 99e6c8c9e..74ed27285 100644
--- a/src/main/java/org/scijava/menu/DefaultMenuService.java
+++ b/src/main/java/org/scijava/menu/DefaultMenuService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -78,25 +76,22 @@ public ShadowMenu getMenu(final String menuRoot) {
// -- Event handlers --
@EventHandler
- protected void onEvent(final ModulesAddedEvent event) {
- if (rootMenus == null) {
- // add *all* known modules, which includes the ones given here
- rootMenus();
- return;
- }
- // data structure already exists; add *these* modules only
+ protected synchronized void onEvent(final ModulesAddedEvent event) {
+ if (rootMenus == null) return; // menus not yet initialized
addModules(event.getItems());
}
@EventHandler
- protected void onEvent(final ModulesRemovedEvent event) {
+ protected synchronized void onEvent(final ModulesRemovedEvent event) {
+ if (rootMenus == null) return; // menus not yet initialized
for (final ShadowMenu menu : rootMenus().values()) {
menu.removeAll(event.getItems());
}
}
@EventHandler
- protected void onEvent(final ModulesUpdatedEvent event) {
+ protected synchronized void onEvent(final ModulesUpdatedEvent event) {
+ if (rootMenus == null) return; // menus not yet initialized
for (final ShadowMenu menu : rootMenus().values()) {
menu.updateAll(event.getItems());
}
@@ -164,9 +159,7 @@ private synchronized void addModules(final Collection items,
*
*/
private HashMap rootMenus() {
- if (rootMenus == null) {
- initRootMenus();
- }
+ if (rootMenus == null) initRootMenus();
return rootMenus;
}
diff --git a/src/main/java/org/scijava/menu/MenuConstants.java b/src/main/java/org/scijava/menu/MenuConstants.java
index 6ee40aa5f..8c38942c9 100644
--- a/src/main/java/org/scijava/menu/MenuConstants.java
+++ b/src/main/java/org/scijava/menu/MenuConstants.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 84691ad71..386771672 100644
--- a/src/main/java/org/scijava/menu/MenuCreator.java
+++ b/src/main/java/org/scijava/menu/MenuCreator.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 b0b611c13..ce9a5d36c 100644
--- a/src/main/java/org/scijava/menu/MenuService.java
+++ b/src/main/java/org/scijava/menu/MenuService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 604492b65..c5eaea4cd 100644
--- a/src/main/java/org/scijava/menu/ShadowMenu.java
+++ b/src/main/java/org/scijava/menu/ShadowMenu.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,6 +54,7 @@
import org.scijava.plugin.Parameter;
import org.scijava.util.ClassUtils;
import org.scijava.util.MiscUtils;
+import org.scijava.util.Types;
/**
* A tree representing a menu structure independent of any particular user
@@ -172,7 +171,8 @@ public MenuEntry getMenuEntry() {
/**
* Gets how deep into the menu structure this node is. For example, "File"
- * would be at depth 1, whereas "Exit" (of "File>Exit") would be at depth 2.
+ * would be at depth 1, whereas "Exit" (of "File>Exit") would be at depth
+ * 2.
*/
public int getMenuDepth() {
return menuDepth;
@@ -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 = ClassUtils.loadClass(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;
@@ -490,7 +497,7 @@ public T[] toArray(final T[] a) {
// -- Helper methods --
private ShadowMenu addInternal(final ModuleInfo o) {
- if (o.getMenuPath().isEmpty()) return null; // no menu
+ if (o.getMenuPath() == null || o.getMenuPath().isEmpty()) return null; // no menu
return addChild(o, 0);
}
@@ -546,14 +553,8 @@ private ShadowMenu addChild(final ModuleInfo info, final int depth) {
else if (existingChild != null) {
if (log != null) {
final ModuleInfo childInfo = existingChild.getModuleInfo();
- if (childInfo != null && info.getPriority() == childInfo.getPriority())
- {
- log.warn("ShadowMenu: menu item already exists:\n" + //
- "\texisting: " + details(childInfo) + "\n" + //
- "\t ignored: " + details(info));
- }
- else {
- log.debug("ShadowMenu: higher-priority menu item already exists:\n" +
+ if (log.isDebug()) {
+ log.debug("ShadowMenu: menu item already exists:\n" + //
"\texisting: " + details(childInfo) + "\n" + //
"\t ignored: " + details(info));
}
@@ -572,7 +573,7 @@ private String details(final ModuleInfo info) {
try {
final Class> c = info.loadDelegateClass();
className = c.getName();
- classLocation = ClassUtils.getLocation(c).toString();
+ classLocation = Types.location(c).toString();
}
catch (final ClassNotFoundException exc) {
className = info.getDelegateClassName();
diff --git a/src/main/java/org/scijava/menu/ShadowMenuIterator.java b/src/main/java/org/scijava/menu/ShadowMenuIterator.java
index bb1784cba..ea69e7a2f 100644
--- a/src/main/java/org/scijava/menu/ShadowMenuIterator.java
+++ b/src/main/java/org/scijava/menu/ShadowMenuIterator.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 e3220196f..ae0deaa13 100644
--- a/src/main/java/org/scijava/menu/event/MenuEvent.java
+++ b/src/main/java/org/scijava/menu/event/MenuEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 c22b02cf6..a3fd70e2f 100644
--- a/src/main/java/org/scijava/menu/event/MenusAddedEvent.java
+++ b/src/main/java/org/scijava/menu/event/MenusAddedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 b1a1ac583..24e3a8f93 100644
--- a/src/main/java/org/scijava/menu/event/MenusRemovedEvent.java
+++ b/src/main/java/org/scijava/menu/event/MenusRemovedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 c7063001f..951daace8 100644
--- a/src/main/java/org/scijava/menu/event/MenusUpdatedEvent.java
+++ b/src/main/java/org/scijava/menu/event/MenusUpdatedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 2b63ba440..4ca6a4528 100644
--- a/src/main/java/org/scijava/module/AbstractModule.java
+++ b/src/main/java/org/scijava/module/AbstractModule.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -35,6 +33,8 @@
import java.util.HashSet;
import java.util.Map;
+import org.scijava.Initializable;
+
/**
* Abstract superclass of {@link Module} implementations.
*
@@ -76,11 +76,16 @@ public void cancel() {
public void initialize() throws MethodCallException {
// execute global module initializer
final Object delegateObject = getDelegateObject();
- if (initializerRef == null) {
- final String initializer = getInfo().getInitializer();
- initializerRef = new MethodRef(delegateObject.getClass(), initializer);
+ if (delegateObject instanceof Initializable) {
+ ((Initializable) delegateObject).initialize();
+ }
+ else {
+ if (initializerRef == null) {
+ final String initializer = getInfo().getInitializer();
+ initializerRef = new MethodRef(delegateObject.getClass(), initializer);
+ }
+ initializerRef.execute(delegateObject);
}
- initializerRef.execute(delegateObject);
// execute individual module item initializers
for (final ModuleItem> item : getInfo().inputs()) {
@@ -155,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 ffd48cf37..c11351573 100644
--- a/src/main/java/org/scijava/module/AbstractModuleInfo.java
+++ b/src/main/java/org/scijava/module/AbstractModuleInfo.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -38,7 +36,7 @@
import java.util.Map;
import org.scijava.AbstractUIDetails;
-import org.scijava.util.ConversionUtils;
+import org.scijava.util.Types;
/**
* Abstract superclass of {@link ModuleInfo} implementation.
@@ -160,23 +158,6 @@ protected List> outputList() {
return outputList;
}
- // -- Helper methods --
-
- private ModuleItem castItem(final ModuleItem> item,
- final Class type)
- {
- final Class> itemType = item.getType();
- // if (!type.isAssignableFrom(itemType)) {
- final Class> saneItemType = ConversionUtils.getNonprimitiveType(itemType);
- if (!ConversionUtils.canCast(type, saneItemType)) {
- throw new IllegalArgumentException("Type " + type.getName() +
- " is incompatible with item of type " + itemType.getName());
- }
- @SuppressWarnings("unchecked")
- final ModuleItem typedItem = (ModuleItem) item;
- return typedItem;
- }
-
// -- Helper methods - lazy initialization --
/** Initializes data structures and parses parameters. */
@@ -193,4 +174,20 @@ private synchronized void initParameters() {
initialized = true;
}
+ // -- Helper methods --
+
+ private ModuleItem castItem(final ModuleItem> item,
+ final Class type)
+ {
+ final Class> itemType = item.getType();
+ // if (!type.isAssignableFrom(itemType)) {
+ final Class> saneItemType = Types.box(itemType);
+ if (!Types.isAssignable(type, saneItemType)) {
+ throw new IllegalArgumentException("Type " + type.getName() +
+ " is incompatible with item of type " + itemType.getName());
+ }
+ @SuppressWarnings("unchecked")
+ final ModuleItem typedItem = (ModuleItem) item;
+ return typedItem;
+ }
}
diff --git a/src/main/java/org/scijava/module/AbstractModuleItem.java b/src/main/java/org/scijava/module/AbstractModuleItem.java
index e7ce1f0d2..e2648cd2f 100644
--- a/src/main/java/org/scijava/module/AbstractModuleItem.java
+++ b/src/main/java/org/scijava/module/AbstractModuleItem.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -38,11 +36,11 @@
import org.scijava.AbstractBasicDetails;
import org.scijava.ItemIO;
import org.scijava.ItemVisibility;
-import org.scijava.util.ClassUtils;
import org.scijava.util.ConversionUtils;
import org.scijava.util.NumberUtils;
import org.scijava.util.Prefs;
import org.scijava.util.StringMaker;
+import org.scijava.util.Types;
/**
* Abstract superclass of {@link ModuleItem} implementations.
@@ -212,7 +210,12 @@ public void validate(final Module module) throws MethodCallException {
if (validaterRef == null) {
validaterRef = new MethodRef(delegateObject.getClass(), getValidater());
}
- validaterRef.execute(module.getDelegateObject());
+ final Object result = validaterRef.executeWithResult(module.getDelegateObject());
+ // If the validater returns a non-empty String, treat it as an error message.
+ if (result instanceof String) {
+ final String message = (String) result;
+ if (!message.isEmpty()) throw new MethodCallException(message);
+ }
}
@Override
@@ -263,7 +266,7 @@ public T getSoftMaximum() {
@Override
public Number getStepSize() {
- if (!ClassUtils.isNumber(getType())) return null;
+ if (!Types.isNumber(getType())) return null;
return NumberUtils.toNumber("1", getType());
}
diff --git a/src/main/java/org/scijava/module/DefaultModuleService.java b/src/main/java/org/scijava/module/DefaultModuleService.java
index f7a7f7a46..c6dee7f42 100644
--- a/src/main/java/org/scijava/module/DefaultModuleService.java
+++ b/src/main/java/org/scijava/module/DefaultModuleService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -62,7 +60,7 @@
import org.scijava.service.AbstractService;
import org.scijava.service.Service;
import org.scijava.thread.ThreadService;
-import org.scijava.util.ClassUtils;
+import org.scijava.util.Types;
/**
* Default service for keeping track of and executing available modules.
@@ -76,7 +74,7 @@ public class DefaultModuleService extends AbstractService implements
ModuleService
{
- @Parameter
+ @Parameter(required = false)
private LogService log;
@Parameter
@@ -172,7 +170,7 @@ public Module createModule(final ModuleInfo info) {
return module;
}
catch (final ModuleException exc) {
- log.error("Cannot create module: " + info.getDelegateClassName(), exc);
+ if (log != null) log.error("Cannot create module: " + info.getDelegateClassName(), exc);
}
return null;
}
@@ -253,10 +251,10 @@ public M waitFor(final Future future) {
return future.get();
}
catch (final InterruptedException e) {
- log.error("Module execution interrupted", e);
+ if (log != null) log.error("Module execution interrupted", e);
}
catch (final ExecutionException e) {
- log.error("Error during module execution", e);
+ if (log != null) log.error("Error during module execution", e);
}
return null;
}
@@ -293,21 +291,16 @@ public void save(final ModuleItem item, final T value) {
// NB: Do not persist the value if it is the default.
// This is nice if the default value might change later,
// such as when iteratively developing a script.
+ prefService.remove(prefClass(item), prefKey(item));
return;
}
- final String sValue = value == null ? "" : value.toString();
+ final String sValue = value == null ? "" : convertService.convert(value, String.class);
// do not persist if object cannot be converted back from a string
if (!convertService.supports(sValue, item.getType())) return;
- final String persistKey = item.getPersistKey();
- if (persistKey == null || persistKey.isEmpty()) {
- final Class> prefClass = delegateClass(item);
- final String prefKey = item.getName();
- prefService.put(prefClass, prefKey, sValue);
- }
- else prefService.put(persistKey, sValue);
+ prefService.put(prefClass(item), prefKey(item), sValue);
}
@Override
@@ -315,14 +308,7 @@ public T load(final ModuleItem item) {
// if there is nothing to load from persistence return nothing
if (!item.isPersisted()) return null;
- final String sValue;
- final String persistKey = item.getPersistKey();
- if (persistKey == null || persistKey.isEmpty()) {
- final Class> prefClass = delegateClass(item);
- final String prefKey = item.getName();
- sValue = prefService.get(prefClass, prefKey);
- }
- else sValue = prefService.get(persistKey);
+ final String sValue = prefService.get(prefClass(item), prefKey(item));
// if persisted value has never been set before return null
if (sValue == null) return null;
@@ -350,6 +336,16 @@ public T getDefaultValue(final ModuleItem item) {
return null;
}
+ @Override
+ public void saveInputs(final Module module) {
+ module.getInfo().inputs().forEach(item -> saveInput(module, item));
+ }
+
+ @Override
+ public void loadInputs(final Module module) {
+ module.getInfo().inputs().forEach(item -> loadInput(module, item));
+ }
+
// -- Service methods --
@Override
@@ -381,7 +377,7 @@ private List extends PostprocessorPlugin> post(final boolean process) {
* {@link ModuleInfo} argument is called).
*/
private Module getRegisteredModuleInstance(final ModuleInfo info) {
- final Class> type = ClassUtils.loadClass(info.getDelegateClassName());
+ final Class> type = Types.load(info.getDelegateClassName());
if (type == null || !Module.class.isAssignableFrom(type)) return null;
// the module metadata's delegate class extends Module, so there is hope
@@ -396,8 +392,10 @@ private Module getRegisteredModuleInstance(final ModuleInfo info) {
}
if (objects.size() > 1) {
// there are multiple instances; it's not clear which one to use
- log.warn("Ignoring multiple candidate module instances for class: " +
- type.getName());
+ if (log != null) {
+ log.warn("Ignoring multiple candidate module instances for class: " +
+ type.getName());
+ }
return null;
}
// found exactly one instance; return it!
@@ -420,7 +418,7 @@ private Map createMap(final Object[] values) {
final Map, ?> valueMap = (Map, ?>) values[0];
for (final Object key : valueMap.keySet()) {
if (!(key instanceof String)) {
- log.error("Invalid input name: " + key);
+ if (log != null) log.error("Invalid input name: " + key);
continue;
}
final String name = (String) key;
@@ -431,7 +429,7 @@ private Map createMap(final Object[] values) {
}
if (values.length % 2 != 0) {
- log.error("Ignoring extraneous argument: " + values[values.length - 1]);
+ if (log != null) log.error("Ignoring extraneous argument: " + values[values.length - 1]);
}
// loop over list of key/value pairs
@@ -440,7 +438,7 @@ private Map createMap(final Object[] values) {
final Object key = values[2 * i];
final Object value = values[2 * i + 1];
if (!(key instanceof String)) {
- log.error("Invalid input name: " + key);
+ if (log != null) log.error("Invalid input name: " + key);
continue;
}
final String name = (String) key;
@@ -463,7 +461,7 @@ private void assignInputs(final Module module,
if (input == null) {
// inputs whose name starts with a dot are implicitly known by convention
if (!name.startsWith(".")) {
- log.warn("Unmatched input: " + name);
+ if (log != null) log.warn("Unmatched input: " + name);
}
converted = value;
}
@@ -471,8 +469,10 @@ private void assignInputs(final Module module,
final Class> type = input.getType();
converted = convertService.convert(value, type);
if (value != null && converted == null) {
- log.error("For input " + name + ": incompatible object " +
- value.getClass().getName() + " for type " + type.getName());
+ if (log != null) {
+ log.error("For input " + name + ": incompatible object " +
+ value.getClass().getName() + " for type " + type.getName());
+ }
continue;
}
}
@@ -498,9 +498,8 @@ private ModuleItem> getSingleItem(final Module module,
for (final ModuleItem> item : items) {
final String name = item.getName();
- final boolean resolved = module.isInputResolved(name);
- if (resolved) continue; // skip resolved inputs
if (!item.isAutoFill()) continue; // skip unfillable inputs
+ if (module.isInputResolved(name)) continue; // skip resolved inputs
final Class> itemType = item.getType();
for (final Class> type : types) {
if (type.isAssignableFrom(itemType)) {
@@ -523,4 +522,43 @@ private Class> delegateClass(final ModuleItem item) {
}
}
+ private Class> prefClass(final ModuleItem> item) {
+ final String persistKey = item.getPersistKey();
+ return persistKey == null || persistKey.isEmpty() ? //
+ delegateClass(item) : null;
+ }
+
+ private String prefKey(final ModuleItem> item) {
+ final String persistKey = item.getPersistKey();
+ return persistKey == null || persistKey.isEmpty() ? //
+ item.getName() : persistKey;
+ }
+
+ /** Saves the value of the given module item to persistent storage. */
+ private void saveInput(final Module module, final ModuleItem item) {
+ final T value = item.getValue(module);
+ save(item, value);
+ }
+
+ /** Loads the value of the given module item from persistent storage. */
+ private void loadInput(final Module module, final ModuleItem item) {
+ // skip input that has already been resolved
+ if (module.isInputResolved(item.getName())) return;
+
+ final T prefValue = load(item);
+ final Class type = item.getType();
+ final T defaultValue = item.getValue(module);
+ final T value = getBestValue(prefValue, defaultValue, type);
+ item.setValue(module, value);
+ }
+
+ private T getBestValue(final Object prefValue,
+ final Object defaultValue, final Class type)
+ {
+ if (prefValue != null) return convertService.convert(prefValue, type);
+ if (defaultValue != null) {
+ return convertService.convert(defaultValue, type);
+ }
+ return Types.nullValue(type);
+ }
}
diff --git a/src/main/java/org/scijava/module/DefaultMutableModule.java b/src/main/java/org/scijava/module/DefaultMutableModule.java
index c368a602c..0e646a212 100644
--- a/src/main/java/org/scijava/module/DefaultMutableModule.java
+++ b/src/main/java/org/scijava/module/DefaultMutableModule.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/DefaultMutableModuleInfo.java b/src/main/java/org/scijava/module/DefaultMutableModuleInfo.java
index ff6a07fb1..372776ab9 100644
--- a/src/main/java/org/scijava/module/DefaultMutableModuleInfo.java
+++ b/src/main/java/org/scijava/module/DefaultMutableModuleInfo.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/DefaultMutableModuleItem.java b/src/main/java/org/scijava/module/DefaultMutableModuleItem.java
index 363388587..c16ea80ec 100644
--- a/src/main/java/org/scijava/module/DefaultMutableModuleItem.java
+++ b/src/main/java/org/scijava/module/DefaultMutableModuleItem.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -53,6 +51,7 @@ public class DefaultMutableModuleItem extends AbstractModuleItem
private final Type genericType;
private ItemIO ioType;
private ItemVisibility visibility;
+ private boolean autoFill;
private boolean required;
private boolean persisted;
private String persistKey;
@@ -87,10 +86,12 @@ public DefaultMutableModuleItem(final ModuleInfo info, final String name,
genericType = type;
ioType = super.getIOType();
visibility = super.getVisibility();
+ autoFill = super.isAutoFill();
required = super.isRequired();
persisted = super.isPersisted();
persistKey = super.getPersistKey();
initializer = super.getInitializer();
+ validater = super.getValidater();
callback = super.getCallback();
widgetStyle = super.getWidgetStyle();
minimumValue = super.getMinimumValue();
@@ -113,10 +114,12 @@ public DefaultMutableModuleItem(final ModuleInfo info,
genericType = item.getGenericType();
ioType = item.getIOType();
visibility = item.getVisibility();
+ autoFill = item.isAutoFill();
required = item.isRequired();
persisted = item.isPersisted();
persistKey = item.getPersistKey();
initializer = item.getInitializer();
+ validater = item.getValidater();
callback = item.getCallback();
widgetStyle = item.getWidgetStyle();
minimumValue = item.getMinimumValue();
@@ -143,6 +146,11 @@ public void setVisibility(final ItemVisibility visibility) {
this.visibility = visibility;
}
+ @Override
+ public void setAutoFill(final boolean autoFill) {
+ this.autoFill = autoFill;
+ }
+
@Override
public void setRequired(final boolean required) {
this.required = required;
@@ -241,6 +249,11 @@ public ItemVisibility getVisibility() {
return visibility;
}
+ @Override
+ public boolean isAutoFill() {
+ return autoFill;
+ }
+
@Override
public boolean isRequired() {
return required;
diff --git a/src/main/java/org/scijava/module/MethodCallException.java b/src/main/java/org/scijava/module/MethodCallException.java
index 25058c382..fc37d1313 100644
--- a/src/main/java/org/scijava/module/MethodCallException.java
+++ b/src/main/java/org/scijava/module/MethodCallException.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/MethodRef.java b/src/main/java/org/scijava/module/MethodRef.java
index 35d44b0dd..f77883204 100644
--- a/src/main/java/org/scijava/module/MethodRef.java
+++ b/src/main/java/org/scijava/module/MethodRef.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -64,9 +62,15 @@ public MethodRef(final Class> clazz, final String methodName,
public void execute(final Object obj, final Object... args)
throws MethodCallException
{
- if (method == null) return;
+ executeWithResult(obj, args);
+ }
+
+ public Object executeWithResult(final Object obj, final Object... args)
+ throws MethodCallException
+ {
+ if (method == null) return null;
try {
- method.invoke(obj, args);
+ return method.invoke(obj, args);
}
catch (final Exception exc) {
// NB: Several types of exceptions; simpler to handle them all the same.
diff --git a/src/main/java/org/scijava/module/Module.java b/src/main/java/org/scijava/module/Module.java
index 3cb81187e..21e6730bf 100644
--- a/src/main/java/org/scijava/module/Module.java
+++ b/src/main/java/org/scijava/module/Module.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/ModuleCanceledException.java b/src/main/java/org/scijava/module/ModuleCanceledException.java
index 1eb8c6b8f..f500a4167 100644
--- a/src/main/java/org/scijava/module/ModuleCanceledException.java
+++ b/src/main/java/org/scijava/module/ModuleCanceledException.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/ModuleException.java b/src/main/java/org/scijava/module/ModuleException.java
index 01d8612f8..9f9f3cb7c 100644
--- a/src/main/java/org/scijava/module/ModuleException.java
+++ b/src/main/java/org/scijava/module/ModuleException.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/ModuleIndex.java b/src/main/java/org/scijava/module/ModuleIndex.java
index b4a8e146d..998568f10 100644
--- a/src/main/java/org/scijava/module/ModuleIndex.java
+++ b/src/main/java/org/scijava/module/ModuleIndex.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/ModuleInfo.java b/src/main/java/org/scijava/module/ModuleInfo.java
index f6972555d..c9b687177 100644
--- a/src/main/java/org/scijava/module/ModuleInfo.java
+++ b/src/main/java/org/scijava/module/ModuleInfo.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -41,7 +39,7 @@
import org.scijava.Versioned;
import org.scijava.event.EventService;
import org.scijava.module.event.ModulesUpdatedEvent;
-import org.scijava.util.ClassUtils;
+import org.scijava.util.Types;
import org.scijava.util.VersionUtils;
/**
@@ -219,7 +217,7 @@ default String getLocation() {
// If the same delegate class is used for more than one module, though,
// it may need to override this method to indicate a different location.
try {
- return ClassUtils.getLocation(loadDelegateClass()).toExternalForm();
+ return Types.location(loadDelegateClass()).toExternalForm();
}
catch (final ClassNotFoundException exc) {
return null;
diff --git a/src/main/java/org/scijava/module/ModuleItem.java b/src/main/java/org/scijava/module/ModuleItem.java
index 6def93c65..95ea748b4 100644
--- a/src/main/java/org/scijava/module/ModuleItem.java
+++ b/src/main/java/org/scijava/module/ModuleItem.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -128,11 +126,52 @@ public interface ModuleItem extends BasicDetails {
/**
* Invokes this item's validation function, if any, on the given module.
- *
+ *
+ * The validation function may signal failure either by throwing an exception
+ * or by returning a non-empty {@link String} error message.
+ *
+ *
+ * @throws MethodCallException if validation fails or the method cannot be
+ * invoked. When the validater returns a non-empty String, a
+ * {@link MethodCallException} is thrown with that string as its
+ * message.
* @see #getValidater()
+ * @see #validateMessage(Module)
*/
void validate(Module module) throws MethodCallException;
+ /**
+ * Validates this item's value in the given module, returning any error
+ * message rather than throwing.
+ *
+ * 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.
+ *
+ *
+ * @return an error message if the value is invalid, or {@code null} if valid.
+ * @see #getValidater()
+ * @see #validate(Module)
+ */
+ default String validateMessage(final Module module) {
+ try {
+ validate(module);
+ return null;
+ }
+ catch (final MethodCallException exc) {
+ // Unwrap to find the most informative message.
+ final Throwable cause = exc.getCause();
+ if (cause != null && cause.getMessage() != null &&
+ !cause.getMessage().isEmpty())
+ {
+ return cause.getMessage();
+ }
+ final String msg = exc.getMessage();
+ return msg != null && !msg.isEmpty() ? msg : exc.toString();
+ }
+ }
+
/**
* Gets the function that is called whenever this item changes.
*
diff --git a/src/main/java/org/scijava/module/ModuleRunner.java b/src/main/java/org/scijava/module/ModuleRunner.java
index a3a1253b6..ece995d8a 100644
--- a/src/main/java/org/scijava/module/ModuleRunner.java
+++ b/src/main/java/org/scijava/module/ModuleRunner.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -41,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;
@@ -126,12 +125,10 @@ public Module call() {
run();
}
catch (final RuntimeException exc) {
- if (log != null) log.error("Module threw exception", exc);
- throw exc;
+ throw new RuntimeException("Module threw exception", exc);
}
catch (final Error err) {
- if (log != null) log.error("Module threw error", err);
- throw err;
+ throw new RuntimeException("Module threw error", err);
}
return module;
}
@@ -148,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 --
@@ -194,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 165d83ffe..25bc64947 100644
--- a/src/main/java/org/scijava/module/ModuleService.java
+++ b/src/main/java/org/scijava/module/ModuleService.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -49,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.
*
@@ -310,4 +308,9 @@ Future run(M module,
/** Gets the default value of the given {@link ModuleItem}. */
T getDefaultValue(final ModuleItem item);
+ /** Saves values to persistent storage from the given {@link Module}. */
+ void saveInputs(final Module module);
+
+ /** Loads values from persistent storage into the given {@link Module}. */
+ void loadInputs(final Module module);
}
diff --git a/src/main/java/org/scijava/module/MutableModule.java b/src/main/java/org/scijava/module/MutableModule.java
index 63771f2ca..3a907c854 100644
--- a/src/main/java/org/scijava/module/MutableModule.java
+++ b/src/main/java/org/scijava/module/MutableModule.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/MutableModuleInfo.java b/src/main/java/org/scijava/module/MutableModuleInfo.java
index b4b6d3ca1..31aeb7280 100644
--- a/src/main/java/org/scijava/module/MutableModuleInfo.java
+++ b/src/main/java/org/scijava/module/MutableModuleInfo.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/MutableModuleItem.java b/src/main/java/org/scijava/module/MutableModuleItem.java
index c546fb8ef..413950216 100644
--- a/src/main/java/org/scijava/module/MutableModuleItem.java
+++ b/src/main/java/org/scijava/module/MutableModuleItem.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -48,6 +46,8 @@ public interface MutableModuleItem extends ModuleItem {
void setVisibility(ItemVisibility visibility);
+ void setAutoFill(boolean autoFill);
+
void setRequired(boolean required);
void setPersisted(boolean persisted);
diff --git a/src/main/java/org/scijava/module/event/ModuleCanceledEvent.java b/src/main/java/org/scijava/module/event/ModuleCanceledEvent.java
index e6955c597..baa6ce737 100644
--- a/src/main/java/org/scijava/module/event/ModuleCanceledEvent.java
+++ b/src/main/java/org/scijava/module/event/ModuleCanceledEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModuleErroredEvent.java b/src/main/java/org/scijava/module/event/ModuleErroredEvent.java
new file mode 100644
index 000000000..381cc0a4e
--- /dev/null
+++ b/src/main/java/org/scijava/module/event/ModuleErroredEvent.java
@@ -0,0 +1,52 @@
+/*
+ * #%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.module.event;
+
+import org.scijava.module.Module;
+
+/**
+ * An event indicating a module execution has thrown an exception.
+ *
+ * @author Gabriel Selzer
+ */
+public class ModuleErroredEvent extends ModuleExecutionEvent {
+
+ private final Throwable exc;
+
+ public ModuleErroredEvent(final Module module, final Throwable exc) {
+ super(module);
+ this.exc = exc;
+ }
+
+ public Throwable getException() {
+ return exc;
+ }
+
+}
diff --git a/src/main/java/org/scijava/module/event/ModuleEvent.java b/src/main/java/org/scijava/module/event/ModuleEvent.java
index f17936668..9d402399a 100644
--- a/src/main/java/org/scijava/module/event/ModuleEvent.java
+++ b/src/main/java/org/scijava/module/event/ModuleEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModuleExecutedEvent.java b/src/main/java/org/scijava/module/event/ModuleExecutedEvent.java
index 095ee07b6..c8ec50ca5 100644
--- a/src/main/java/org/scijava/module/event/ModuleExecutedEvent.java
+++ b/src/main/java/org/scijava/module/event/ModuleExecutedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModuleExecutingEvent.java b/src/main/java/org/scijava/module/event/ModuleExecutingEvent.java
index e24bd4f4f..5110625e8 100644
--- a/src/main/java/org/scijava/module/event/ModuleExecutingEvent.java
+++ b/src/main/java/org/scijava/module/event/ModuleExecutingEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModuleExecutionEvent.java b/src/main/java/org/scijava/module/event/ModuleExecutionEvent.java
index 1dcc35616..73bfa9d14 100644
--- a/src/main/java/org/scijava/module/event/ModuleExecutionEvent.java
+++ b/src/main/java/org/scijava/module/event/ModuleExecutionEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModuleFinishedEvent.java b/src/main/java/org/scijava/module/event/ModuleFinishedEvent.java
index 294cd8637..4c0f71dbf 100644
--- a/src/main/java/org/scijava/module/event/ModuleFinishedEvent.java
+++ b/src/main/java/org/scijava/module/event/ModuleFinishedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModulePostprocessEvent.java b/src/main/java/org/scijava/module/event/ModulePostprocessEvent.java
index 95b2379a1..e59f94ff2 100644
--- a/src/main/java/org/scijava/module/event/ModulePostprocessEvent.java
+++ b/src/main/java/org/scijava/module/event/ModulePostprocessEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModulePreprocessEvent.java b/src/main/java/org/scijava/module/event/ModulePreprocessEvent.java
index b81f1ffa1..7efdc11c7 100644
--- a/src/main/java/org/scijava/module/event/ModulePreprocessEvent.java
+++ b/src/main/java/org/scijava/module/event/ModulePreprocessEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModuleProcessEvent.java b/src/main/java/org/scijava/module/event/ModuleProcessEvent.java
index 55305f2af..f5ac20fe8 100644
--- a/src/main/java/org/scijava/module/event/ModuleProcessEvent.java
+++ b/src/main/java/org/scijava/module/event/ModuleProcessEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModuleStartedEvent.java b/src/main/java/org/scijava/module/event/ModuleStartedEvent.java
index 7a5df739f..2b8f59393 100644
--- a/src/main/java/org/scijava/module/event/ModuleStartedEvent.java
+++ b/src/main/java/org/scijava/module/event/ModuleStartedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModulesAddedEvent.java b/src/main/java/org/scijava/module/event/ModulesAddedEvent.java
index d695e8d27..30107c4e7 100644
--- a/src/main/java/org/scijava/module/event/ModulesAddedEvent.java
+++ b/src/main/java/org/scijava/module/event/ModulesAddedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModulesListEvent.java b/src/main/java/org/scijava/module/event/ModulesListEvent.java
index 2bd2b150d..051edef8d 100644
--- a/src/main/java/org/scijava/module/event/ModulesListEvent.java
+++ b/src/main/java/org/scijava/module/event/ModulesListEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModulesRemovedEvent.java b/src/main/java/org/scijava/module/event/ModulesRemovedEvent.java
index cc9038d05..495f9e7e2 100644
--- a/src/main/java/org/scijava/module/event/ModulesRemovedEvent.java
+++ b/src/main/java/org/scijava/module/event/ModulesRemovedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/event/ModulesUpdatedEvent.java b/src/main/java/org/scijava/module/event/ModulesUpdatedEvent.java
index 3278acceb..df5a9065d 100644
--- a/src/main/java/org/scijava/module/event/ModulesUpdatedEvent.java
+++ b/src/main/java/org/scijava/module/event/ModulesUpdatedEvent.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/process/AbstractPostprocessorPlugin.java b/src/main/java/org/scijava/module/process/AbstractPostprocessorPlugin.java
index cc33882a2..6e786753b 100644
--- a/src/main/java/org/scijava/module/process/AbstractPostprocessorPlugin.java
+++ b/src/main/java/org/scijava/module/process/AbstractPostprocessorPlugin.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/process/AbstractPreprocessorPlugin.java b/src/main/java/org/scijava/module/process/AbstractPreprocessorPlugin.java
index a0f3e4ab1..b9cb5de9d 100644
--- a/src/main/java/org/scijava/module/process/AbstractPreprocessorPlugin.java
+++ b/src/main/java/org/scijava/module/process/AbstractPreprocessorPlugin.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/process/AbstractSingleInputPreprocessor.java b/src/main/java/org/scijava/module/process/AbstractSingleInputPreprocessor.java
index a6f69dc0d..63e07cd2b 100644
--- a/src/main/java/org/scijava/module/process/AbstractSingleInputPreprocessor.java
+++ b/src/main/java/org/scijava/module/process/AbstractSingleInputPreprocessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/process/CheckInputsPreprocessor.java b/src/main/java/org/scijava/module/process/CheckInputsPreprocessor.java
index 117a1e7fe..8bbfac881 100644
--- a/src/main/java/org/scijava/module/process/CheckInputsPreprocessor.java
+++ b/src/main/java/org/scijava/module/process/CheckInputsPreprocessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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/process/DebugPostprocessor.java b/src/main/java/org/scijava/module/process/DebugPostprocessor.java
index 368702df6..e614afada 100644
--- a/src/main/java/org/scijava/module/process/DebugPostprocessor.java
+++ b/src/main/java/org/scijava/module/process/DebugPostprocessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -44,7 +42,7 @@
*
* @author Curtis Rueden
*/
-@Plugin(type = PostprocessorPlugin.class, priority = Priority.FIRST_PRIORITY)
+@Plugin(type = PostprocessorPlugin.class, priority = Priority.FIRST)
public class DebugPostprocessor extends AbstractPostprocessorPlugin {
@Parameter(required = false)
diff --git a/src/main/java/org/scijava/module/process/DebugPreprocessor.java b/src/main/java/org/scijava/module/process/DebugPreprocessor.java
index 43f4498ac..15f6afe1f 100644
--- a/src/main/java/org/scijava/module/process/DebugPreprocessor.java
+++ b/src/main/java/org/scijava/module/process/DebugPreprocessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,7 +40,7 @@
*
* @author Curtis Rueden
*/
-@Plugin(type = PreprocessorPlugin.class, priority = Priority.FIRST_PRIORITY)
+@Plugin(type = PreprocessorPlugin.class, priority = Priority.FIRST)
public class DebugPreprocessor extends AbstractPreprocessorPlugin {
@Parameter(required = false)
diff --git a/src/main/java/org/scijava/module/process/DefaultValuePreprocessor.java b/src/main/java/org/scijava/module/process/DefaultValuePreprocessor.java
index 6d1cecf01..fed93f7e9 100644
--- a/src/main/java/org/scijava/module/process/DefaultValuePreprocessor.java
+++ b/src/main/java/org/scijava/module/process/DefaultValuePreprocessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,7 +37,7 @@
import org.scijava.module.ModuleService;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.Plugin;
-import org.scijava.util.ConversionUtils;
+import org.scijava.util.Types;
/**
* A preprocessor plugin that populates default parameter values.
@@ -49,7 +47,7 @@
*
* @author Curtis Rueden
*/
-@Plugin(type = PreprocessorPlugin.class, priority = Priority.VERY_HIGH_PRIORITY)
+@Plugin(type = PreprocessorPlugin.class, priority = Priority.VERY_HIGH)
public class DefaultValuePreprocessor extends AbstractPreprocessorPlugin {
@Parameter
@@ -73,7 +71,7 @@ private void assignDefaultValue(final Module module,
final ModuleItem item)
{
if (module.isInputResolved(item.getName())) return;
- final T nullValue = ConversionUtils.getNullValue(item.getType());
+ final T nullValue = Types.nullValue(item.getType());
if (!Objects.equals(item.getValue(module), nullValue)) return;
final T defaultValue = moduleService.getDefaultValue(item);
if (defaultValue == null) return;
diff --git a/src/main/java/org/scijava/module/process/GatewayPreprocessor.java b/src/main/java/org/scijava/module/process/GatewayPreprocessor.java
index 3cc9d45ac..b3889d479 100644
--- a/src/main/java/org/scijava/module/process/GatewayPreprocessor.java
+++ b/src/main/java/org/scijava/module/process/GatewayPreprocessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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,8 +50,7 @@
*
* @author Curtis Rueden
*/
-@Plugin(type = PreprocessorPlugin.class, //
- priority = 2 * Priority.VERY_HIGH_PRIORITY)
+@Plugin(type = PreprocessorPlugin.class, priority = 2 * Priority.VERY_HIGH)
public class GatewayPreprocessor extends AbstractPreprocessorPlugin {
@Parameter
@@ -64,7 +61,8 @@ public class GatewayPreprocessor extends AbstractPreprocessorPlugin {
@Override
public void process(final Module module) {
for (final ModuleItem> input : module.getInfo().inputs()) {
- if (!input.isAutoFill()) continue; // cannot auto-fill this input
+ if (!input.isAutoFill()) continue; // skip unfillable inputs
+ if (module.isInputResolved(input.getName())) continue; // skip resolved inputs
final Class> type = input.getType();
if (Gateway.class.isAssignableFrom(type)) {
// input is a gateway
@@ -87,22 +85,10 @@ private void setGatewayValue(final Context context,
try {
gateway = type.getConstructor(Context.class).newInstance(context);
}
- catch (final IllegalArgumentException exc) {
- exception = exc;
- }
- catch (final SecurityException exc) {
- exception = exc;
- }
- catch (final InstantiationException exc) {
- exception = exc;
- }
- catch (final IllegalAccessException exc) {
- exception = exc;
- }
- catch (final InvocationTargetException exc) {
- exception = exc;
- }
- catch (final NoSuchMethodException exc) {
+ catch (final IllegalArgumentException | SecurityException
+ | InstantiationException | IllegalAccessException
+ | InvocationTargetException | NoSuchMethodException exc)
+ {
exception = exc;
}
if (exception != null) {
diff --git a/src/main/java/org/scijava/module/process/InitPreprocessor.java b/src/main/java/org/scijava/module/process/InitPreprocessor.java
index a853ba785..d135614d6 100644
--- a/src/main/java/org/scijava/module/process/InitPreprocessor.java
+++ b/src/main/java/org/scijava/module/process/InitPreprocessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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 +45,7 @@
*
* @author Curtis Rueden
*/
-@Plugin(type = PreprocessorPlugin.class, priority = Priority.HIGH_PRIORITY)
+@Plugin(type = PreprocessorPlugin.class, priority = Priority.HIGH)
public class InitPreprocessor extends AbstractPreprocessorPlugin {
@Parameter(required = false)
diff --git a/src/main/java/org/scijava/module/process/LoadInputsPreprocessor.java b/src/main/java/org/scijava/module/process/LoadInputsPreprocessor.java
index 426be0bed..eb405b424 100644
--- a/src/main/java/org/scijava/module/process/LoadInputsPreprocessor.java
+++ b/src/main/java/org/scijava/module/process/LoadInputsPreprocessor.java
@@ -2,9 +2,7 @@
* #%L
* SciJava Common shared library for SciJava software.
* %%
- * Copyright (C) 2009 - 2017 Board of Regents of the University of
- * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
- * Institute of Molecular Cell Biology and Genetics.
+ * 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:
@@ -31,13 +29,10 @@
package org.scijava.module.process;
-import org.scijava.convert.ConvertService;
import org.scijava.module.Module;
-import org.scijava.module.ModuleItem;
import org.scijava.module.ModuleService;
import org.scijava.plugin.Parameter;
import org.scijava.plugin.Plugin;
-import org.scijava.util.ConversionUtils;
import org.scijava.widget.InputHarvester;
/**
@@ -58,41 +53,8 @@ public class LoadInputsPreprocessor extends AbstractPreprocessorPlugin {
@Parameter
private ModuleService moduleService;
- @Parameter
- private ConvertService conversionService;
-
- // -- ModuleProcessor methods --
-
@Override
public void process(final Module module) {
- final Iterable> inputs = module.getInfo().inputs();
- for (final ModuleItem> item : inputs) {
- loadValue(module, item);
- }
- }
-
- // -- Helper methods --
-
- /** Loads the value of the given module item from persistent storage. */
- private void loadValue(final Module module, final ModuleItem item) {
- // skip input that has already been resolved
- if (module.isInputResolved(item.getName())) return;
-
- final T prefValue = moduleService.load(item);
- final Class type = item.getType();
- final T defaultValue = item.getValue(module);
- final T value = getBestValue(prefValue, defaultValue, type);
- item.setValue(module, value);
+ moduleService.loadInputs(module);
}
-
- private T getBestValue(final Object prefValue,
- final Object defaultValue, final Class