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/.gitignore b/.gitignore index 54e060e75..436cdc7f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,13 @@ *.swp + +# Maven # +/target/ + +# Eclipse # /.classpath /.project /.settings/ -/target/ + +# IntelliJ # +/*.iml +/.idea/ diff --git a/.mailmap b/.mailmap index 902cb036c..5157cbbe1 100644 --- a/.mailmap +++ b/.mailmap @@ -1,5 +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/LICENSE.txt b/LICENSE.txt index 242746407..55024a7c5 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,4 @@ -Copyright (c) 2009 - 2015, 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 d046a41e6..ba1f15173 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -[![](http://jenkins.imagej.net/job/SciJava-common/lastBuild/badge/icon)](http://jenkins.imagej.net/job/SciJava-common/) +[![](https://img.shields.io/maven-central/v/org.scijava/scijava-common.svg)](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/badge.svg)](https://github.com/scijava/scijava-common/actions/workflows/build.yml) +[![developer chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](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 aada3996f..771234c9d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,22 +1,25 @@ - + 4.0.0 org.scijava pom-scijava - 8.2.0 + 44.0.0 scijava-common - 2.47.1-SNAPSHOT + 2.100.2-SNAPSHOT SciJava Common - SciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by both ImageJ and SCIFIO. - http://scijava.org/ + SciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by downstream projects in the SciJava ecosystem, such as ImageJ and SCIFIO. + https://github.com/scijava/scijava-common 2009 - + + SciJava + https://scijava.org/ + Simplified BSD License @@ -28,77 +31,114 @@ ctrueden Curtis Rueden - ctrueden@wisc.edu - http://loci.wisc.edu/people/curtis-rueden - UW-Madison LOCI - http://loci.wisc.edu/ - - architect - developer - - -6 - - - dscho - Johannes Schindelin - schindelin@wisc.edu - http://loci.wisc.edu/people/johannes-schindelin - UW-Madison LOCI - http://loci.wisc.edu/ - - architect - developer - - -6 - - - hinerm - Mark Hiner - hiner@wisc.edu - http://loci.wisc.edu/people/mark-hiner - UW-Madison LOCI - http://loci.wisc.edu/ + https://imagej.net/people/ctrueden + founder + lead developer + debugger + reviewer + support + maintainer - -6 - + + Mark Hiner + https://imagej.net/people/hinerm + founder + hinerm + + + Johannes Schindelin + https://imagej.net/people/dscho + dscho + + + Chris Allan + chris-allan + + + Nicolas Chiaruttini + https://imagej.net/people/NicoKiaru + NicoKiaru + Barry DeZonia - http://loci.wisc.edu/people/barry-dezonia - UW-Madison LOCI - http://loci.wisc.edu/ - - developer - - -6 + https://imagej.net/people/bdezonia + bdezonia + + + Christian Dietz + https://imagej.net/people/dietzc + dietzc + + + Richard Domander + https://imagej.net/people/rimadoma + rimadoma + + + Gabriel Einsdorf + https://imagej.net/people/gab1one + gab1one + + + Aivar Grislis + https://imagej.net/people/grislis + grislis + + + Jonathan Hale + https://imagej.net/people/Squareys + Squareys + + + Grant Harris + https://imagej.net/people/tnargsirrah + tnargsirrah Lee Kamentsky - leek@broadinstitute.org - http://www.broadinstitute.org/~leek/ - Broad Institute of MIT and Harvard - http://www.broadinstitute.org/ - - developer - - -5 + https://imagej.net/people/LeeKamentsky + LeeKamentsky - Christian Dietz - christian.dietz@uni-konstanz.de - http://www.informatik.uni-konstanz.de/berthold/mitglieder/christian-dietz/ - University of Konstanz - http://www.informatik.uni-konstanz.de/ - - developer - - +1 + Rick Lentz + https://imagej.net/people/ricklentz + ricklentz + + + Melissa Linkert + https://imagej.net/people/melissalinkert + melissalinkert + + + Kevin Mader + https://imagej.net/people/kmader + kmader + + + Hadrien Mary + https://imagej.net/people/hadim + hadim + + + Alison Walter + https://imagej.net/people/awalter17 + awalter17 + + + Jay Warrick + https://imagej.net/people/jaywarrick + jaywarrick + + + Christian Tischer + https://imagej.net/people/tischi + tischi - Jay Warrick @@ -112,33 +152,39 @@ - scm:git:git://github.com/scijava/scijava-common + scm:git:https://github.com/scijava/scijava-common scm:git:git@github.com:scijava/scijava-common HEAD https://github.com/scijava/scijava-common - GitHub Issues https://github.com/scijava/scijava-common/issues - - Jenkins - http://jenkins.imagej.net/job/SciJava-common/ + GitHub Actions + https://github.com/scijava/scijava-common/actions + + org.scijava + 8 + 11 + + bsd_2 + SciJava Common shared library for SciJava software. + SciJava developers. + **/bushe/** + + + 2.22.2 + + - + - com.googlecode.gentyref - gentyref - 1.1.0 - - - org.bushe - eventbus - 1.4 + org.scijava + parsington @@ -147,31 +193,26 @@ junit test + + org.mockito + mockito-core + test + - - maven-jar-plugin - - - - org.scijava - - - - - - org.codehaus.mojo - license-maven-plugin - - bsd_2 - 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 Common shared library for SciJava software. - - + org.apache.maven.plugins maven-compiler-plugin @@ -184,11 +225,22 @@ Institute of Molecular Cell Biology and Genetics. exec-maven-plugin + index-annotations process-classes java + + index-test-annotations + process-test-classes + + java + + + test + + org.scijava.annotations.EclipseHelper @@ -196,50 +248,4 @@ Institute of Molecular Cell Biology and Genetics. - - - - only-eclipse - - - m2e.version - - - - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - org.codehaus.mojo - exec-maven-plugin - 1.2.1 - - java - - - - - - - - - - - - - - - - diff --git a/src/it/apt-test/pom.xml b/src/it/apt-test/pom.xml index 570cbb5ec..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 - 2015 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 82580215d..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 - 2015 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 8b4abd6a7..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 - 2015 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 502f5596e..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 - 2015 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 2df5895fc..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 - 2015 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 7e66aef87..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 - 2015 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 6688600b2..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 - 2015 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,7 +51,7 @@ public abstract class AbstractBasicDetails implements BasicDetails { private String description; /** Table of extra key/value pairs. */ - private Map values = new HashMap(); + private final Map values = new HashMap<>(); // -- Object methods -- @@ -102,7 +100,7 @@ public void setDescription(final String description) { } @Override - public void set(String key, String value) { + public void set(final String key, final String value) { values.put(key, value); } diff --git a/src/main/java/org/scijava/AbstractContextual.java b/src/main/java/org/scijava/AbstractContextual.java index 74d95ab5d..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 - 2015 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,10 +60,4 @@ public Context context() { public Context getContext() { return context; } - - @Override - public void setContext(final Context context) { - context.inject(this); - } - } diff --git a/src/main/java/org/scijava/AbstractGateway.java b/src/main/java/org/scijava/AbstractGateway.java index a915e3953..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 - 2015 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,9 @@ 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; import org.scijava.module.ModuleService; import org.scijava.object.ObjectService; @@ -52,14 +52,17 @@ 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; import org.scijava.tool.ToolService; +import org.scijava.ui.UIService; import org.scijava.widget.WidgetService; /** @@ -82,11 +85,47 @@ 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 -- + @Override + public void launch(final String... args) { + // parse command line arguments + console().processArgs(args); + + // launch main methods + final int mainCount = main().execMains(); + + // display the user interface (NB: does not block) + // 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(); + } + } + + @Override + public String getShortName() { + final String pluginName = getInfo() == null ? null : getInfo().getName(); + if (pluginName != null && !pluginName.isEmpty()) return pluginName; + return getClass().getSimpleName().toLowerCase(); + } + @Override public S get(final Class serviceClass) { return context().service(serviceClass); @@ -152,12 +191,22 @@ public InputService input() { public IOService io() { return get(IOService.class); } + + @Override + public LocationService location() { + return get(LocationService.class); + } @Override public LogService log() { return get(LogService.class); } + @Override + public MainService main() { + return get(MainService.class); + } + @Override public MenuService menu() { return get(MenuService.class); @@ -202,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); @@ -222,6 +276,11 @@ public ToolService tool() { return get(ToolService.class); } + @Override + public UIService ui() { + return get(UIService.class); + } + @Override public WidgetService widget() { return get(WidgetService.class); @@ -240,13 +299,15 @@ public String getTitle() { } @Override - public String getVersion() { - return getApp().getVersion(); + public String getInfo(final boolean mem) { + return getApp().getInfo(mem); } + // -- Versioned methods -- + @Override - public String getInfo(final boolean mem) { - return getApp().getInfo(mem); + public String getVersion() { + return getApp().getVersion(); } } diff --git a/src/main/java/org/scijava/AbstractUIDetails.java b/src/main/java/org/scijava/AbstractUIDetails.java index 409dcefd6..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 - 2015 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,8 +29,6 @@ package org.scijava; -import org.scijava.util.ClassUtils; -import org.scijava.util.MiscUtils; import org.scijava.util.StringMaker; /** @@ -52,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; @@ -88,31 +84,6 @@ public String toString() { // -- UIDetails methods -- - @Override - public String getTitle() { - // use object label, if available - if (getLabel() != null && !getLabel().isEmpty()) return getLabel(); - - // use name of leaf menu item, if available - if (menuPath != null && menuPath.size() > 0) { - final MenuEntry menuLeaf = menuPath.getLeaf(); - final String menuName = menuLeaf.getName(); - if (menuName != null && !menuName.isEmpty()) return menuName; - } - - // use object name, if available - if (getName() != null && !getName().isEmpty()) return getName(); - - // use the unique identifier, if available - if (this instanceof Identifiable) { - final String id = ((Identifiable) this).getIdentifier(); - if (id != null) return id; - } - - // use class name as a last resort - return getClass().getSimpleName(); - } - @Override public MenuPath getMenuPath() { return menuPath; @@ -209,34 +180,4 @@ public double getPriority() { public void setPriority(final double priority) { this.priority = priority; } - - // -- Comparable methods -- - - @Override - public int compareTo(final Prioritized that) { - if (that == null) return 1; - - // compare priorities - final int priorityCompare = Priority.compare(this, that); - if (priorityCompare != 0) return priorityCompare; - - // compare classes - final int classCompare = ClassUtils.compare(getClass(), that.getClass()); - if (classCompare != 0) return classCompare; - - if (!(that instanceof UIDetails)) return 1; - final UIDetails uiDetails = (UIDetails) that; - - // compare names - final String thisName = getName(); - final String thatName = uiDetails.getName(); - final int nameCompare = MiscUtils.compare(thisName, thatName); - if (nameCompare != 0) return nameCompare; - - // compare titles - final String thisTitle = getTitle(); - final String thatTitle = uiDetails.getTitle(); - return MiscUtils.compare(thisTitle, thatTitle); - } - } diff --git a/src/main/java/org/scijava/BasicDetails.java b/src/main/java/org/scijava/BasicDetails.java index 4294428fc..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 - 2015 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 9ce0ed337..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 - 2015 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 a533f3858..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 - 2015 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,15 +53,16 @@ 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 * of services. - * + * * @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,9 +109,15 @@ 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. - * + * * @see #Context(Collection, PluginIndex, boolean) */ public Context() { @@ -109,15 +126,26 @@ public Context() { /** * Creates a new SciJava application context. - * - * @param empty If true, the context will be empty; otherwise, it will be - * initialized with all available services. - * @see #Context(Collection, PluginIndex, boolean) + * + * @param empty If true, the context will be empty of services; otherwise, it + * will be initialized with all available services. + * @see #Context(boolean, boolean) */ - @SuppressWarnings("unchecked") public Context(final boolean empty) { - this(empty ? Collections.> emptyList() : Arrays - .> asList(Service.class)); + this(empty, false); + } + + /** + * Creates a new SciJava application context. + * + * @param noServices If true, the context will contain no services; otherwise, + * it will be initialized with all available services. + * @param noPlugins If true, the context will contain no plugins; otherwise, + * it will be initialized with all available plugins. + * @see #Context(Collection, PluginIndex, boolean) + */ + public Context(final boolean noServices, final boolean noPlugins) { + this(services(noServices), plugins(noPlugins)); } /** @@ -130,17 +158,18 @@ public Context(final boolean empty) { * {@code new Context(DisplayService.class)}) yield the potentially confusing * warning: *

- *
Type safety: A generic array of Class is - * created for a varargs parameter
+ *
Type safety: A generic array of + * {@code Class} is created for a varargs + * parameter
*

* To avoid this, we have opted to use raw types and suppress the relevant * warnings here instead. *

- * + * * @param serviceClasses A list of types that implement the {@link Service} * interface (e.g., {@code DisplayService.class}). Compatible - * services will be loaded in the order given, - * regardless of their relative priorities. + * services will be loaded in the order given, regardless of + * their relative priorities. * @see #Context(Collection, PluginIndex, boolean) * @throws ClassCastException If any of the given arguments do not implement * the {@link Service} interface. @@ -152,7 +181,7 @@ public Context(@SuppressWarnings("rawtypes") final Class... serviceClasses) { /** * Creates a new SciJava application context with the specified services (and * any required service dependencies). - * + * * @param serviceClasses A collection of types that implement the * {@link Service} interface (e.g., {@code DisplayService.class}). * Compatible services will be loaded according to the order of the @@ -166,13 +195,13 @@ public Context(final Collection> serviceClasses) { /** * Creates a new SciJava application context with the specified services (and * any required service dependencies). - * + * * @param serviceClasses A collection of types that implement the * {@link Service} interface (e.g., {@code DisplayService.class}). * Compatible services will be loaded according to the order of the * collection, regardless of their relative priorities. - * @param strict Whether context creation will fail fast when there is - * an error instantiating a required service. + * @param strict Whether context creation will fail fast when there is an + * error instantiating a required service. * @see #Context(Collection, PluginIndex, boolean) */ public Context(final Collection> serviceClasses, @@ -186,7 +215,7 @@ public Context(final Collection> serviceClasses, * the specified PluginIndex. This allows a base set of available plugins to * be defined, and is useful when plugins that would not be returned by the * {@link PluginIndex}'s {@link org.scijava.plugin.PluginFinder} are desired. - * + * * @param pluginIndex The plugin index to use when discovering and indexing * plugins. If you wish to completely control how services are * discovered (i.e., use your own @@ -195,9 +224,8 @@ public Context(final Collection> serviceClasses, * result in a default plugin index being constructed and used. * @see #Context(Collection, PluginIndex, boolean) */ - @SuppressWarnings("unchecked") public Context(final PluginIndex pluginIndex) { - this(Arrays.> asList(Service.class), pluginIndex); + this(services(false), pluginIndex); } /** @@ -205,7 +233,7 @@ public Context(final PluginIndex pluginIndex) { * any required service dependencies). Service dependency candidates are * selected from those discovered by the given {@link PluginIndex}'s * associated {@link org.scijava.plugin.PluginFinder}. - * + * * @param serviceClasses A collection of types that implement the * {@link Service} interface (e.g., {@code DisplayService.class}). * Compatible services will be loaded according to the order of the @@ -238,7 +266,14 @@ 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}). * Compatible services will be loaded according to the order of the @@ -249,8 +284,8 @@ public Context(final Collection> serviceClasses, * {@link org.scijava.plugin.PluginFinder} implementation), then you * can pass a custom {@link PluginIndex} here. Passing null will * result in a default plugin index being constructed and used. - * @param strict Whether context creation will fail fast when there is - * an error instantiating a required service. + * @param strict Whether context creation will fail fast when there is an + * error instantiating a required service. */ public Context(final Collection> serviceClasses, final PluginIndex pluginIndex, final boolean strict) @@ -262,9 +297,31 @@ public Context(final Collection> serviceClasses, setStrict(strict); - final ServiceHelper serviceHelper = - new ServiceHelper(this, serviceClasses, strict); - serviceHelper.loadServices(); + if (!serviceClasses.isEmpty()) { + final ServiceHelper serviceHelper = // + 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 -- @@ -287,31 +344,29 @@ public void setStrict(final boolean strict) { /** * Gets the service of the given class. - * + * * @throws NoSuchServiceException if the context does not have the requested * service. */ public S service(final Class c) { final S service = getService(c); if (service == null) { - throw new NoSuchServiceException("Service " + c.getName() + " not found."); + throw new NoSuchServiceException("Service " + c.getName() + + " not found."); } return service; } /** * Gets the service of the given class name (useful for scripts). - * + * * @throws IllegalArgumentException if the class does not exist, or is not a * service class. * @throws NoSuchServiceException if the context does not have the requested * service. */ public Service service(final String className) { - final Class c = ClassUtils.loadClass(className); - if (c == null) { - throw new IllegalArgumentException("No such class: " + className); - } + final Class c = Types.load(className, false); if (!Service.class.isAssignableFrom(c)) { throw new IllegalArgumentException("Not a service class: " + c.getName()); } @@ -330,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") @@ -341,17 +396,18 @@ public Service getService(final String className) { /** * Injects the application context into the given object. This does three * distinct things: - *
    + *
      *
    1. If the given object has any non-final {@link Context} fields annotated - * with @{@link Parameter}, sets the value of those fields to this context.
    2. + * with @{@link Parameter}, sets the value of those fields to this context. + * *
    3. If the given object has any non-final {@link Service} fields annotated * with @{@link Parameter}, sets the value of those fields to the * corresponding service available from this context.
    4. *
    5. Calls {@link EventService#subscribe(Object)} with the object to - * register any @{@link EventHandler} annotated methods as event subscribers.
    6. - * . - *
- * + * 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. * @throws IllegalArgumentException If the object has a required @@ -361,7 +417,7 @@ public Service getService(final String className) { public void inject(final Object o) { // Ensure parameter fields and event handler methods are cached for this // object. - Query query = new Query(); + final Query query = new Query(); query.put(Parameter.class, Field.class); query.put(EventHandler.class, Method.class); ClassUtils.cacheAnnotatedObjects(o.getClass(), query); @@ -377,20 +433,37 @@ public void inject(final Object o) { subscribeToEvents(o); } + /** + * Reports whether a parameter of the given type would be assigned a value as + * a consequence of calling {@link #inject(Object)}. + *

+ * This method is notably useful for downstream code to discern between + * {@link Parameter} fields whose values would be injected, versus those whose + * values would not, without needing to hardcode type comparison checks + * against the {@link Service} and {@link Context} types. + *

+ * + * @param type The type of the @{@link Parameter}-annotated field. + * @return True iff a member field of the given type would have its value + * assigned. + */ + public boolean isInjectable(final Class type) { + if (Service.class.isAssignableFrom(type)) return true; + return Context.class.isAssignableFrom(type) && type.isInstance(this); + } + // -- Disposable methods -- @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 -- @@ -403,13 +476,26 @@ public void dispose() { public static List> serviceClassList( final Class... serviceClasses) { - return serviceClasses != null ? (List) Arrays.asList(serviceClasses) - : Arrays.asList(Service.class); + return serviceClasses != null ? // + 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(Object o) { + private List getParameterFields(final Object o) { try { return ClassUtils.getAnnotatedFields(o.getClass(), Parameter.class); } @@ -426,32 +512,47 @@ private void inject(final Field f, final Object o) { final Class type = f.getType(); if (Service.class.isAssignableFrom(type)) { final Service existingService = (Service) ClassUtils.getValue(f, o); - if (existingService != null) { - throw new IllegalStateException("Context already injected: " + - f.getDeclaringClass().getName() + "#" + f.getName()); + if (strict && existingService != null) { + throw new IllegalStateException("Context already injected: " + // + f.getDeclaringClass().getName() + "#" + f.getName()); } // populate Service parameter @SuppressWarnings("unchecked") final Class serviceType = - (Class) type; + (Class) type; final Service service = getService(serviceType); if (service == null && f.getAnnotation(Parameter.class).required()) { - throw new IllegalArgumentException( + throw new IllegalArgumentException(// createMissingServiceMessage(serviceType)); } + if (existingService != null && existingService != service) { + // NB: Can only happen in non-strict mode. + throw new IllegalStateException("Mismatched context: " + // + f.getDeclaringClass().getName() + "#" + f.getName()); + } ClassUtils.setValue(f, o, service); } else if (Context.class.isAssignableFrom(type) && type.isInstance(this)) { final Context existingContext = (Context) ClassUtils.getValue(f, o); - if (existingContext != null) { - throw new IllegalStateException("Context already injected: " + - f.getDeclaringClass().getName() + "#" + f.getName()); + if (strict && existingContext != null) { + throw new IllegalStateException("Context already injected: " + // + f.getDeclaringClass().getName() + "#" + f.getName()); + } + if (existingContext != null && existingContext != this) { + // NB: Can only happen in non-strict mode. + throw new IllegalStateException("Mismatched context: " + // + f.getDeclaringClass().getName() + "#" + f.getName()); } // populate Context parameter ClassUtils.setValue(f, o, this); } + else if (!type.isPrimitive()) { + // the parameter is some other object; if it is non-null, we recurse + final Object value = ClassUtils.getValue(f, o); + if (value != null) inject(value); + } } catch (final Throwable t) { handleSafely(t); @@ -482,11 +583,9 @@ private String createMissingServiceMessage( final Class serviceType) { final String nl = System.getProperty("line.separator"); - final ClassLoader classLoader = - Thread.currentThread().getContextClassLoader(); - final StringBuilder msg = - new StringBuilder("Required service is missing: " + - serviceType.getName() + nl); + final ClassLoader classLoader = getClassLoader(); + final StringBuilder msg = new StringBuilder( + "Required service is missing: " + serviceType.getName() + nl); msg.append("Context: " + this + nl); msg.append("ClassLoader: " + classLoader + nl); @@ -504,14 +603,40 @@ private String createMissingServiceMessage( } } else { - msg - .append("ClassLoader was not a URLClassLoader. Could not print classpath."); + msg.append( + "ClassLoader was not a URLClassLoader. Could not print classpath."); } 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); + } + 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 dfd218345..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 - 2015 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: @@ -69,12 +67,14 @@ public interface Contextual { *

* * @see Context#inject(Object) - * @see AbstractContextual for an example of how to implement this interface * @throws IllegalStateException If the object already has a context. * @throws IllegalArgumentException If the object has a required * {@link Service} parameter (see {@link Parameter#required()}) * which is not available from the context. */ - void setContext(Context context); + default void setContext(final Context context) { + context.inject(this); + } + } diff --git a/src/main/java/org/scijava/Disposable.java b/src/main/java/org/scijava/Disposable.java index 0d7c4ec38..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 - 2015 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 @@ public interface Disposable { * Performs any needed cleanup of the object's services, in preparation for * the object being retired (e.g., to make garbage collection possible). */ - void dispose(); - + default void dispose() { + // NB: Do nothing by default. + } } diff --git a/src/main/java/org/scijava/Gateway.java b/src/main/java/org/scijava/Gateway.java index 9a191ec2c..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 - 2015 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,9 @@ 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; import org.scijava.module.ModuleService; import org.scijava.object.ObjectService; @@ -54,10 +54,12 @@ 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; import org.scijava.tool.ToolService; +import org.scijava.ui.UIService; import org.scijava.widget.WidgetService; /** @@ -116,7 +118,31 @@ * @author Mark Hiner * @author Curtis Rueden */ -public interface Gateway extends RichPlugin, Versioned { +public interface Gateway extends RichPlugin, Disposable { + + /** + * Perform launch operations associated with this gateway. + *

+ * Typical operations might include: + *

+ *
    + *
  • Handle the given command line arguments using the + * {@link ConsoleService}.
  • + *
  • Execute registered main classes of the {@link MainService}.
  • + *
  • Display the default user interface using the {@link UIService}.
  • + *
  • In some circumstances (e.g., when running headless), dispose the + * context after launch operations are complete.
  • + *
+ * + * @param args The arguments to pass to the application. + */ + void launch(String... args); + + /** + * Gets a very succinct name for use referring to this gateway, e.g. as a + * variable name for scripting. + */ + String getShortName(); /** * Returns an implementation of the requested {@link Service}, if it exists in @@ -213,13 +239,28 @@ public interface Gateway extends RichPlugin, Versioned { */ 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}. * * @return The {@link LogService} of this application context. */ + @Override LogService log(); + /** + * Gets this application context's {@link MainService}. + * + * @return The {@link MainService} of this application context. + */ + MainService main(); + /** * Gets this application context's {@link MenuService}. * @@ -276,6 +317,13 @@ public interface Gateway extends RichPlugin, Versioned { */ 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}. * @@ -304,6 +352,13 @@ public interface Gateway extends RichPlugin, Versioned { */ ToolService tool(); + /** + * Gets this application context's {@link UIService}. + * + * @return The {@link UIService} of this application context. + */ + UIService ui(); + /** * Gets this application context's {@link WidgetService}. * @@ -322,10 +377,8 @@ public interface Gateway extends RichPlugin, Versioned { /** @see org.scijava.app.App#getInfo(boolean) */ String getInfo(boolean mem); - // -- Versioned methods -- - - /** @see org.scijava.app.App#getVersion() */ @Override - String getVersion(); - + default void dispose() { + context().dispose(); + } } diff --git a/src/main/java/org/scijava/Identifiable.java b/src/main/java/org/scijava/Identifiable.java index f1afd148c..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 - 2015 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 new file mode 100644 index 000000000..56aa725ae --- /dev/null +++ b/src/main/java/org/scijava/Initializable.java @@ -0,0 +1,43 @@ +/* + * #%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; + +/** + * Interface for objects which can be initialized. + * + * @author Curtis Rueden + */ +public interface Initializable { + + /** Initializes the object. */ + default void initialize() { + // NB: Do nothing by default. + } +} diff --git a/src/main/java/org/scijava/Instantiable.java b/src/main/java/org/scijava/Instantiable.java index 867f29ea2..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 - 2015 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 43d976e1c..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 - 2015 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 6f79e2791..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 - 2015 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 a5ebdfc9c..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 - 2015 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 6cf85f384..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 - 2015 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; +import java.net.URL; + +import org.scijava.util.Types; + /** * An object whose location is defined by a URL string. * @@ -39,6 +41,9 @@ public interface Locatable { /** Gets the URL string defining the object's location. */ - String getLocation(); + default String getLocation() { + 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 8d37b10a8..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 - 2015 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 e44b86899..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 - 2015 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,20 +54,38 @@ public MenuPath() { * the argument will make a copy. */ public MenuPath(final Collection menuEntries) { - addAll(menuEntries); + if (menuEntries != null) addAll(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 */ public MenuPath(final String path) { + this(path, PATH_SEPARATOR); + } + + /** + * Creates a menu path with entries parsed from the given string, splitting on + * the specified separator. + */ + public MenuPath(final String path, final String separator) { + this(path, separator, true); + } + + /** + * Creates a menu path with entries parsed from the given string, splitting on + * the specified separator, and trimming whitespace if indicated. + */ + public MenuPath(final String path, final String separator, + final boolean trim) + { if (path != null && !path.isEmpty()) { - final String[] tokens = path.split(PATH_SEPARATOR); + final String[] tokens = path.split(separator); for (final String token : tokens) { - add(new MenuEntry(token.trim())); + add(new MenuEntry(trim ? token.trim() : token)); } } } diff --git a/src/main/java/org/scijava/Named.java b/src/main/java/org/scijava/Named.java index 2f7cac60d..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 - 2015 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 225f6242e..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 - 2015 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 71a3d344d..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 - 2015 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 16ef8e8b3..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 - 2015 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 57dd7a943..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 - 2015 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,8 @@ package org.scijava; +import org.scijava.util.ClassUtils; + /** * An object that can be sorted according to priority. * @@ -52,4 +52,18 @@ public interface Prioritized extends Comparable { */ void setPriority(double priority); + // -- Comparable methods -- + + @Override + default int compareTo(final Prioritized that) { + if (that == null) return 1; + + // compare priorities + final int priorityCompare = Priority.compare(this, that); + if (priorityCompare != 0) return priorityCompare; + + // compare classes + return ClassUtils.compare(getClass(), that.getClass()); + } + } diff --git a/src/main/java/org/scijava/Priority.java b/src/main/java/org/scijava/Priority.java index af2774e97..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 - 2015 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 ee7a94eaf..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 - 2015 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 is - * created for a varargs parameter
+ *
Type safety: A generic array of + * {@code Class} is created for a varargs + * parameter
*

* To avoid this, we have opted to use raw types and suppress the relevant * warning here instead. @@ -111,5 +110,4 @@ public SciJava(final Collection> serviceClasses) { public SciJava(final Context context) { super(SciJavaApp.NAME, context); } - } diff --git a/src/main/java/org/scijava/Typed.java b/src/main/java/org/scijava/Typed.java index 9100a8400..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 - 2015 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,12 +40,18 @@ 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. *

*/ - boolean supports(T data); + 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. */ Class getType(); diff --git a/src/main/java/org/scijava/UIDetails.java b/src/main/java/org/scijava/UIDetails.java index 47428d198..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 - 2015 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; +import org.scijava.util.ClassUtils; +import org.scijava.util.MiscUtils; + /** * An interface defining details useful for generating relevant user interface * elements. @@ -56,7 +57,30 @@ public interface UIDetails extends BasicDetails, Prioritized { *
  • Item's class name, without package prefix
  • * */ - String getTitle(); + default String getTitle() { + // use object label, if available + if (getLabel() != null && !getLabel().isEmpty()) return getLabel(); + + // use name of leaf menu item, if available + final MenuPath menuPath = getMenuPath(); + if (menuPath != null && menuPath.size() > 0) { + final MenuEntry menuLeaf = menuPath.getLeaf(); + final String menuName = menuLeaf.getName(); + if (menuName != null && !menuName.isEmpty()) return menuName; + } + + // use object name, if available + if (getName() != null && !getName().isEmpty()) return getName(); + + // use the unique identifier, if available + if (this instanceof Identifiable) { + final String id = ((Identifiable) this).getIdentifier(); + if (id != null) return id; + } + + // use class name as a last resort + return getClass().getSimpleName(); + } /** Gets the path to the object's suggested position in the menu structure. */ MenuPath getMenuPath(); @@ -124,4 +148,32 @@ public interface UIDetails extends BasicDetails, Prioritized { */ void setSelected(boolean selected); + // -- Comparable methods -- + + @Override + default int compareTo(final Prioritized that) { + if (that == null) return 1; + + // compare priorities + final int priorityCompare = Priority.compare(this, that); + if (priorityCompare != 0) return priorityCompare; + + // compare classes + final int classCompare = ClassUtils.compare(getClass(), that.getClass()); + if (classCompare != 0) return classCompare; + + if (!(that instanceof UIDetails)) return 1; + final UIDetails uiDetails = (UIDetails) that; + + // compare names + final String thisName = getName(); + final String thatName = uiDetails.getName(); + final int nameCompare = MiscUtils.compare(thisName, thatName); + if (nameCompare != 0) return nameCompare; + + // compare titles + final String thisTitle = getTitle(); + final String thatTitle = uiDetails.getTitle(); + return MiscUtils.compare(thisTitle, thatTitle); + } } diff --git a/src/main/java/org/scijava/Validated.java b/src/main/java/org/scijava/Validated.java index 9512db816..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 - 2015 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 a716e3679..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 - 2015 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 ab3c9069a..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 - 2015 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,8 @@ package org.scijava; +import org.scijava.util.VersionUtils; + /** * An object that knows its version. * @@ -39,6 +39,9 @@ public interface Versioned { /** Gets the version of the object. */ - String getVersion(); + default String getVersion() { + return VersionUtils.getVersion(getClass()); + } + } diff --git a/src/main/java/org/scijava/annotations/AbstractIndexWriter.java b/src/main/java/org/scijava/annotations/AbstractIndexWriter.java index 27a4f6592..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 - 2015 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 abstract class AbstractIndexWriter { private final Map> map = - new ConcurrentSkipListMap>(); + new ConcurrentSkipListMap<>(); protected synchronized boolean foundAnnotations() { return !map.isEmpty(); @@ -72,10 +70,10 @@ protected synchronized void add(final Map annotationValues, { Map list = map.get(annotationName); if (list == null) { - list = new LinkedHashMap(); + list = new LinkedHashMap<>(); map.put(annotationName, list); } - final Map o = new TreeMap(); + final Map o = new TreeMap<>(); o.put("class", className); o.put("values", annotationValues); list.put(className, o); @@ -128,7 +126,7 @@ protected synchronized void merge(final String annotationName, } Map m = map.get(annotationName); if (m == null) { - m = new LinkedHashMap(); + m = new LinkedHashMap<>(); map.put(annotationName, m); } /* @@ -184,7 +182,7 @@ else if (o instanceof Enum) { } protected Map adapt(A annotation) { - Map result = new TreeMap(); + Map result = new TreeMap<>(); for (Method method : annotation.annotationType().getMethods()) try { if (method.getDeclaringClass() == annotation.annotationType()) { @@ -204,7 +202,7 @@ protected Map adapt(A annotation) { } private static Map adapt(Enum e) { - Map result = new TreeMap(); + Map result = new TreeMap<>(); result.put("enum", e.getClass().getName()); result.put("value", e.name()); return result; diff --git a/src/main/java/org/scijava/annotations/AnnotationCombiner.java b/src/main/java/org/scijava/annotations/AnnotationCombiner.java index c224590df..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 - 2015 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()); @@ -87,11 +86,11 @@ public void combine(File outputDirectory) throws Exception { /** Scans for annotations files in every resource on the classpath. */ public Set getAnnotationFiles() throws IOException { - final HashSet files = new HashSet(); + final HashSet files = new HashSet<>(); 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 cf473c161..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 - 2015 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 elements, final RoundEnvironment env) @@ -111,7 +131,7 @@ public boolean process(final Set elements, private class Writer extends AbstractIndexWriter implements StreamFactory { private final Map> originatingElements = - new HashMap>(); + new HashMap<>(); private final Filer filer = processingEnv.getFiler(); private final Elements utils = processingEnv.getElementUtils(); private final Types typeUtils = processingEnv.getTypeUtils(); @@ -124,7 +144,7 @@ public void add(final TypeElement element) { // remember originating elements List originating = originatingElements.get(annotationName); if (originating == null) { - originating = new ArrayList(); + originating = new ArrayList<>(); originatingElements.put(annotationName, originating); } @@ -159,7 +179,7 @@ private Map adapt( final List mirrors, final TypeMirror annotationType) { - final Map result = new TreeMap(); + final Map result = new TreeMap<>(); for (final AnnotationMirror mirror : mirrors) { if (typeUtils.isSameType(mirror.getAnnotationType(), annotationType)) { return (Map) adapt(mirror); @@ -172,7 +192,7 @@ private Map adapt( protected Object adapt(final Object o) { if (o instanceof AnnotationMirror) { final AnnotationMirror mirror = (AnnotationMirror) o; - final Map result = new TreeMap(); + final Map result = new TreeMap<>(); for (final Entry entry : mirror .getElementValues().entrySet()) { @@ -184,7 +204,7 @@ protected Object adapt(final Object o) { } else if (o instanceof List) { final List list = (List) o; - final List result = new ArrayList(list.size()); + final List result = new ArrayList<>(list.size()); for (final Object item : list) { result.add(adapt(item)); } @@ -197,7 +217,7 @@ else if (o instanceof TypeMirror) { } else if (o instanceof VariableElement) { final VariableElement element = (VariableElement) o; - final Map result = new TreeMap(); + final Map result = new TreeMap<>(); final String enumName = utils.getBinaryName((TypeElement) element.getEnclosingElement()) .toString(); @@ -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 8c42da122..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 - 2015 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: @@ -107,6 +105,8 @@ private double getDoubleConstant(final int index) { getU4(offset + 5)); } + // See https://en.wikipedia.org/wiki/Java_class_file#The_constant_pool for the + // meaning of the offsets behind these numbers private void getConstantPoolOffsets() { final int poolCount = getU2(8) - 1; poolOffsets = new int[poolCount]; @@ -114,9 +114,10 @@ private void getConstantPoolOffsets() { for (int i = 0; i < poolCount; i++) { poolOffsets[i] = offset; final int tag = getU1(offset); - if (tag == 7 || tag == 8) offset += 3; - else if (tag == 9 || tag == 10 || tag == 11 || tag == 3 || tag == 4 || - tag == 12) offset += 5; + if (tag == 7 || tag == 8 || tag == 16) offset += 3; + else if (tag == 15) offset += 4; + else if (tag == 3 || tag == 4 || tag == 9 || tag == 10 + || tag == 11 || tag == 12 || tag == 18) offset += 5; else if (tag == 5 || tag == 6) { poolOffsets[++i] = offset; offset += 9; @@ -201,7 +202,7 @@ private String getName() { private Map> getAnnotations() { final Map> annotations = - new TreeMap>(); + new TreeMap<>(); for (final Attribute attr : attributes) { if ("RuntimeVisibleAnnotations".equals(attr.getName())) { final byte[] buf = attr.attribute; @@ -212,7 +213,7 @@ private Map> getAnnotations() { raw2className(getStringConstant(getU2(buf, offset))); offset += 2; final Map values = - new TreeMap(); + new TreeMap<>(); annotations.put(className, values); offset = parseAnnotationValues(buf, offset, values); } @@ -294,7 +295,7 @@ private int parseAnnotationValue(byte[] buf, int offset, } case 'e': { final Map enumValue = - new TreeMap(); + new TreeMap<>(); enumValue.put("enum", raw2className(getStringConstant(getU2(buf, offset)))); offset += 2; @@ -306,7 +307,7 @@ private int parseAnnotationValue(byte[] buf, int offset, case '@': { // skipping annotation type offset += 2; - final Map values = new TreeMap(); + final Map values = new TreeMap<>(); offset = parseAnnotationValues(buf, offset, values); value = values; break; diff --git a/src/main/java/org/scijava/annotations/DirectoryIndexer.java b/src/main/java/org/scijava/annotations/DirectoryIndexer.java index 133909d4a..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 - 2015 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 d460d49e5..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 - 2015 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; /** @@ -98,7 +97,7 @@ public class EclipseHelper extends DirectoryIndexer { private static final String FORCE_ANNOTATION_INDEX_PROPERTY = "force.annotation.index"; - static Set indexed = new HashSet(); + static Set indexed = new HashSet<>(); private boolean bannerShown; private static boolean debug = @@ -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 9109f67a9..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 - 2015 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()
    + * }
    + * 
    + * * @author Johannes Schindelin */ public class Index implements Iterable> { @@ -74,8 +73,6 @@ public static Index load(final Class annotation) /** * Loads the index of all classes annotated with the specified annotation. - *

    - *

    * * @param annotation the annotation type * @param loader the class loader to use when loading {@link Class}-type @@ -86,7 +83,7 @@ public static
    Index load(final Class annotation, final ClassLoader loader) { EclipseHelper.updateAnnotationIndex(loader); - return new Index(annotation, loader); + return new Index<>(annotation, loader); } static final String INDEX_PREFIX = "META-INF/json/"; @@ -110,9 +107,9 @@ private class IndexItemIterator implements Iterator> { private Map legacyURLs; public IndexItemIterator(final Class annotation) { - seen = new HashSet(); + seen = new HashSet<>(); try { - legacyURLs = new LinkedHashMap(); + legacyURLs = new LinkedHashMap<>(); final Enumeration legacy = loader.getResources(LEGACY_INDEX_PREFIX + annotation.getName()); final int legacySuffixLength = @@ -160,7 +157,7 @@ private void readNext() throws IOException { @SuppressWarnings("unchecked") final Map values = (Map) map.get("values"); - next = new IndexItem(annotation, loader, className, values); + next = new IndexItem<>(annotation, loader, className, values); return; } indexReader.close(); diff --git a/src/main/java/org/scijava/annotations/IndexItem.java b/src/main/java/org/scijava/annotations/IndexItem.java index 0193c4cc5..e5265fc21 100644 --- a/src/main/java/org/scijava/annotations/IndexItem.java +++ b/src/main/java/org/scijava/annotations/IndexItem.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/IndexReader.java b/src/main/java/org/scijava/annotations/IndexReader.java index 0ba5ee233..4eab67552 100644 --- a/src/main/java/org/scijava/annotations/IndexReader.java +++ b/src/main/java/org/scijava/annotations/IndexReader.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -84,7 +82,7 @@ public Object next() throws IOException { return null; } if (c == '{') { - Map map = new LinkedHashMap(); + Map map = new LinkedHashMap<>(); for (;;) { if (expect('"', '}') == 1) { return map; @@ -99,7 +97,7 @@ public Object next() throws IOException { } } if (c == '[') { - List list = new ArrayList(); + List list = new ArrayList<>(); c = in.read(); if (c == ']') { return list; diff --git a/src/main/java/org/scijava/annotations/Indexable.java b/src/main/java/org/scijava/annotations/Indexable.java index 5486809f4..325739efd 100644 --- a/src/main/java/org/scijava/annotations/Indexable.java +++ b/src/main/java/org/scijava/annotations/Indexable.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/legacy/LegacyReader.java b/src/main/java/org/scijava/annotations/legacy/LegacyReader.java index 51307062d..9df62abcc 100644 --- a/src/main/java/org/scijava/annotations/legacy/LegacyReader.java +++ b/src/main/java/org/scijava/annotations/legacy/LegacyReader.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -83,7 +81,7 @@ public LegacyReader(final InputStream in) throws IOException { if (version != STREAM_VERSION) { throw new IOException("Unsupported version: " + version); } - references = new ArrayList(); + references = new ArrayList<>(); } public void close() throws IOException { @@ -260,7 +258,7 @@ protected NonPrimitiveClassDesc(final String className, if ((fields.length % 2) != 0) { throw new RuntimeException("That's odd: " + fields.length); } - this.fields = new LinkedHashMap(); + this.fields = new LinkedHashMap<>(); for (int i = 0; i < fields.length; i += 2) { String name = (String) fields[i]; ClassDesc classDesc; @@ -300,7 +298,7 @@ else if (c != TC_OBJECT) { @Override protected final Object readWithoutClassDesc() throws IOException { - final Map map = new LinkedHashMap(); + final Map map = new LinkedHashMap<>(); int index = references.size(); references.add(map); for (final String fieldName : order) { @@ -357,7 +355,7 @@ public static String toSimpleName(Class clazz) { } private final Map classDescs = - new HashMap(); + new HashMap<>(); { new ClassDesc("B") { @@ -477,7 +475,7 @@ public Object readExtra(final Map map) throws IOException expectToken(TC_BLOCKDATA); expectToken(4); int capacity = read32(); - final List list = new ArrayList(capacity); + final List list = new ArrayList<>(capacity); for (int i = 0; i < size; i++) { list.add(readObject()); } diff --git a/src/main/java/org/scijava/app/AbstractApp.java b/src/main/java/org/scijava/app/AbstractApp.java index 0fe3142f0..33ff29ff4 100644 --- a/src/main/java/org/scijava/app/AbstractApp.java +++ b/src/main/java/org/scijava/app/AbstractApp.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,12 +29,9 @@ package org.scijava.app; -import java.io.File; - import org.scijava.log.LogService; import org.scijava.plugin.AbstractRichPlugin; import org.scijava.plugin.Parameter; -import org.scijava.util.AppUtils; import org.scijava.util.Manifest; import org.scijava.util.POM; @@ -56,15 +51,7 @@ public abstract class AbstractApp extends AbstractRichPlugin implements App { /** JAR manifest with metadata about the application. */ private Manifest manifest; - @Override - public String getTitle() { - return getInfo().getName(); - } - - @Override - public String getVersion() { - return getPOM() == null ? "Unknown" : getPOM().getVersion(); - } + // -- App methods -- @Override public POM getPOM() { @@ -84,48 +71,8 @@ public Manifest getManifest() { return manifest; } - @Override - public String getInfo(boolean mem) { - final String appTitle = getTitle(); - final String appVersion = getVersion(); - final String javaVersion = System.getProperty("java.version"); - final String osArch = System.getProperty("os.arch"); - final long maxMem = Runtime.getRuntime().maxMemory(); - final long totalMem = Runtime.getRuntime().totalMemory(); - final long freeMem = Runtime.getRuntime().freeMemory(); - final long usedMem = totalMem - freeMem; - final long usedMB = usedMem / 1048576; - final long maxMB = maxMem / 1048576; - final StringBuilder sb = new StringBuilder(); - sb.append(appTitle + " " + appVersion); - sb.append("; Java " + javaVersion + " [" + osArch + "]"); - if (mem) sb.append("; " + usedMB + "MB of " + maxMB + "MB"); - return sb.toString(); - } - - @Override - public String getSystemProperty() { - return getInfo().getName().toLowerCase() + ".dir"; - } - - @Override - public File getBaseDirectory() { - return AppUtils.getBaseDirectory(getSystemProperty(), getClass(), null); - } - @Override public void about() { if (log != null) log.info(getInfo(false)); } - - @Override - public void prefs() { - // NB: Do nothing. - } - - @Override - public void quit() { - getContext().dispose(); - } - } diff --git a/src/main/java/org/scijava/app/App.java b/src/main/java/org/scijava/app/App.java index e4edcca0f..95cf249e7 100644 --- a/src/main/java/org/scijava/app/App.java +++ b/src/main/java/org/scijava/app/App.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,10 +31,10 @@ import java.io.File; -import org.scijava.Versioned; import org.scijava.plugin.Plugin; import org.scijava.plugin.RichPlugin; import org.scijava.plugin.SingletonPlugin; +import org.scijava.util.AppUtils; import org.scijava.util.Manifest; import org.scijava.util.POM; @@ -54,10 +52,13 @@ * @see Plugin * @see AppService */ -public interface App extends RichPlugin, SingletonPlugin, Versioned { +public interface App extends RichPlugin, SingletonPlugin { /** Gets the title of the application. */ - String getTitle(); + default String getTitle() { + return getInfo().getName(); + } + /** The Maven {@code groupId} of the application. */ String getGroupId(); @@ -81,20 +82,40 @@ public interface App extends RichPlugin, SingletonPlugin, Versioned { * * @param mem If true, memory usage information is included. */ - String getInfo(boolean mem); + default String getInfo(final boolean mem) { + final String appTitle = getTitle(); + final String appVersion = getVersion(); + final String javaVersion = System.getProperty("java.version"); + final String osArch = System.getProperty("os.arch"); + final long maxMem = Runtime.getRuntime().maxMemory(); + final long totalMem = Runtime.getRuntime().totalMemory(); + final long freeMem = Runtime.getRuntime().freeMemory(); + final long usedMem = totalMem - freeMem; + final long usedMB = usedMem / 1048576; + final long maxMB = maxMem / 1048576; + final StringBuilder sb = new StringBuilder(); + sb.append(appTitle + " " + appVersion); + sb.append("; Java " + javaVersion + " [" + osArch + "]"); + if (mem) sb.append("; " + usedMB + "MB of " + maxMB + "MB"); + return sb.toString(); + } /** * A system property which, if set, overrides the base directory of the * application. */ - String getSystemProperty(); + default String getSystemProperty() { + return getInfo().getName().toLowerCase() + ".dir"; + } /** * Gets the application's root directory. If the application's system property * is set, it is used. Otherwise, we scan up the tree from this class for a * suitable directory. */ - File getBaseDirectory(); + default File getBaseDirectory() { + return AppUtils.getBaseDirectory(getSystemProperty(), getClass(), null); + } /** * Displays information about the application. Typically this action @@ -106,26 +127,48 @@ public interface App extends RichPlugin, SingletonPlugin, Versioned { * Displays application preferences. The behavior is application-specific, but * typically a preferences dialog will be shown onscreen. */ - void prefs(); + default void prefs() { + // NB: Do nothing. + } /** * Quits the application. Typically this action will prompt the user about any * unsaved work first. */ - void quit(); + default void quit() { + getContext().dispose(); + } // -- Versioned methods -- /** * Gets the version of the application. *

    - * SciJava conforms to the Semantic + * SciJava conforms to the Semantic * Versioning specification. *

    * * @return The application version, in {@code major.minor.micro} format. */ @Override - String getVersion(); - + default String getVersion() { + // NB: We do not use VersionUtils.getVersion(c, groupId, artifactId) + // because that method does not cache the parsed Manifest and/or POM. + // We might have them already parsed here, and if not, we want to + // parse then cache locally, rather than discarding them afterwards. + + // try the manifest first, since it might know its build number + final Manifest m = getManifest(); + if (m != null) { + final String v = m.getVersion(); + if (v != null) return v; + } + // try the POM + final POM p = getPOM(); + if (p != null) { + final String v = p.getVersion(); + if (v != null) return v; + } + return "Unknown"; + } } diff --git a/src/main/java/org/scijava/app/AppService.java b/src/main/java/org/scijava/app/AppService.java index fedb50e38..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 - 2015 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,19 +29,25 @@ package org.scijava.app; +import java.util.List; import java.util.Map; import org.scijava.plugin.SingletonService; +import org.scijava.service.SciJavaService; /** * Interface for application-level functionality. * * @author Curtis Rueden */ -public interface AppService extends SingletonService { +public interface AppService extends SingletonService, SciJavaService { /** Gets the foremost application (the one with the highest priority). */ - App getApp(); + default App getApp() { + final List appList = getInstances(); + if (appList == null || appList.isEmpty()) return null; + return appList.get(0); + } /** Gets an application by name. */ App getApp(final String name); @@ -55,4 +59,10 @@ public interface AppService extends SingletonService { */ Map getApps(); + // -- SingletonService methods -- + + @Override + default Class getPluginType() { + return App.class; + } } diff --git a/src/main/java/org/scijava/app/DefaultAppService.java b/src/main/java/org/scijava/app/DefaultAppService.java index a68223bef..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 - 2015 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,6 @@ import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.scijava.event.EventHandler; @@ -66,13 +63,6 @@ public class DefaultAppService extends AbstractSingletonService implements // -- AppService methods -- - @Override - public App getApp() { - final List appList = getInstances(); - if (appList == null || appList.isEmpty()) return null; - return appList.get(0); - } - @Override public App getApp(final String name) { return apps().get(name); @@ -83,13 +73,6 @@ public Map getApps() { return apps(); } - // -- SingletonService methods -- - - @Override - public Class getPluginType() { - return App.class; - } - // -- Event handlers -- @EventHandler(key = "org.scijava.app.AppService#about") @@ -121,7 +104,7 @@ private Map apps() { /** Initializes {@link #apps}. */ private synchronized void initApps() { if (apps != null) return; // already initialized - final HashMap map = new HashMap(); + final HashMap map = new HashMap<>(); for (final App app : getInstances()) { final String name = app.getInfo().getName(); diff --git a/src/main/java/org/scijava/app/DefaultStatusService.java b/src/main/java/org/scijava/app/DefaultStatusService.java index 22d481b06..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 - 2015 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 01499f353..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 - 2015 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 fde11880a..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 - 2015 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 6edabc536..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 - 2015 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 a39a43eea..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 - 2015 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: @@ -63,5 +61,11 @@ public interface CacheService extends SciJavaService { * of the value loader. * @throws ExecutionException */ - V get(Object key, Callable valueLoader) throws ExecutionException; + @SuppressWarnings("unchecked") + default V get(final Object key, final Callable valueLoader) + throws ExecutionException + { + return (V)get(key); + } + } diff --git a/src/main/java/org/scijava/cache/DefaultCacheService.java b/src/main/java/org/scijava/cache/DefaultCacheService.java index 2cf81d4ad..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 - 2015 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,8 +31,6 @@ import java.util.Map; import java.util.WeakHashMap; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import org.scijava.Priority; import org.scijava.plugin.Plugin; @@ -44,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 { @@ -61,18 +57,10 @@ public Object get(final Object key) { return map.get(key); } - @SuppressWarnings("unchecked") - @Override - public V get(final Object key, final Callable valueLoader) - throws ExecutionException - { - return (V)get(key); - } - // -- Service Methods -- @Override public void initialize() { - map = new WeakHashMap(); + map = new WeakHashMap<>(); } } diff --git a/src/main/java/org/scijava/command/Command.java b/src/main/java/org/scijava/command/Command.java index d20e232e2..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 - 2015 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 3cc61e6ec..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 - 2015 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,6 @@ import java.util.Map; import org.scijava.Cancelable; -import org.scijava.Context; import org.scijava.InstantiableException; import org.scijava.ItemIO; import org.scijava.ItemVisibility; @@ -58,6 +55,7 @@ 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}. @@ -94,21 +92,21 @@ public class CommandInfo extends PluginInfo implements ModuleInfo { /** List of problems detected when parsing command parameters. */ private final List problems = - new ArrayList(); + new ArrayList<>(); /** Table of inputs, keyed on name. */ private final Map> inputMap = - new HashMap>(); + new HashMap<>(); /** Table of outputs, keyed on name. */ private final Map> outputMap = - new HashMap>(); + new HashMap<>(); /** Ordered list of input items. */ - private final List> inputList = new ArrayList>(); + private final List> inputList = new ArrayList<>(); /** Ordered list of output items. */ - private final List> outputList = new ArrayList>(); + private final List> outputList = new ArrayList<>(); // -- Constructors -- @@ -179,7 +177,7 @@ protected CommandInfo(final PluginInfo info, final String className, /** Sets the table of items with fixed, preset values. */ public void setPresets(final Map presets) { if (presets == null) { - this.presets = new HashMap(); + this.presets = new HashMap<>(); } else { this.presets = presets; @@ -301,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); } } @@ -451,11 +447,6 @@ private void checkFields(final Class type) { for (final Field f : fields) { f.setAccessible(true); // expose private fields - // NB: Skip types handled by the application framework itself. - // I.e., these parameters get injected by Context#inject(Object). - if (Service.class.isAssignableFrom(f.getType())) continue; - if (Context.class.isAssignableFrom(f.getType())) continue; - final Parameter param = f.getAnnotation(Parameter.class); boolean valid = true; @@ -470,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)); @@ -495,7 +487,7 @@ private void checkFields(final Class type) { // add item to the relevant list (inputs or outputs) final CommandModuleItem item = - new CommandModuleItem(this, f); + new CommandModuleItem<>(this, f); if (item.isInput()) { inputMap.put(name, item); if (!isPreset) inputList.add(item); @@ -510,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 7ae33aa2e..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 - 2015 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,6 @@ import org.scijava.InstantiableException; import org.scijava.NullContextException; import org.scijava.module.AbstractModule; -import org.scijava.module.MethodCallException; import org.scijava.module.Module; import org.scijava.module.ModuleException; import org.scijava.module.ModuleInfo; @@ -81,12 +78,17 @@ public class CommandModule extends AbstractModule implements Cancelable, private final CommandInfo info; /** The command instance handled by this module. */ + @Parameter private final Command command; @Parameter private Context context; - /** Reason for cancelation, or null if not canceled. */ + /** + * Reason for cancelation, or null if not canceled. Note that this field is + * only relevant if the delegate {@link Command} is not itself + * {@link Cancelable}. + */ private String cancelReason; /** Creates a command module for the given {@link PluginInfo}. */ @@ -140,13 +142,6 @@ public void cancel() { previewPlugin.cancel(); } - @Override - public void initialize() throws MethodCallException { - // NB: Inject the context into the command before initializing. - getContext().inject(command); - super.initialize(); - } - @Override public CommandInfo getInfo() { return info; @@ -205,20 +200,26 @@ public void run() { @Override public boolean isCanceled() { + if (command instanceof Cancelable) { + return ((Cancelable) command).isCanceled(); + } return cancelReason != null; } @Override public void cancel(final String reason) { - cancelReason = reason == null ? "" : reason; if (command instanceof Cancelable) { - // propagate cancelation to the command instance itself ((Cancelable) command).cancel(reason); + return; } + cancelReason = reason == null ? "" : reason; } @Override public String getCancelReason() { + if (command instanceof Cancelable) { + return ((Cancelable) command).getCancelReason(); + } return cancelReason; } @@ -256,7 +257,7 @@ private void assignPresets() { for (final String name : presets.keySet()) { final Object value = presets.get(name); setInput(name, value); - setResolved(name, true); + resolveInput(name); } } diff --git a/src/main/java/org/scijava/command/CommandModuleItem.java b/src/main/java/org/scijava/command/CommandModuleItem.java index a952a9947..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 - 2015 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 @@ -123,6 +120,11 @@ public String getInitializer() { return getParameter().initializer(); } + @Override + public String getValidater() { + return getParameter().validater(); + } + @Override public String getCallback() { return getParameter().callback(); @@ -143,9 +145,49 @@ public T getMaximumValue() { return tValue(getParameter().max()); } + @Override + public T getDefaultValue() { + // NB: The default value for a command is the initial field value. + // E.g.: + // + // @Parameter + // private int weekdays = 5; + // + // To obtain this information, we need to instantiate the module, then + // extract the value of the associated field. + // + // Of course, the command might do evil things like: + // + // @Parameter + // private long time = System.currentTimeMillis(); + // + // In which case the default value will vary by instance. But there is + // nothing we can really do about that. This is only a best effort. + + try { + final Object dummy = getInfo().loadDelegateClass().newInstance(); + @SuppressWarnings("unchecked") + final T value = (T) getField().get(dummy); + return value; + } + catch (final InstantiationException | IllegalAccessException + | ClassNotFoundException exc) + { + throw new IllegalStateException(exc); + } + } + @Override public Number getStepSize() { - return tValue(getParameter().stepSize(), Number.class); + // FIXME: stepSize should be typed on T, not Number! + final String value = getParameter().stepSize(); + try { + final double stepSize = Double.parseDouble(value); + return stepSize; + } + catch (final NumberFormatException exc) { + return tValue(value, Number.class); + } } @Override @@ -155,11 +197,14 @@ public int getColumnCount() { @Override public List getChoices() { - final ArrayList choices = new ArrayList(); - for (final String choice : getParameter().choices()) { - choices.add(tValue(choice)); + final String[] choices = getParameter().choices(); + if (choices.length == 0) return super.getChoices(); + + final ArrayList choiceList = new ArrayList<>(); + for (final String choice : choices) { + choiceList.add(tValue(choice)); } - return choices; + return choiceList; } // -- BasicDetails methods -- @@ -205,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 4f76f8e93..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 - 2015 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}. *

    @@ -64,9 +62,13 @@ */ public interface CommandService extends PTService, SciJavaService { - EventService getEventService(); + default EventService eventService() { + return context().getService(EventService.class); + } - ModuleService getModuleService(); + default ModuleService moduleService() { + return context().getService(ModuleService.class); + } /** Gets the list of all available {@link Command}s). */ List getCommands(); @@ -257,4 +259,17 @@ Future run(Class commandClass, Future run(CommandInfo info, boolean process, Map inputMap); + // -- Deprecated methods -- + + /** @deprecated Use {@link #eventService()} instead. */ + @Deprecated + default EventService getEventService() { + return eventService(); + } + + /** @deprecated Use {@link #moduleService()} instead. */ + @Deprecated + default ModuleService getModuleService() { + return moduleService(); + } } diff --git a/src/main/java/org/scijava/command/ContextCommand.java b/src/main/java/org/scijava/command/ContextCommand.java index e13198833..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 - 2015 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 3246e9afc..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 - 2015 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; @@ -83,17 +82,17 @@ public class DefaultCommandService extends AbstractPTService implements // -- CommandService methods -- @Override - public EventService getEventService() { + public EventService eventService() { return eventService; } @Override - public PluginService getPluginService() { + public PluginService pluginService() { return pluginService; } @Override - public ModuleService getModuleService() { + public ModuleService moduleService() { return moduleService; } @@ -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 -- @@ -208,7 +203,7 @@ public Class getPluginType() { @Override public void initialize() { - commandMap = new HashMap, CommandInfo>(); + commandMap = new HashMap<>(); // inform the module service of available commands final List> plugins = @@ -226,7 +221,7 @@ protected void onEvent(final PluginsRemovedEvent event) { @EventHandler protected void onEvent(final PluginsAddedEvent event) { final ArrayList> commands = - new ArrayList>(); + new ArrayList<>(); findCommandPlugins(event.getItems(), commands); addCommands(commands); } @@ -258,7 +253,7 @@ private CommandInfo getOrCreate( /** Adds new commands to the module service. */ private void addCommands(final List> plugins) { // extract commands from the list of plugins - final List commands = new ArrayList(); + final List commands = new ArrayList<>(); for (final PluginInfo info : plugins) { final CommandInfo commandInfo = wrapAsCommand(info); commands.add(commandInfo); @@ -291,7 +286,7 @@ private void removeCommands(final List> plugins) { private List getCommandsUnknown( final List> plugins) { - final List commands = new ArrayList(); + final List commands = new ArrayList<>(); for (final PluginInfo info : plugins) { final CommandInfo commandInfo = commandMap.get(info); if (commandInfo == null) continue; @@ -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 ff622847b..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 - 2015 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 39ac54354..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 - 2015 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 @@ -310,7 +308,7 @@ private void populateItems() { /** Creates a mutable copy of the given module item. */ private DefaultMutableModuleItem copy(final ModuleItem item) { - return new DefaultMutableModuleItem(this, item); + return new DefaultMutableModuleItem<>(this, item); } } 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}. + *

    + *

    + * Here is are some examples of usage: + *

    + * + *
    + * {@code
    + * // Single input, no configuration.
    + * Inputs inputs = new Inputs(context);
    + * inputs.addInput("sigma", Double.class);
    + * Double sigma = (Double) inputs.harvest().get("sigma");
    + *
    + * // Two inputs, no configuration.
    + * Inputs inputs = new Inputs(context);
    + * inputs.addInput("name", String.class);
    + * inputs.addInput("age", Integer.class);
    + * Map values = inputs.harvest();
    + * String name = (String) values.get("name");
    + * Integer age = (Integer) values.get("age");
    + *
    + * // Inputs with configuration.
    + * Inputs inputs = new Inputs(context);
    + * MutableModuleItem wordInput = inputs.addInput("word", String.class);
    + * wordInput.setLabel("Favorite word");
    + * wordInput.setChoices(Arrays.asList("quick", "brown", "fox"));
    + * wordInput.setDefaultValue("fox");
    + * MutableModuleItem opacityInput = inputs.addInput("opacity", Double.class);
    + * opacityInput.setMinimumValue(0.0);
    + * opacityInput.setMaximumValue(1.0);
    + * opacityInput.setDefaultValue(0.5);
    + * opacityInput.setWidgetStyle(NumberWidget.SCROLL_BAR_STYLE);
    + * inputs.harvest();
    + * String word = wordInput.getValue(inputs);
    + * Double opacity = opacityInput.getValue(inputs);
    + * }
    + * 
    + * + * @author Curtis Rueden + */ +public final class Inputs extends DynamicCommand { + + public Inputs(final Context context) { + context.inject(this); + } + + public Map harvest() { + try { + final List pre = // + pluginService.createInstancesOfType(PreprocessorPlugin.class); + return moduleService.run(this, pre, null).get().getInputs(); + } + catch (final InterruptedException | ExecutionException exc) { + throw new RuntimeException(exc); + } + } +} diff --git a/src/main/java/org/scijava/command/Interactive.java b/src/main/java/org/scijava/command/Interactive.java index 0ea3369ef..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 - 2015 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 b1f14d62a..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 - 2015 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,6 +35,7 @@ import org.scijava.event.EventHandler; import org.scijava.event.EventService; import org.scijava.log.LogService; +import org.scijava.log.Logged; import org.scijava.module.MethodCallException; import org.scijava.module.ModuleItem; import org.scijava.plugin.Parameter; @@ -60,7 +59,7 @@ * @author Curtis Rueden */ public abstract class InteractiveCommand extends DynamicCommand implements - Interactive, Previewable + Interactive, Previewable, Logged { @Parameter @@ -94,6 +93,7 @@ public InteractiveCommand(final String... listenerNames) { public void preview() { // NB: Interactive commands call run upon any parameter change. run(); + saveInputs(); } @Override @@ -102,6 +102,13 @@ public void cancel() { // That is, closing the non-modal dialog does nothing. } + // -- Logged methods -- + + @Override + public LogService log() { + return log; + } + // -- Internal methods -- protected void updateInput(final ModuleItem item) { @@ -137,10 +144,6 @@ protected void update(final ModuleItem item, final T newValue) { } } - protected LogService log() { - return log; - } - // -- Event handlers -- @EventHandler diff --git a/src/main/java/org/scijava/command/ModuleCommand.java b/src/main/java/org/scijava/command/ModuleCommand.java index 211f3b5ca..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 - 2015 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 fcea8a72e..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 - 2015 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 2565239f0..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 - 2015 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 6c09e8cc8..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 - 2015 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,50 +30,87 @@ package org.scijava.command.console; import java.util.LinkedList; +import java.util.Map; import org.scijava.command.CommandInfo; import org.scijava.command.CommandService; import org.scijava.console.AbstractConsoleArgument; import org.scijava.console.ConsoleArgument; +import org.scijava.console.ConsoleUtils; +import org.scijava.log.LogService; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; /** - * Handles the {@code --run} command line argument. - * - * @author Curtis Rueden - * @author Johannes Schindelin + * @deprecated Use {@link org.scijava.run.console.RunArgument} instead. */ +@Deprecated @Plugin(type = ConsoleArgument.class) public class RunArgument extends AbstractConsoleArgument { @Parameter private CommandService commandService; + @Parameter + private LogService log; + + // -- Constructor -- + + public RunArgument() { + super(2, "--class"); + } + // -- ConsoleArgument methods -- @Override public void handle(final LinkedList args) { - if (!supports(args)) return; + if (!supports(args)) + return; - args.removeFirst(); // --run + log.warn("The --class flag is deprecated, and will\n" + + "be removed in a future release. Use --run instead."); + + args.removeFirst(); // --class final String commandToRun = args.removeFirst(); - final String optionString = args.isEmpty() ? "" : args.removeFirst(); + final String paramString = ConsoleUtils.hasParam(args) ? args.removeFirst() : ""; - run(commandToRun, optionString); + run(commandToRun, paramString); } // -- Typed methods -- @Override public boolean supports(final LinkedList args) { - return args != null && args.size() >= 2 && args.getFirst().equals("--run"); + if (!super.supports(args)) + return false; + return getInfo(args.get(1)) != null; } // -- Helper methods -- /** Implements the {@code --run} command line argument. */ private void run(final String commandToRun, final String optionString) { + // get the command info + final CommandInfo info = getInfo(commandToRun); + + // couldn't find anything to run + if (info == null) + return; + + // TODO: parse the optionString a la ImageJ1 + final Map inputMap = ConsoleUtils.parseParameterString(optionString, info, log); + + try { + commandService.run(info, true, inputMap).get(); + } catch (final Exception exc) { + log.error(exc); + } + } + + /** + * Try to convert the given string to a {@link CommandInfo} + */ + private CommandInfo getInfo(final String commandToRun) { CommandInfo info = commandService.getCommand(commandToRun); if (info == null) { // command was not a class name; search for command by title instead @@ -87,9 +122,6 @@ private void run(final String commandToRun, final String optionString) { } } } - if (info == null) return; - // TODO: parse the optionString a la ImageJ1 - commandService.run(info, true); + return info; } - } diff --git a/src/main/java/org/scijava/command/run/CommandCodeRunner.java b/src/main/java/org/scijava/command/run/CommandCodeRunner.java new file mode 100644 index 000000000..bc8550275 --- /dev/null +++ b/src/main/java/org/scijava/command/run/CommandCodeRunner.java @@ -0,0 +1,111 @@ +/* + * #%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.run; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +import org.scijava.command.Command; +import org.scijava.command.CommandInfo; +import org.scijava.command.CommandService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.run.AbstractCodeRunner; +import org.scijava.run.CodeRunner; + +/** + * Runs the given {@link Command} class. + * + * @author Curtis Rueden + */ +@Plugin(type = CodeRunner.class) +public class CommandCodeRunner extends AbstractCodeRunner { + + @Parameter + private CommandService commandService; + + // -- CodeRunner methods -- + + @Override + public void run(final Object code, final Object... args) + throws InvocationTargetException + { + final Class c = getCommandClass(code); + if (c != null) waitFor(commandService.run(c, true, args)); + + final CommandInfo info = getCommandInfo(code); + if (info != null) waitFor(commandService.run(info, true, args)); + } + + @Override + public void run(final Object code, final Map inputMap) + throws InvocationTargetException + { + final Class c = getCommandClass(code); + if (c != null) waitFor(commandService.run(c, true, inputMap)); + + final CommandInfo info = getCommandInfo(code); + if (info != null) waitFor(commandService.run(info, true, inputMap)); + } + + // -- Typed methods -- + + @Override + public boolean supports(final Object code) { + return getCommandClass(code) != null || getCommandInfo(code) != null; + } + + // -- Helper methods -- + + private Class getCommandClass(final Object code) { + if (!(code instanceof Class)) return null; + final Class c = (Class) code; + if (!Command.class.isAssignableFrom(c)) return null; + @SuppressWarnings("unchecked") + final Class commandClass = (Class) c; + return commandClass; + } + + private CommandInfo getCommandInfo(final Object code) { + if (!(code instanceof String)) return null; + final String command = (String) code; + + final CommandInfo info = commandService.getCommand(command); + if (info != null) return info; + + // command was not a class name; search for command by title instead + for (final CommandInfo ci : commandService.getCommands()) { + if (command.equals(ci.getTitle())) return ci; + } + + return null; + } + +} diff --git a/src/main/java/org/scijava/console/AbstractConsoleArgument.java b/src/main/java/org/scijava/console/AbstractConsoleArgument.java index 4112bfc9b..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 - 2015 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,9 @@ package org.scijava.console; +import java.util.HashSet; import java.util.LinkedList; +import java.util.Set; import org.scijava.plugin.AbstractHandlerPlugin; @@ -43,13 +43,54 @@ public abstract class AbstractConsoleArgument extends AbstractHandlerPlugin> implements ConsoleArgument { + private int numArgs; + private Set flags; + + public AbstractConsoleArgument() { + this(1, new String[0]); + } + + public AbstractConsoleArgument(final String... flags) { + this(1, flags); + } + + public AbstractConsoleArgument(final int requiredArgs, final String... flags) { + numArgs = requiredArgs; + this.flags = new HashSet<>(); + for (final String s : flags) this.flags.add(s); + } // -- Typed methods -- @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Class> getType() { - return (Class) String.class; + public boolean supports(final LinkedList args) { + if (args == null || args.size() < numArgs) return false; + return isFlag(args); } + // -- Internal methods -- + + /** + * Check if the given list of arguments starts with a flag that matches this + * {@link ConsoleArgument}. + * + * @return true iff one of this argument's flags matches the first string in + * the given list, or this argument has no explicit flags. + */ + protected boolean isFlag(final LinkedList args) { + return flags.isEmpty() || flags.contains(args.getFirst()); + } + + /** + * If the next argument is an appropriate parameter to a + * {@link ConsoleArgument}, retrieves it; otherwise, returns null. + * + * @return The first argument of the given list, if it does not + * start with a {@code '-'} character; or null otherwise. + */ + protected String getParam(final LinkedList args) { + if (args.isEmpty()) return null; + final String arg = args.getFirst(); + return arg.startsWith("-") ? null : arg; + } } diff --git a/src/main/java/org/scijava/console/ConsoleArgument.java b/src/main/java/org/scijava/console/ConsoleArgument.java index 4a9232da2..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 - 2015 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,4 +52,11 @@ public interface ConsoleArgument extends HandlerPlugin> { /** Handles the front of the given list of arguments. */ void handle(final LinkedList args); + // -- Typed methods -- + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + default Class> getType() { + return (Class) String.class; + } } diff --git a/src/main/java/org/scijava/console/ConsoleService.java b/src/main/java/org/scijava/console/ConsoleService.java index 51d1d7f0b..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 - 2015 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. */ @@ -62,4 +61,18 @@ public interface ConsoleService extends /** Notifies listeners of output sent to {@code stdout} or {@code stderr}. */ void notifyListeners(OutputEvent event); + // -- PTService methods -- + + @Override + default Class getPluginType() { + return ConsoleArgument.class; + } + + // -- Typed methods -- + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + default Class> getType() { + return (Class) LinkedList.class; + } } diff --git a/src/main/java/org/scijava/console/ConsoleUtils.java b/src/main/java/org/scijava/console/ConsoleUtils.java new file mode 100644 index 000000000..2f62f0c14 --- /dev/null +++ b/src/main/java/org/scijava/console/ConsoleUtils.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.console; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import org.scijava.command.CommandInfo; +import org.scijava.log.LogService; +import org.scijava.module.ModuleInfo; +import org.scijava.module.ModuleItem; +import org.scijava.parse.ParseService; + +/** @deprecated Use alternatives instead (see individual method docs). */ +@Deprecated +public final class ConsoleUtils { + + /** @deprecated Use {@link ParseService} instead. */ + @Deprecated + public static Map parseParameterString(final String parameterString) { + return parseParameterString(parameterString, (CommandInfo)null); + } + + /** @deprecated Use {@link ParseService} instead. */ + @Deprecated + public static Map parseParameterString(final String parameterString, final ModuleInfo info) { + return parseParameterString(parameterString, info, null); + } + + /** @deprecated Use {@link ParseService} instead. */ + @Deprecated + public static Map parseParameterString(final String parameterString, final LogService log) { + return parseParameterString(parameterString, null, log); + } + + /** @deprecated Use {@link ParseService} instead. */ + @Deprecated + public static Map parseParameterString(final String parameterString, final ModuleInfo info, final LogService log) { + final Map inputMap = new HashMap<>(); + + if (!parameterString.isEmpty()) { + Iterator> inputs = null; + if (info != null) { + inputs = info.inputs().iterator(); + } + final String[] pairs = parameterString.split(","); + for (final String pair : pairs) { + final String[] split = pair.split("="); + if (split.length == 2) + inputMap.put(split[0], split[1]); + else if (inputs != null && inputs.hasNext() && split.length == 1) { + inputMap.put(inputs.next().getName(), split[0]); + } + else if (log != null) + log.error("Parameters must be formatted as a comma-separated list of key=value pairs"); + + } + } + + return inputMap; + + } + + /** + * @deprecated Use {@link AbstractConsoleArgument#getParam(LinkedList)} + * instead. + */ + @Deprecated + public static boolean hasParam(final LinkedList args) { + return !(args.isEmpty() || args.getFirst().startsWith("-")); + } +} diff --git a/src/main/java/org/scijava/console/DefaultConsoleService.java b/src/main/java/org/scijava/console/DefaultConsoleService.java index 75821a5db..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 - 2015 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.io.PrintStream; 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; @@ -67,21 +67,21 @@ 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 -- @Override public void processArgs(final String... args) { log.debug("Received command line arguments:"); - final LinkedList argList = new LinkedList(); + final LinkedList argList = new LinkedList<>(); for (final String arg : args) { log.debug("\t" + arg); argList.add(arg); } + final List previousArgs = new ArrayList<>(); + while (!argList.isEmpty()) { final ConsoleArgument handler = getHandler(argList); if (handler == null) { @@ -90,51 +90,44 @@ public void processArgs(final String... args) { log.warn("Ignoring invalid argument: " + arg); continue; } + + // keep a copy of the argument list prior to handling + previousArgs.clear(); + previousArgs.addAll(argList); + + // process the argument handler.handle(argList); + + // verify that the handler did something to the list; + // this guards against bugs which would cause infinite loops + if (sameElements(previousArgs, argList)) { + // skip improperly handled argument + final String arg = argList.removeFirst(); + log.warn("Plugin '" + handler.getClass().getName() + + "' failed to handle argument: " + arg); + } } } @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); } - // -- PTService methods -- - - @Override - public Class getPluginType() { - return ConsoleArgument.class; - } - - // -- Typed methods -- - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Class> getType() { - return (Class) LinkedList.class; - } - // -- Disposable methods -- @Override @@ -159,21 +152,33 @@ 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); } + /** + * Gets whether two lists have exactly the same elements in them. + *

    + * We cannot use {@link List#equals(Object)} because want to check for + * identical references, not per-element object equality. + *

    + */ + private boolean sameElements(final List l1, + final List l2) + { + if (l1.size() != l2.size()) return false; + for (int i = 0; i < l1.size(); i++) { + if (l1.get(i) != l2.get(i)) return false; + } + return true; + } + // -- Helper classes -- /** diff --git a/src/main/java/org/scijava/console/MultiOutputStream.java b/src/main/java/org/scijava/console/MultiOutputStream.java index 655575082..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 - 2015 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 13c2ff1f5..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 - 2015 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 cddafcb19..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 - 2015 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 9c9111374..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 - 2015 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 f8b968458..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 - 2015 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,9 +44,15 @@ @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 -- + + public SystemPropertyArgument() { + super(1); + } + // -- ConsoleArgument methods -- @Override @@ -69,7 +73,7 @@ public void handle(final LinkedList args) { @Override public boolean supports(final LinkedList args) { - if (args == null || args.isEmpty()) return false; + if (!super.supports(args)) return false; final String arg = args.getFirst(); if (!arg.startsWith("-D")) return false; return SYS_PROP_PAT.matcher(arg).matches(); diff --git a/src/main/java/org/scijava/convert/AbstractConvertService.java b/src/main/java/org/scijava/convert/AbstractConvertService.java index 200460a60..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 - 2015 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,47 +29,16 @@ package org.scijava.convert; -import java.lang.reflect.Type; - 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 +public abstract class AbstractConvertService // + extends AbstractHandlerService> // + implements ConvertService { - - // -- ConversionService methods -- - - @Override - public Object convert(Object src, Type dest) { - return convert(new ConversionRequest(src, dest)); - } - - @Override - public T convert(Object src, Class dest) { - // NB: repeated code with convert(ConversionRequest), because the - // handler's convert method respects the T provided - Converter handler = getHandler(src, dest); - return handler == null ? null : handler.convert(src, dest); - } - - @Override - public Object convert(ConversionRequest request) { - Converter handler = getHandler(request); - return handler == null ? null : handler.convert(request); - } - - // -- Service methods -- - - @Override - public void initialize() { - ConversionUtils.setDelegateService(this, getPriority()); - } + // NB: This layer remains merely for backwards compatibility. } diff --git a/src/main/java/org/scijava/convert/AbstractConverter.java b/src/main/java/org/scijava/convert/AbstractConverter.java index 26602d0e3..c6132c396 100644 --- a/src/main/java/org/scijava/convert/AbstractConverter.java +++ b/src/main/java/org/scijava/convert/AbstractConverter.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,38 +29,17 @@ package org.scijava.convert; -import java.lang.reflect.Type; import java.util.Collection; import org.scijava.object.ObjectService; import org.scijava.plugin.AbstractHandlerPlugin; import org.scijava.plugin.Parameter; -import org.scijava.util.ConversionUtils; -import org.scijava.util.GenericUtils; /** * Abstract superclass for {@link Converter} plugins. Performs appropriate * dispatching of {@link #canConvert(ConversionRequest)} and * {@link #convert(ConversionRequest)} calls based on the actual state of the * given {@link ConversionRequest}. - *

    - * Note that the {@link #supports(ConversionRequest)} method is overridden as - * well, to delegate to the appropriate {@link #canConvert}. - *

    - *

    - * NB: by default, the {@link #populateInputCandidates(Collection)} method has a - * dummy implementation. Effectively, this is opt-in behavior. If a converter - * implementation would like to suggest candidates for conversion, this method - * can be overridden. - *

    - *

    - * NB: by default, the provied {@link #canConvert} methods will return - * {@code false} if the input is {@code null}. This allows {@link Converter} - * implementors to assume any input is non-{@code null} - but this behavior is - * overridden. Casting {@code null Object} inputs is handled by the - * {@link NullConverter}, while {@code null class} inputs are handled by the - * {@link DefaultConverter}. - *

    * * @author Mark Hiner */ @@ -72,63 +49,14 @@ public abstract class AbstractConverter extends // -- Parameters -- - @Parameter + @Parameter(required = false) private ObjectService objectService; - // -- ConversionHandler methods -- - - @Override - public boolean canConvert(final ConversionRequest request) { - Object src = request.sourceObject(); - if (src == null) { - Class srcClass = request.sourceClass(); - if (request.destType() != null) return canConvert(srcClass, request.destType()); - return canConvert(srcClass, request.destClass()); - } - - if (request.destType() != null) return canConvert(src, request.destType()); - return canConvert(src, request.destClass()); - } - - @Override - public boolean canConvert(final Object src, final Type dest) { - if (src == null) return false; - final Class srcClass = src.getClass(); - return canConvert(srcClass, dest); - } - - @Override - public boolean canConvert(final Object src, final Class dest) { - if (src == null) return false; - final Class srcClass = src.getClass(); - - return canConvert(srcClass, dest); - } - - @Override - public boolean canConvert(final Class src, final Class dest) { - if (src == null) return false; - return ConversionUtils.canCast(src, getInputType()) && - ConversionUtils.canCast(getOutputType(), dest); - } - - @Override - public Object convert(final Object src, final Type dest) { - final Class destClass = GenericUtils.getClass(dest); - return convert(src, destClass); - } - - @Override - public Object convert(final ConversionRequest request) { - if (request.destType() != null) { - return convert(request.sourceObject(), request.destType()); - } - - return convert(request.sourceObject(), request.destClass()); - } + // -- Converter methods -- @Override public void populateInputCandidates(final Collection objects) { + if (objectService == null) return; for (final Object candidate : objectService.getObjects(getInputType())) { if (canConvert(candidate, getOutputType())) objects.add(candidate); } @@ -138,20 +66,8 @@ public void populateInputCandidates(final Collection objects) { @Override public boolean supports(final ConversionRequest request) { - return canConvert(request); - } - - @Override - public Class getType() { - return ConversionRequest.class; - } - - // -- Deprecated API -- - - @Override - @Deprecated - public boolean canConvert(final Class src, final Type dest) { - final Class destClass = GenericUtils.getClass(dest); - return canConvert(src, destClass); + // NB: Overridden just for backwards compatibility, so that + // downstream classes which call super.supports do the right thing. + return Converter.super.supports(request); } } diff --git a/src/main/java/org/scijava/convert/AbstractDelegateConverter.java b/src/main/java/org/scijava/convert/AbstractDelegateConverter.java new file mode 100644 index 000000000..97b8b23ea --- /dev/null +++ b/src/main/java/org/scijava/convert/AbstractDelegateConverter.java @@ -0,0 +1,57 @@ +/*- + * #%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.convert; + +import org.scijava.plugin.Parameter; + +/** + * Abstract superclass for {@link Converter} plugins that delegate to other + * converters to chain two conversion steps together. + * + * @author Jan Eglinger + * @param the input type + * @param the delegate type + * @param the output type + */ +public abstract class AbstractDelegateConverter extends + AbstractConverter +{ + + @Parameter + private ConvertService convertService; + + @Override + public T convert(Object src, Class dest) { + D delegate = convertService.convert(src, getDelegateType()); + return convertService.convert(delegate, dest); + } + + protected abstract Class getDelegateType(); +} diff --git a/src/main/java/org/scijava/convert/ArrayConverters.java b/src/main/java/org/scijava/convert/ArrayConverters.java index 7971c0872..0c16551a0 100644 --- a/src/main/java/org/scijava/convert/ArrayConverters.java +++ b/src/main/java/org/scijava/convert/ArrayConverters.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,7 +51,7 @@ public class ArrayConverters { // -- Integer array converters -- - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class IntArrayWrapper extends PrimitiveArrayWrapper { @@ -69,7 +67,7 @@ public Class getInputType() { } } - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class IntArrayUnwrapper extends PrimitiveArrayUnwrapper { @@ -87,7 +85,7 @@ public Class getInputType() { // -- Byte array converters -- - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class ByteArrayWrapper extends PrimitiveArrayWrapper { @@ -103,7 +101,7 @@ public Class getInputType() { } } - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class ByteArrayUnwrapper extends PrimitiveArrayUnwrapper { @@ -121,7 +119,7 @@ public Class getInputType() { // -- Bool array converters -- - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class BoolArrayWrapper extends PrimitiveArrayWrapper { @@ -137,7 +135,7 @@ public Class getInputType() { } } - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class BoolArrayUnwrapper extends PrimitiveArrayUnwrapper { @@ -155,7 +153,7 @@ public Class getInputType() { // -- Char array converters -- - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class CharArrayWrapper extends PrimitiveArrayWrapper { @@ -171,7 +169,7 @@ public Class getInputType() { } } - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class CharArrayUnwrapper extends PrimitiveArrayUnwrapper { @@ -189,7 +187,7 @@ public Class getInputType() { // -- Short array converters -- - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class ShortArrayWrapper extends PrimitiveArrayWrapper { @@ -205,7 +203,7 @@ public Class getInputType() { } } - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class ShortArrayUnwrapper extends PrimitiveArrayUnwrapper { @@ -223,7 +221,7 @@ public Class getInputType() { // -- Float array converters -- - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class FloatArrayWrapper extends PrimitiveArrayWrapper { @@ -239,7 +237,7 @@ public Class getInputType() { } } - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class FloatArrayUnwrapper extends PrimitiveArrayUnwrapper { @@ -257,7 +255,7 @@ public Class getInputType() { // -- Double array converters -- - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class DoubleArrayWrapper extends PrimitiveArrayWrapper { @@ -273,7 +271,7 @@ public Class getInputType() { } } - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class DoubleArrayUnwrapper extends PrimitiveArrayUnwrapper { @@ -291,7 +289,7 @@ public Class getInputType() { // -- Long array converters -- - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class LongArrayWrapper extends PrimitiveArrayWrapper { @@ -307,7 +305,7 @@ public Class getInputType() { } } - @Plugin(type = Converter.class, priority = Priority.HIGH_PRIORITY) + @Plugin(type = Converter.class, priority = Priority.HIGH) public static class LongArrayUnwrapper extends PrimitiveArrayUnwrapper { diff --git a/src/main/java/org/scijava/convert/ArrayToStringConverter.java b/src/main/java/org/scijava/convert/ArrayToStringConverter.java new file mode 100644 index 000000000..e1e3ce96b --- /dev/null +++ b/src/main/java/org/scijava/convert/ArrayToStringConverter.java @@ -0,0 +1,128 @@ +/* + * #%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.convert; + +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.util.stream.Collectors; + +import org.scijava.Priority; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.util.ArrayUtils; + +/** + * A {@link Converter} that specializes in converting n-dimensional arrays into + * {@link String}s. This {@link Converter} can convert any array whose component + * types can be converted into {@link String}s. By default, this + * {@link Converter} delimits the array elements with commas. + * + * @author Gabriel Selzer + */ +@Plugin(type = Converter.class, priority = Priority.VERY_LOW) +public class ArrayToStringConverter extends AbstractConverter { + + @Parameter(required = false) + private ConvertService convertService; + + @Override + public boolean canConvert(final Class src, final Class dest) { + return src != null && src.isArray() && dest == String.class; + } + + @Override + public boolean canConvert(final Object src, final Class dest) { + if (convertService == null || src == null) return false; + if (!canConvert(src.getClass(), dest)) return false; + if (Array.getLength(src) == 0) return true; + return convertService.supports(Array.get(src, 0), dest); + } + + @Override + public Object convert(Object src, final Type dest) { + // Preprocess the "string-likes" + final Class srcClass = src.getClass(); + if (srcClass == String[].class || // + srcClass == Character[].class || // + srcClass == char[].class) // + { + src = preprocessCharacters(src); + } + // Convert each element to Strings + final String elementString = ArrayUtils.toCollection(src).stream() // + .map(object -> convertService.convert(object, String.class)) // + .collect(Collectors.joining(", ")); + return "{" + elementString + "}"; + } + + private String[] preprocessStrings(final Object src) { + final int numElements = Array.getLength(src); + final String[] processed = new String[numElements]; + for (int i = 0; i < numElements; i++) { + processed[i] = preprocessString(Array.get(src, i)); + } + return processed; + } + + private String preprocessString(final Object o) { + if (o == null) return null; + String s = o.toString(); + s = s.replace("\\", "\\\\"); + s = s.replace("\"", "\\\""); + return "\"" + s + "\""; + } + + private String[] preprocessCharacters(Object src) { + final String[] processed = new String[Array.getLength(src)]; + for (int i = 0; i < processed.length; i++) { + final Object value = Array.get(src, i); + processed[i] = value == null ? null : value.toString(); + } + return preprocessStrings(processed); + } + + @Override + public T convert(final Object src, final Class dest) { + final Type destType = dest; + @SuppressWarnings("unchecked") + final T converted = (T) convert(src, destType); + return converted; + } + + @Override + public Class getOutputType() { + return String.class; + } + + @Override + public Class getInputType() { + return Object.class; + } +} diff --git a/src/main/java/org/scijava/convert/CastingConverter.java b/src/main/java/org/scijava/convert/CastingConverter.java new file mode 100644 index 000000000..3b5a4e91f --- /dev/null +++ b/src/main/java/org/scijava/convert/CastingConverter.java @@ -0,0 +1,92 @@ +/* + * #%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.convert; + +import java.lang.reflect.Type; + +import org.scijava.Priority; +import org.scijava.plugin.Plugin; +import org.scijava.util.Types; + +/** + * Minimal {@link Converter} implementation to do direct casting. + * + * @author Mark Hiner + */ +@Plugin(type = Converter.class, priority = Priority.EXTREMELY_HIGH - 1) +public class CastingConverter extends AbstractConverter { + + @Override + public boolean canConvert(final Object src, final Class dest) { + return Types.isInstance(src, dest); + } + + @Override + public boolean canConvert(final Class src, final Type dest) { + // NB: You might think we want to use Types.isAssignable(src, dest) + // directly here. And you might be right. However, assignment involving + // generic types gets very tricky. If dest is e.g. a wildcard type such as + // "? extends Object", or a type variable such as "C extends Object", then + // no specific class will be assignable to it, because for that ? or C we + // do not know anything about the bound type other than that it's something + // that extends Object, so it could be anything, including things that + // aren't assignable from whatever src is. + // + // Unfortunately, when this casting conversion code was originally written, + // it did not have generics in mind, and calling code will pass in capture + // types (e.g. Type objects gleaned via ModuleItem#getGenericType()) + // expecting them to be convertible as long as dest's raw type(s) are + // compatible targets for src. + // + // And so for backwards compatibility, we continue to behave that way here. + + return dest != null && // + Types.raws(dest).stream().allMatch(c -> c.isAssignableFrom(src)); + } + + @Override + public boolean canConvert(final Class src, final Class dest) { + return dest != null && dest.isAssignableFrom(src); + } + + @Override + public T convert(final Object src, final Class dest) { + return Types.cast(src, dest); + } + + @Override + public Class getOutputType() { + return Object.class; + } + + @Override + public Class getInputType() { + return Object.class; + } +} diff --git a/src/main/java/org/scijava/convert/ConversionRequest.java b/src/main/java/org/scijava/convert/ConversionRequest.java index 4ca2b0fdd..c4ee0fd2b 100644 --- a/src/main/java/org/scijava/convert/ConversionRequest.java +++ b/src/main/java/org/scijava/convert/ConversionRequest.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,29 +31,27 @@ import java.lang.reflect.Type; -import org.scijava.util.GenericUtils; +import org.scijava.plugin.HandlerService; +import org.scijava.util.Types; /** - * Currency for use in {@link Converter} and {@link ConvertService} - * methods. + * Currency for use in {@link Converter} and {@link ConvertService} methods. *

    * {@link #ConversionRequest} provides a variety of constructors. Note that only * one destination type needs to be set (e.g. either a {@link Type} or a * {@link Class}). *

    *

    - * Only the {@link Class} source needs to be set for {@link Converter} - * lookup, such as through - * {@link ConvertService#getHandler(ConversionRequest)}. However, to perform - * an actual conversion, e.g. using + * Only the {@link Class} source needs to be set for {@link Converter} lookup, + * such as through {@link HandlerService#getHandler(Object)}. However, to + * perform an actual conversion, e.g. using * {@link Converter#convert(ConversionRequest)}, you must provide an * {@link Object} source. *

    *

    - * NB: once a {@link Converter} has been acquired, the - * {@code ConversionRequest} used for lookup can be reused to cast to the same - * destination type, simply by updating the source object using the - * {@link #setSourceObject(Object)} method. + * NB: once a {@link Converter} has been acquired, the {@code ConversionRequest} + * used for lookup can be reused to cast to the same destination type, simply by + * updating the source object using the {@link #setSourceObject(Object)} method. *

    * * @author Mark Hiner @@ -101,7 +97,7 @@ public Type sourceType() { * @return Source class for conversion or lookup. */ public Class sourceClass() { - return GenericUtils.getClass(srcType); + return Types.raw(srcType); } /** @@ -122,7 +118,7 @@ public Type destType() { * @return Destination class for conversion. */ public Class destClass() { - return GenericUtils.getClass(destType); + return Types.raw(destType); } // -- Setters -- diff --git a/src/main/java/org/scijava/convert/ConvertService.java b/src/main/java/org/scijava/convert/ConvertService.java index 3feec3e81..ff3309baf 100644 --- a/src/main/java/org/scijava/convert/ConvertService.java +++ b/src/main/java/org/scijava/convert/ConvertService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,85 +31,171 @@ 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.HandlerService; +import org.scijava.service.SciJavaService; /** - * Service for converting between types using an extensible plugin: - * {@link Converter}. Contains convenience signatures for the - * {@link #getHandler} and {@link #supports} methods to avoid the need to create + * Service for converting between types using an {@link Converter} plugins. + * Contains convenience signatures for the {@link #getHandler} and + * {@link #supports} methods to avoid the need to create * {@link ConversionRequest} objects. * * @see ConversionRequest * @author Mark Hiner */ public interface ConvertService extends - HandlerService> + HandlerService>, SciJavaService { + /** * @see Converter#convert(Object, Type) */ - Object convert(Object src, Type dest); + default Object convert(final Object src, final Type dest) { + return convert(new ConversionRequest(src, dest)); + } /** * @see Converter#convert(Object, Class) */ - T convert(Object src, Class dest); + default T convert(final Object src, final Class dest) { + // NB: repeated code with convert(ConversionRequest), because the + // handler's convert method respects the T provided + final Converter handler = getHandler(src, dest); + return handler == null ? null : handler.convert(src, dest); + } /** * @see Converter#convert(ConversionRequest) */ - Object convert(ConversionRequest request); + default Object convert(final ConversionRequest request) { + final Converter handler = getHandler(request); + return handler == null ? null : handler.convert(request); + } + + /** + * @see HandlerService#supports(Object) + */ + default Converter getHandler(final Object src, final Type dest) { + return getHandler(new ConversionRequest(src, dest)); + } /** - * @see #getHandler(ConversionRequest) + * @see HandlerService#supports(Object) */ - Converter getHandler(Object src, Class dest); + default Converter getHandler(final Object src, final Class dest) { + return getHandler(new ConversionRequest(src, dest)); + } /** - * @see #getHandler(ConversionRequest) + * @see HandlerService#getHandler(Object) */ - Converter getHandler(Object src, Type dest); + default Converter getHandler(final Class src, final Type dest) { + return getHandler(new ConversionRequest(src, dest)); + } /** - * @see #supports(ConversionRequest) + * @see HandlerService#getHandler(Object) */ - boolean supports(Object src, Class dest); + default Converter getHandler(final Class src, final Class dest) { + return getHandler(new ConversionRequest(src, dest)); + } /** - * @see #supports(ConversionRequest) + * @see HandlerService#supports(Object) */ - boolean supports(Object src, Type dest); + default boolean supports(final Object src, final Type dest) { + return supports(new ConversionRequest(src, dest)); + } - // -- Deprecated API -- + /** + * @see HandlerService#supports(Object) + */ + default boolean supports(final Object src, final Class dest) { + return supports(new ConversionRequest(src, dest)); + } /** - * @see #getHandler(ConversionRequest) - * @deprecated Use {@link #getHandler(Object, Class)} + * @see HandlerService#supports(Object) */ - @Deprecated - Converter getHandler(Class src, Class dest); + default boolean supports(final Class src, final Type dest) { + return supports(new ConversionRequest(src, dest)); + } /** - * @see #getHandler(ConversionRequest) - * @deprecated Use {@link #getHandler(Object, Type)} + * @see HandlerService#supports(Object) */ - @Deprecated - Converter getHandler(Class src, Type dest); + default boolean supports(final Class src, final Class dest) { + return supports(new ConversionRequest(src, dest)); + } /** - * @see #supports(ConversionRequest) - * @deprecated Use {@link #supports(Object, Class)} + * @return A collection of instances that could be converted to the + * specified class. */ - @Deprecated - boolean supports(Class src, Class dest); + default Collection getCompatibleInputs(final Class dest) { + final Set objects = new LinkedHashSet<>(); + + for (final Converter c : getInstances()) { + if (dest.isAssignableFrom(c.getOutputType())) { + c.populateInputCandidates(objects); + } + } + + return objects; + } /** - * @see #supports(ConversionRequest) - * @deprecated Use {@link #supports(Object, Type)} + * @return A collection of all classes that could potentially be converted + * to the specified class. */ - @Deprecated - boolean supports(Class src, Type dest); + default Collection> getCompatibleInputClasses(final Class dest) { + final Set> compatibleClasses = new HashSet<>(); + + for (final Converter converter : getInstances()) { + if (dest == converter.getOutputType()) // + compatibleClasses.add(converter.getInputType()); + } - Collection getCompatibleInputs(Class dest); + return compatibleClasses; + } + + /** + * @return A collection of all classes that could potentially be converted + * from the specified class. + */ + default Collection> getCompatibleOutputClasses(final Class source) { + final Set> compatibleClasses = new HashSet<>(); + + for (final Converter converter : getInstances()) { + try { + if (source == converter.getInputType()) // + compatibleClasses.add(converter.getOutputType()); + } + catch (final Throwable t) { + log().error("Malfunctioning converter plugin: " + // + converter.getClass().getName(), t); + } + } + + return compatibleClasses; + } + + // -- PTService methods -- + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + default Class> getPluginType() { + return (Class) Converter.class; + } + + // -- Typed methods -- + + @Override + default Class getType() { + return ConversionRequest.class; + } } diff --git a/src/main/java/org/scijava/convert/Converter.java b/src/main/java/org/scijava/convert/Converter.java index 2ad015dfe..1578a96dd 100644 --- a/src/main/java/org/scijava/convert/Converter.java +++ b/src/main/java/org/scijava/convert/Converter.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,15 +32,24 @@ import java.lang.reflect.Type; import java.util.Collection; import java.util.List; +import java.util.Queue; import java.util.Set; import org.scijava.object.ObjectService; import org.scijava.plugin.HandlerPlugin; import org.scijava.plugin.Plugin; +import org.scijava.util.Types; /** * Extensible conversion {@link Plugin} for converting between classes and * types. + *

    + * NB: by default, the provided {@link #canConvert} methods will return + * {@code false} if the input is {@code null}. This allows {@link Converter} + * implementors to assume any input is non-{@code null}. Casting + * {@code null Object} inputs is handled by the {@link NullConverter}, while + * {@code null} class inputs are handled by the {@link DefaultConverter}. + *

    * * @see ConversionRequest * @author Mark Hiner @@ -50,49 +57,114 @@ public interface Converter extends HandlerPlugin { /** - * Checks whether a given {@ConversionRequest} can be - * processed, by converting the desired - * {@link ConversionRequest#sourceClass()} to its + * Checks whether a given {@link ConversionRequest} can be processed, by + * converting the desired {@link ConversionRequest#sourceClass()} to its * {@link ConversionRequest#destClass()} or * {@link ConversionRequest#destType()}. * * @see #convert(ConversionRequest) */ - boolean canConvert(ConversionRequest request); + default boolean canConvert(final ConversionRequest request) { + if (request == null) return false; + final Object src = request.sourceObject(); + final Type destType = request.destType(); + if (src != null && destType != null) { + return canConvert(src, destType); + } + if (src != null) { + return canConvert(src, request.destClass()); + } + if (destType != null) { + return canConvert(request.sourceClass(), destType); + } + return canConvert(request.sourceClass(), request.destClass()); + } /** * Checks whether the given object's type can be converted to the specified * type. + * + * @see #convert(Object, Type) + */ + default boolean canConvert(final Object src, final Type dest) { + if (src == null || dest == null) return false; + return canConvert(src.getClass(), dest); + } + + /** + * Checks whether the given object's type can be converted to the specified + * type. + * + * @see #convert(Object, Class) + */ + default boolean canConvert(final Object src, final Class dest) { + if (src == null) return false; + Class srcClass = src.getClass(); + return canConvert(srcClass, dest); + } + + /** + * Checks whether objects of the given class can be converted to the specified + * type. *

    * Note that this does not necessarily entail that - * {@link #convert(Object, Type)} on that specific object will succeed. For - * example: {@code canConvert("5.1", int.class)} will return {@code true} - * because a {@link String} can in general be converted to an {@code int}, but - * calling {@code convert("5.1", int.class)} will throw a + * {@link #convert(Object, Type)} on a specific object of the given source + * class will succeed. For example: + * {@code canConvert(String.class, List)} will return {@code true} + * because a {@link String} can in general be converted to an {@code Integer} + * and then wrapped into a {@code List}, but calling + * {@code convert("5.1", List)} will throw a * {@link NumberFormatException} when the conversion is actually attempted via * the {@link Integer#Integer(String)} constructor. *

    - * + * * @see #convert(Object, Type) */ - boolean canConvert(Object src, Type dest); + default boolean canConvert(final Class src, final Type dest) { + final Class destClass = Types.raw(dest); + return canConvert(src, destClass); + } /** - * Checks whether the given object's type can be converted to the specified + * Checks whether objects of the given class can be converted to the specified * type. *

    * Note that this does not necessarily entail that - * {@link #convert(Object, Class)} on that specific object will succeed. For - * example: {@code canConvert("5.1", int.class)} will return {@code true} + * {@link #convert(Object, Class)} on a specific object of the given source + * class will succeed. For example: + * {@code canConvert(String.class, int.class)} will return {@code true} * because a {@link String} can in general be converted to an {@code int}, but * calling {@code convert("5.1", int.class)} will throw a * {@link NumberFormatException} when the conversion is actually attempted via * the {@link Integer#Integer(String)} constructor. *

    + * + * @see #convert(Object, Class) + */ + default boolean canConvert(final Class src, final Class dest) { + if (src == null) return false; + final Class saneSrc = Types.box(src); + final Class saneDest = Types.box(dest); + return Types.isAssignable(saneSrc, getInputType()) && // + Types.isAssignable(getOutputType(), saneDest); + } + + /** + * Converts the given {@link ConversionRequest#sourceObject()} to the + * specified {@link ConversionRequest#destClass()} or + * {@link ConversionRequest#destType()}. * * @see #convert(Object, Class) + * @see #convert(Object, Type) + * @param request {@link ConversionRequest} to process. + * @return The conversion output */ - boolean canConvert(Object src, Class dest); + default Object convert(final ConversionRequest request) { + if (request.destType() != null) { + return convert(request.sourceObject(), request.destType()); + } + return convert(request.sourceObject(), request.destClass()); + } /** * As {@link #convert(Object, Class)} but capable of creating and populating @@ -105,14 +177,17 @@ public interface Converter extends HandlerPlugin { *

    * NB: This method should be capable of creating any array type, but if a * {@link Collection} interface or abstract class is provided we can only make - * a best guess as to what container type to instantiate. Defaults are - * provided for {@link Set} and {@link List} subclasses. + * a best guess as to what container type to instantiate; defaults are + * provided for {@link Set}, {@link Queue}, and {@link List}. *

    * * @param src The object to convert. * @param dest Type to which the object should be converted. */ - Object convert(Object src, Type dest); + default Object convert(final Object src, final Type dest) { + final Class destClass = Types.raw(dest); + return convert(src, destClass); + } /** * Converts the given object to an object of the specified type. The object is @@ -129,18 +204,6 @@ public interface Converter extends HandlerPlugin { */ T convert(Object src, Class dest); - /** - * Converts the given {@link ConversionRequest#sourceObject()} to the - * specified {@link ConversionRequest#destClass()} or - * {@link ConversionRequest#destType()}. - * - * @see #convert(Object, Class) - * @see #convert(Object, Type) - * @param request {@link ConversionRequest} to process. - * @return The conversion output - */ - Object convert(ConversionRequest request); - /** * Populates the given collection with objects which are known to exist, and * which are usable as inputs for this converter. @@ -173,25 +236,15 @@ public interface Converter extends HandlerPlugin { */ Class getInputType(); - // -- Deprecated API -- + // -- Typed methods -- - /** - * Checks whether objects of the given class can be converted to the specified - * type. - * - * @see #convert(Object, Type) - * @deprecated Use {@link #canConvert(Object, Type)} - */ - @Deprecated - boolean canConvert(Class src, Type dest); + @Override + default boolean supports(final ConversionRequest request) { + return canConvert(request); + } - /** - * Checks whether objects of the given class can be converted to the specified - * type. - * - * @see #convert(Object, Class) - * @deprecated Use {@link #canConvert(Object, Class)} - */ - @Deprecated - boolean canConvert(Class src, Class dest); + @Override + default Class getType() { + return ConversionRequest.class; + } } diff --git a/src/main/java/org/scijava/convert/DefaultConvertService.java b/src/main/java/org/scijava/convert/DefaultConvertService.java index b28ce6383..2ddd292ad 100644 --- a/src/main/java/org/scijava/convert/DefaultConvertService.java +++ b/src/main/java/org/scijava/convert/DefaultConvertService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,12 +29,6 @@ package org.scijava.convert; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Set; - import org.scijava.plugin.Plugin; import org.scijava.service.Service; @@ -48,70 +40,5 @@ @Plugin(type = Service.class) public class DefaultConvertService extends AbstractConvertService { - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public Class> getPluginType() { - return (Class)Converter.class; - } - - @Override - public Class getType() { - return ConversionRequest.class; - } - - // -- ConversionService methods -- - - @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 getCompatibleInputs(Class dest) { - Set objects = new LinkedHashSet(); - - for (final Converter c : getInstances()) { - if (dest.isAssignableFrom(c.getOutputType())) { - c.populateInputCandidates(objects); - } - } - - return new ArrayList(objects); - } + // NB: No implementation needed. } diff --git a/src/main/java/org/scijava/convert/DefaultConverter.java b/src/main/java/org/scijava/convert/DefaultConverter.java index c86a4f28e..c99c5a889 100644 --- a/src/main/java/org/scijava/convert/DefaultConverter.java +++ b/src/main/java/org/scijava/convert/DefaultConverter.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,118 +31,91 @@ import java.lang.reflect.Array; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; +import java.util.Deque; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Queue; import java.util.Set; +import org.scijava.Priority; import org.scijava.plugin.Plugin; import org.scijava.util.ArrayUtils; -import org.scijava.util.ClassUtils; -import org.scijava.util.ConversionUtils; -import org.scijava.util.GenericUtils; +import org.scijava.util.Types; /** * Default {@link Converter} implementation. Provides useful conversion * functionality for many common conversion cases. + *

    + * Supported conversions include: + *

    + *
      + *
    • Object to Array
    • + *
    • Object to Collection
    • + *
    • Number to Number
    • + *
    • Object to String
    • + *
    • String to Character
    • + *
    • String to Enum
    • + *
    • Objects where the destination Class has a constructor which takes that + * Object + *
    • + *
    * * @author Mark Hiner */ -@Plugin(type = Converter.class) +@Plugin(type = Converter.class, priority = Priority.EXTREMELY_LOW) public class DefaultConverter extends AbstractConverter { // -- ConversionHandler methods -- @Override public Object convert(final Object src, final Type dest) { - // NB: Regardless of whether the destination type is an array or collection, - // we still want to cast directly if doing so is possible. But note that in - // general, this check does not detect cases of incompatible generic - // parameter types. If this limitation becomes a problem in the future we - // can extend the logic here to provide additional signatures of canCast - // which operate on Types in general rather than only Classes. However, the - // logic could become complex very quickly in various subclassing cases, - // generic parameters resolved vs. propagated, etc. - final Class c = GenericUtils.getClass(dest); - if (c != null && ConversionUtils.canCast(src, c)) return ConversionUtils - .cast(src, c); + // special case: CharSequence -> char[] + // otherwise, String -> char[] ends up length 1 with first char only + if (src instanceof CharSequence && dest == char[].class) { + return ((CharSequence) src).toString().toCharArray(); + } // Handle array types, including generic array types. - if (isArray(dest)) { - return convertToArray(src, GenericUtils.getComponentClass(dest)); + final Type componentType = Types.component(dest); + if (componentType != null) { + // NB: Destination is an array type. + return convertToArray(src, Types.raw(componentType)); } - // Handle parameterized collection types. - if (dest instanceof ParameterizedType && isCollection(dest)) { - return convertToCollection(src, (ParameterizedType) dest); + // Handle collection types, either raw or parameterized. + Class cClass = collectionClass(dest); + if (cClass != null) { + Type elementType = Types.param(dest, Collection.class, 0); + if (elementType == null) elementType = Object.class; // raw collection + final Object collection = convertToCollection(src, cClass, elementType); + if (collection != null) return collection; + // NB: If this conversion failed, it might still succeed later + // when looking for a wrapping constructor. So let's keep going. + // In particular, see ConvertServiceTest#testConvertSubclass(). } - // This wasn't a collection or array, so convert it as a single element. - return convert(src, GenericUtils.getClass(dest)); - } - - @Override - public T convert(final Object src, final Class dest) { - if (dest == null) return null; - if (src == null) return ConversionUtils.getNullValue(dest); + // Ensure type is a well-behaved class, rather than a primitive type. + final Class destClass = Types.raw(dest); + final Class saneDest = Types.box(destClass); - // ensure type is well-behaved, rather than a primitive type - final Class saneDest = ConversionUtils.getNonprimitiveType(dest); - - // cast the existing object, if possible - if (ConversionUtils.canCast(src, saneDest)) return ConversionUtils.cast( - src, saneDest); - - // Handle array types - if (isArray(dest)) { - @SuppressWarnings("unchecked") - T array = (T) convertToArray(src, GenericUtils.getComponentClass(dest)); - return array; - } + // Object is already the requested type. + if (Types.isInstance(src, saneDest)) return src; // special case for conversion from number to number if (src instanceof Number) { final Number number = (Number) src; - if (saneDest == Byte.class) { - final Byte result = number.byteValue(); - @SuppressWarnings("unchecked") - final T typedResult = (T) result; - return typedResult; - } - if (saneDest == Double.class) { - final Double result = number.doubleValue(); - @SuppressWarnings("unchecked") - final T typedResult = (T) result; - return typedResult; - } - if (saneDest == Float.class) { - final Float result = number.floatValue(); - @SuppressWarnings("unchecked") - final T typedResult = (T) result; - return typedResult; - } - if (saneDest == Integer.class) { - final Integer result = number.intValue(); - @SuppressWarnings("unchecked") - final T typedResult = (T) result; - return typedResult; - } - if (saneDest == Long.class) { - final Long result = number.longValue(); - @SuppressWarnings("unchecked") - final T typedResult = (T) result; - return typedResult; - } - if (saneDest == Short.class) { - final Short result = number.shortValue(); - @SuppressWarnings("unchecked") - final T typedResult = (T) result; - return typedResult; - } + if (saneDest == Byte.class) return number.byteValue(); + if (saneDest == Double.class) return number.doubleValue(); + if (saneDest == Float.class) return number.floatValue(); + if (saneDest == Integer.class) return number.intValue(); + if (saneDest == Long.class) return number.longValue(); + if (saneDest == Short.class) return number.shortValue(); } // special cases for strings @@ -153,46 +124,55 @@ public T convert(final Object src, final Class dest) { final String s = (String) src; if (s.isEmpty()) { // return null for empty strings - return ConversionUtils.getNullValue(dest); + return Types.nullValue(saneDest); } // use first character when converting to Character if (saneDest == Character.class) { - final Character c = new Character(s.charAt(0)); - @SuppressWarnings("unchecked") - final T result = (T) c; - return result; + return new Character(s.charAt(0)); } // special case for conversion to enum - if (dest.isEnum()) { - final T result = ConversionUtils.convertToEnum(s, dest); - if (result != null) return result; + if (saneDest.isEnum()) { + try { + return Types.enumFromString(s, saneDest); + } + catch (final IllegalArgumentException exc) { + // NB: No action needed. + } } } + if (saneDest == String.class) { // destination type is String; use Object.toString() method - final String sValue = src.toString(); - @SuppressWarnings("unchecked") - final T result = (T) sValue; - return result; + return src.toString(); } // wrap the original object with one of the new type, using a constructor try { final Constructor ctor = getConstructor(saneDest, src.getClass()); if (ctor == null) return null; - @SuppressWarnings("unchecked") - final T instance = (T) ctor.newInstance(src); - return instance; + return ctor.newInstance(src); } - catch (final Exception exc) { - // TODO: Best not to catch blanket Exceptions here. + catch (final InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException exc) + { // no known way to convert - return null; + return Types.nullValue(destClass); } } + @Override + public T convert(final Object src, final Class dest) { + // NB: Invert functional flow from Converter interface: + // Converter: convert(Class, Type) calling convert(Class, Class) + // becomes: convert(Class, Class) calling convert(Class, Type) + final Type destType = dest; + @SuppressWarnings("unchecked") + final T result = (T) convert(src, destType); + return result; + } + @Override public Class getOutputType() { return Object.class; @@ -210,7 +190,9 @@ private Constructor getConstructor(final Class type, { for (final Constructor ctor : type.getConstructors()) { final Class[] params = ctor.getParameterTypes(); - if (params.length == 1 && ConversionUtils.canCast(argType, params[0])) { + if (params.length == 1 && // + Types.isAssignable(Types.box(argType), Types.box(params[0]))) + { return ctor; } } @@ -218,12 +200,13 @@ private Constructor getConstructor(final Class type, } private boolean isArray(final Type type) { - return GenericUtils.getComponentClass(type) != null; + return Types.component(type) != null; } - private boolean isCollection(final Type type) { - return ConversionUtils.canCast(GenericUtils.getClass(type), - Collection.class); + private Class collectionClass(final Type type) { + return Types.raws(type).stream() // + .filter(t -> Types.isAssignable(t, Collection.class)) // + .findFirst().orElse(null); } private Object @@ -247,35 +230,32 @@ private boolean isCollection(final Type type) { } private Object convertToCollection(final Object value, - final ParameterizedType pType) + final Class collectionType, final Type elementType) { - final Collection collection = - createCollection(GenericUtils.getClass(pType)); + final Collection collection = createCollection(collectionType); if (collection == null) return null; // Populate the collection. final Collection items = ArrayUtils.toCollection(value); - // TODO: The following can fail; e.g. "Foo extends ArrayList" - final Type collectionType = pType.getActualTypeArguments()[0]; for (final Object item : items) { - collection.add(convert(item, collectionType)); + collection.add(convert(item, elementType)); } return collection; } - private Collection createCollection(final Class type) { - // If we were given an interface or abstract class, and not a concrete - // class, we attempt to make default implementations. - if (type.isInterface() || Modifier.isAbstract(type.getModifiers())) { - // We don't have a concrete class. If it's a set or a list, we use - // the typical default implementation. Otherwise we won't convert. - if (ConversionUtils.canCast(type, List.class)) return new ArrayList(); - if (ConversionUtils.canCast(type, Set.class)) return new HashSet(); + private Collection createCollection(Class type) { + // Support conversion to common collection interface types. + if (type == Queue.class || type == Deque.class) type = ArrayDeque.class; + else if (type == Set.class) type = LinkedHashSet.class; + else if (type == List.class || type == Collection.class) type = ArrayList.class; + else if (type.isInterface() || Modifier.isAbstract(type.getModifiers())) { + // We were given an interface or abstract class, and not a concrete + // class, and we don't know what default implementation to use. return null; } - // Got a concrete type. Instantiate it. + // We now have a concrete type. Instantiate it. try { @SuppressWarnings("unchecked") final Collection c = (Collection) type.newInstance(); @@ -292,62 +272,37 @@ private Collection createCollection(final Class type) { // -- Deprecated API -- @Override - @Deprecated - public boolean canConvert(final Class src, final Type dest) { - - // Handle array types, including generic array types. - if (isArray(dest)) return true; - - // Handle parameterized collection types. - if (dest instanceof ParameterizedType && isCollection(dest)) { - return createCollection(GenericUtils.getClass(dest)) != null; - } - - return super.canConvert(src, dest); - } - - @Override - @Deprecated public boolean canConvert(final Class src, final Class dest) { - - if (src == null || dest == null) return true; + // OK for array and collection types. + if (isArray(dest)) return true; + Class cClass = collectionClass(dest); + if (cClass != null && createCollection(cClass) != null) return true; // ensure type is well-behaved, rather than a primitive type - final Class saneDest = ConversionUtils.getNonprimitiveType(dest); - - // OK if the existing object can be casted - if (ConversionUtils.canCast(src, saneDest)) return true; - + final Class saneDest = Types.box(dest); + // OK for numerical conversions - if (ConversionUtils.canCast(ConversionUtils.getNonprimitiveType(src), - Number.class) && - (ClassUtils.isByte(dest) || ClassUtils.isDouble(dest) || - ClassUtils.isFloat(dest) || ClassUtils.isInteger(dest) || - ClassUtils.isLong(dest) || ClassUtils.isShort(dest))) + if (Types.isAssignable(Types.box(src), Number.class) && // + (Types.isByte(saneDest) || Types.isDouble(saneDest) || // + Types.isFloat(saneDest) || Types.isInteger(saneDest) || // + Types.isLong(saneDest) || Types.isShort(saneDest))) { return true; } - + // OK if string if (saneDest == String.class) return true; - - if (ConversionUtils.canCast(src, String.class)) { + + if (Types.isAssignable(src, String.class)) { // OK if source type is string and destination type is character // (in this case, the first character of the string would be used) if (saneDest == Character.class) return true; - + // OK if source type is string and destination type is an enum if (dest.isEnum()) return true; } - + // OK if appropriate wrapper constructor exists - try { - return getConstructor(saneDest, src) != null; - } - catch (final Exception exc) { - // TODO: Best not to catch blanket Exceptions here. - // no known way to convert - return false; - } + return getConstructor(saneDest, src) != null; } } diff --git a/src/main/java/org/scijava/convert/FileListConverters.java b/src/main/java/org/scijava/convert/FileListConverters.java new file mode 100644 index 000000000..8b5d86b73 --- /dev/null +++ b/src/main/java/org/scijava/convert/FileListConverters.java @@ -0,0 +1,160 @@ +/* + * #%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.convert; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.scijava.plugin.Plugin; +import org.scijava.util.StringUtils; + +/** + * A collection of {@link Converter} plugins for going between {@link String}, + * {@link File} and {@code File[]}. + * + * @author Jan Eglinger + * @author Curtis Rueden + */ +public class FileListConverters { + // -- String to File (list) converters -- + + @Plugin(type = Converter.class) + public static class StringToFileConverter extends + AbstractConverter + { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + return (T) new File((String) src); + } + + @Override + public Class getOutputType() { + return File.class; + } + + @Override + public Class getInputType() { + return String.class; + } + + } + + @Plugin(type = Converter.class) + public static class StringToFileArrayConverter extends + AbstractConverter + { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + final String[] tokens = StringUtils.splitUnquoted((String) src, ","); + final List fileList = new ArrayList<>(); + for (final String filePath : tokens) { + if ( filePath.isEmpty() ) + continue; + fileList.add(new File(filePath.replaceAll("^\"|\"$", ""))); + } + return (T) fileList.toArray(new File[fileList.size()]); + } + + @Override + public Class getOutputType() { + return File[].class; + } + + @Override + public Class getInputType() { + return String.class; + } + + } + + // TODO add StringToFileListConverter + + // -- File (list) to String converters -- + + @Plugin(type = Converter.class) + public static class FileToStringConverter extends + AbstractConverter + { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + return (T) ((File) src).getAbsolutePath(); + } + + @Override + public Class getOutputType() { + return String.class; + } + + @Override + public Class getInputType() { + return File.class; + } + + } + + @Plugin(type = Converter.class) + public static class FileArrayToStringConverter extends + AbstractConverter + { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + final List result = Arrays.asList((File[]) src).stream().map( + f -> { + final String path = f.getAbsolutePath(); + return path.contains(",") ? "\"" + path + "\"" : path; + }).collect(Collectors.toList()); + return (T) String.join(",", result); + } + + @Override + public Class getOutputType() { + return String.class; + } + + @Override + public Class getInputType() { + return File[].class; + } + + } + + // TODO add FileListToStringConverter +} diff --git a/src/main/java/org/scijava/convert/FileToPathConverter.java b/src/main/java/org/scijava/convert/FileToPathConverter.java new file mode 100644 index 000000000..9c8717286 --- /dev/null +++ b/src/main/java/org/scijava/convert/FileToPathConverter.java @@ -0,0 +1,61 @@ +/*- + * #%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.convert; + +import java.io.File; +import java.nio.file.Path; + +import org.scijava.plugin.Plugin; + +/** + * A {@link Converter} used to convert {@link File}s into {@link Path}s. + * + * @author Gabriel Selzer + */ +@Plugin(type = Converter.class) +public class FileToPathConverter extends AbstractConverter { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + File f = (File) src; + return (T) f.toPath(); + } + + @Override + public Class getOutputType() { + return Path.class; + } + + @Override + public Class getInputType() { + return File.class; + } +} diff --git a/src/main/java/org/scijava/convert/NullConverter.java b/src/main/java/org/scijava/convert/NullConverter.java index 6eb29874b..5c91943db 100644 --- a/src/main/java/org/scijava/convert/NullConverter.java +++ b/src/main/java/org/scijava/convert/NullConverter.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,56 +33,47 @@ import org.scijava.Priority; import org.scijava.plugin.Plugin; -import org.scijava.util.ConversionUtils; +import org.scijava.util.Types; /** - * {@link Converter} implementation for handling {@code null} values. Performs - * basic casting when given a {@code null} source and returns {@code} null - * directly when given a {@code} null destination. + * {@link Converter} implementation for handling {@code null} values. Returns + * {@code null} when given a {@code null} source or {@code null} destination. *

    - * By running at {@link Priority#FIRST_PRIORITY}, other converters should - * not need to worry about {@code null} source or destination parameters. - *

    - *

    - * NB: if a {@link Class} source is queried for the {@link #canConvert}, - * this converter will always return false (as there is no way of knowing - * if the source object will be null or not). + * By running at {@link Priority#EXTREMELY_HIGH}, other converters should not + * need to worry about {@code null} source or destination parameters. *

    * * @author Mark Hiner */ -@Plugin(type = Converter.class, priority = Priority.FIRST_PRIORITY) +@Plugin(type = Converter.class, priority = Priority.EXTREMELY_HIGH) public class NullConverter extends AbstractConverter { @Override - public boolean canConvert(final ConversionRequest request) { - if (request == null) return false; - if (request.destType() == null && request.destClass() == null) return false; - return request.sourceObject() == null && request.sourceClass() == null; + public boolean canConvert(final Object src, final Type dest) { + return src == null || dest == null; } @Override - public boolean canConvert(final Object src, final Type dest) { - return src == null && dest != null; + public boolean canConvert(final Object src, final Class dest) { + return src == null || dest == null; } @Override - public boolean canConvert(final Object src, final Class dest) { - return src == null && dest != null; + public boolean canConvert(final Class src, final Type dest) { + return src == null || dest == null; } @Override public boolean canConvert(final Class src, final Class dest) { - return src == null && dest != null; + return src == null || dest == null; } @Override public T convert(final Object src, final Class dest) { if (dest == null) return null; - if (src == null) return ConversionUtils.getNullValue(dest); - + if (src == null) return Types.nullValue(dest); throw new IllegalArgumentException("Attempting non-null conversion: " + - src + " > " + dest + " using NullConverter."); + src + " -> " + dest + " using NullConverter."); } @Override @@ -96,5 +85,4 @@ public Class getOutputType() { public Class getInputType() { return Object.class; } - } diff --git a/src/main/java/org/scijava/convert/NumberConverters.java b/src/main/java/org/scijava/convert/NumberConverters.java new file mode 100644 index 000000000..4046d6503 --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberConverters.java @@ -0,0 +1,171 @@ +/* + * #%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.convert; + +import java.math.BigInteger; + +import org.scijava.plugin.Plugin; + +/** + * Converter plugins that convert from primitive numeric types to other + * primitive numeric types. + * + * @author Alison Walter + */ +public final class NumberConverters { + + private NumberConverters() { + // prevent instantiation of container class + } + + //convert to short + @Plugin(type = Converter.class) + public static class ByteToShortConverter extends NumberToShortConverter { + @Override public Class getInputType() { return Byte.class; } + } + + //convert to int + @Plugin(type = Converter.class) + public static class ByteToIntegerConverter extends NumberToIntegerConverter { + @Override public Class getInputType() { return Byte.class; } + } + + @Plugin(type = Converter.class) + public static class ShortToIntegerConverter extends NumberToIntegerConverter { + @Override public Class getInputType() { return Short.class; } + } + + //convert to long + @Plugin(type = Converter.class) + public static class ByteToLongConverter extends NumberToLongConverter { + @Override public Class getInputType() { return Byte.class; } + } + + @Plugin(type = Converter.class) + public static class ShortToLongConverter extends NumberToLongConverter { + @Override public Class getInputType() { return Short.class; } + } + + @Plugin(type = Converter.class) + public static class IntegerToLongConverter extends NumberToLongConverter { + @Override public Class getInputType() { return Integer.class; } + } + + //convert to float + @Plugin(type = Converter.class) + public static class ByteToFloatConverter extends NumberToFloatConverter { + @Override public Class getInputType() { return Byte.class; } + } + + @Plugin(type = Converter.class) + public static class ShortToFloatConverter extends NumberToFloatConverter { + @Override public Class getInputType() { return Short.class; } + } + + + //convert to double + @Plugin(type = Converter.class) + public static class ByteToDoubleConverter extends NumberToDoubleConverter { + @Override public Class getInputType() { return Byte.class; } + } + + @Plugin(type = Converter.class) + public static class ShortToDoubleConverter extends NumberToDoubleConverter { + @Override public Class getInputType() { return Short.class; } + } + + @Plugin(type = Converter.class) + public static class IntegerToDoubleConverter extends NumberToDoubleConverter { + @Override public Class getInputType() { return Integer.class; } + } + + @Plugin(type = Converter.class) + public static class FloatToDoubleConverter extends NumberToDoubleConverter { + @Override public Class getInputType() { return Float.class; } + } + + //convert to BigInteger + @Plugin(type = Converter.class) + public static class ByteToBigIntegerConverter extends NumberToBigIntegerConverter { + @Override public Class getInputType() { return Byte.class; } + } + + @Plugin(type = Converter.class) + public static class ShortToBigIntegerConverter extends NumberToBigIntegerConverter { + @Override public Class getInputType() { return Short.class; } + } + + @Plugin(type = Converter.class) + public static class IntegerToBigIntegerConverter extends NumberToBigIntegerConverter { + @Override public Class getInputType() { return Integer.class; } + } + + @Plugin(type = Converter.class) + public static class LongToBigIntegerConverter extends NumberToBigIntegerConverter { + @Override public Class getInputType() { return Long.class; } + } + + //convert to BigDecimal + @Plugin(type = Converter.class) + public static class ByteToBigDecimalConverter extends NumberToBigDecimalConverter { + @Override public Class getInputType() { return Byte.class; } + } + + @Plugin(type = Converter.class) + public static class ShortToBigDecimalConverter extends NumberToBigDecimalConverter { + @Override public Class getInputType() { return Short.class; } + } + + @Plugin(type = Converter.class) + public static class IntegerToBigDecimalConverter extends NumberToBigDecimalConverter { + @Override public Class getInputType() { return Integer.class; } + } + + @Plugin(type = Converter.class) + public static class LongToBigDecimalConverter extends NumberToBigDecimalConverter { + @Override public Class getInputType() { return Long.class; } + } + + @Plugin(type = Converter.class) + public static class FloatToBigDecimalConverter extends NumberToBigDecimalConverter { + @Override public Class getInputType() { return Float.class; } + } + + @Plugin(type = Converter.class) + public static class DoubleToBigDecimalConverter extends NumberToBigDecimalConverter { + @Override public Class getInputType() { return Double.class; } + } + + @Plugin(type = Converter.class) + public static class BigIntegerToBigDecimalConverter extends NumberToBigDecimalConverter { + @Override public Class getInputType() { return BigInteger.class; } + } + +} diff --git a/src/main/java/org/scijava/convert/NumberToBigDecimalConverter.java b/src/main/java/org/scijava/convert/NumberToBigDecimalConverter.java new file mode 100644 index 000000000..910d5a02a --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberToBigDecimalConverter.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.convert; + +import java.math.BigDecimal; + +import org.scijava.util.NumberUtils; + +/** + * Converts numbers to BigDecimals. + * + * @author Alison Walter + */ +public abstract class NumberToBigDecimalConverter extends NumberToNumberConverter { + + @Override + public BigDecimal convert(Number n) { + return NumberUtils.asBigDecimal(n); + } + + @Override + public Class getOutputType() { + return BigDecimal.class; + } + +} diff --git a/src/main/java/org/scijava/convert/NumberToBigIntegerConverter.java b/src/main/java/org/scijava/convert/NumberToBigIntegerConverter.java new file mode 100644 index 000000000..ba3279439 --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberToBigIntegerConverter.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.convert; + +import java.math.BigInteger; + +import org.scijava.util.NumberUtils; + +/** + * Converts numbers to BigIntegers. + * + * @author Alison Walter + */ +public abstract class NumberToBigIntegerConverter extends + NumberToNumberConverter +{ + + @Override + public BigInteger convert(final Number n) { + return NumberUtils.asBigInteger(n); + } + + @Override + public Class getOutputType() { + return BigInteger.class; + } + +} diff --git a/src/main/java/org/scijava/convert/NumberToDoubleConverter.java b/src/main/java/org/scijava/convert/NumberToDoubleConverter.java new file mode 100644 index 000000000..302ea4d22 --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberToDoubleConverter.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.convert; + +/** + * Converts numbers to doubles. + * + * @author Alison Walter + */ +public abstract class NumberToDoubleConverter extends NumberToNumberConverter { + + @Override + public Double convert(Number n) { + return n.doubleValue(); + } + + @Override + public Class getOutputType() { + return Double.class; + } + +} diff --git a/src/main/java/org/scijava/convert/NumberToFloatConverter.java b/src/main/java/org/scijava/convert/NumberToFloatConverter.java new file mode 100644 index 000000000..afd969dbe --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberToFloatConverter.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.convert; + +/** + * Converts numbers to floats. + * + * @author Alison Walter + */ +public abstract class NumberToFloatConverter extends NumberToNumberConverter { + + @Override + public Float convert(Number n) { + return n.floatValue(); + } + + @Override + public Class getOutputType() { + return Float.class; + } + +} diff --git a/src/main/java/org/scijava/convert/NumberToIntegerConverter.java b/src/main/java/org/scijava/convert/NumberToIntegerConverter.java new file mode 100644 index 000000000..e68c4cf29 --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberToIntegerConverter.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.convert; + +/** + * Converts numbers to integers. + * + * @author Alison Walter + */ +public abstract class NumberToIntegerConverter extends NumberToNumberConverter { + + @Override + public Integer convert(Number n) { + return n.intValue(); + } + + @Override + public Class getOutputType() { + return Integer.class; + } + +} diff --git a/src/main/java/org/scijava/convert/NumberToLongConverter.java b/src/main/java/org/scijava/convert/NumberToLongConverter.java new file mode 100644 index 000000000..40c2c0bb8 --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberToLongConverter.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.convert; + +/** + * Converts numbers to longs. + * + * @author Alison Walter + */ +public abstract class NumberToLongConverter extends NumberToNumberConverter { + + @Override + public Long convert(Number n) { + return n.longValue(); + } + + @Override + public Class getOutputType() { + return Long.class; + } + +} diff --git a/src/main/java/org/scijava/convert/NumberToNumberConverter.java b/src/main/java/org/scijava/convert/NumberToNumberConverter.java new file mode 100644 index 000000000..40035dc1f --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberToNumberConverter.java @@ -0,0 +1,64 @@ +/* + * #%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.convert; + +import org.scijava.util.Types; + +/** + * Converts numbers to numbers, and throws IllegalArgumentException for null or + * invalid input. + * + * @author Alison Walter + */ +public abstract class NumberToNumberConverter + extends AbstractConverter +{ + + @Override + public T convert(final Object src, final Class dest) { + if (src == null || dest == null) // + throw new IllegalArgumentException("Null input"); + if (!getInputType().isInstance(src)) { + throw new IllegalArgumentException("Expected input of type " + + getInputType().getSimpleName() + ", but got " + // + src.getClass().getSimpleName()); + } + if (Types.box(dest) != getOutputType()) { + throw new IllegalArgumentException( + "Expected output class of " + getOutputType().getSimpleName() + + ", but got " + dest.getSimpleName()); + } + @SuppressWarnings("unchecked") + final T result = (T) convert((Number) src); + return result; + } + + public abstract O convert(Number n); +} diff --git a/src/main/java/org/scijava/convert/NumberToShortConverter.java b/src/main/java/org/scijava/convert/NumberToShortConverter.java new file mode 100644 index 000000000..eda48e07f --- /dev/null +++ b/src/main/java/org/scijava/convert/NumberToShortConverter.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.convert; + +/** + * Converts numbers to shorts. + * + * @author Alison Walter + */ +public abstract class NumberToShortConverter extends NumberToNumberConverter { + + @Override + public Short convert(Number n) { + return n.shortValue(); + } + + @Override + public Class getOutputType() { + return Short.class; + } + +} diff --git a/src/main/java/org/scijava/convert/PathToFileConverter.java b/src/main/java/org/scijava/convert/PathToFileConverter.java new file mode 100644 index 000000000..cc8aa967d --- /dev/null +++ b/src/main/java/org/scijava/convert/PathToFileConverter.java @@ -0,0 +1,61 @@ +/*- + * #%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.convert; + +import java.io.File; +import java.nio.file.Path; + +import org.scijava.plugin.Plugin; + +/** + * A {@link Converter} used to convert {@link Path}s into {@link File}s. + * + * @author Gabriel Selzer + */ +@Plugin(type = Converter.class) +public class PathToFileConverter extends AbstractConverter { + + @SuppressWarnings("unchecked") + @Override + public T convert(final Object src, final Class dest) { + final Path p = (Path) src; + return (T) p.toFile(); + } + + @Override + public Class getOutputType() { + return File.class; + } + + @Override + public Class getInputType() { + return Path.class; + } +} diff --git a/src/main/java/org/scijava/convert/PrimitiveArrayUnwrapper.java b/src/main/java/org/scijava/convert/PrimitiveArrayUnwrapper.java index 7b63df197..6292aee6d 100644 --- a/src/main/java/org/scijava/convert/PrimitiveArrayUnwrapper.java +++ b/src/main/java/org/scijava/convert/PrimitiveArrayUnwrapper.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,7 +41,7 @@ public abstract class PrimitiveArrayUnwrapper T convert(Object src, Class dest) { + public T convert(final Object src, final Class dest) { final W primitiveArray = (W) src; return (T) primitiveArray.getArray(); diff --git a/src/main/java/org/scijava/convert/PrimitiveArrayWrapper.java b/src/main/java/org/scijava/convert/PrimitiveArrayWrapper.java index 0c3a7992b..f022900a8 100644 --- a/src/main/java/org/scijava/convert/PrimitiveArrayWrapper.java +++ b/src/main/java/org/scijava/convert/PrimitiveArrayWrapper.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/convert/StringToArrayConverter.java b/src/main/java/org/scijava/convert/StringToArrayConverter.java new file mode 100644 index 000000000..deeb58010 --- /dev/null +++ b/src/main/java/org/scijava/convert/StringToArrayConverter.java @@ -0,0 +1,170 @@ +/* + * #%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.convert; + +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.scijava.Priority; +import org.scijava.parse.Item; +import org.scijava.parse.Items; +import org.scijava.parse.ParseService; +import org.scijava.parsington.Token; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.util.Types; + +/** + * A {@link Converter} that specializes in converting {@link String}s to + * n-dimensional arrays. This {@link Converter} can convert any array whose + * component types can be created from a {@link String}. By default, this + * {@link Converter} delimits the {@link String} based on commas. + * + * @author Gabriel Selzer + */ +@Plugin(type = Converter.class, priority = Priority.VERY_LOW) +public class StringToArrayConverter extends AbstractConverter { + + @Parameter(required = false) + private ConvertService convertService; + + @Parameter(required = false) + private ParseService parseService; + + @Override + public boolean canConvert(final Object src, final Type dest) { + return canConvert(src, Types.raw(dest)); + } + + @Override + public boolean canConvert(final Object src, final Class dest) { + if (convertService == null || parseService == null) return false; + + // First, ensure the base types conform + if (!canConvert(src.getClass(), dest)) return false; + // Then, ensure we can parse the string + try { + parseService.parse((String) src, false); + } + catch (final IllegalArgumentException e) { + return false; + } + return true; + } + + @Override + public boolean canConvert(final Class src, final Class dest) { + return src == String.class && dest.isArray(); + } + + @Override + public Object convert(final Object src, final Type dest) { + final Type componentType = Types.component(dest); + if (componentType == null) { + throw new IllegalArgumentException(dest + " is not an array type!"); + } + final List items = parse((String) src); + return convertToArray(items, Types.raw(componentType)); + } + + @Override + public T convert(final Object src, final Class dest) { + // NB: Invert functional flow from Converter interface: + // Converter: convert(Object, Type) calling convert(Object, Class) + // becomes: convert(Object, Class) calling convert(Object, Type) + final Type destType = dest; + @SuppressWarnings("unchecked") + T result = (T) convert(src, destType); + return result; + } + + @Override + public Class getOutputType() { + return Object.class; + } + + @Override + public Class getInputType() { + return String.class; + } + + // -- Helper methods -- + + /** + * Converts {@code src} into an array of component type {@code componentType}. + * + * @param tree the {@link String} to convert + * @param componentType the component type of the output array + * @return an array of {@code componentType} whose elements were created from + * {@code src} + */ + private Object convertToArray(final List tree, + final Class componentType) + { + // Create the array + final Object array = Array.newInstance(componentType, tree.size()); + // Set each element of the array + for (int i = 0; i < tree.size(); i++) { + Object element = tree.get(i); + final Object converted = convertService.convert(element, componentType); + Array.set(array, i, converted); + } + return array; + } + + /** Parses a string to a list, using the {@link ParseService}. */ + private List parse(final String s) { + try { + Items items = parseService.parse(s, false); + return (List) unwrap(items); + } + catch (final IllegalArgumentException e) { + return null; + } + } + + private Object unwrap(final Object o) { + if (o instanceof Collection) { + return ((Collection) o).stream() // + .map(item -> unwrap(item)) // + .collect(Collectors.toList()); + } + if (o instanceof Item) { + return unwrap(((Item) o).value()); + } + if (o instanceof Token) { + return ((Token) o).getToken(); + } + return o; + } +} diff --git a/src/main/java/org/scijava/convert/StringToNumberConverter.java b/src/main/java/org/scijava/convert/StringToNumberConverter.java new file mode 100644 index 000000000..00a3f2640 --- /dev/null +++ b/src/main/java/org/scijava/convert/StringToNumberConverter.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.convert; + +import org.scijava.plugin.Plugin; +import org.scijava.util.Types; + +/** + * Converts a {@link String} to a {@link Number}. Currently handles all boxed + * and unboxed primitives, along with the Numbers. In particular, + * {@link String}s converted {@link Number}s are just {@link Double}s, as this + * features the broadest range of integers. + * + * @author Gabriel Selzer + */ +@Plugin(type = Converter.class) +public class StringToNumberConverter extends AbstractConverter { + + @Override + @SuppressWarnings("unchecked") + public T convert(Object src, Class dest) { + // ensure type is well-behaved, rather than a primitive type + Class saneDest = sane(dest); + if (!(src instanceof String)) throw new IllegalArgumentException( + "Expected src to be a String but got a " + src.getClass()); + if (!(Number.class.isAssignableFrom(saneDest))) + throw new IllegalArgumentException( + "Expected dest to be Number.class (or a subclass of Number, or a numerical primitive), but got " + + saneDest); + String srcString = (String) src; + if (saneDest == Byte.class) return (T) new Byte(srcString); + if (saneDest == Short.class) return (T) new Short(srcString); + if (saneDest == Integer.class) return (T) new Integer(srcString); + if (saneDest == Long.class) return (T) new Long(srcString); + if (saneDest == Float.class) return (T) new Float(srcString); + if (saneDest == Double.class) return (T) new Double(srcString); + else throw new IllegalArgumentException("Unknown destination type: " + + saneDest); + } + + @Override + public Class getOutputType() { + return Number.class; + } + + @Override + public Class getInputType() { + return String.class; + } + + @Override + public boolean canConvert(Object src, Class dest) { + if (!Types.isAssignable(src.getClass(), String.class)) return false; + // The only way to know if the conversion is valid is to actually do it. + try { + String srcString = (String) src; + sane(dest).getConstructor(String.class).newInstance(srcString); + return true; + } + catch (Exception e) { + return false; + } + } + + // -- Helper functionality -- // + + @SuppressWarnings("unchecked") + private Class sane(Class c) { + if (c == Number.class) return (Class) Double.class; + return Types.box(c); + } +} diff --git a/src/main/java/org/scijava/display/AbstractDisplay.java b/src/main/java/org/scijava/display/AbstractDisplay.java index 9c4d43669..e190f844c 100644 --- a/src/main/java/org/scijava/display/AbstractDisplay.java +++ b/src/main/java/org/scijava/display/AbstractDisplay.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -75,7 +73,7 @@ public abstract class AbstractDisplay extends AbstractRichPlugin implements public AbstractDisplay(final Class type) { this.type = type; - objects = new ArrayList(); + objects = new ArrayList<>(); } // -- AbstractDisplay methods -- @@ -109,12 +107,6 @@ public boolean canDisplay(final Class c) { return type.isAssignableFrom(c); } - @Override - public boolean canDisplay(final Object o) { - if (o == null) return false; - return canDisplay(o.getClass()); - } - @Override public void display(final Object o) { checkObject(o); @@ -123,11 +115,6 @@ public void display(final Object o) { add(typedObj); } - @Override - public boolean isDisplaying(final Object o) { - return contains(o); - } - @Override public void update() { if (eventService != null && !isClosed) { diff --git a/src/main/java/org/scijava/display/ActiveDisplayPreprocessor.java b/src/main/java/org/scijava/display/ActiveDisplayPreprocessor.java index 09ba6c725..5997d66e8 100644 --- a/src/main/java/org/scijava/display/ActiveDisplayPreprocessor.java +++ b/src/main/java/org/scijava/display/ActiveDisplayPreprocessor.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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 = Priority.VERY_HIGH_PRIORITY) +@Plugin(type = PreprocessorPlugin.class, priority = Priority.VERY_HIGH) public class ActiveDisplayPreprocessor extends AbstractPreprocessorPlugin { @Parameter(required = false) @@ -82,7 +79,7 @@ public void process(final Module module) { final String name = displayInput.getName(); module.setInput(name, activeDisplay); - module.setResolved(name, true); + module.resolveInput(name); } } diff --git a/src/main/java/org/scijava/display/DefaultDisplay.java b/src/main/java/org/scijava/display/DefaultDisplay.java index 2041330a6..8b7755d44 100644 --- a/src/main/java/org/scijava/display/DefaultDisplay.java +++ b/src/main/java/org/scijava/display/DefaultDisplay.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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 @@ // using Object#toString()), but until it exists, discovery of this display // merely causes the UIService to eventually issue some warnings anyway // ("No suitable viewer found for display" and "No viewer found for display"). -//@Plugin(type = Display.class, priority = Priority.VERY_LOW_PRIORITY) +//@Plugin(type = Display.class, priority = Priority.VERY_LOW) /** * Default display for objects, when no other displays are available. * diff --git a/src/main/java/org/scijava/display/DefaultDisplayService.java b/src/main/java/org/scijava/display/DefaultDisplayService.java index f193decfb..23a0c9c7f 100644 --- a/src/main/java/org/scijava/display/DefaultDisplayService.java +++ b/src/main/java/org/scijava/display/DefaultDisplayService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -80,22 +78,22 @@ public final class DefaultDisplayService extends AbstractService implements // -- instance variables -- private final LinkedList> displayList = - new LinkedList>(); + new LinkedList<>(); // -- DisplayService methods -- @Override - public EventService getEventService() { + public EventService eventService() { return eventService; } @Override - public ObjectService getObjectService() { + public ObjectService objectService() { return objectService; } @Override - public PluginService getPluginService() { + public PluginService pluginService() { return pluginService; } @@ -183,7 +181,7 @@ public Display getDisplay(final String name) { @Override public List> getDisplays(final Object o) { - final ArrayList> displays = new ArrayList>(); + final ArrayList> displays = new ArrayList<>(); for (final Display display : getDisplays()) { if (display.isDisplaying(o)) displays.add(display); } @@ -209,6 +207,15 @@ public Display createDisplay(final Object o) { @Override public Display createDisplay(final String name, final Object o) { + final Display display = createDisplayQuietly(o); + if (display == null) return null; + if (name != null) display.setName(name); + eventService.publish(new DisplayCreatedEvent(display)); + return display; + } + + @Override + public Display createDisplayQuietly(final Object o) { // get available display plugins from the plugin service final List>> displayPlugins = getDisplayPlugins(); @@ -219,8 +226,6 @@ public Display createDisplay(final String name, final Object o) { // TODO: how to handle multiple matches? prompt user with dialog box? if (display.canDisplay(o)) { display.display(o); - if (name != null) display.setName(name); - eventService.publish(new DisplayCreatedEvent(display)); return display; } } diff --git a/src/main/java/org/scijava/display/DefaultTextDisplay.java b/src/main/java/org/scijava/display/DefaultTextDisplay.java index de8b1cfba..380507bdf 100644 --- a/src/main/java/org/scijava/display/DefaultTextDisplay.java +++ b/src/main/java/org/scijava/display/DefaultTextDisplay.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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 @@ * * @author Curtis Rueden */ -@Plugin(type = Display.class, priority = Priority.LOW_PRIORITY) +@Plugin(type = Display.class, priority = Priority.LOW) public class DefaultTextDisplay extends AbstractDisplay implements TextDisplay { diff --git a/src/main/java/org/scijava/display/Display.java b/src/main/java/org/scijava/display/Display.java index 6405499c1..2221db211 100644 --- a/src/main/java/org/scijava/display/Display.java +++ b/src/main/java/org/scijava/display/Display.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -66,7 +64,10 @@ public interface Display extends List, RichPlugin, Named { boolean canDisplay(Class c); /** Tests whether the display is capable of visualizing the given object. */ - boolean canDisplay(Object o); + default boolean canDisplay(final Object o) { + if (o == null) return false; + return canDisplay(o.getClass()); + } /** * Displays the given object in this display. @@ -92,7 +93,9 @@ public interface Display extends List, RichPlugin, Named { * {@code DatasetView} that wraps that {@code Dataset}. *

    */ - boolean isDisplaying(Object o); + default boolean isDisplaying(final Object o) { + return contains(o); + } /** Updates and redraws the display onscreen. */ void update(); diff --git a/src/main/java/org/scijava/display/DisplayPostprocessor.java b/src/main/java/org/scijava/display/DisplayPostprocessor.java index eea4a5c42..7a6c54264 100644 --- a/src/main/java/org/scijava/display/DisplayPostprocessor.java +++ b/src/main/java/org/scijava/display/DisplayPostprocessor.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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.List; import java.util.Map; +import org.scijava.Named; import org.scijava.Priority; import org.scijava.log.LogService; import org.scijava.module.Module; @@ -52,8 +51,7 @@ * @author Lee Kamentsky * @author Barry DeZonia */ -@Plugin(type = PostprocessorPlugin.class, - priority = Priority.VERY_LOW_PRIORITY) +@Plugin(type = PostprocessorPlugin.class, priority = Priority.VERY_LOW) public class DisplayPostprocessor extends AbstractPostprocessorPlugin { @Parameter(required = false) @@ -67,9 +65,11 @@ public void process(final Module module) { if (displayService == null) return; for (final ModuleItem outputItem : module.getInfo().outputs()) { + if (module.isOutputResolved(outputItem.getName())) continue; final Object value = outputItem.getValue(module); final String name = defaultName(outputItem); - handleOutput(name, value); + final boolean resolved = handleOutput(name, value); + if (resolved) module.resolveOutput(name); } } @@ -81,21 +81,18 @@ public void process(final Module module) { * @param defaultName The default name for the display, if not already set. * @param output The object to display. */ - private void handleOutput(final String defaultName, final Object output) { - if (output == null) { - // ignore null outputs - return; - } + private boolean handleOutput(final String defaultName, final Object output) { + if (output == null) return false; // ignore null outputs if (output instanceof Display) { // output is itself a display; just update it final Display display = (Display) output; display.update(); - return; + return true; } final boolean addToExisting = addToExisting(output); - final ArrayList> displays = new ArrayList>(); + final ArrayList> displays = new ArrayList<>(); // get list of existing displays currently visualizing this output final List> existingDisplays = @@ -113,13 +110,16 @@ private void handleOutput(final String defaultName, final Object output) { } else { // create a new display for the output - final Display display = displayService.createDisplay(output); + String name = null; + + // TODO rework how displays are named + if (output instanceof Named) name = ((Named)output).getName(); + + if (name == null) name = defaultName; + + final Display display = displayService.createDisplay(name, output); if (display != null) { displays.add(display); - if (display.getName() == null) { - // set a default name based on the parameter - display.setName(defaultName); - } } } } @@ -129,7 +129,7 @@ private void handleOutput(final String defaultName, final Object output) { for (final Display display : displays) { display.update(); } - return; + return true; } if (output instanceof Map) { @@ -140,7 +140,7 @@ private void handleOutput(final String defaultName, final Object output) { final Object itemValue = map.get(key); handleOutput(itemName, itemValue); } - return; + return true; } if (output instanceof Collection) { @@ -149,7 +149,7 @@ private void handleOutput(final String defaultName, final Object output) { for (final Object item : collection) { handleOutput(defaultName, item); } - return; + return true; } // no available displays for this type of output @@ -158,6 +158,7 @@ private void handleOutput(final String defaultName, final Object output) { log.warn("Ignoring unsupported output: " + defaultName + " [" + valueClass + "]"); } + return false; } private boolean addToExisting(final Object output) { diff --git a/src/main/java/org/scijava/display/DisplayService.java b/src/main/java/org/scijava/display/DisplayService.java index cb2abddbb..fcc13e0a0 100644 --- a/src/main/java/org/scijava/display/DisplayService.java +++ b/src/main/java/org/scijava/display/DisplayService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,8 @@ import java.util.List; +import org.scijava.display.event.DisplayCreatedEvent; +import org.scijava.display.event.DisplayDeletedEvent; import org.scijava.event.EventService; import org.scijava.object.ObjectService; import org.scijava.plugin.PluginInfo; @@ -48,11 +48,17 @@ */ public interface DisplayService extends SciJavaService { - EventService getEventService(); + default EventService eventService() { + return context().getService(EventService.class); + } - ObjectService getObjectService(); + default ObjectService objectService() { + return context().getService(ObjectService.class); + } - PluginService getPluginService(); + default PluginService pluginService() { + return context().getService(PluginService.class); + } /** Gets the currently active display (of any Display type). */ Display getActiveDisplay(); @@ -114,7 +120,17 @@
    > List> getDisplayPluginsOfType( boolean isUniqueName(String name); /** - * Creates a display for the given object. + * Creates a display for the given object, publishing a + * {@link DisplayCreatedEvent} to notify interested parties. In particular: + *
      + *
    • Visible UIs will respond to this event by showing the display.
    • + *
    • The {@link ObjectService} will add the new display to its index, until + * a corresponding {@link DisplayDeletedEvent} is later published.
    • + *
    + *

    + * To create a {@link Display} without publishing an event, see + * {@link #createDisplayQuietly}. + *

    * * @param o The object for which a display should be created. The object is * then added to the display. @@ -129,7 +145,17 @@
    > List> getDisplayPluginsOfType( Display createDisplay(Object o); /** - * Creates a display for the given object. + * Creates a display for the given object, publishing a + * {@link DisplayCreatedEvent} to notify interested parties. In particular: + *
      + *
    • Visible UIs will respond to this event by showing the display.
    • + *
    • The {@link ObjectService} will add the new display to its index, until + * a corresponding {@link DisplayDeletedEvent} is later published.
    • + *
    + *

    + * To create a {@link Display} without publishing an event, see + * {@link #createDisplayQuietly}. + *

    * * @param name The name to be assigned to the display. * @param o The object for which a display should be created. The object is @@ -144,4 +170,40 @@
    > List> getDisplayPluginsOfType( */ Display createDisplay(String name, Object o); + /** + * Creates a display for the given object, without publishing a + * {@link DisplayCreatedEvent}. Hence, the display will not be automatically + * shown or tracked. + * + * @param o The object for which a display should be created. The object is + * then added to the display. + * @return Newly created {@code Display} containing the given object. The + * Display is typed with ? rather than T matching the Object because + * it is possible for the Display to be a collection of some other + * sort of object than the one being added. For example, ImageDisplay + * is a {@code Display} with the DataView wrapping a + * Dataset, yet the ImageDisplay supports adding Datasets directly, + * taking care of wrapping them in a DataView as needed. + */ + Display createDisplayQuietly(Object o); + + // -- Deprecated methods -- + + /** @deprecated Use {@link #eventService()} instead. */ + @Deprecated + default EventService getEventService() { + return eventService(); + } + + /** @deprecated Use {@link #objectService()} instead. */ + @Deprecated + default ObjectService getObjectService() { + return objectService(); + } + + /** @deprecated Use {@link #pluginService()} instead. */ + @Deprecated + default PluginService getPluginService() { + return pluginService(); + } } diff --git a/src/main/java/org/scijava/display/Displayable.java b/src/main/java/org/scijava/display/Displayable.java index 292e0b53d..67c0e26ee 100644 --- a/src/main/java/org/scijava/display/Displayable.java +++ b/src/main/java/org/scijava/display/Displayable.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/TextDisplay.java b/src/main/java/org/scijava/display/TextDisplay.java index 3c06c5ef0..0ada43aad 100644 --- a/src/main/java/org/scijava/display/TextDisplay.java +++ b/src/main/java/org/scijava/display/TextDisplay.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/DisplayActivatedEvent.java b/src/main/java/org/scijava/display/event/DisplayActivatedEvent.java index 315f0215f..1fe30c96c 100644 --- a/src/main/java/org/scijava/display/event/DisplayActivatedEvent.java +++ b/src/main/java/org/scijava/display/event/DisplayActivatedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/DisplayCreatedEvent.java b/src/main/java/org/scijava/display/event/DisplayCreatedEvent.java index 6e45ab5ec..347557f2f 100644 --- a/src/main/java/org/scijava/display/event/DisplayCreatedEvent.java +++ b/src/main/java/org/scijava/display/event/DisplayCreatedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/DisplayDeletedEvent.java b/src/main/java/org/scijava/display/event/DisplayDeletedEvent.java index 24a74e2a7..125ce25e3 100644 --- a/src/main/java/org/scijava/display/event/DisplayDeletedEvent.java +++ b/src/main/java/org/scijava/display/event/DisplayDeletedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/DisplayEvent.java b/src/main/java/org/scijava/display/event/DisplayEvent.java index 4637037b9..99deab3ea 100644 --- a/src/main/java/org/scijava/display/event/DisplayEvent.java +++ b/src/main/java/org/scijava/display/event/DisplayEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/DisplayUpdatedEvent.java b/src/main/java/org/scijava/display/event/DisplayUpdatedEvent.java index f0e40254d..cbb1fc3ab 100644 --- a/src/main/java/org/scijava/display/event/DisplayUpdatedEvent.java +++ b/src/main/java/org/scijava/display/event/DisplayUpdatedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/InputEvent.java b/src/main/java/org/scijava/display/event/input/InputEvent.java index ca46e80e7..4e278a185 100644 --- a/src/main/java/org/scijava/display/event/input/InputEvent.java +++ b/src/main/java/org/scijava/display/event/input/InputEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/KyEvent.java b/src/main/java/org/scijava/display/event/input/KyEvent.java index d31534b81..058c986dc 100644 --- a/src/main/java/org/scijava/display/event/input/KyEvent.java +++ b/src/main/java/org/scijava/display/event/input/KyEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/KyPressedEvent.java b/src/main/java/org/scijava/display/event/input/KyPressedEvent.java index 28f326837..e74dae63d 100644 --- a/src/main/java/org/scijava/display/event/input/KyPressedEvent.java +++ b/src/main/java/org/scijava/display/event/input/KyPressedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/KyReleasedEvent.java b/src/main/java/org/scijava/display/event/input/KyReleasedEvent.java index 593e557f7..15659de2d 100644 --- a/src/main/java/org/scijava/display/event/input/KyReleasedEvent.java +++ b/src/main/java/org/scijava/display/event/input/KyReleasedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/KyTypedEvent.java b/src/main/java/org/scijava/display/event/input/KyTypedEvent.java index 568aec3ba..9f5d96c6e 100644 --- a/src/main/java/org/scijava/display/event/input/KyTypedEvent.java +++ b/src/main/java/org/scijava/display/event/input/KyTypedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsButtonEvent.java b/src/main/java/org/scijava/display/event/input/MsButtonEvent.java index bf0a38d98..09a91dabd 100644 --- a/src/main/java/org/scijava/display/event/input/MsButtonEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsButtonEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsClickedEvent.java b/src/main/java/org/scijava/display/event/input/MsClickedEvent.java index 6d180c2fe..f13524996 100644 --- a/src/main/java/org/scijava/display/event/input/MsClickedEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsClickedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsDraggedEvent.java b/src/main/java/org/scijava/display/event/input/MsDraggedEvent.java index bed510b88..f683dc4b9 100644 --- a/src/main/java/org/scijava/display/event/input/MsDraggedEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsDraggedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsEnteredEvent.java b/src/main/java/org/scijava/display/event/input/MsEnteredEvent.java index ab1e8843c..310bcb444 100644 --- a/src/main/java/org/scijava/display/event/input/MsEnteredEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsEnteredEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsEvent.java b/src/main/java/org/scijava/display/event/input/MsEvent.java index 61f4614c0..b370e1e79 100644 --- a/src/main/java/org/scijava/display/event/input/MsEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsExitedEvent.java b/src/main/java/org/scijava/display/event/input/MsExitedEvent.java index f08d93bb6..921e674f1 100644 --- a/src/main/java/org/scijava/display/event/input/MsExitedEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsExitedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsMovedEvent.java b/src/main/java/org/scijava/display/event/input/MsMovedEvent.java index 0afc917a7..658b1b904 100644 --- a/src/main/java/org/scijava/display/event/input/MsMovedEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsMovedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsPressedEvent.java b/src/main/java/org/scijava/display/event/input/MsPressedEvent.java index 841f5c5de..ea2593493 100644 --- a/src/main/java/org/scijava/display/event/input/MsPressedEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsPressedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsReleasedEvent.java b/src/main/java/org/scijava/display/event/input/MsReleasedEvent.java index 4328779c0..61786d727 100644 --- a/src/main/java/org/scijava/display/event/input/MsReleasedEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsReleasedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/input/MsWheelEvent.java b/src/main/java/org/scijava/display/event/input/MsWheelEvent.java index 4908ff5d9..32d92bd2f 100644 --- a/src/main/java/org/scijava/display/event/input/MsWheelEvent.java +++ b/src/main/java/org/scijava/display/event/input/MsWheelEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/window/WinActivatedEvent.java b/src/main/java/org/scijava/display/event/window/WinActivatedEvent.java index 8ee0f7938..f8c81ec30 100644 --- a/src/main/java/org/scijava/display/event/window/WinActivatedEvent.java +++ b/src/main/java/org/scijava/display/event/window/WinActivatedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/window/WinClosedEvent.java b/src/main/java/org/scijava/display/event/window/WinClosedEvent.java index 684c9abf4..24476f5aa 100644 --- a/src/main/java/org/scijava/display/event/window/WinClosedEvent.java +++ b/src/main/java/org/scijava/display/event/window/WinClosedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/window/WinClosingEvent.java b/src/main/java/org/scijava/display/event/window/WinClosingEvent.java index 51d93a605..ea6192681 100644 --- a/src/main/java/org/scijava/display/event/window/WinClosingEvent.java +++ b/src/main/java/org/scijava/display/event/window/WinClosingEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/window/WinDeactivatedEvent.java b/src/main/java/org/scijava/display/event/window/WinDeactivatedEvent.java index 8fb256d7f..8a27cfdce 100644 --- a/src/main/java/org/scijava/display/event/window/WinDeactivatedEvent.java +++ b/src/main/java/org/scijava/display/event/window/WinDeactivatedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/window/WinDeiconifiedEvent.java b/src/main/java/org/scijava/display/event/window/WinDeiconifiedEvent.java index d1a58d19c..b6f620918 100644 --- a/src/main/java/org/scijava/display/event/window/WinDeiconifiedEvent.java +++ b/src/main/java/org/scijava/display/event/window/WinDeiconifiedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/window/WinEvent.java b/src/main/java/org/scijava/display/event/window/WinEvent.java index 8b9bd41c5..8190bfcde 100644 --- a/src/main/java/org/scijava/display/event/window/WinEvent.java +++ b/src/main/java/org/scijava/display/event/window/WinEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/window/WinIconifiedEvent.java b/src/main/java/org/scijava/display/event/window/WinIconifiedEvent.java index 2333ccd22..044182b2f 100644 --- a/src/main/java/org/scijava/display/event/window/WinIconifiedEvent.java +++ b/src/main/java/org/scijava/display/event/window/WinIconifiedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/display/event/window/WinOpenedEvent.java b/src/main/java/org/scijava/display/event/window/WinOpenedEvent.java index 1a3904883..4f10240f0 100644 --- a/src/main/java/org/scijava/display/event/window/WinOpenedEvent.java +++ b/src/main/java/org/scijava/display/event/window/WinOpenedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/download/DefaultDownloadService.java b/src/main/java/org/scijava/download/DefaultDownloadService.java new file mode 100644 index 000000000..44c1b9aac --- /dev/null +++ b/src/main/java/org/scijava/download/DefaultDownloadService.java @@ -0,0 +1,193 @@ +/*- + * #%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.download; + +import java.io.IOException; +import java.util.Date; + +import org.scijava.io.handle.DataHandle; +import org.scijava.io.handle.DataHandleService; +import org.scijava.io.handle.DataHandles; +import org.scijava.io.location.Location; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.service.AbstractService; +import org.scijava.service.Service; +import org.scijava.task.Task; +import org.scijava.task.TaskService; + +/** + * Default implementation of {@link DownloadService}. + * + * @author Curtis Rueden + */ +@Plugin(type = Service.class) +public class DefaultDownloadService extends AbstractService implements + DownloadService +{ + + @Parameter + private DataHandleService dataHandleService; + + @Parameter + private TaskService taskService; + + @Override + public Download download(final Location source, final Location destination) { + final Task task = taskService.createTask("Download"); + return new DefaultDownload(source, destination, task, () -> { + try (final DataHandle in = dataHandleService.create(source); + final DataHandle out = dataHandleService.create( + destination)) + { + task.setStatusMessage("Downloading " + source.getURI()); + DataHandles.copy(in, out, task); + } + catch (final IOException exc) { + // TODO: Improve error handling: + // 1. Consider a better exception handling design here. + // 2. Retry at least a few times if something goes wrong. + throw new RuntimeException(exc); + } + }); + } + + @Override + public Download download(final Location source, final Location destination, + final LocationCache cache) + { + if (cache == null || !cache.canCache(source)) { + // Caching this location is not supported. + return download(source, destination); + } + + final Task task = taskService.createTask("Download"); + return new DefaultDownload(source, destination, task, () -> { + final Location cached = cache.cachedLocation(source); + try ( + final DataHandle sourceHandle = dataHandleService.create(source); + final DataHandle cachedHandle = dataHandleService.create(cached); + final DataHandle destHandle = dataHandleService.create(destination) + ) + { + if (isCachedHandleValid(source, cache, sourceHandle, cachedHandle)) { + // The data is cached; download from the cached source instead. + task.setStatusMessage("Retrieving " + source.getURI()); + DataHandles.copy(cachedHandle, destHandle, task); + } + else { + // Data is not yet cached; write to the destination _and_ the cache. + task.setStatusMessage("Downloading + caching " + source.getURI()); + DataHandles.copy(sourceHandle, // + new MultiWriteHandle(cachedHandle, destHandle), task); + } + } + catch (final IOException exc) { + // TODO: Improve error handling: + // 1. Consider a better exception handling design here. + // 2. Retry at least a few times if something goes wrong. + throw new RuntimeException(exc); + } + }); + } + + // -- Helper methods -- + + private boolean isCachedHandleValid(final Location source, + final LocationCache cache, final DataHandle sourceHandle, + final DataHandle cachedHandle) throws IOException + { + if (!cachedHandle.exists()) return false; // No cached data is present. + + // Compare data lengths. + final long sourceLen = sourceHandle.length(); + final long cachedLen = cachedHandle.length(); + if (sourceLen >= 0 && cachedLen >= 0 && sourceLen != cachedLen) { + // Original and cached sources report different lengths; cache is invalid. + return false; + } + + // Compare last modified timestamps. + final Date sourceDate = sourceHandle.lastModified(); + final Date cachedDate = cachedHandle.lastModified(); + if (sourceDate != null && cachedDate != null && // + sourceDate.after(cachedDate)) + { + // Source was changed after cache was written; cache is invalid. + return false; + } + + // Compare checksums. + final String sourceChecksum = sourceHandle.checksum(); + final String cachedChecksum = cache.loadChecksum(source); + if (sourceChecksum != null && cachedChecksum != null && // + !sourceChecksum.equals(cachedChecksum)) + { + // Checksums do not match; cache is invalid. + return false; + } + + // Everything matched; we're all good. + return true; + } + + // -- Helper classes -- + + private class DefaultDownload implements Download { + + private Location source; + private Location destination; + private Task task; + + private DefaultDownload(final Location source, final Location destination, + final Task task, final Runnable r) + { + this.source = source; + this.destination = destination; + this.task = task; + task.run(r); + } + + @Override + public Location source() { + return source; + } + + @Override + public Location destination() { + return destination; + } + + @Override + public Task task() { + return task; + } + } +} diff --git a/src/main/java/org/scijava/download/DiskLocationCache.java b/src/main/java/org/scijava/download/DiskLocationCache.java new file mode 100644 index 000000000..ed576b4c4 --- /dev/null +++ b/src/main/java/org/scijava/download/DiskLocationCache.java @@ -0,0 +1,123 @@ +/*- + * #%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.download; + +import java.io.File; +import java.io.IOException; + +import org.scijava.io.location.FileLocation; +import org.scijava.io.location.Location; +import org.scijava.util.DigestUtils; +import org.scijava.util.FileUtils; + +/** + * A file-based implementation of {@link LocationCache}. + * + * @author Curtis Rueden + */ +public class DiskLocationCache implements LocationCache { + + private File baseDir = new File(System.getProperty("user.home") + + File.separator + ".scijava" + File.separator + "cache" + File.separator); + + private boolean cacheFileLocations; + + // -- DiskLocationCache methods -- + + public File getBaseDirectory() { + return baseDir; + } + + public void setBaseDirectory(final File baseDir) { + if (!baseDir.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + baseDir); + } + this.baseDir = baseDir; + } + + public boolean isFileLocationCachingEnabled() { + return cacheFileLocations; + } + + public void setFileLocationCachingEnabled(final boolean enabled) { + // NB: It is possible the input file is stored on a volume which is much + // slower than the local disk cache, so we make this setting configurable. + cacheFileLocations = enabled; + } + + // -- LocationCache methods -- + + @Override + public boolean canCache(final Location source) { + if (source instanceof FileLocation && !isFileLocationCachingEnabled()) { + // The cache is not configured to cache files to other files. + return false; + } + return source.getURI() != null; + } + + @Override + public Location cachedLocation(final Location source) { + if (!canCache(source)) { + throw new IllegalArgumentException("Uncacheable source: " + source); + } + return new FileLocation(cachedData(source)); + } + + @Override + public String loadChecksum(final Location source) throws IOException { + final File cachedChecksum = cachedChecksum(source); + if (!cachedChecksum.exists()) return null; + return DigestUtils.string(FileUtils.readFile(cachedChecksum)); + } + + @Override + public void saveChecksum(final Location source, final String checksum) + throws IOException + { + final File cachedChecksum = cachedChecksum(source); + FileUtils.writeFile(cachedChecksum, DigestUtils.bytes(checksum)); + } + + // -- Helper methods -- + + private File cachedData(final Location source) { + return cachedFile(source, ".data"); + } + + private File cachedChecksum(final Location source) { + return cachedFile(source, ".checksum"); + } + + private File cachedFile(final Location source, final String suffix) { + final String hexCode = Integer.toHexString(source.hashCode()); + return new File(getBaseDirectory(), hexCode + suffix); + } +} diff --git a/src/main/java/org/scijava/download/Download.java b/src/main/java/org/scijava/download/Download.java new file mode 100644 index 000000000..f311000b4 --- /dev/null +++ b/src/main/java/org/scijava/download/Download.java @@ -0,0 +1,45 @@ +/*- + * #%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.download; + +import org.scijava.io.location.Location; +import org.scijava.task.Task; + +/** + * Object representing an asynchronous download task. + * + * @author Curtis Rueden + * @see Task + */ +public interface Download { + + Location source(); + Location destination(); + Task task(); +} diff --git a/src/main/java/org/scijava/download/DownloadService.java b/src/main/java/org/scijava/download/DownloadService.java new file mode 100644 index 000000000..ecf576475 --- /dev/null +++ b/src/main/java/org/scijava/download/DownloadService.java @@ -0,0 +1,62 @@ +/*- + * #%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.download; + +import org.scijava.io.location.Location; +import org.scijava.service.SciJavaService; + +/** + * Service for managing retrieval of remote resources. + * + * @author Curtis Rueden + */ +public interface DownloadService extends SciJavaService { + + /** + * Downloads data from the given source, storing it into the given + * destination. + * + * @param source The location of the needed data. + * @param destination The location where the needed data should be stored. + */ + Download download(Location source, Location destination); + + /** + * Downloads data from the given source, storing it into the given + * destination. + * + * @param source The location of the needed data. + * @param destination The location where the needed data should be stored. + * @param cache The cache from which already-downloaded data should be pulled + * preferentially, and to which newly-downloaded data should be + * stored for next time. + */ + Download download(Location source, Location destination, LocationCache cache); +} diff --git a/src/main/java/org/scijava/download/LocationCache.java b/src/main/java/org/scijava/download/LocationCache.java new file mode 100644 index 000000000..3a5f7c031 --- /dev/null +++ b/src/main/java/org/scijava/download/LocationCache.java @@ -0,0 +1,76 @@ +/*- + * #%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.download; + +import java.io.IOException; + +import org.scijava.io.handle.DataHandle; +import org.scijava.io.location.Location; + +/** + * An object which knows how to convert a slow (typically remote) + * {@link Location} to a faster (typically local) one. + * + * @author Curtis Rueden + */ +public interface LocationCache { + + /** Gets whether the given location can be cached by this cache. */ + boolean canCache(Location source); + + /** + * Gets the cache location of a given data source. + * + * @return A {@link Location} where the source data is, or would be, cached. + * @throws IllegalArgumentException if the given source cannot be cached (see + * {@link #canCache}). + */ + Location cachedLocation(Location source); + + /** + * Loads the checksum value which corresponds to the cached location. + * + * @param source The source location for which the cached checksum is desired. + * @return The loaded checksum, or null if one is not available. + * @see DataHandle#checksum() + * @throws IOException If something goes wrong accessing the checksum. + */ + String loadChecksum(Location source) throws IOException; + + /** + * Associates the given checksum value with the specified source location. + * + * @param source The source location for which the checksum should be cached. + * @param checksum The checksum value to cache. + * @see DataHandle#checksum() + * @throws IOException If something goes wrong caching the checksum. + */ + void saveChecksum(Location source, String checksum) throws IOException; +} diff --git a/src/main/java/org/scijava/download/MultiWriteHandle.java b/src/main/java/org/scijava/download/MultiWriteHandle.java new file mode 100644 index 000000000..4dd40b874 --- /dev/null +++ b/src/main/java/org/scijava/download/MultiWriteHandle.java @@ -0,0 +1,155 @@ +/*- + * #%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.download; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.scijava.io.handle.AbstractDataHandle; +import org.scijava.io.handle.DataHandle; +import org.scijava.io.location.Location; + +/** + * {@link DataHandle} plugin for writing to multiple {@link DataHandle}s. + * + * @author Curtis Rueden + */ +public class MultiWriteHandle extends AbstractDataHandle { + + private final List> handles; + + public MultiWriteHandle(final DataHandle... handles) { + this.handles = new ArrayList<>(Arrays.asList(handles)); + } + + // -- DataHandle methods -- + + @Override + public boolean isReadable() { + return false; + } + + @Override + public boolean isWritable() { + boolean writable = true; + // NB: Somewhat arbitrarily, we are writable iff all our constituents are. + for (final DataHandle h : handles) + writable &= h.isWritable(); + return writable; + } + + @Override + public boolean exists() throws IOException { + boolean exists = true; + // NB: Somewhat arbitrarily, we exist iff any of our constituents exist. + for (final DataHandle h : handles) + exists |= h.isWritable(); + return exists; + } + + @Override + public Date lastModified() throws IOException { + for (final DataHandle h : handles) { + final Date lastModified = h.lastModified(); + if (lastModified != null) return lastModified; + } + return null; + } + + @Override + public String checksum() throws IOException { + for (final DataHandle h : handles) { + final String checksum = h.checksum(); + if (checksum != null) return checksum; + } + return null; + } + + @Override + public long offset() throws IOException { + return handles.get(0).offset(); + } + + @Override + public void seek(long pos) throws IOException { + // TODO: parallelStream().forEach() for performance. + for (final DataHandle h : handles) + h.seek(pos); + } + + @Override + public long length() throws IOException { + return handles.get(0).length(); + } + + @Override + public void setLength(long length) throws IOException { + for (final DataHandle h : handles) + h.setLength(length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Class getType() { + return null; + } + + @Override + public byte readByte() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void write(final int b) throws IOException { + // TODO: parallelStream().forEach() for performance. + for (final DataHandle h : handles) + h.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + // TODO: parallelStream().forEach() for performance. + for (final DataHandle h : handles) + h.write(b, off, len); + } + + @Override + public void close() throws IOException { + // TODO: parallelStream().forEach() for performance. + for (final DataHandle h : handles) + h.close(); + } +} diff --git a/src/main/java/org/scijava/event/ContextCreatedEvent.java b/src/main/java/org/scijava/event/ContextCreatedEvent.java new file mode 100644 index 000000000..c279e9a7e --- /dev/null +++ b/src/main/java/org/scijava/event/ContextCreatedEvent.java @@ -0,0 +1,38 @@ +/* + * #%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.event; + +/** + * Event to be published immediately after a context has been fully created + * with all services initialized. + * + * @author Curtis Rueden + */ +public class ContextCreatedEvent extends SciJavaEvent { } diff --git a/src/main/java/org/scijava/event/ContextDisposingEvent.java b/src/main/java/org/scijava/event/ContextDisposingEvent.java index dfee4e284..570c0974c 100644 --- a/src/main/java/org/scijava/event/ContextDisposingEvent.java +++ b/src/main/java/org/scijava/event/ContextDisposingEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,6 @@ /** * Event to be published just before disposing a context. * - * @author Johannes Schindein + * @author Johannes Schindelin */ public class ContextDisposingEvent extends SciJavaEvent { } diff --git a/src/main/java/org/scijava/event/DefaultEventBus.java b/src/main/java/org/scijava/event/DefaultEventBus.java index cfd847797..5587f6d59 100644 --- a/src/main/java/org/scijava/event/DefaultEventBus.java +++ b/src/main/java/org/scijava/event/DefaultEventBus.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,18 +34,17 @@ import java.util.Arrays; import java.util.List; -import org.bushe.swing.event.CleanupEvent; -import org.bushe.swing.event.ThreadSafeEventService; +import org.scijava.event.bushe.ThreadSafeEventService; import org.scijava.log.LogService; import org.scijava.service.Service; import org.scijava.thread.ThreadService; /** - * An {@link org.bushe.swing.event.EventService} implementation for SciJava. + * An {@code org.scijava.event.bushe.EventService} implementation for SciJava. *

    * It is called "DefaultEventBus" rather than "DefaultEventService" to avoid a * name clash with {@link DefaultEventService}, which is not an - * {@link org.bushe.swing.event.EventService} but rather a SciJava + * {@code org.scijava.event.bushe.EventService} but rather a SciJava * {@link Service} implementation. *

    * @@ -61,7 +58,7 @@ public class DefaultEventBus extends ThreadSafeEventService { public DefaultEventBus(final ThreadService threadService, final LogService log) { - super(200L, false, null, null, null); + super(200L, null, null, null); this.threadService = threadService; this.log = log; } @@ -114,37 +111,10 @@ public void publishLater(final String topicName, final Object eventObj) { getVetoEventListeners(topicName), null); } - // -- org.bushe.swing.event.EventService methods -- + // -- org.scijava.event.bushe.EventService methods -- @Override public void publish(final Object event) { - // HACK: Work around a deadlock problem caused by ThreadSafeEventService: - - // 1) The ThreadSafeEventService superclass has a special cleanup thread - // that takes care of cleaning up stale references. Every time it runs, it - // publishes some CleanupEvents using publish(Object) to announce that this - // is occurring. Normally, such publication delegates to - // publishNow, which calls ThreadService#invoke, which calls - // EventQueue.invokeAndWait, which queues the publication for execution on - // the EDT and then blocks until publication is complete. - - // 2) When the ThreadSafeEventService publishes the CleanupEvents, it does - // so inside a synchronized block that locks on a "listenerLock" object. - - // 3) Unfortunately, since the CleanupEvent publication is merely *queued*, - // any other pending operations on the EDT happen first. If one such - // operation meanwhile calls e.g. - // ThreadSafeEventService#getSubscribers(Class), it will deadlock because - // those getter methods are also synchronized on the listenerLock object. - - // Hence, our hack workaround is to instead use publishLater for the - // CleanupEvents, since no one really cares about them anyway. ;-) - - if (event instanceof CleanupEvent) { - publishLater(event); - return; - } - publishNow(event); } diff --git a/src/main/java/org/scijava/event/DefaultEventHistory.java b/src/main/java/org/scijava/event/DefaultEventHistory.java index 72eb38add..4b94b8bc3 100644 --- a/src/main/java/org/scijava/event/DefaultEventHistory.java +++ b/src/main/java/org/scijava/event/DefaultEventHistory.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,10 +51,10 @@ public class DefaultEventHistory extends AbstractService implements private EventService eventService; /** Event details that have been recorded. */ - private ArrayList history = new ArrayList(); + private ArrayList history = new ArrayList<>(); private ArrayList listeners = - new ArrayList(); + new ArrayList<>(); private boolean active; diff --git a/src/main/java/org/scijava/event/DefaultEventService.java b/src/main/java/org/scijava/event/DefaultEventService.java index 855cd8fec..3eca42db0 100644 --- a/src/main/java/org/scijava/event/DefaultEventService.java +++ b/src/main/java/org/scijava/event/DefaultEventService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,10 +40,9 @@ import java.util.Map; import java.util.WeakHashMap; -import org.bushe.swing.event.annotation.AbstractProxySubscriber; -import org.bushe.swing.event.annotation.BaseProxySubscriber; -import org.bushe.swing.event.annotation.ReferenceStrength; import org.scijava.Priority; +import org.scijava.event.bushe.AbstractProxySubscriber; +import org.scijava.event.bushe.ReferenceStrength; import org.scijava.log.LogService; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; @@ -73,7 +70,7 @@ public class DefaultEventService extends AbstractService implements * {@code priority = DefaultEventService.PRIORITY + 1} or similar. *

    */ - public static final double PRIORITY = 10 * Priority.VERY_HIGH_PRIORITY; + public static final double PRIORITY = 10 * Priority.VERY_HIGH; @Parameter private LogService log; @@ -87,13 +84,13 @@ public class DefaultEventService extends AbstractService implements * A cache for mapping {@link Method}s to the {@link SciJavaEvent} class taken * as parameters. Only methods with event parameters will cached here. */ - private final Map> eventClasses = new HashMap>(); + private final Map> eventClasses = new HashMap<>(); /** * Set of claimed {@link EventHandler#key()}s. Additional event handlers * specifying the same key will be ignored rather than subscribed. */ - private final HashSet keys = new HashSet(); + private final HashSet keys = new HashSet<>(); // -- EventService methods -- @@ -113,36 +110,39 @@ public void publishLater(final E e) { @Override public List> subscribe(final Object o) { - List> subscribers = Collections.emptyList(); final List eventHandlers = ClassUtils.getAnnotatedMethods(o.getClass(), EventHandler.class); + if (eventHandlers.isEmpty()) return Collections.emptyList(); + + final ArrayList> subscribers = new ArrayList<>(); + for (final Method m : eventHandlers) { + // verify that the event handler method is valid + final Class eventClass = getEventClass(m); + if (eventClass == null) { + log.warn("Invalid EventHandler method: " + m); + continue; + } - if (!eventHandlers.isEmpty()) { - subscribers = new ArrayList>(); - for (final Method m : eventHandlers) { - // verify that the event handler method is valid - final Class eventClass = getEventClass(m); - if (eventClass == null) { - log.warn("Invalid EventHandler method: " + m); - continue; - } - - // verify that the event handler key isn't already claimed - final String key = m.getAnnotation(EventHandler.class).key(); - if (!key.isEmpty()) { - synchronized (keys) { - if (keys.contains(key)) continue; - keys.add(key); - } + // verify that the event handler key isn't already claimed + final String key = m.getAnnotation(EventHandler.class).key(); + if (!key.isEmpty()) { + synchronized (keys) { + if (keys.contains(key)) continue; + keys.add(key); } - - // subscribe the event handler - subscribers.add(subscribe(eventClass, o, m)); } + + // subscribe the event handler + subscribers.add(subscribe(eventClass, o, m)); } 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) { @@ -203,7 +203,7 @@ private void unsubscribe(final Class c, private EventSubscriber subscribe( final Class c, final Object o, final Method m) { - final ProxySubscriber subscriber = new ProxySubscriber(c, o, m); + final ProxySubscriber subscriber = new ProxySubscriber<>(c, o, m); subscribe(c, subscriber); return subscriber; } @@ -233,7 +233,7 @@ private Class getEventClass(final Method m) { // -- Event handlers garbage collection preventer -- private WeakHashMap>> keepEm = - new WeakHashMap>>(); + new WeakHashMap<>(); /** * Prevents {@link ProxySubscriber} instances from being garbage collected @@ -255,7 +255,7 @@ private Class getEventClass(final Method m) { private synchronized void keepIt(final Object o, final ProxySubscriber subscriber) { List> list = keepEm.get(o); if (list == null) { - list = new ArrayList>(); + list = new ArrayList<>(); keepEm.put(o, list); } list.add(subscriber); @@ -266,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 de79229e0..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 - 2015 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 93f94c4b5..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 - 2015 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 be6e1b6fb..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 - 2015 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,14 +32,14 @@ import java.util.Set; -import org.scijava.service.Service; +import org.scijava.service.SciJavaService; /** * Interface for service that keeps a history of SciJava events. * * @author Curtis Rueden */ -public interface EventHistory extends Service { +public interface EventHistory extends SciJavaService { /** Activates or deactivates event history tracking. */ void setActive(boolean active); diff --git a/src/main/java/org/scijava/event/EventHistoryListener.java b/src/main/java/org/scijava/event/EventHistoryListener.java index 8d813b798..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 - 2015 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 4f269c36d..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 - 2015 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 7b7ddf9a6..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 - 2015 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 f30b9c7fb..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 - 2015 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. + *

    + *

    + * See the overview for an general introduction + * and package documentation for usage details and examples. + *

    + *

    + * 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. + *

    + *
      + *
    1. 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). + *
    2. 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 allSubscriberMaps = new ArrayList(); + allSubscriberMaps.add(subscribersByEventType); + allSubscriberMaps.add(subscribersByEventClass); + allSubscriberMaps.add(subscribersByExactEventClass); + allSubscriberMaps.add(subscribersByTopic); + allSubscriberMaps.add(subscribersByTopicPattern); + allSubscriberMaps.add(vetoListenersByClass); + allSubscriberMaps.add(vetoListenersByExactClass); + allSubscriberMaps.add(vetoListenersByTopic); + allSubscriberMaps.add(vetoListenersByTopicPattern); + + int staleCount = 0; + for (Map subscriberMap : allSubscriberMaps) { + Set subscriptions = subscriberMap.keySet(); + for (Object subscription : subscriptions) { + List subscribers = (List) subscriberMap.get(subscription); + for (Iterator iter = subscribers.iterator(); iter.hasNext();) { + Object subscriber = iter.next(); + Object realSubscriber = getRealSubscriberAndCleanStaleSubscriberIfNecessary(iter, subscriber); + if (realSubscriber == null) { + staleCount++; + } + } + } + } + } + } + } + + private static class PrioritizedSubscriberComparator implements Comparator { + public int compare(Prioritized prioritized1, Prioritized prioritized2) { + if (prioritized1 == null) { + return -1; + } + if (prioritized2 == null) { + return 1; + } + if (prioritized1.getPriority() < prioritized2.getPriority()) { + return -1; + } else if (prioritized1.getPriority() > prioritized2.getPriority()) { + return 1; + } else { + return 0; + } + } + } + + /** + * Since Pattern doesn't implement equals(), we need one of these + */ + private class PatternWrapper { + private Pattern pattern; + + public PatternWrapper(Pattern pat) { + pattern = pat; + } + + public boolean matches(CharSequence input) { + return pattern.matcher(input).matches(); + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PatternWrapper that = (PatternWrapper) o; + + if (pattern != null) { + if (!pattern.equals(that.pattern)) {//give the JVM a shot for forward compatibility + return pattern.pattern() != null && this.pattern.pattern().equals(this.pattern.pattern()); + } + } else { + if (that.pattern != null) { + return false; + } + } + + return true; + } + + public int hashCode() { + if (this.pattern != null && this.pattern.pattern() != null) { + return this.pattern.pattern().hashCode(); + } + return (pattern != null ? pattern.hashCode() : 0); + } + } +} diff --git a/src/main/java/org/scijava/event/bushe/TypeReference.java b/src/main/java/org/scijava/event/bushe/TypeReference.java new file mode 100644 index 000000000..06ff73a8c --- /dev/null +++ b/src/main/java/org/scijava/event/bushe/TypeReference.java @@ -0,0 +1,52 @@ +package org.scijava.event.bushe; + +import java.lang.reflect.Type; +import java.lang.reflect.Constructor; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.InvocationTargetException; + +/** + * Courtesy of Neil Gafter's blog. + * Thanks to Curt Cox for the pointer. + */ +abstract class TypeReference { + + private final Type type; + private volatile Constructor constructor; + + protected TypeReference() { + Type superclass = getClass().getGenericSuperclass(); + if (superclass instanceof Class) { + throw new RuntimeException("Missing type parameter."); + } + this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; + } + + /** + * @return a new instance of {@code T} using the default, no-arg + * constructor. + * @throws IllegalAccessException on security reflection issues + * @throws NoSuchMethodException there's not getRawType on the type + * @throws java.lang.reflect.InvocationTargetException if a reflective call causes an exception in the underlying instance + * @throws InstantiationException if the instance cannot be instantiated + */ + @SuppressWarnings("unchecked") + public T newInstance() + throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException, InstantiationException { + if (constructor == null) { + Class rawType = type instanceof Class + ? (Class) type + : (Class) ((ParameterizedType) type).getRawType(); + constructor = rawType.getConstructor(); + } + return (T) constructor.newInstance(); + } + + /** + * @return the referenced type. + */ + public Type getType() { + return this.type; + } +} diff --git a/src/main/java/org/scijava/event/bushe/VetoEventListener.java b/src/main/java/org/scijava/event/bushe/VetoEventListener.java new file mode 100644 index 000000000..4b377c1c1 --- /dev/null +++ b/src/main/java/org/scijava/event/bushe/VetoEventListener.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; + +/** + * Interface for classes that can veto class-based event publication from the {@link EventService}. + * + * @author Michael Bushe michael@bushe.com + */ +interface VetoEventListener { + + /** + * Determine whether an event should be vetoed or published. + *

    + * The EventService calls this method before class-based publication of objects. If any of the + * VetoEventListeners return true, then none of the subscribers for that event are called.

    Prerequisite: + * VetoEventListener has to be subscribed with the EventService for the event object's class.

    Guaranteed to be + * called in the SwingEventThread when using the SwingEventService (EventBus). See {@link EventService}

    + *

    + * + * @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 ea745910f..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 - 2015 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 3e2c47495..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 - 2015 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: @@ -69,9 +67,9 @@ public class DefaultInputService extends AbstractService implements private boolean metaDown = false; private boolean shiftDown = false; - private HashSet pressedKeys = new HashSet(); + private HashSet pressedKeys = new HashSet<>(); - private HashSet buttonsDown = new HashSet(); + private HashSet buttonsDown = new HashSet<>(); private Display display; private int lastX = -1, lastY = -1; @@ -79,7 +77,7 @@ public class DefaultInputService extends AbstractService implements // -- InputService methods -- @Override - public EventService getEventService() { + public EventService eventService() { return eventService; } diff --git a/src/main/java/org/scijava/input/InputModifiers.java b/src/main/java/org/scijava/input/InputModifiers.java index 0464c301f..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 - 2015 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 b0cf29958..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 - 2015 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,9 @@ */ public interface InputService extends SciJavaService { - EventService getEventService(); + default EventService eventService() { + return context().getService(EventService.class); + } InputModifiers getModifiers(); @@ -98,4 +98,11 @@ public interface InputService extends SciJavaService { */ boolean isButtonDown(int button); + // -- Deprecated methods -- + + /** @deprecated Use {@link #eventService()} instead. */ + @Deprecated + default EventService getEventService() { + return eventService(); + } } diff --git a/src/main/java/org/scijava/input/KeyCode.java b/src/main/java/org/scijava/input/KeyCode.java index fe8b8d4a7..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 - 2015 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,46 +563,46 @@ 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); private static final Map CODES = - new HashMap(); + new HashMap<>(); private static final Map NAMES = - new HashMap(); + new HashMap<>(); static { for (final KeyCode keyCode : values()) { @@ -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 39e7f3061..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 - 2015 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/AbstractDataHandle.java b/src/main/java/org/scijava/io/AbstractDataHandle.java deleted file mode 100644 index cfb917810..000000000 --- a/src/main/java/org/scijava/io/AbstractDataHandle.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * #%L - * SciJava Common shared library for SciJava software. - * %% - * Copyright (C) 2009 - 2015 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.IOException; -import java.io.InputStreamReader; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import org.scijava.plugin.AbstractWrapperPlugin; - -/** - * Abstract base class for {@link DataHandle} plugins. - * - * @author Curtis Rueden - */ -public abstract class AbstractDataHandle extends - AbstractWrapperPlugin implements DataHandle -{ - - // -- Constants -- - - /** Block size to use when searching through the stream. */ - private static final int DEFAULT_BLOCK_SIZE = 256 * 1024; // 256 KB - - /** Maximum number of bytes to search when searching through the stream. */ - private static final int MAX_SEARCH_SIZE = 512 * 1024 * 1024; // 512 MB - - // -- Fields -- - - private ByteOrder order = ByteOrder.BIG_ENDIAN; - private String encoding = "UTF-8"; - - // -- DataHandle methods -- - - @Override - public ByteOrder getOrder() { - return order; - } - - @Override - public boolean isLittleEndian() { - return getOrder() == ByteOrder.LITTLE_ENDIAN; - } - - @Override - public void setOrder(final ByteOrder order) { - this.order = order; - } - - @Override - public void setOrder(final boolean little) { - setOrder(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); - } - - @Override - public String getEncoding() { - return encoding; - } - - @Override - public void setEncoding(final String encoding) { - this.encoding = encoding; - } - - @Override - public int read(final ByteBuffer buf) throws IOException { - return read(buf, buf.remaining()); - } - - @Override - public 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; - } - - @Override - public void write(final ByteBuffer buf) throws IOException { - write(buf, buf.remaining()); - } - - @Override - public 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); - } - } - - @Override - public String readCString() throws IOException { - final String line = findString("\0"); - return line.length() == 0 ? null : line; - } - - @Override - public 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, encoding); - } - - @Override - public 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); - } - - @Override - public String findString(final String... terminators) throws IOException { - return findString(true, DEFAULT_BLOCK_SIZE, terminators); - } - - @Override - public String findString(final boolean saveString, - final String... terminators) throws IOException - { - return findString(saveString, DEFAULT_BLOCK_SIZE, terminators); - } - - @Override - public String findString(final int blockSize, final String... terminators) - throws IOException - { - return findString(true, blockSize, terminators); - } - - @Override - public 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 -- - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0, b.length); - } - - @Override - public 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/AbstractIOPlugin.java b/src/main/java/org/scijava/io/AbstractIOPlugin.java index 9091ae38f..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 - 2015 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,56 +29,60 @@ package org.scijava.io; -import java.io.IOException; - +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 { - // -- IOPlugin methods -- + @Parameter + private LocationService locationService; @Override public boolean supportsOpen(final String source) { - return false; + try { + return supportsOpen(locationService.resolve(source)); + } catch (URISyntaxException e) { + return false; + } } @Override public boolean supportsSave(final String destination) { - return false; - } - - @Override - public boolean supportsSave(final Object data, final String destination) { - return supportsSave(destination) && getDataType().isInstance(data); - } - - @Override - public D open(final String source) throws IOException { - throw new UnsupportedOperationException(); + try { + return supportsSave(locationService.resolve(destination)); + } catch (URISyntaxException e) { + return false; + } } @Override public void save(final D data, final String destination) throws IOException { - throw new UnsupportedOperationException(); - } - - // -- Typed methods -- - - @Override - public boolean supports(final String descriptor) { - return supportsOpen(descriptor) || supportsSave(descriptor); + try { + save(data, locationService.resolve(destination)); + } catch (URISyntaxException e) { + throw new IOException(e); + } } @Override - public Class getType() { - return String.class; + 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 24162ef77..000000000 --- a/src/main/java/org/scijava/io/DataHandle.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * #%L - * SciJava Common shared library for SciJava software. - * %% - * Copyright (C) 2009 - 2015 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.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 -{ - - /** 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. */ - boolean isLittleEndian(); - - /** - * Sets the byte order of the stream. - * - * @param order Order to set. - */ - void setOrder(ByteOrder order); - - /** Sets the endianness of the stream. */ - void setOrder(final boolean little); - - /** 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}. - */ - int read(ByteBuffer buf) throws IOException; - - /** - * 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. - */ - int read(ByteBuffer buf, int len) throws IOException; - - /** - * 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. - */ - void write(ByteBuffer buf) throws IOException; - - /** - * Writes up to len bytes of data from the given ByteBuffer to the stream. - */ - void write(ByteBuffer buf, int len) throws IOException; - - /** Reads a string of arbitrary length, terminated by a null char. */ - String readCString() throws IOException; - - /** Reads a string of up to length n. */ - String readString(int n) throws IOException; - - /** - * Reads a string ending with one of the characters in the given string. - * - * @see #findString(String...) - */ - String readString(String lastChars) throws IOException; - - /** - * 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. - */ - String findString(String... terminators) throws IOException; - - /** - * 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. - */ - String findString(boolean saveString, String... terminators) - throws IOException; - - /** - * 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. - */ - String findString(int blockSize, String... terminators) throws IOException; - - /** - * 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. - */ - String findString(boolean saveString, int blockSize, String... terminators) - throws IOException; - - // -- 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. - */ - int read(byte[] b) throws IOException; - - /** - * 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. - */ - long skip(long n) throws IOException; - -} diff --git a/src/main/java/org/scijava/io/DefaultIOService.java b/src/main/java/org/scijava/io/DefaultIOService.java index 2ad822f72..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 - 2015 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,62 +59,76 @@ public final class DefaultIOService @Parameter private EventService eventService; - // -- IOService methods -- - + @Parameter + private LocationService locationService; + @Override - public IOPlugin getOpener(final String source) { - for (final IOPlugin handler : getInstances()) { - if (handler.supportsOpen(source)) return handler; + public IOPlugin getOpener(final String source) throws IOException { + try { + return getOpener(locationService.resolve(source)); + } catch (URISyntaxException e) { + throw new IOException(e); } - return null; } @Override - public IOPlugin getSaver(final D data, final String destination) { - for (final IOPlugin handler : getInstances()) { - if (handler.supportsSave(data, destination)) { - @SuppressWarnings("unchecked") - final IOPlugin typedHandler = (IOPlugin) handler; - return typedHandler; - } + public IOPlugin getSaver(D data, String destination) throws IOException { + try { + return getSaver(data, locationService.resolve(destination)); + } catch (URISyntaxException e) { + throw new IOException(e); } - return null; } @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() + "."); } } - - // -- HandlerService methods -- - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Class> getPluginType() { - return (Class) IOPlugin.class; - } - - @Override - public Class getType() { - return String.class; - } - + } diff --git a/src/main/java/org/scijava/io/DefaultRecentFileService.java b/src/main/java/org/scijava/io/DefaultRecentFileService.java index ce0ddd9e6..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 - 2015 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; @@ -147,7 +147,7 @@ public boolean remove(final String path) { @Override public void clear() { recentFiles.clear(); - prefService.clear(RECENT_FILES_KEY); + prefService.clear(RecentFileService.class, RECENT_FILES_KEY); // unregister the modules with the module service moduleService.removeModules(recentModules.values()); @@ -165,7 +165,7 @@ public List getRecentFiles() { @Override public void initialize() { loadList(); - recentModules = new HashMap(); + recentModules = new HashMap<>(); for (final String path : recentFiles) { recentModules.put(path, createInfo(path)); } @@ -174,23 +174,32 @@ 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 -- /** Loads the list of recent files from persistent storage. */ private void loadList() { - recentFiles = prefService.getList(RECENT_FILES_KEY); + recentFiles = prefService.getList(RecentFileService.class, + RECENT_FILES_KEY); } /** Saves the list of recent files to persistent storage. */ private void saveList() { - prefService.putList(recentFiles, RECENT_FILES_KEY); + prefService.putList(RecentFileService.class, recentFiles, RECENT_FILES_KEY); } /** Creates a {@link ModuleInfo} to reopen data at the given path. */ @@ -201,7 +210,7 @@ private ModuleInfo createInfo(final String path) { final CommandInfo info = new CommandInfo(commandClassName); // hard code path to open as a preset - final HashMap presets = new HashMap(); + final HashMap presets = new HashMap<>(); presets.put("inputFile", path); info.setPresets(presets); diff --git a/src/main/java/org/scijava/io/IOPlugin.java b/src/main/java/org/scijava/io/IOPlugin.java index aacaade90..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 - 2015 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,27 +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. */ - boolean supportsOpen(String source); + 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. */ - boolean supportsSave(String destination); + 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. */ - boolean supportsSave(Object data, String destination); + 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. */ - D open(String source) throws IOException; + @SuppressWarnings("unused") + default D open(final String source) throws IOException { + throw new UnsupportedOperationException(); + } + + /** 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. */ - void save(D data, String destination) throws IOException; + 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 -- + + default boolean supports(final String descriptor) { + return supportsOpen(descriptor) || supportsSave(descriptor); + } + @Override + 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 b82fd0f1e..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 - 2015 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,24 +39,48 @@ * 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. */ - IOPlugin getOpener(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; + } + return null; + } + + /** + * Gets the most appropriate {@link IOPlugin} for saving data to the given + * location. + */ + IOPlugin getSaver(final D data, final String destination) throws IOException; /** * Gets the most appropriate {@link IOPlugin} for saving data to the given - * destination. + * location. */ - IOPlugin getSaver(D data, String destination); + default IOPlugin getSaver(D data, Location destination) { + for (final IOPlugin handler : getInstances()) { + if (handler.supportsSave(data, destination)) { + @SuppressWarnings("unchecked") + final IOPlugin typedHandler = (IOPlugin) handler; + return typedHandler; + } + } + return null; + } /** * Loads data from the given source. For extensibility, the nature of the @@ -67,7 +90,7 @@ public interface IOService extends HandlerService>, * 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 @@ -76,6 +99,22 @@ public interface IOService extends HandlerService>, */ 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. @@ -83,7 +122,7 @@ public interface IOService extends HandlerService>, * 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. @@ -91,4 +130,31 @@ public interface IOService extends HandlerService>, */ 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 + @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/RecentFileService.java b/src/main/java/org/scijava/io/RecentFileService.java index 6f1d606bf..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 - 2015 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 1f5457e6b..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 - 2015 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,15 +49,24 @@ @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 -- + + public OpenArgument() { + super(2, "--open"); + } + // -- ConsoleArgument methods -- @Override @@ -68,20 +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 args != null && args.size() >= 2 && args.getFirst().equals("--open"); + 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 03028ecb2..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 - 2015 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 8f5f2955a..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 - 2015 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 d678ae19c..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 - 2015 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/handle/AbstractDataHandle.java b/src/main/java/org/scijava/io/handle/AbstractDataHandle.java new file mode 100644 index 000000000..f2446f90a --- /dev/null +++ b/src/main/java/org/scijava/io/handle/AbstractDataHandle.java @@ -0,0 +1,78 @@ +/* + * #%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 org.scijava.io.location.Location; +import org.scijava.plugin.AbstractWrapperPlugin; + +/** + * Abstract base class for {@link DataHandle} plugins. + * + * @author Curtis Rueden + */ +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; + private String encoding = "UTF-8"; + + // -- DataHandle methods -- + + @Override + public ByteOrder getOrder() { + return order; + } + + @Override + public void setOrder(final ByteOrder order) { + this.order = order; + } + + @Override + public String getEncoding() { + return encoding; + } + + @Override + public void setEncoding(final String encoding) { + this.encoding = encoding; + } + +} 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 757c4b738..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 - 2015 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 1d341f449..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 - 2015 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 42ec2d104..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 - 2015 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 78% rename from src/main/java/org/scijava/io/DefaultDataHandleService.java rename to src/main/java/org/scijava/io/handle/DefaultDataHandleService.java index 2be00711f..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 - 2015 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; @@ -45,20 +44,5 @@ public class DefaultDataHandleService extends AbstractWrapperService> implements DataHandleService { - - // -- PTService methods -- - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Class> getPluginType() { - return (Class) DataHandle.class; - } - - // -- Typed methods -- - - @Override - public Class getType() { - return Location.class; - } - + // NB: No implementation needed. } 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 045c254fd..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 - 2015 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/location/DummyLocation.java b/src/main/java/org/scijava/io/location/DummyLocation.java new file mode 100644 index 000000000..0a9dd546f --- /dev/null +++ b/src/main/java/org/scijava/io/location/DummyLocation.java @@ -0,0 +1,39 @@ +/* + * #%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; + +/** + * {@link Location} backed by nothing whatsoever. + * + * @author Curtis Rueden + */ +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 63% rename from src/main/java/org/scijava/io/Location.java rename to src/main/java/org/scijava/io/location/Location.java index 4f08be1b9..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 - 2015 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 { @@ -53,6 +54,34 @@ public interface Location { * Gets the location expressed as a {@link URI}, or null if the location * cannot be expressed as such. */ - URI getURI(); + 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 91% rename from src/main/java/org/scijava/io/URILocation.java rename to src/main/java/org/scijava/io/location/URILocation.java index 87f9e7d4d..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 - 2015 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; @@ -92,7 +90,7 @@ public URI getURI() { * @return A map of the decoded key/value pairs. */ private Map decodeQuery(final String query) { - final Map map = new LinkedHashMap(); + final Map map = new LinkedHashMap<>(); if (query == null) return map; for (final String param : query.split("&")) { final int equals = param.indexOf("="); @@ -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 4148d668a..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 - 2015 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 96dc08240..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 - 2015 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,260 +36,160 @@ import org.scijava.service.AbstractService; /** - * Base implementation of an abstract {@link LogService}. - * + * Base class for {@link LogService} implementations. + * * @author Johannes Schindelin + * @author Curtis Rueden + * @author Matthias Arzt */ -public abstract class AbstractLogService extends AbstractService implements LogService { - - private int currentLevel = System.getenv("DEBUG") == null ? INFO : DEBUG; +@IgnoreAsCallingClass +public abstract class AbstractLogService extends AbstractService implements + LogService +{ - private Map classAndPackageLevels = - new HashMap(); + private int currentLevel = levelFromEnvironment(); - // -- abstract methods -- + private final Map classAndPackageLevels; - /** - * 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))); - } - - } - - // -- 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); + this(System.getProperties()); } - protected String getPrefix(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 -- - - @Override - public void debug(Object msg) { - log(DEBUG, msg, null); - } + // -- AbstractLogService methods -- @Override - public void debug(Throwable t) { - log(DEBUG, null, t); + public void setLevel(final int level) { + currentLevel = level; } @Override - public void debug(Object msg, Throwable t) { - log(DEBUG, msg, t); + public void setLevel(final String classOrPackageName, final int level) { + classAndPackageLevels.put(classOrPackageName, level); } @Override - public void error(Object msg) { - log(ERROR, msg, null); + public void setLevelForLogger(final String source, final int level) { + rootLogger.getSource().subSource(source).setLogLevel(level); } - @Override - public void error(Throwable t) { - log(ERROR, null, t); - } + abstract protected void messageLogged(LogMessage message); - @Override - public void error(Object msg, Throwable t) { - log(ERROR, msg, t); - } + // -- Logger methods -- @Override - public void info(Object msg) { - log(INFO, msg, null); + public void alwaysLog(final int level, final Object msg, final Throwable t) { + rootLogger.alwaysLog(level, msg, t); } @Override - public void info(Throwable t) { - log(INFO, null, t); + public LogSource getSource() { + return rootLogger.getSource(); } @Override - public void info(Object msg, Throwable t) { - log(INFO, msg, t); + public int getLevel() { + if (classAndPackageLevels.isEmpty()) return currentLevel; + return getLevelForClass(CallingClassUtils.getCallingClassName(), + currentLevel); } @Override - public void trace(Object msg) { - log(TRACE, msg, null); + public Logger subLogger(String name, int level) { + return rootLogger.subLogger(name, level); } @Override - public void trace(Throwable t) { - log(TRACE, null, t); + public void addLogListener(final LogListener listener) { + rootLogger.addLogListener(listener); } @Override - public void trace(Object msg, Throwable t) { - log(TRACE, msg, t); + public void removeLogListener(final LogListener listener) { + rootLogger.removeLogListener(listener); } @Override - public void warn(Object msg) { - log(WARN, msg, null); + public void notifyListeners(final LogMessage event) { + rootLogger.notifyListeners(event); } - @Override - public void warn(Throwable t) { - log(WARN, null, t); - } + // -- Deprecated -- - @Override - public void warn(Object msg, 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) { - 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 9270e1f9f..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 - 2015 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 190077f0f..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 - 2015 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,67 +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 new file mode 100644 index 000000000..bee37e0fa --- /dev/null +++ b/src/main/java/org/scijava/log/Logged.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; + +/** + * Interface for the objects which want to log their activities. + * + * @author Curtis Rueden + */ +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 9524a5427..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 - 2015 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,37 +29,37 @@ 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; /** * Implementation of {@link LogService} using the standard error stream. + *

    + * Actually, this service is somewhat misnamed now, since it prints {@code WARN} + * and {@code ERROR} messages to stderr, but messages at lesser severities to + * stdout. + *

    * * @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 { - /** - * Prints a message to stderr. - * - * @param message the message - */ - @Override - protected void log(final String message) { - System.err.println(message); + private Function levelToStream = + level -> (level <= LogLevel.WARN) ? System.err : System.out; + + 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 new file mode 100644 index 000000000..699a78132 --- /dev/null +++ b/src/main/java/org/scijava/main/DefaultMainService.java @@ -0,0 +1,123 @@ +/* + * #%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.main; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +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; +import org.scijava.util.Types; + +/** + * Default implementation of {@link MainService}. + * + * @author Curtis Rueden + */ +@Plugin(type = Service.class) +public class DefaultMainService extends AbstractService implements MainService { + + @Parameter(required = false) + private LogService log; + + private final List
    mains = new ArrayList<>(); + + @Override + public int execMains() { + int mainCount = 0; + for (final Main main : mains) { + main.exec(); + mainCount++; + } + return mainCount; + } + + @Override + public void addMain(final String className, final String... args) { + mains.add(new DefaultMain(className, args)); + } + + @Override + public Main[] getMains() { + return mains.toArray(new Main[mains.size()]); + } + + // -- Helper classes -- + + /** Default implementation of {@link MainService.Main}. */ + private class DefaultMain implements Main { + private String className; + private String[] args; + + public DefaultMain(final String className, final String... args) { + this.className = className; + this.args = args.clone(); + } + + @Override + public String className() { + return className; + } + + @Override + public String[] args() { + return args; + } + + @Override + public void exec() { + try { + final Class mainClass = Types.load(className, false); + final Method main = mainClass.getMethod("main", String[].class); + main.invoke(null, new Object[] { args }); + } + catch (final IllegalArgumentException exc) { + if (log != null) log.error(exc); + } + catch (final NoSuchMethodException exc) { + if (log != null) { + log.error("No main method for class: " + className, exc); + } + } + catch (final IllegalAccessException exc) { + if (log != null) log.error(exc); + } + catch (final InvocationTargetException exc) { + if (log != null) log.error(exc); + } + } + } + +} diff --git a/src/main/java/org/scijava/main/MainService.java b/src/main/java/org/scijava/main/MainService.java new file mode 100644 index 000000000..36b92b89b --- /dev/null +++ b/src/main/java/org/scijava/main/MainService.java @@ -0,0 +1,67 @@ +/* + * #%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.main; + +import org.scijava.service.SciJavaService; + +/** + * Interface for services which manage dynamic execution of main methods. + * + * @author Curtis Rueden + */ +public interface MainService extends SciJavaService { + + /** + * Executes registered main classes, in the order they were registered. + * + * @return The number of main methods which were executed. + */ + int execMains(); + + /** Registers a main class to be executed by {@link #execMains()}. */ + void addMain(final String className, final String... args); + + /** Gets the registered main classes to execute. */ + Main[] getMains(); + + /** Data structure containing main class and argument values. */ + interface Main { + + /** Gets the name of the class containing the {@code main} method to run. */ + String className(); + + /** Gets the arguments to pass to the class's {@code main} method. */ + String[] args(); + + /** Runs the {@code main} method with the associated arguments. */ + void exec(); + } + +} diff --git a/src/main/java/org/scijava/main/console/MainArgument.java b/src/main/java/org/scijava/main/console/MainArgument.java new file mode 100644 index 000000000..0e0ebe29d --- /dev/null +++ b/src/main/java/org/scijava/main/console/MainArgument.java @@ -0,0 +1,97 @@ +/* + * #%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.main.console; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.scijava.console.AbstractConsoleArgument; +import org.scijava.console.ConsoleArgument; +import org.scijava.log.LogService; +import org.scijava.main.MainService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +/** + * Handles the {@code --main} command line argument, which launches an + * alternative main class. + * + * @author Curtis Rueden + */ +@Plugin(type = ConsoleArgument.class) +public class MainArgument extends AbstractConsoleArgument { + + @Parameter(required = false) + private MainService mainService; + + @Parameter(required = false) + private LogService log; + + // -- Constructor -- + + public MainArgument() { + super(2, "--main", "--main-class"); + } + + // -- ConsoleArgument methods -- + + @Override + public void handle(final LinkedList args) { + if (!supports(args)) return; + + args.removeFirst(); // --main / --main-class + final String className = args.removeFirst(); + + final List argList = new ArrayList<>(); + while (!args.isEmpty() && !isFlag(args) && !isSeparator(args)) { + argList.add(args.removeFirst()); + } + if (isSeparator(args)) args.removeFirst(); // remove the -- separator + final String[] mainArgs = argList.toArray(new String[argList.size()]); + + mainService.addMain(className, mainArgs); + } + + // -- Typed methods -- + + @Override + public boolean supports(final LinkedList args) { + return mainService != null && super.supports(args); + } + + // -- Helper methods -- + + private boolean isSeparator(final LinkedList args) { + if (args == null || args.isEmpty()) return false; + return args.getFirst().equals("--"); + } + +} diff --git a/src/main/java/org/scijava/main/run/MainCodeRunner.java b/src/main/java/org/scijava/main/run/MainCodeRunner.java new file mode 100644 index 000000000..a18ae6b3b --- /dev/null +++ b/src/main/java/org/scijava/main/run/MainCodeRunner.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.main.run; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; + +import org.scijava.Priority; +import org.scijava.log.LogService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.run.AbstractCodeRunner; +import org.scijava.run.CodeRunner; +import org.scijava.util.Types; + +/** + * Executes the given class's {@code main} method. + * + * @author Curtis Rueden + */ +@Plugin(type = CodeRunner.class, priority = Priority.LOW) +public class MainCodeRunner extends AbstractCodeRunner { + + @Parameter(required = false) + private LogService log; + + // -- CodeRunner methods -- + + @Override + public void run(final Object code, final Object... args) + throws InvocationTargetException + { + final Object[] sArgs = stringify(args); + try { + getMain(code).invoke(null, new Object[] { sArgs }); + } + catch (final IllegalArgumentException exc) { + throw new InvocationTargetException(exc); + } + catch (final IllegalAccessException exc) { + throw new InvocationTargetException(exc); + } + } + + @Override + public void run(final Object code, final Map inputMap) + throws InvocationTargetException + { + throw new UnsupportedOperationException( + "Cannot execute main method with a map of inputs"); + } + + // -- Typed methods -- + + @Override + public boolean supports(final Object code) { + return getMain(code) != null; + } + + // -- Helper methods -- + + private Method getMain(final Object code) { + final Class c = getClass(code); + if (c == null) return null; + try { + return c.getMethod("main", String[].class); + } + catch (final SecurityException exc) { + if (log != null) log.debug(exc); + return null; + } + catch (final NoSuchMethodException exc) { + if (log != null) log.debug(exc); + return null; + } + } + + private Class getClass(final Object code) { + if (code instanceof Class) return (Class) code; + if (code instanceof String) return Types.load((String) code); + return null; + } + + /** Ensures each element is a {@link String}. */ + private String[] stringify(final Object... o) { + final String[] s; + if (o == null) s = null; + else { + s = new String[o.length]; + for (int i = 0; i < o.length; i++) { + if (o[i] == null) s[i] = null; + else if (o[i] instanceof String) s[i] = (String) o[i]; + else s[i] = o[i].toString(); + } + } + return s; + } + +} diff --git a/src/main/java/org/scijava/menu/AbstractMenuCreator.java b/src/main/java/org/scijava/menu/AbstractMenuCreator.java index 0b5951096..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 - 2015 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 8072c50f6..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 - 2015 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,7 +35,6 @@ import java.util.List; import java.util.Map; -import org.scijava.UIDetails; import org.scijava.event.EventHandler; import org.scijava.event.EventService; import org.scijava.module.ModuleInfo; @@ -71,51 +68,30 @@ public class DefaultMenuService extends AbstractService implements MenuService // -- MenuService methods -- - @Override - public ShadowMenu getMenu() { - return getMenu(UIDetails.APPLICATION_MENU_ROOT); - } - @Override public ShadowMenu getMenu(final String menuRoot) { return rootMenus().get(menuRoot); } - @Override - public T createMenus(final MenuCreator creator, final T menu) { - return createMenus(UIDetails.APPLICATION_MENU_ROOT, creator, menu); - } - - @Override - public T createMenus(final String menuRoot, - final MenuCreator creator, final T menu) - { - creator.createMenus(getMenu(menuRoot), menu); - return menu; - } - // -- 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()); } @@ -145,12 +121,12 @@ private synchronized void addModules(final Collection items, { // categorize modules by menu root final HashMap> modulesByMenuRoot = - new HashMap>(); + new HashMap<>(); for (final ModuleInfo info : items) { final String menuRoot = info.getMenuRoot(); ArrayList modules = modulesByMenuRoot.get(menuRoot); if (modules == null) { - modules = new ArrayList(); + modules = new ArrayList<>(); modulesByMenuRoot.put(menuRoot, modules); } modules.add(info); @@ -183,16 +159,14 @@ private synchronized void addModules(final Collection items, *

    */ private HashMap rootMenus() { - if (rootMenus == null) { - initRootMenus(); - } + if (rootMenus == null) initRootMenus(); return rootMenus; } /** Initializes {@link #rootMenus}. */ private synchronized void initRootMenus() { if (rootMenus != null) return; - final HashMap map = new HashMap(); + final HashMap map = new HashMap<>(); final List allModules = moduleService.getModules(); addModules(allModules, map); diff --git a/src/main/java/org/scijava/menu/MenuConstants.java b/src/main/java/org/scijava/menu/MenuConstants.java index 101e1d125..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 - 2015 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 872efd0a0..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 - 2015 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 5970c3db8..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 - 2015 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,7 @@ package org.scijava.menu; +import org.scijava.UIDetails; import org.scijava.module.ModuleInfo; import org.scijava.service.SciJavaService; @@ -42,7 +41,9 @@ public interface MenuService extends SciJavaService { /** Gets the root node of the application menu structure. */ - ShadowMenu getMenu(); + default ShadowMenu getMenu() { + return getMenu(UIDetails.APPLICATION_MENU_ROOT); + } /** * Gets the root node of a menu structure. @@ -58,7 +59,9 @@ public interface MenuService extends SciJavaService { * @param creator the {@link MenuCreator} to use to populate the menus. * @param menu the destination menu structure to populate. */ - T createMenus(MenuCreator creator, T menu); + default T createMenus(final MenuCreator creator, final T menu) { + return createMenus(UIDetails.APPLICATION_MENU_ROOT, creator, menu); + } /** * Populates a UI-specific menu structure. @@ -68,6 +71,10 @@ public interface MenuService extends SciJavaService { * @param creator the {@link MenuCreator} to use to populate the menus. * @param menu the destination menu structure to populate. */ - T createMenus(String menuRoot, MenuCreator creator, T menu); - + default T createMenus(final String menuRoot, + final MenuCreator creator, final T menu) + { + creator.createMenus(getMenu(menuRoot), menu); + return menu; + } } diff --git a/src/main/java/org/scijava/menu/ShadowMenu.java b/src/main/java/org/scijava/menu/ShadowMenu.java index 4ba28d799..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 - 2015 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 @@ -135,7 +134,7 @@ private ShadowMenu(final Context context, final ModuleInfo moduleInfo, } this.menuDepth = menuDepth; this.parent = parent; - children = new HashMap(); + children = new HashMap<>(); } // -- ShadowMenu methods -- @@ -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; @@ -187,7 +187,7 @@ public ShadowMenu getParent() { public List getChildren() { // copy the children table into an ordered list final List childList = - new ArrayList(children.values()); + new ArrayList<>(children.values()); // sort the list by weight then alphabetically Collections.sort(childList); return childList; @@ -230,14 +230,28 @@ public URL getIconURL() { if (isLeaf()) iconPath = DEFAULT_ICON_PATH; else return null; } - final String className = moduleInfo.getDelegateClassName(); - final Class c = ClassUtils.loadClass(className); - if (c == null) return null; - final URL iconURL = c.getResource(iconPath); - if (iconURL == null) { - if (log != null) log.error("Could not load icon: " + iconPath); + try { + 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: " + + moduleInfo.getDelegateClassName(); + if (log.isDebug()) log.debug(message, exc); + else log.error(message); + return null; } - return iconURL; } /** @@ -263,7 +277,7 @@ public boolean update(final ModuleInfo module) { * @return true if at least one module was successfully updated */ public boolean updateAll(final Collection c) { - final HashSet nodes = new HashSet(); + final HashSet nodes = new HashSet<>(); for (final ModuleInfo info : c) { final ShadowMenu removed = removeInternal(info); if (removed == null) continue; // was not in menu structure @@ -354,7 +368,7 @@ public boolean add(final ModuleInfo o) { */ @Override public boolean addAll(final Collection c) { - final HashSet nodes = new HashSet(); + final HashSet nodes = new HashSet<>(); for (final ModuleInfo info : c) { if (!info.isVisible()) continue; final ShadowMenu node = addInternal(info); @@ -409,7 +423,7 @@ public boolean remove(final Object o) { @Override public boolean removeAll(final Collection c) { - final HashSet nodes = new HashSet(); + final HashSet nodes = new HashSet<>(); for (final Object o : c) { if (!(o instanceof ModuleInfo)) continue; final ModuleInfo info = (ModuleInfo) o; @@ -423,7 +437,7 @@ public boolean removeAll(final Collection c) { @Override public boolean retainAll(final Collection c) { - final ArrayList toRemove = new ArrayList(); + final ArrayList toRemove = new ArrayList<>(); for (final ModuleInfo info : this) { if (!c.contains(info)) toRemove.add(info); } @@ -483,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); } @@ -539,14 +553,10 @@ 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: " + - childInfo + "\n\t ignored: " + info); - } - else { - log.debug("ShadowMenu: higher-priority menu item already exists:\n" + - "\texisting: " + childInfo + "\n\t ignored: " + info); + if (log.isDebug()) { + log.debug("ShadowMenu: menu item already exists:\n" + // + "\texisting: " + details(childInfo) + "\n" + // + "\t ignored: " + details(info)); } } } @@ -557,6 +567,21 @@ private boolean isLeaf(final int depth, final MenuPath path) { return depth == path.size() - 1; } + private String details(final ModuleInfo info) { + if (info == null) return ""; + String className, classLocation; + try { + final Class c = info.loadDelegateClass(); + className = c.getName(); + classLocation = Types.location(c).toString(); + } + catch (final ClassNotFoundException exc) { + className = info.getDelegateClassName(); + classLocation = ""; + } + return info.getMenuPath() + " : " + className + " [" + classLocation + "]"; + } + private ShadowMenu getMenu(final MenuPath menuPath, final int index) { final MenuEntry entry = menuPath.get(index); diff --git a/src/main/java/org/scijava/menu/ShadowMenuIterator.java b/src/main/java/org/scijava/menu/ShadowMenuIterator.java index cb9e48353..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 - 2015 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 @@ public class ShadowMenuIterator implements Iterator { public ShadowMenuIterator(final ShadowMenu node) { this.node = node; final List children = node.getChildren(); - childIterators = new ArrayList(); + childIterators = new ArrayList<>(); for (final ShadowMenu child : children) { childIterators.add(new ShadowMenuIterator(child)); } diff --git a/src/main/java/org/scijava/menu/event/MenuEvent.java b/src/main/java/org/scijava/menu/event/MenuEvent.java index 2d5ac867b..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 - 2015 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 a9a7163ad..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 - 2015 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 b097317e1..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 - 2015 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 9980be59d..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 - 2015 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 b4c0c1922..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 - 2015 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. *

    @@ -48,15 +48,16 @@ public abstract class AbstractModule implements Module { private final HashMap inputs; private final HashMap outputs; - /** Table indicating resolved inputs. */ private final HashSet resolvedInputs; + private final HashSet resolvedOutputs; private MethodRef initializerRef; public AbstractModule() { - inputs = new HashMap(); - outputs = new HashMap(); - resolvedInputs = new HashSet(); + inputs = new HashMap<>(); + outputs = new HashMap<>(); + resolvedInputs = new HashSet<>(); + resolvedOutputs = new HashSet<>(); } // -- Module methods -- @@ -75,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()) { @@ -137,14 +143,48 @@ public void setOutputs(final Map outputs) { } @Override - public boolean isResolved(final String name) { + public boolean isInputResolved(final String name) { return resolvedInputs.contains(name); } @Override - public void setResolved(final String name, final boolean resolved) { - if (resolved) resolvedInputs.add(name); - else resolvedInputs.remove(name); + public boolean isOutputResolved(final String name) { + return resolvedOutputs.contains(name); + } + + @Override + public void resolveInput(final String name) { + final ModuleItem item = getInputItem(name); + if (item != null) { + try { + item.validate(this); + } + catch (final MethodCallException 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); + } + + @Override + public void resolveOutput(final String name) { + resolvedOutputs.add(name); + } + + @Override + public void unresolveInput(final String name) { + resolvedInputs.remove(name); + } + + @Override + public void unresolveOutput(final String name) { + resolvedOutputs.remove(name); } // -- Helper methods -- @@ -152,7 +192,7 @@ public void setResolved(final String name, final boolean resolved) { private Map createMap(final Iterable> items, final boolean outputMap) { - final Map map = new HashMap(); + final Map map = new HashMap<>(); for (final ModuleItem item : items) { final String name = item.getName(); final Object value = outputMap ? getOutput(name) : getInput(name); @@ -161,4 +201,10 @@ private Map createMap(final Iterable> items, return map; } + private ModuleItem getInputItem(final String name) { + for (final ModuleItem item : getInfo().inputs()) { + if (item.getName().equals(name)) return item; + } + return null; + } } diff --git a/src/main/java/org/scijava/module/AbstractModuleInfo.java b/src/main/java/org/scijava/module/AbstractModuleInfo.java index 551664197..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 - 2015 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,15 +36,7 @@ import java.util.Map; import org.scijava.AbstractUIDetails; -import org.scijava.Identifiable; -import org.scijava.Locatable; -import org.scijava.ValidityProblem; -import org.scijava.Versioned; -import org.scijava.event.EventService; -import org.scijava.module.event.ModulesUpdatedEvent; -import org.scijava.util.ClassUtils; -import org.scijava.util.ConversionUtils; -import org.scijava.util.VersionUtils; +import org.scijava.util.Types; /** * Abstract superclass of {@link ModuleInfo} implementation. @@ -58,7 +48,7 @@ * @author Curtis Rueden */ public abstract class AbstractModuleInfo extends AbstractUIDetails implements - ModuleInfo, Identifiable, Locatable, Versioned + ModuleInfo { /** Table of inputs, keyed on name. */ @@ -108,102 +98,6 @@ public Iterable> outputs() { return Collections.unmodifiableList(outputList()); } - @Override - public boolean isInteractive() { - return false; - } - - @Override - public boolean canPreview() { - return false; - } - - @Override - public boolean canCancel() { - return true; - } - - @Override - public boolean canRunHeadless() { - return false; - } - - @Override - public String getInitializer() { - return null; - } - - @Override - public void update(final EventService eventService) { - eventService.publish(new ModulesUpdatedEvent(this)); - } - - // -- UIDetails methods -- - - @Override - public String getTitle() { - final String title = super.getTitle(); - if (!title.equals(getClass().getSimpleName())) return title; - - // use delegate class name rather than actual class name - final String className = getDelegateClassName(); - final int dot = className.lastIndexOf("."); - return dot < 0 ? className : className.substring(dot + 1); - } - - // -- Validated methods -- - - @Override - public boolean isValid() { - return true; - } - - @Override - public List getProblems() { - return null; - } - - // -- Identifiable methods -- - - @Override - public String getIdentifier() { - // NB: By default, we assume that the delegate class name uniquely - // distinguishes the module from others. If the same delegate class is used - // for more than one module, though, it may need to override this method to - // provide more differentiating details. - return "module:" + getDelegateClassName(); - } - - // -- Locatable methods -- - - @Override - public String getLocation() { - // NB: By default, we use the location of the delegate class. - // 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(); - } - catch (final ClassNotFoundException exc) { - return null; - } - } - - // -- Versioned methods -- - - @Override - public String getVersion() { - // NB: By default, we use the version of the delegate class's JAR archive. - // If the same delegate class is used for more than one module, though, - // it may need to override this method to indicate a different version. - try { - return VersionUtils.getVersion(loadDelegateClass()); - } - catch (final ClassNotFoundException exc) { - return null; - } - } - // -- Internal methods -- /** @@ -264,6 +158,22 @@ protected List> outputList() { return outputList; } + // -- Helper methods - lazy initialization -- + + /** Initializes data structures and parses parameters. */ + private synchronized void initParameters() { + if (initialized) return; // already initialized + + inputMap = new HashMap<>(); + outputMap = new HashMap<>(); + inputList = new ArrayList<>(); + outputList = new ArrayList<>(); + + parseParameters(); + + initialized = true; + } + // -- Helper methods -- private ModuleItem castItem(final ModuleItem item, @@ -271,8 +181,8 @@ private ModuleItem castItem(final ModuleItem item, { final Class itemType = item.getType(); // if (!type.isAssignableFrom(itemType)) { - final Class saneItemType = ConversionUtils.getNonprimitiveType(itemType); - if (!ConversionUtils.canCast(type, saneItemType)) { + 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()); } @@ -280,21 +190,4 @@ private ModuleItem castItem(final ModuleItem item, final ModuleItem typedItem = (ModuleItem) item; return typedItem; } - - // -- Helper methods - lazy initialization -- - - /** Initializes data structures and parses parameters. */ - private synchronized void initParameters() { - if (initialized) return; // already initialized - - inputMap = new HashMap>(); - outputMap = new HashMap>(); - inputList = new ArrayList>(); - outputList = new ArrayList>(); - - parseParameters(); - - initialized = true; - } - } diff --git a/src/main/java/org/scijava/module/AbstractModuleItem.java b/src/main/java/org/scijava/module/AbstractModuleItem.java index 678c08b3e..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 - 2015 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,16 +30,17 @@ package org.scijava.module; import java.lang.reflect.Type; +import java.util.Arrays; import java.util.List; 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. @@ -55,6 +54,7 @@ public abstract class AbstractModuleItem extends AbstractBasicDetails private final ModuleInfo info; private MethodRef initializerRef; + private MethodRef validaterRef; private MethodRef callbackRef; public AbstractModuleItem(final ModuleInfo info) { @@ -199,6 +199,25 @@ public void initialize(final Module module) throws MethodCallException { initializerRef.execute(module.getDelegateObject()); } + @Override + public String getValidater() { + return null; + } + + @Override + public void validate(final Module module) throws MethodCallException { + final Object delegateObject = module.getDelegateObject(); + if (validaterRef == null) { + validaterRef = new MethodRef(delegateObject.getClass(), getValidater()); + } + 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 public String getCallback() { return null; @@ -247,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()); } @@ -258,7 +277,8 @@ public int getColumnCount() { @Override public List getChoices() { - return null; + final T[] choices = getType().getEnumConstants(); + return choices == null ? null : Arrays.asList(choices); } @Override diff --git a/src/main/java/org/scijava/module/DefaultModuleService.java b/src/main/java/org/scijava/module/DefaultModuleService.java index dcdebc5de..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 - 2015 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,8 +31,11 @@ import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -59,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. @@ -73,7 +74,7 @@ public class DefaultModuleService extends AbstractService implements ModuleService { - @Parameter + @Parameter(required = false) private LogService log; @Parameter @@ -137,6 +138,16 @@ public List getModules() { return moduleIndex.getAll(); } + @Override + public ModuleInfo getModuleById(final String id) { + // TODO: Cache identifiers in a hash? + for (final ModuleInfo info : getModules()) { + final String infoID = info.getIdentifier(); + if (id.equals(infoID)) return info; + } + return null; + } + @Override public ModuleInfo getModuleForAccelerator(final Accelerator acc) { for (final ModuleInfo info : getModules()) { @@ -159,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; } @@ -240,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; } @@ -252,32 +263,44 @@ public M waitFor(final Future future) { public ModuleItem getSingleInput(final Module module, final Class type) { - return getSingleItem(module, type, module.getInfo().inputs()); + return getTypedSingleItem(module, type, module.getInfo().inputs()); } @Override public ModuleItem getSingleOutput(final Module module, final Class type) { - return getSingleItem(module, type, module.getInfo().outputs()); + return getTypedSingleItem(module, type, module.getInfo().outputs()); + } + + @Override + public ModuleItem getSingleInput(Module module, Collection> types) { + return getSingleItem(module, types, module.getInfo().inputs()); + } + + @Override + public ModuleItem getSingleOutput(Module module, Collection> types) { + return getSingleItem(module, types, module.getInfo().outputs()); } @Override public void save(final ModuleItem item, final T value) { if (!item.isPersisted()) return; - final String sValue = value == null ? "" : value.toString(); + if (Objects.equals(item.getDefaultValue(), 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 ? "" : 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 @@ -285,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; @@ -320,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 @@ -351,7 +377,7 @@ private List 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 @@ -366,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! @@ -378,10 +406,30 @@ private Module getRegisteredModuleInstance(final ModuleInfo info) { private Map createMap(final Object[] values) { if (values == null || values.length == 0) return null; - final HashMap inputMap = new HashMap(); + final HashMap inputMap = new HashMap<>(); + + if (values.length == 1 && values[0] instanceof Map) { + // NB: This hack works around an issue where some script languages, + // notably Jython but potentially others too, invoke the wrong run + // method when called with a map. The Object... varargs method is + // chosen instead of the Map method, with the map being passed as + // the sole element of the object array. The code below detects + // this situation, propagating the map entries into the new map. + final Map valueMap = (Map) values[0]; + for (final Object key : valueMap.keySet()) { + if (!(key instanceof String)) { + if (log != null) log.error("Invalid input name: " + key); + continue; + } + final String name = (String) key; + final Object value = valueMap.get(key); + inputMap.put(name, value); + } + return inputMap; + } 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 @@ -390,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; @@ -413,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; } @@ -421,30 +469,46 @@ 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; } } module.setInput(name, converted); - module.setResolved(name, true); + module.resolveInput(name); } } - private ModuleItem getSingleItem(final Module module, + private ModuleItem getTypedSingleItem(final Module module, final Class type, final Iterable> items) { - ModuleItem result = null; + Set> types = new HashSet<>(); + types.add(type); + @SuppressWarnings("unchecked") + ModuleItem result = (ModuleItem) getSingleItem(module, types, items); + return result; + } + + private ModuleItem getSingleItem(final Module module, + final Collection> types, final Iterable> items) + { + ModuleItem result = null; + for (final ModuleItem item : items) { final String name = item.getName(); - final boolean resolved = module.isResolved(name); - if (resolved) continue; // skip resolved inputs if (!item.isAutoFill()) continue; // skip unfillable inputs - if (!type.isAssignableFrom(item.getType())) continue; - if (result != null) return null; // multiple matching items - @SuppressWarnings("unchecked") - final ModuleItem typedItem = (ModuleItem) item; - result = typedItem; + if (module.isInputResolved(name)) continue; // skip resolved inputs + final Class itemType = item.getType(); + for (final Class type : types) { + if (type.isAssignableFrom(itemType)) { + if (result != null) return null; // multiple matching module items + result = item; + // This module item matches, so no need to check more classes. + break; + } + } } return result; } @@ -458,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 1fa266b0f..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 - 2015 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,60 +50,10 @@ public DefaultMutableModule(final MutableModuleInfo info) { info.setModuleClass(getClass()); } - // -- MutableModule methods -- - - @Override - public MutableModuleItem addInput(final String name, - final Class type) - { - final DefaultMutableModuleItem item = - new DefaultMutableModuleItem(this, name, type); - addInput(item); - return item; - } - - @Override - public void addInput(final ModuleItem input) { - getInfo().addInput(input); - } - - @Override - public MutableModuleItem addOutput(final String name, - final Class type) - { - final DefaultMutableModuleItem item = - new DefaultMutableModuleItem(this, name, type); - addOutput(item); - return item; - } - - @Override - public void addOutput(final ModuleItem output) { - getInfo().addOutput(output); - } - - @Override - public void removeInput(final ModuleItem input) { - getInfo().removeInput(input); - } - - @Override - public void removeOutput(final ModuleItem output) { - getInfo().removeOutput(output); - } - // -- Module methods -- @Override public MutableModuleInfo getInfo() { return info; } - - // -- Runnable methods -- - - @Override - public void run() { - // do nothing by default - } - } diff --git a/src/main/java/org/scijava/module/DefaultMutableModuleInfo.java b/src/main/java/org/scijava/module/DefaultMutableModuleInfo.java index 630fd311e..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 - 2015 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: @@ -87,28 +85,4 @@ public void removeOutput(final ModuleItem output) { outputMap().remove(output.getName()); outputList().remove(output); } - - // -- ModuleInfo methods -- - - @Override - public String getDelegateClassName() { - return getModuleClass().getName(); - } - - @Override - public Class loadDelegateClass() { - return getModuleClass(); - } - - @Override - public Module createModule() throws ModuleException { - try { - return getModuleClass().newInstance(); - } - catch (final Exception e) { - // NB: Several types of exceptions; simpler to handle them all the same. - throw new ModuleException(e); - } - } - } diff --git a/src/main/java/org/scijava/module/DefaultMutableModuleItem.java b/src/main/java/org/scijava/module/DefaultMutableModuleItem.java index 1ff2d6bbe..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 - 2015 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,10 +51,12 @@ 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; private String initializer; + private String validater; private String callback; private String widgetStyle; private T defaultValue; @@ -66,7 +66,7 @@ public class DefaultMutableModuleItem extends AbstractModuleItem private T softMaximum; private Number stepSize; private int columnCount; - private final List choices = new ArrayList(); + private final List choices = new ArrayList<>(); private String name; private String label; private String description; @@ -86,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(); @@ -112,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(); @@ -142,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; @@ -162,6 +171,11 @@ public void setInitializer(final String initializer) { this.initializer = initializer; } + @Override + public void setValidater(final String validater) { + this.validater = validater; + } + @Override public void setCallback(final String callback) { this.callback = callback; @@ -235,6 +249,11 @@ public ItemVisibility getVisibility() { return visibility; } + @Override + public boolean isAutoFill() { + return autoFill; + } + @Override public boolean isRequired() { return required; @@ -255,6 +274,11 @@ public String getInitializer() { return initializer; } + @Override + public String getValidater() { + return validater; + } + @Override public String getCallback() { return callback; diff --git a/src/main/java/org/scijava/module/MethodCallException.java b/src/main/java/org/scijava/module/MethodCallException.java index 09363f3ca..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 - 2015 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 ba7194645..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 - 2015 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: @@ -51,7 +49,7 @@ public class MethodRef implements Validated { /** List of problems when initializing the method reference. */ private final List problems = - new ArrayList(); + new ArrayList<>(); public MethodRef(final Class clazz, final String methodName, final Class... params) @@ -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 2816a2ddf..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 - 2015 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,11 @@ import java.util.Map; +import org.scijava.display.DisplayPostprocessor; +import org.scijava.module.process.ModulePostprocessor; +import org.scijava.module.process.ModulePreprocessor; +import org.scijava.widget.InputHarvester; + /** * A module is an encapsulated piece of functionality with inputs and outputs. *

    @@ -126,15 +129,71 @@ public interface Module extends Runnable { void setOutputs(Map outputs); /** - * Gets the resolution status of the input with the given name. A "resolved" - * input is known to have a final, valid value for use with the module. + * Gets the resolution status of the input with the given name. + * + * @see #resolveInput(String) + */ + boolean isInputResolved(String name); + + /** + * Gets the resolution status of the output with the given name. + * + * @see #resolveOutput(String) + */ + boolean isOutputResolved(String name); + + /** + * Marks the input with the given name as resolved. A "resolved" input is + * known to have a final, valid value for use with the module. + *

    + * {@link ModulePreprocessor}s in the module execution chain that populate + * input values (e.g. {@link InputHarvester} plugins) will typically skip over + * inputs which have already been resolved. + *

    + */ + void resolveInput(String name); + + /** + * Marks the output with the given name as resolved. A "resolved" output has + * been handled by the framework somehow, typically displayed to the user. + *

    + * {@link ModulePostprocessor}s in the module execution chain that handle + * output values (e.g. the {@link DisplayPostprocessor}) will typically skip + * over outputs which have already been resolved. + *

    + */ + void resolveOutput(String name); + + /** + * Marks the input with the given name as unresolved. + * + * @see #resolveInput(String) */ - boolean isResolved(String name); + void unresolveInput(String name); + + /** + * Marks the output with the given name as unresolved. + * + * @see #resolveOutput(String) + */ + void unresolveOutput(String name); + + // -- Deprecated -- + + /** @deprecated Use {@link #isInputResolved(String)} instead. */ + @Deprecated + default boolean isResolved(final String name) { + return isInputResolved(name); + } /** - * Sets the resolution status of the input with the given name. A "resolved" - * input is known to have a final, valid value for use with the module. + * @deprecated Use {@link #resolveInput(String)} and + * {@link #unresolveInput(String)} instead. */ - void setResolved(String name, boolean resolved); + @Deprecated + default void setResolved(final String name, final boolean resolved) { + if (resolved) resolveInput(name); + else unresolveInput(name); + } } diff --git a/src/main/java/org/scijava/module/ModuleCanceledException.java b/src/main/java/org/scijava/module/ModuleCanceledException.java index 15a76ab30..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 - 2015 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 6576e5814..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 - 2015 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 eb3fac56a..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 - 2015 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 f55541d82..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 - 2015 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,10 +29,18 @@ package org.scijava.module; +import java.util.List; + +import org.scijava.Identifiable; +import org.scijava.Locatable; import org.scijava.UIDetails; import org.scijava.Validated; +import org.scijava.ValidityProblem; +import org.scijava.Versioned; import org.scijava.event.EventService; import org.scijava.module.event.ModulesUpdatedEvent; +import org.scijava.util.Types; +import org.scijava.util.VersionUtils; /** * A ModuleInfo object encapsulates metadata about a particular {@link Module} @@ -49,7 +55,9 @@ * @author Aivar Grislis * @author Curtis Rueden */ -public interface ModuleInfo extends UIDetails, Validated { +public interface ModuleInfo extends UIDetails, Validated, Identifiable, + Locatable, Versioned +{ /** Gets the input item with the given name. */ ModuleItem getInput(String name); @@ -116,7 +124,9 @@ public interface ModuleInfo extends UIDetails, Validated { * means its inputs are supposed to be presented in a non-modal dialog box, * with {@link Module#run()} being called whenever any of the values change. */ - boolean isInteractive(); + default boolean isInteractive() { + return false; + } /** * Gets whether the module supports previews. A preview is a quick @@ -124,7 +134,9 @@ public interface ModuleInfo extends UIDetails, Validated { * the module with {@link Module#run()}. If this method returns false, then * calling {@link Module#preview()} will have no effect. */ - boolean canPreview(); + default boolean canPreview() { + return false; + } /** * Gets whether the module condones cancellation. Strictly speaking, any @@ -133,7 +145,9 @@ public interface ModuleInfo extends UIDetails, Validated { * If this method returns false, then calling {@link Module#cancel()} will * have no effect. */ - boolean canCancel(); + default boolean canCancel() { + return true; + } /** * Gets whether the module condones headless execution. Strictly speaking, @@ -141,21 +155,87 @@ public interface ModuleInfo extends UIDetails, Validated { * declares itself so, but this flag hints that headless execution is likely * to succeed (if flag is true), or fail (if flag is false). */ - boolean canRunHeadless(); + default boolean canRunHeadless() { + return false; + } /** Gets the function that is called to initialize the module's values. */ - String getInitializer(); + default String getInitializer() { + return null; + } /** * Notifies interested parties that the module info has been modified. This * mechanism is useful for updating any corresponding user interface such as * menu items that are linked to the module. - *

    - * For classes implementing this interface directly, this method should - * publish a {@link ModulesUpdatedEvent} to the event bus (see - * {@link AbstractModuleInfo#update(EventService)} for an example). - *

    */ - void update(EventService eventService); - + default void update(final EventService eventService) { + eventService.publish(new ModulesUpdatedEvent(this)); + } + + // -- UIDetails methods -- + + @Override + default String getTitle() { + final String title = UIDetails.super.getTitle(); + if (!title.equals(getClass().getSimpleName())) return title; + + // use delegate class name rather than actual class name + final String className = getDelegateClassName(); + final int dot = className.lastIndexOf("."); + return dot < 0 ? className : className.substring(dot + 1); + } + + // -- Validated methods -- + + @Override + default boolean isValid() { + return true; + } + + @Override + default List getProblems() { + return null; + } + + // -- Identifiable methods -- + + @Override + default String getIdentifier() { + // NB: By default, we assume that the delegate class name uniquely + // distinguishes the module from others. If the same delegate class is used + // for more than one module, though, it may need to override this method to + // provide more differentiating details. + return "module:" + getDelegateClassName(); + } + + // -- Locatable methods -- + + @Override + default String getLocation() { + // NB: By default, we use the location of the delegate class. + // 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 Types.location(loadDelegateClass()).toExternalForm(); + } + catch (final ClassNotFoundException exc) { + return null; + } + } + + // -- Versioned methods -- + + @Override + default String getVersion() { + // NB: By default, we use the version of the delegate class's JAR archive. + // If the same delegate class is used for more than one module, though, + // it may need to override this method to indicate a different version. + try { + return VersionUtils.getVersion(loadDelegateClass()); + } + 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 1016ce653..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 - 2015 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: @@ -123,6 +121,57 @@ public interface ModuleItem extends BasicDetails { */ void initialize(Module module) throws MethodCallException; + /** Gets the function that is called to validate the item's value. */ + String getValidater(); + + /** + * 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 e03e8dd73..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 - 2015 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 3573e2b3e..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 - 2015 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.Map; import java.util.concurrent.Future; +import org.scijava.Identifiable; import org.scijava.Prioritized; import org.scijava.input.Accelerator; import org.scijava.module.process.ModulePostprocessor; @@ -48,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. *

    @@ -90,6 +89,14 @@ public interface ModuleService extends SciJavaService { /** Gets the list of available modules. */ List getModules(); + /** + * Gets the module with the given identifier string. + * + * @param id The identifier string corresponding to the desired module. + * @return The {@link Identifiable} module with the given identifier. + */ + ModuleInfo getModuleById(String id); + /** * Gets the module for a given keyboard shortcut. * @@ -274,6 +281,18 @@ Future run(M module, */ ModuleItem getSingleOutput(Module module, Class type); + /** + * As {@link #getSingleInput(Module, Class)} but will match with a set of + * potential classes, at the cost of generic parameter safety. + */ + ModuleItem getSingleInput(Module module, Collection> types); + + /** + * As {@link #getSingleOutput(Module, Class)} but will match with a set of + * potential classes, at the cost of generic parameter safety. + */ + ModuleItem getSingleOutput(Module module, Collection> types); + /** * Registers the given value for the given {@link ModuleItem} using the * {@link PrefService}. @@ -289,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 189c6cf87..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 - 2015 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,28 +45,54 @@ public interface MutableModule extends Module { /** Adds an input to the list. */ - MutableModuleItem addInput(String name, Class type); + default MutableModuleItem addInput(final String name, + final Class type) + { + final DefaultMutableModuleItem item = + new DefaultMutableModuleItem<>(this, name, type); + addInput(item); + return item; + } /** Adds an input to the list. */ - void addInput(ModuleItem input); + default void addInput(final ModuleItem input) { + getInfo().addInput(input); + } /** Adds an output to the list. */ - MutableModuleItem addOutput(String name, Class type); + default MutableModuleItem addOutput(final String name, + final Class type) + { + final DefaultMutableModuleItem item = + new DefaultMutableModuleItem<>(this, name, type); + addOutput(item); + return item; + } /** Adds an output to the list. */ - void addOutput(ModuleItem output); + default void addOutput(final ModuleItem output) { + getInfo().addOutput(output); + } /** Removes an input from the list. */ - void removeInput(ModuleItem input); + default void removeInput(final ModuleItem input) { + getInfo().removeInput(input); + } /** Removes an output from the list. */ - void removeOutput(ModuleItem output); - - // NB: Type narrowing. + default void removeOutput(final ModuleItem output) { + getInfo().removeOutput(output); + } // -- ModuleInfo methods -- @Override MutableModuleInfo getInfo(); + // -- Runnable methods -- + + @Override + default void run() { + // do nothing by default + } } diff --git a/src/main/java/org/scijava/module/MutableModuleInfo.java b/src/main/java/org/scijava/module/MutableModuleInfo.java index 5e57665bd..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 - 2015 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,4 +62,25 @@ public interface MutableModuleInfo extends ModuleInfo { /** Removes an output from the list. */ void removeOutput(ModuleItem output); + // -- ModuleInfo methods -- + + @Override + default String getDelegateClassName() { + return getModuleClass().getName(); + } + + @Override + default Class loadDelegateClass() { + return getModuleClass(); + } + + @Override + default Module createModule() throws ModuleException { + try { + return getModuleClass().newInstance(); + } + catch (final InstantiationException | IllegalAccessException exc) { + throw new ModuleException(exc); + } + } } diff --git a/src/main/java/org/scijava/module/MutableModuleItem.java b/src/main/java/org/scijava/module/MutableModuleItem.java index efd681ed5..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 - 2015 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); @@ -56,6 +56,8 @@ public interface MutableModuleItem extends ModuleItem { void setInitializer(String initializer); + void setValidater(String validater); + void setCallback(String callback); void setWidgetStyle(String widgetStyle); diff --git a/src/main/java/org/scijava/module/event/ModuleCanceledEvent.java b/src/main/java/org/scijava/module/event/ModuleCanceledEvent.java index 0451da0b1..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 - 2015 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 6cdc33195..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 - 2015 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 e84b6a3fa..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 - 2015 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 a1ede59c8..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 - 2015 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 357732926..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 - 2015 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 847747ac7..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 - 2015 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 501feae67..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 - 2015 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 8aed828df..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 - 2015 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 bb969177d..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 - 2015 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 069915064..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 - 2015 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 9f8714aae..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 - 2015 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 9e754000b..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 - 2015 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 a52cd32b9..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 - 2015 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 e75d25ee0..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 - 2015 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 1af137a8b..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 - 2015 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 39f957be5..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 - 2015 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 3fc602696..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 - 2015 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 7e031019e..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 - 2015 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 6115bb080..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 - 2015 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 ed5218174..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 - 2015 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 a4ee85713..fed93f7e9 100644 --- a/src/main/java/org/scijava/module/process/DefaultValuePreprocessor.java +++ b/src/main/java/org/scijava/module/process/DefaultValuePreprocessor.java @@ -2,19 +2,17 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: - * + * * 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 @@ -31,12 +29,15 @@ package org.scijava.module.process; +import java.util.Objects; + import org.scijava.Priority; 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.Types; /** * A preprocessor plugin that populates default parameter values. @@ -46,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 @@ -69,7 +70,9 @@ public void process(final Module module) { private void assignDefaultValue(final Module module, final ModuleItem item) { - if (module.isResolved(item.getName())) return; + if (module.isInputResolved(item.getName())) return; + 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; item.setValue(module, defaultValue); diff --git a/src/main/java/org/scijava/module/process/GatewayPreprocessor.java b/src/main/java/org/scijava/module/process/GatewayPreprocessor.java index 7f805e9c3..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 - 2015 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 = 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 (IllegalArgumentException exc) { - exception = exc; - } - catch (SecurityException exc) { - exception = exc; - } - catch (InstantiationException exc) { - exception = exc; - } - catch (IllegalAccessException exc) { - exception = exc; - } - catch (InvocationTargetException exc) { - exception = exc; - } - catch (NoSuchMethodException exc) { + catch (final IllegalArgumentException | SecurityException + | InstantiationException | IllegalAccessException + | InvocationTargetException | NoSuchMethodException exc) + { exception = exc; } if (exception != null) { @@ -110,7 +96,7 @@ private void setGatewayValue(final Context context, return; } input.setValue(module, gateway); - module.setResolved(input.getName(), true); + module.resolveInput(input.getName()); } } diff --git a/src/main/java/org/scijava/module/process/InitPreprocessor.java b/src/main/java/org/scijava/module/process/InitPreprocessor.java index 3a723d21a..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 - 2015 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,7 @@ package org.scijava.module.process; +import org.scijava.Cancelable; import org.scijava.Priority; import org.scijava.log.LogService; import org.scijava.module.MethodCallException; @@ -46,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) @@ -58,6 +57,7 @@ public class InitPreprocessor extends AbstractPreprocessorPlugin { public void process(final Module module) { try { module.initialize(); + if (isCanceled(module)) cancel(getCancelReason(module)); } catch (final MethodCallException exc) { if (log != null) log.error(exc); @@ -66,4 +66,16 @@ public void process(final Module module) { } } + // -- Helper methods -- + + private boolean isCanceled(final Module module) { + return module instanceof Cancelable && ((Cancelable) module).isCanceled(); + } + + private String getCancelReason(final Module module) { + if (!(module instanceof Cancelable)) return null; + final String cancelReason = ((Cancelable) module).getCancelReason(); + return cancelReason == null ? "" : cancelReason; + } + } diff --git a/src/main/java/org/scijava/module/process/LoadInputsPreprocessor.java b/src/main/java/org/scijava/module/process/LoadInputsPreprocessor.java index f61f484d5..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 - 2015 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.isResolved(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 type) - { - if (prefValue != null) return conversionService.convert(prefValue, type); - if (defaultValue != null) { - return conversionService.convert(defaultValue, type); - } - return ConversionUtils.getNullValue(type); - } - } diff --git a/src/main/java/org/scijava/module/process/LoggerPreprocessor.java b/src/main/java/org/scijava/module/process/LoggerPreprocessor.java new file mode 100644 index 000000000..b096c5593 --- /dev/null +++ b/src/main/java/org/scijava/module/process/LoggerPreprocessor.java @@ -0,0 +1,77 @@ +/* + * #%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.process; + +import org.scijava.Priority; +import org.scijava.log.LogService; +import org.scijava.log.Logger; +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; + +/** + * This {@link PreprocessorPlugin} affects {@link Module}s with a single + * {@link Parameter} of type {@link Logger}. It will assign a Logger to that + * Parameter, that is named like the modules class. + * + * @author Matthias Arzt + */ +@Plugin(type = PreprocessorPlugin.class, priority = Priority.VERY_HIGH) +public class LoggerPreprocessor extends AbstractPreprocessorPlugin { + + @Parameter(required = false) + private LogService logService; + + @Parameter(required = false) + private ModuleService moduleService; + + // -- ModuleProcessor methods -- + + @Override + public void process(final Module module) { + if (logService == null || moduleService == null) return; + + final ModuleItem loggerInput = moduleService.getSingleInput(module, + Logger.class); + if (loggerInput == null || !loggerInput.isAutoFill()) return; + + String loggerName = loggerInput.getLabel(); + if(loggerName == null || loggerName.isEmpty()) + loggerName = module.getDelegateObject().getClass().getSimpleName(); + Logger logger = logService.subLogger(loggerName); + + final String name = loggerInput.getName(); + module.setInput(name, logger); + module.resolveInput(name); + } + +} diff --git a/src/main/java/org/scijava/module/process/ModulePostprocessor.java b/src/main/java/org/scijava/module/process/ModulePostprocessor.java index c25986a4c..50a82900e 100644 --- a/src/main/java/org/scijava/module/process/ModulePostprocessor.java +++ b/src/main/java/org/scijava/module/process/ModulePostprocessor.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/ModulePreprocessor.java b/src/main/java/org/scijava/module/process/ModulePreprocessor.java index 555473f3b..ee9d2af22 100644 --- a/src/main/java/org/scijava/module/process/ModulePreprocessor.java +++ b/src/main/java/org/scijava/module/process/ModulePreprocessor.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/ModuleProcessor.java b/src/main/java/org/scijava/module/process/ModuleProcessor.java index e27c6b891..605194d03 100644 --- a/src/main/java/org/scijava/module/process/ModuleProcessor.java +++ b/src/main/java/org/scijava/module/process/ModuleProcessor.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/PostprocessorPlugin.java b/src/main/java/org/scijava/module/process/PostprocessorPlugin.java index de83e0b01..a010e0a6f 100644 --- a/src/main/java/org/scijava/module/process/PostprocessorPlugin.java +++ b/src/main/java/org/scijava/module/process/PostprocessorPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/PreprocessorPlugin.java b/src/main/java/org/scijava/module/process/PreprocessorPlugin.java index ae66f51d4..57ccdc92f 100644 --- a/src/main/java/org/scijava/module/process/PreprocessorPlugin.java +++ b/src/main/java/org/scijava/module/process/PreprocessorPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/SaveInputsPreprocessor.java b/src/main/java/org/scijava/module/process/SaveInputsPreprocessor.java index 220027507..8f4596f76 100644 --- a/src/main/java/org/scijava/module/process/SaveInputsPreprocessor.java +++ b/src/main/java/org/scijava/module/process/SaveInputsPreprocessor.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,6 @@ import org.scijava.Priority; 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; @@ -43,37 +40,23 @@ *

    * This preprocessor runs late in the chain, giving other preprocessors every * chance to populate the inputs first. In particular, it executes after the - * {@link org.scijava.widget.InputHarvester} has run, so that user-specified values - * are persisted for next time. + * {@link org.scijava.widget.InputHarvester} has run, so that user-specified + * values are persisted for next time. *

    * * @author Curtis Rueden */ @Plugin(type = PreprocessorPlugin.class, - priority = Priority.VERY_LOW_PRIORITY - 1) + priority = SaveInputsPreprocessor.PRIORITY) public class SaveInputsPreprocessor extends AbstractPreprocessorPlugin { - public static final double PRIORITY = Priority.VERY_LOW_PRIORITY - 1; + public static final double PRIORITY = Priority.VERY_LOW - 1; @Parameter private ModuleService moduleService; - // -- ModuleProcessor methods -- - @Override public void process(final Module module) { - final Iterable> inputs = module.getInfo().inputs(); - for (final ModuleItem item : inputs) { - saveValue(module, item); - } + moduleService.saveInputs(module); } - - // -- Helper methods -- - - /** Saves the value of the given module item to persistent storage. */ - private void saveValue(final Module module, final ModuleItem item) { - final T value = item.getValue(module); - moduleService.save(item, value); - } - } diff --git a/src/main/java/org/scijava/module/process/ServicePreprocessor.java b/src/main/java/org/scijava/module/process/ServicePreprocessor.java index 127bf992b..15ac626c5 100644 --- a/src/main/java/org/scijava/module/process/ServicePreprocessor.java +++ b/src/main/java/org/scijava/module/process/ServicePreprocessor.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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 @@ *

    * Many modules (e.g., most {@link Command}s) use @{@link Parameter}-annotated * service fields, resulting in those parameters being populated when the - * SciJava application context is injected (via {@link Context#inject(Object)}. + * SciJava application context is injected (via {@link Context#inject(Object)}). * However, some modules may have service parameters which are programmatically * generated (i.e., returned directly as inputs from {@link ModuleInfo#inputs()} * and as such not populated by context injection. E.g., this situation is the @@ -61,8 +59,7 @@ * * @author Curtis Rueden */ -@Plugin(type = PreprocessorPlugin.class, - priority = Priority.VERY_HIGH_PRIORITY) +@Plugin(type = PreprocessorPlugin.class, priority = 2 * Priority.VERY_HIGH) public class ServicePreprocessor extends AbstractPreprocessorPlugin { // -- ModuleProcessor methods -- @@ -70,7 +67,8 @@ public class ServicePreprocessor 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 (Service.class.isAssignableFrom(type)) { // input is a service @@ -79,11 +77,13 @@ public void process(final Module module) { (ModuleItem) input; setServiceValue(getContext(), module, serviceInput); } - if (type.isAssignableFrom(getContext().getClass())) { + if (Context.class.isAssignableFrom(type) && // + type.isAssignableFrom(getContext().getClass())) + { // input is a compatible context final String name = input.getName(); module.setInput(name, getContext()); - module.setResolved(name, true); + module.resolveInput(name); } } } @@ -95,7 +95,7 @@ private void setServiceValue(final Context context, { final S service = context.getService(input.getType()); input.setValue(module, service); - module.setResolved(input.getName(), true); + module.resolveInput(input.getName()); } } diff --git a/src/main/java/org/scijava/module/process/ValidityPreprocessor.java b/src/main/java/org/scijava/module/process/ValidityPreprocessor.java index 918f922d7..e1cbdb29e 100644 --- a/src/main/java/org/scijava/module/process/ValidityPreprocessor.java +++ b/src/main/java/org/scijava/module/process/ValidityPreprocessor.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,8 +41,7 @@ * * @author Curtis Rueden */ -@Plugin(type = PreprocessorPlugin.class, - priority = Priority.VERY_HIGH_PRIORITY + 1) +@Plugin(type = PreprocessorPlugin.class, priority = 3 * Priority.VERY_HIGH) public class ValidityPreprocessor extends AbstractPreprocessorPlugin { // -- ModuleProcessor methods -- diff --git a/src/main/java/org/scijava/module/run/ModuleCodeRunner.java b/src/main/java/org/scijava/module/run/ModuleCodeRunner.java new file mode 100644 index 000000000..0369dee67 --- /dev/null +++ b/src/main/java/org/scijava/module/run/ModuleCodeRunner.java @@ -0,0 +1,86 @@ +/* + * #%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.run; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +import org.scijava.Identifiable; +import org.scijava.module.ModuleInfo; +import org.scijava.module.ModuleService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.run.AbstractCodeRunner; +import org.scijava.run.CodeRunner; + +/** + * Runs the given {@link Identifiable} SciJava module. + * + * @author Curtis Rueden + * @see ModuleInfo + */ +@Plugin(type = CodeRunner.class) +public class ModuleCodeRunner extends AbstractCodeRunner { + + @Parameter + private ModuleService moduleService; + + // -- CodeRunner methods -- + + @Override + public void run(final Object code, final Object... args) + throws InvocationTargetException + { + waitFor(moduleService.run(getModuleInfo(code), true, args)); + } + + @Override + public void run(final Object code, final Map inputMap) + throws InvocationTargetException + { + waitFor(moduleService.run(getModuleInfo(code), true, inputMap)); + } + + // -- Typed methods -- + + @Override + public boolean supports(final Object code) { + return getModuleInfo(code) != null; + } + + // -- Helper methods -- + + private ModuleInfo getModuleInfo(final Object code) { + if (!(code instanceof String)) return null; + final String id = (String) code; + return moduleService.getModuleById(id); + } + +} diff --git a/src/main/java/org/scijava/object/DefaultObjectService.java b/src/main/java/org/scijava/object/DefaultObjectService.java index aee218631..885f2025c 100644 --- a/src/main/java/org/scijava/object/DefaultObjectService.java +++ b/src/main/java/org/scijava/object/DefaultObjectService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,14 +29,10 @@ package org.scijava.object; -import java.util.List; - import org.scijava.event.EventHandler; import org.scijava.event.EventService; import org.scijava.object.event.ObjectCreatedEvent; import org.scijava.object.event.ObjectDeletedEvent; -import org.scijava.object.event.ObjectsAddedEvent; -import org.scijava.object.event.ObjectsRemovedEvent; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; import org.scijava.service.AbstractService; @@ -67,45 +61,25 @@ public final class DefaultObjectService extends AbstractService implements private EventService eventService; /** Index of registered objects. */ - private ObjectIndex objectIndex; + private NamedObjectIndex objectIndex; // -- ObjectService methods -- @Override - public EventService getEventService() { + public EventService eventService() { return eventService; } @Override - public ObjectIndex getIndex() { + public NamedObjectIndex getIndex() { return objectIndex; } - @Override - public List getObjects(final Class type) { - final List list = objectIndex.get(type); - @SuppressWarnings("unchecked") - final List result = (List) list; - return result; - } - - @Override - public void addObject(final Object obj) { - objectIndex.add(obj); - eventService.publish(new ObjectsAddedEvent(obj)); - } - - @Override - public void removeObject(final Object obj) { - objectIndex.remove(obj); - eventService.publish(new ObjectsRemovedEvent(obj)); - } - // -- Service methods -- @Override public void initialize() { - objectIndex = new ObjectIndex(Object.class); + objectIndex = new NamedObjectIndex<>(Object.class); } // -- Event handlers -- @@ -119,5 +93,4 @@ protected void onEvent(final ObjectCreatedEvent event) { protected void onEvent(final ObjectDeletedEvent event) { removeObject(event.getObject()); } - } diff --git a/src/main/java/org/scijava/object/LazyObjects.java b/src/main/java/org/scijava/object/LazyObjects.java index 975efa89b..3676085bb 100644 --- a/src/main/java/org/scijava/object/LazyObjects.java +++ b/src/main/java/org/scijava/object/LazyObjects.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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.object; import java.util.Collection; +import java.util.function.Supplier; /** * Interface for objects created lazily. This interface provides a mechanism to @@ -40,9 +39,10 @@ * * @author Curtis Rueden */ -public interface LazyObjects { +public interface LazyObjects extends Supplier> { /** Gets the collection of objects. */ + @Override Collection get(); } diff --git a/src/main/java/org/scijava/object/NamedObjectIndex.java b/src/main/java/org/scijava/object/NamedObjectIndex.java new file mode 100644 index 000000000..0d807d166 --- /dev/null +++ b/src/main/java/org/scijava/object/NamedObjectIndex.java @@ -0,0 +1,62 @@ +/*- + * #%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.object; + +import java.util.WeakHashMap; + +/** + * An {@link ObjectIndex} where each object can have an associated name. + * + * @author Jan Eglinger + */ +public class NamedObjectIndex extends ObjectIndex { + + private WeakHashMap nameMap; + + public NamedObjectIndex(final Class baseClass) { + super(baseClass); + nameMap = new WeakHashMap<>(); + } + + public boolean add(E object, String name) { + if (name != null) + nameMap.put(object, name); + return add(object); + } + + public boolean add(E object, Class type, String name, boolean batch) { + if (name != null) + nameMap.put(object, name); + return add(object, type, batch); + } + + public String getName(E object) { + return nameMap.get(object); + } +} diff --git a/src/main/java/org/scijava/object/ObjectIndex.java b/src/main/java/org/scijava/object/ObjectIndex.java index 1baee99ff..5eb81dde1 100644 --- a/src/main/java/org/scijava/object/ObjectIndex.java +++ b/src/main/java/org/scijava/object/ObjectIndex.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -80,13 +78,13 @@ public class ObjectIndex implements Collection { * —Russell Hoban, Riddley Walker */ protected final Map, List> hoard = - new ConcurrentHashMap, List>(); + new ConcurrentHashMap<>(); private final Class baseClass; /** List of objects to add later as needed (i.e., lazily). */ private final List> pending = - new LinkedList>(); + new LinkedList<>(); public ObjectIndex(final Class baseClass) { this.baseClass = baseClass; @@ -133,7 +131,7 @@ public List get(final Class type) { List list = retrieveList(type); // NB: Return a copy of the data, to facilitate thread safety. - list = new ArrayList(list); + list = new ArrayList<>(list); return list; } @@ -237,7 +235,7 @@ public void clear() { @Override public String toString() { - final List> classes = new ArrayList>(hoard.keySet()); + final List> classes = new ArrayList<>(hoard.keySet()); Collections.sort(classes, new Comparator>() { @Override @@ -285,13 +283,13 @@ protected boolean remove(final Object o, final boolean batch) { } private Map, List[]> type2Lists = - new HashMap, List[]>(); + new HashMap<>(); protected synchronized List[] retrieveListsForType(final Class type) { final List[] lists = type2Lists.get(type); if (lists != null) return lists; - final ArrayList> listOfLists = new ArrayList>(); + final ArrayList> listOfLists = new ArrayList<>(); for (final Class c : getTypes(type)) { listOfLists.add(retrieveList(c)); } @@ -342,13 +340,13 @@ protected boolean removeFromList(final Object obj, final List list, // -- Helper methods -- private static Map, Class[]> typeMap = - new HashMap, Class[]>(); + new HashMap<>(); /** Gets a new set containing the type and all its supertypes. */ protected static synchronized Class[] getTypes(final Class type) { Class[] types = typeMap.get(type); if (types != null) return types; - final Set>set = new LinkedHashSet>(); + final Set>set = new LinkedHashSet<>(); set.add(All.class); // NB: Always include the "All" class. getTypes(type, set); types = set.toArray(new Class[set.size()]); @@ -374,7 +372,7 @@ private static synchronized void getTypes(final Class type, protected List retrieveList(final Class type) { List list = hoard.get(type); if (list == null) { - list = new ArrayList(); + list = new ArrayList<>(); hoard.put(type, list); } return list; diff --git a/src/main/java/org/scijava/object/ObjectService.java b/src/main/java/org/scijava/object/ObjectService.java index d305dbd3f..fac395e5d 100644 --- a/src/main/java/org/scijava/object/ObjectService.java +++ b/src/main/java/org/scijava/object/ObjectService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,10 @@ import java.util.List; +import org.scijava.Named; import org.scijava.event.EventService; +import org.scijava.object.event.ObjectsAddedEvent; +import org.scijava.object.event.ObjectsRemovedEvent; import org.scijava.service.SciJavaService; /** @@ -43,18 +44,65 @@ */ public interface ObjectService extends SciJavaService { - EventService getEventService(); + default EventService eventService() { + return context().getService(EventService.class); + } /** Gets the index of available objects. */ - ObjectIndex getIndex(); + NamedObjectIndex getIndex(); /** Gets a list of all registered objects compatible with the given type. */ - List getObjects(Class type); + default List getObjects(final Class type) { + final List list = getIndex().get(type); + @SuppressWarnings("unchecked") + final List result = (List) list; + return result; + } + + /** + * Gets the name belonging to a given object. + *

    + * If no explicit name was provided at registration time, the name will be + * derived from {@link Named#getName()} if the object implements + * {@link Named}, or from the {@link Object#toString()} otherwise. It is + * guaranteed that this method will not return {@code null}. + *

    + **/ + default String getName(final Object obj) { + if (obj == null) throw new NullPointerException(); + final String name = getIndex().getName(obj); + if (name != null) return name; + if (obj instanceof Named) { + final String n = ((Named) obj).getName(); + if (n != null) return n; + } + final String s = obj.toString(); + if (s != null) return s; + return obj.getClass().getName() + "@" + Integer.toHexString(obj.hashCode()); + } /** Registers an object with the object service. */ - void addObject(Object obj); + default void addObject(Object obj) { + addObject(obj, null); + } + + /** Registers a named object with the object service. */ + default void addObject(final Object obj, final String name) { + getIndex().add(obj, name); + eventService().publish(new ObjectsAddedEvent(obj)); + } /** Deregisters an object with the object service. */ - void removeObject(Object obj); + default void removeObject(final Object obj) { + getIndex().remove(obj); + eventService().publish(new ObjectsRemovedEvent(obj)); + } + + // -- Deprecated methods -- + /** @deprecated Use {@link #eventService()} instead. */ + @Deprecated + default EventService getEventService() { + return eventService(); + } } diff --git a/src/main/java/org/scijava/object/SortedObjectIndex.java b/src/main/java/org/scijava/object/SortedObjectIndex.java index 08639a1fd..a9d2d1f21 100644 --- a/src/main/java/org/scijava/object/SortedObjectIndex.java +++ b/src/main/java/org/scijava/object/SortedObjectIndex.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -92,16 +90,59 @@ public boolean addAll(final Collection c) { return changed; } + // -- Internal methods -- + + @Override + protected boolean addToList(final E obj, final List list, + final boolean batch) + { + if (batch) { + // adding multiple values; append to end of list, and sort afterward + return super.addToList(obj, list, batch); + } + + // search for the correct location to insert the object + final int result = Collections.binarySearch(list, obj); + // NB: The objects' natural ordering may not be consistent with equals. + // Hence, the index reported may indicate a match with an unequal object + // (i.e., obj.compareTo(match) == 0 but !obj.equals(match)). + // But since we allow duplicate items in the index, this situation is fine; + // either way, we want to insert the object at the given point. + final int index = result < 0 ? -result - 1 : result; + + // insert object at the appropriate location + list.add(index, obj); + return true; + } + + // -- Helper methods -- + + private void sort() { + for (final List list : hoard.values()) { + Collections.sort(list); + } + } + + private int findInList(final Object o, final List list) { + if (!getBaseClass().isAssignableFrom(o.getClass())) { + // wrong type + return list.size(); + } + @SuppressWarnings("unchecked") + final E typedObj = (E) o; + return Collections.binarySearch(list, typedObj); + } + private void mergeAfterSorting(final Collection c) { - final List listToMerge = new ArrayList(c); + final List listToMerge = new ArrayList<>(c); Collections.sort(listToMerge); - final Map, List> map = new HashMap, List>(); + final Map, List> map = new HashMap<>(); for (final E e : listToMerge) { for (final Class clazz : getTypes(getType(e))) { final List list = retrieveList(clazz); List list2 = map.get(clazz); if (list2 == null) { - list2 = list.size() == 0 ? (List)list : new ArrayList(); + list2 = list.size() == 0 ? (List)list : new ArrayList<>(); map.put(clazz, list2); } list2.add(e); @@ -145,47 +186,4 @@ private void mergeInto(final List sorted, final List into) { } } - // -- Internal methods -- - - @Override - protected boolean addToList(final E obj, final List list, - final boolean batch) - { - if (batch) { - // adding multiple values; append to end of list, and sort afterward - return super.addToList(obj, list, batch); - } - - // search for the correct location to insert the object - final int result = Collections.binarySearch(list, obj); - // NB: The objects' natural ordering may not be consistent with equals. - // Hence, the index reported may indicate a match with an unequal object - // (i.e., obj.compareTo(match) == 0 but !obj.equals(match)). - // But since we allow duplicate items in the index, this situation is fine; - // either way, we want to insert the object at the given point. - final int index = result < 0 ? -result - 1 : result; - - // insert object at the appropriate location - list.add(index, obj); - return true; - } - - // -- Helper methods -- - - private void sort() { - for (final List list : hoard.values()) { - Collections.sort(list); - } - } - - private int findInList(final Object o, final List list) { - if (!getBaseClass().isAssignableFrom(o.getClass())) { - // wrong type - return list.size(); - } - @SuppressWarnings("unchecked") - final E typedObj = (E) o; - return Collections.binarySearch(list, typedObj); - } - } diff --git a/src/main/java/org/scijava/object/event/ListEvent.java b/src/main/java/org/scijava/object/event/ListEvent.java index 1ea8c046c..e7e5214f7 100644 --- a/src/main/java/org/scijava/object/event/ListEvent.java +++ b/src/main/java/org/scijava/object/event/ListEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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 @@ */ public abstract class ListEvent extends SciJavaEvent { - private final List items = new ArrayList(); + private final List items = new ArrayList<>(); public ListEvent(final T o) { items.add(o); diff --git a/src/main/java/org/scijava/object/event/ObjectCreatedEvent.java b/src/main/java/org/scijava/object/event/ObjectCreatedEvent.java index 41292faeb..0cbd3dad0 100644 --- a/src/main/java/org/scijava/object/event/ObjectCreatedEvent.java +++ b/src/main/java/org/scijava/object/event/ObjectCreatedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/object/event/ObjectDeletedEvent.java b/src/main/java/org/scijava/object/event/ObjectDeletedEvent.java index c3c0d98f7..793a20c02 100644 --- a/src/main/java/org/scijava/object/event/ObjectDeletedEvent.java +++ b/src/main/java/org/scijava/object/event/ObjectDeletedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/object/event/ObjectEvent.java b/src/main/java/org/scijava/object/event/ObjectEvent.java index 0c8d7de4e..7b1370612 100644 --- a/src/main/java/org/scijava/object/event/ObjectEvent.java +++ b/src/main/java/org/scijava/object/event/ObjectEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/object/event/ObjectModifiedEvent.java b/src/main/java/org/scijava/object/event/ObjectModifiedEvent.java index 1e9eddb86..6e51bb9d2 100644 --- a/src/main/java/org/scijava/object/event/ObjectModifiedEvent.java +++ b/src/main/java/org/scijava/object/event/ObjectModifiedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/object/event/ObjectsAddedEvent.java b/src/main/java/org/scijava/object/event/ObjectsAddedEvent.java index 3082f50fc..c4ce0e854 100644 --- a/src/main/java/org/scijava/object/event/ObjectsAddedEvent.java +++ b/src/main/java/org/scijava/object/event/ObjectsAddedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/object/event/ObjectsListEvent.java b/src/main/java/org/scijava/object/event/ObjectsListEvent.java index 549a5e921..9bab39b51 100644 --- a/src/main/java/org/scijava/object/event/ObjectsListEvent.java +++ b/src/main/java/org/scijava/object/event/ObjectsListEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/object/event/ObjectsRemovedEvent.java b/src/main/java/org/scijava/object/event/ObjectsRemovedEvent.java index 9b9f78c27..c042a73f8 100644 --- a/src/main/java/org/scijava/object/event/ObjectsRemovedEvent.java +++ b/src/main/java/org/scijava/object/event/ObjectsRemovedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/options/DefaultOptionsService.java b/src/main/java/org/scijava/options/DefaultOptionsService.java index d6de6e0c4..aa6792276 100644 --- a/src/main/java/org/scijava/options/DefaultOptionsService.java +++ b/src/main/java/org/scijava/options/DefaultOptionsService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -72,14 +70,6 @@ public O getOptions(final Class optionsClass) { return objects == null || objects.isEmpty() ? null : objects.get(0); } - @Override - public void reset() { - final List optionsPlugins = getInstances(); - for (final OptionsPlugin plugin : optionsPlugins) { - plugin.reset(); - } - } - // -- SingletonService methods -- @Override @@ -93,12 +83,4 @@ public List getInstances() { return instances; } - - // -- PTService methods -- - - @Override - public Class getPluginType() { - return OptionsPlugin.class; - } - } diff --git a/src/main/java/org/scijava/options/OptionsPlugin.java b/src/main/java/org/scijava/options/OptionsPlugin.java index 2d03eb1b5..24208055c 100644 --- a/src/main/java/org/scijava/options/OptionsPlugin.java +++ b/src/main/java/org/scijava/options/OptionsPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,7 +32,6 @@ import org.scijava.command.DynamicCommand; import org.scijava.event.EventService; import org.scijava.module.ModuleItem; -import org.scijava.module.ModuleService; import org.scijava.options.event.OptionsEvent; import org.scijava.plugin.Parameter; import org.scijava.plugin.SingletonPlugin; @@ -71,7 +68,9 @@ * @author Barry DeZonia * @author Curtis Rueden */ -public class OptionsPlugin extends DynamicCommand implements SingletonPlugin { +public abstract class OptionsPlugin extends DynamicCommand implements + SingletonPlugin +{ // -- Parameters -- @@ -81,9 +80,6 @@ public class OptionsPlugin extends DynamicCommand implements SingletonPlugin { @Parameter private PrefService prefService; - @Parameter - private ModuleService moduleService; - // -- OptionsPlugin methods -- /** Loads option values from persistent storage. */ @@ -105,19 +101,20 @@ public void reset() { prefService.clear(getClass()); } + // -- Module methods -- + + @Override + public void cancel() { + resetState(); + } + // -- Runnable methods -- @Override public void run() { save(); - - // NB: Clear "resolved" status of all inputs. - // Otherwise, no inputs are harvested on next run. - for (final ModuleItem input : getInfo().inputs()) { - setResolved(input.getName(), false); - } - eventService.publish(new OptionsEvent(this)); + resetState(); } // -- Helper methods -- @@ -132,4 +129,15 @@ private void saveInput(final ModuleItem input) { moduleService.save(input, value); } + private void resetState() { + // NB: Clear "resolved" status of all inputs. + // Otherwise, no inputs are harvested on next run. + for (final ModuleItem input : getInfo().inputs()) { + unresolveInput(input.getName()); + } + + // NB: Clear "canceled" status. + // Otherwise, the command cannot run again. + uncancel(); + } } diff --git a/src/main/java/org/scijava/options/OptionsService.java b/src/main/java/org/scijava/options/OptionsService.java index 8e3c0b60e..6e659e1c9 100644 --- a/src/main/java/org/scijava/options/OptionsService.java +++ b/src/main/java/org/scijava/options/OptionsService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,8 @@ package org.scijava.options; +import java.util.List; + import org.scijava.plugin.SingletonService; import org.scijava.service.SciJavaService; @@ -53,6 +53,17 @@ public interface OptionsService extends SingletonService, * * @see OptionsPlugin#reset() */ - void reset(); + default void reset() { + final List optionsPlugins = getInstances(); + for (final OptionsPlugin plugin : optionsPlugins) { + plugin.reset(); + } + } + + // -- PTService methods -- + @Override + default Class getPluginType() { + return OptionsPlugin.class; + } } diff --git a/src/main/java/org/scijava/options/event/OptionsEvent.java b/src/main/java/org/scijava/options/event/OptionsEvent.java index 30ea0e09d..9ecea6af8 100644 --- a/src/main/java/org/scijava/options/event/OptionsEvent.java +++ b/src/main/java/org/scijava/options/event/OptionsEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/parse/DefaultParseService.java b/src/main/java/org/scijava/parse/DefaultParseService.java new file mode 100644 index 000000000..0924ab884 --- /dev/null +++ b/src/main/java/org/scijava/parse/DefaultParseService.java @@ -0,0 +1,140 @@ +/* + * #%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.parse; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.scijava.parsington.Variable; +import org.scijava.parsington.eval.DefaultTreeEvaluator; +import org.scijava.plugin.Plugin; +import org.scijava.service.AbstractService; +import org.scijava.service.Service; +import org.scijava.util.ObjectArray; + +/** + * Default service for parsing strings. + * + * @author Curtis Rueden + */ +@Plugin(type = Service.class) +public class DefaultParseService extends AbstractService implements + ParseService +{ + + @Override + public Items parse(final String arg, final boolean strict) { + if (arg == null) throw new NullPointerException("arg must not be null"); + return new ItemsList(arg, strict); + } + + // -- Helper classes -- + + /** + * {@link Items} implementation backed by + * Parsington. + */ + private static class ItemsList extends ObjectArray implements Items { + + public ItemsList(final String arg, final boolean strict) { + super(Item.class); + parseItems(arg, strict); + } + + @Override + public Map asMap() { + final LinkedHashMap map = + new LinkedHashMap<>(); + for (final Item item : this) { + map.put(item.name(), item.value()); + } + return map; + } + + @Override + public boolean isMap() { + for (final Item item : this) { + if (item.name() == null) return false; + } + return true; + } + + @Override + public boolean isList() { + for (final Item item : this) { + if (item.name() != null) return false; + } + return true; + } + + private void parseItems(final String arg, final boolean strict) { + final DefaultTreeEvaluator e = new DefaultTreeEvaluator(); + e.setStrict(strict); + final Object result = e.evaluate("(" + arg + ")"); + if (result == null) { + throw new IllegalStateException("Error parsing string: '" + arg + "'"); + } + final List list; + if (result instanceof List) list = (List) result; + else list = Collections.singletonList(result); + + for (final Object o : list) { + final String name; + final Object value; + if (o instanceof Variable) { + final Variable v = (Variable) o; + name = v.getToken(); + value = e.value(v); + } + else { + name = null; + value = o; + } + add(new Item() { + + @Override + public String name() { + return name; + } + + @Override + public Object value() { + return value; + } + + }); + } + } + + } + +} diff --git a/src/main/java/org/scijava/parse/Item.java b/src/main/java/org/scijava/parse/Item.java new file mode 100644 index 000000000..416538777 --- /dev/null +++ b/src/main/java/org/scijava/parse/Item.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.parse; + +/** + * An item from the list of parsed items; i.e.: a name/value pair. + *

    + * NB: It is unfortunate that we cannot use {@code javafx.util.Pair}, but it is: + * A) Java 8; and B) part of JavaFX rather than core Java. + *

    + * + * @author Curtis Rueden + */ +public interface Item { + + /** + * Gets the name of the item, or {@code null} if unnamed (i.e., raw value with + * no equals sign). + */ + String name(); + + /** Gets the value of the item. */ + Object value(); +} diff --git a/src/main/java/org/scijava/parse/Items.java b/src/main/java/org/scijava/parse/Items.java new file mode 100644 index 000000000..0b3b6becb --- /dev/null +++ b/src/main/java/org/scijava/parse/Items.java @@ -0,0 +1,56 @@ +/* + * #%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.parse; + +import java.util.List; +import java.util.Map; + +/** + * An ordered list of items, some of which might be key/value pairs, and some of + * which might be raw values. + * + * @author Curtis Rueden + */ +public interface Items extends List { + + /** + * Gets the parsed items as a map. The map will have the same iteration + * order as the original list. + */ + Map asMap(); + + /** Returns true iff all items are named key/value pairs. */ + boolean isMap(); + + /** Returns true iff there are no named key/value pairs among the items. */ + boolean isList(); + +} + diff --git a/src/main/java/org/scijava/parse/ParseService.java b/src/main/java/org/scijava/parse/ParseService.java new file mode 100644 index 000000000..fe7ff2946 --- /dev/null +++ b/src/main/java/org/scijava/parse/ParseService.java @@ -0,0 +1,72 @@ +/* + * #%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.parse; + +import org.scijava.service.SciJavaService; + +/** + * Interface for service that parses strings. + * + * @author Curtis Rueden + */ +public interface ParseService extends SciJavaService { + + /** + * Parses a comma-delimited list of data elements. + *

    + * Some data elements might be {@code key=value} pairs, while others might be + * raw values (i.e., no equals sign). + *

    + * + * @param arg The string to parse. + * @return A parsed list of {@link Item}s. + * @throws IllegalArgumentException If the string does not conform to expected + * syntax. + */ + default Items parse(final String arg) { + return parse(arg, true); + } + + /** + * Parses a comma-delimited list of data elements. + *

    + * Some data elements might be {@code key=value} pairs, while others might be + * raw values (i.e., no equals sign). + *

    + * + * @param arg The string to parse. + * @param strict Whether to fail fast when encountering an unassigned variable + * token. + * @return A parsed list of {@link Item}s. + * @throws IllegalArgumentException If the string does not conform to expected + * syntax. + */ + Items parse(String arg, boolean strict); +} diff --git a/src/main/java/org/scijava/platform/AbstractPlatform.java b/src/main/java/org/scijava/platform/AbstractPlatform.java index 6970acc50..9005f3024 100644 --- a/src/main/java/org/scijava/platform/AbstractPlatform.java +++ b/src/main/java/org/scijava/platform/AbstractPlatform.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,82 +40,14 @@ public abstract class AbstractPlatform implements Platform { // -- Platform methods -- - @Override - public String javaVendor() { - return null; - } - - @Override - public String javaVersion() { - return null; - } - - @Override - public String osArch() { - return null; - } - - @Override - public String osName() { - return null; - } - - @Override - public String osVersion() { - return null; - } - - @Override - public boolean isTarget() { - if (javaVendor() != null) { - final String javaVendor = System.getProperty("java.vendor"); - if (!javaVendor.matches(".*" + javaVendor() + ".*")) return false; - } - - if (javaVersion() != null) { - final String javaVersion = System.getProperty("java.version"); - if (javaVersion.compareTo(javaVersion()) < 0) return false; - } - - if (osName() != null) { - final String osName = System.getProperty("os.name"); - if (!osName.matches(".*" + osName() + ".*")) return false; - } - - if (osArch() != null) { - final String osArch = System.getProperty("os.arch"); - if (!osArch.matches(".*" + osArch() + ".*")) return false; - } - - if (osVersion() != null) { - final String osVersion = System.getProperty("os.version"); - if (osVersion.compareTo(osVersion()) < 0) return false; - } - - return true; - } - @Override public void configure(final PlatformService service) { platformService = service; } - @Override - public boolean registerAppMenus(final Object menus) { - return false; - } - - // -- Disposable methods -- - - @Override - public void dispose() { - // NB: Do nothing by default. - } - // -- Internal methods -- protected PlatformService getPlatformService() { return platformService; } - } diff --git a/src/main/java/org/scijava/platform/AppEventService.java b/src/main/java/org/scijava/platform/AppEventService.java index 6b4d31a8f..6227d6563 100644 --- a/src/main/java/org/scijava/platform/AppEventService.java +++ b/src/main/java/org/scijava/platform/AppEventService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/DefaultAppEventService.java b/src/main/java/org/scijava/platform/DefaultAppEventService.java index 43bbcf1cd..07809297d 100644 --- a/src/main/java/org/scijava/platform/DefaultAppEventService.java +++ b/src/main/java/org/scijava/platform/DefaultAppEventService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,7 +46,7 @@ /** @deprecated Use {@link AppService} and {@link App} instead. */ @Deprecated -@Plugin(type = Service.class, priority = Priority.LOW_PRIORITY) +@Plugin(type = Service.class, priority = Priority.LOW) public class DefaultAppEventService extends AbstractService implements AppEventService { diff --git a/src/main/java/org/scijava/platform/DefaultPlatform.java b/src/main/java/org/scijava/platform/DefaultPlatform.java index f7bb7b18c..18473f650 100644 --- a/src/main/java/org/scijava/platform/DefaultPlatform.java +++ b/src/main/java/org/scijava/platform/DefaultPlatform.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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.net.URL; import org.scijava.Priority; +import org.scijava.log.LogService; +import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; /** @@ -43,10 +43,12 @@ * @author Curtis Rueden * @author Johannes Schindelin */ -@Plugin(type = Platform.class, name = "Default", - priority = Priority.VERY_LOW_PRIORITY) +@Plugin(type = Platform.class, name = "Default", priority = Priority.VERY_LOW) public class DefaultPlatform extends AbstractPlatform { + @Parameter(required = false) + private LogService log; + // -- PlatformHandler methods -- /** @@ -69,9 +71,14 @@ public void open(final URL url) throws IOException { try { final int exitCode = getPlatformService().exec(browser, url.toString()); if (exitCode == 0) return; + else if (log != null) { + log.debug("Command '" + browser + + "' failed with exit code " + exitCode); + } } catch (final IOException e) { // browser executable was invalid; try the next one + if (log != null) log.debug("Command '" + browser + "' failed", e); } } throw new IOException("Could not open " + url); diff --git a/src/main/java/org/scijava/platform/DefaultPlatformService.java b/src/main/java/org/scijava/platform/DefaultPlatformService.java index d399caad3..bb8c962a6 100644 --- a/src/main/java/org/scijava/platform/DefaultPlatformService.java +++ b/src/main/java/org/scijava/platform/DefaultPlatformService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -75,12 +73,12 @@ public final class DefaultPlatformService extends // -- PlatformService methods -- @Override - public EventService getEventService() { + public EventService eventService() { return eventService; } @Override - public CommandService getCommandService() { + public CommandService commandService() { return commandService; } diff --git a/src/main/java/org/scijava/platform/Platform.java b/src/main/java/org/scijava/platform/Platform.java index 8b8f1bf47..31cc2e86a 100644 --- a/src/main/java/org/scijava/platform/Platform.java +++ b/src/main/java/org/scijava/platform/Platform.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,22 +54,59 @@ public interface Platform extends SingletonPlugin, Disposable { /** Java Runtime Environment vendor to match. */ - String javaVendor(); + default String javaVendor() { + return null; + } /** Minimum required Java Runtime Environment version. */ - String javaVersion(); + default String javaVersion() { + return null; + } /** Operating system architecture to match. */ - String osArch(); + default String osArch() { + return null; + } /** Operating system name to match. */ - String osName(); + default String osName() { + return null; + } /** Minimum required operating system version. */ - String osVersion(); + default String osVersion() { + return null; + } /** Determines whether the given platform is applicable to this runtime. */ - boolean isTarget(); + default boolean isTarget() { + if (javaVendor() != null) { + final String javaVendor = System.getProperty("java.vendor"); + if (!javaVendor.matches(".*" + javaVendor() + ".*")) return false; + } + + if (javaVersion() != null) { + final String javaVersion = System.getProperty("java.version"); + if (javaVersion.compareTo(javaVersion()) < 0) return false; + } + + if (osName() != null) { + final String osName = System.getProperty("os.name"); + if (!osName.matches(".*" + osName() + ".*")) return false; + } + + if (osArch() != null) { + final String osArch = System.getProperty("os.arch"); + if (!osArch.matches(".*" + osArch() + ".*")) return false; + } + + if (osVersion() != null) { + final String osVersion = System.getProperty("os.version"); + if (osVersion.compareTo(osVersion()) < 0) return false; + } + + return true; + } /** Activates and configures the platform. */ void configure(PlatformService service); @@ -86,6 +121,7 @@ public interface Platform extends SingletonPlugin, Disposable { * @return true iff the menus should not be added to the UI as normal because * the platform did something platform-specific with them instead. */ - boolean registerAppMenus(Object menus); - + default boolean registerAppMenus(final Object menus) { + return false; + } } diff --git a/src/main/java/org/scijava/platform/PlatformService.java b/src/main/java/org/scijava/platform/PlatformService.java index 6a42f6bf3..ff66f8939 100644 --- a/src/main/java/org/scijava/platform/PlatformService.java +++ b/src/main/java/org/scijava/platform/PlatformService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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.net.URL; import java.util.List; +import org.scijava.app.App; +import org.scijava.app.AppService; import org.scijava.command.CommandService; import org.scijava.event.EventService; import org.scijava.plugin.SingletonService; @@ -50,11 +50,13 @@ public interface PlatformService extends SingletonService, SciJavaService { - EventService getEventService(); + default EventService eventService() { + return context().getService(EventService.class); + } - CommandService getCommandService(); - - AppEventService getAppEventService(); + default CommandService commandService() { + return context().getService(CommandService.class); + } /** Gets the platform handlers applicable to this platform. */ List getTargetPlatforms(); @@ -85,4 +87,21 @@ public interface PlatformService extends SingletonService, */ boolean registerAppMenus(Object menus); + // -- Deprecated methods -- + + /** @deprecated Use {@link AppService} and {@link App} instead. */ + @Deprecated + AppEventService getAppEventService(); + + /** @deprecated Use {@link #eventService()} instead. */ + @Deprecated + default EventService getEventService() { + return eventService(); + } + + /** @deprecated Use {@link #commandService()} instead. */ + @Deprecated + default CommandService getCommandService() { + return commandService(); + } } diff --git a/src/main/java/org/scijava/platform/event/AppAboutEvent.java b/src/main/java/org/scijava/platform/event/AppAboutEvent.java index f8bb85e38..cd39d4764 100644 --- a/src/main/java/org/scijava/platform/event/AppAboutEvent.java +++ b/src/main/java/org/scijava/platform/event/AppAboutEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppFocusEvent.java b/src/main/java/org/scijava/platform/event/AppFocusEvent.java index 0dd25f222..e18e88a14 100644 --- a/src/main/java/org/scijava/platform/event/AppFocusEvent.java +++ b/src/main/java/org/scijava/platform/event/AppFocusEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppMenusCreatedEvent.java b/src/main/java/org/scijava/platform/event/AppMenusCreatedEvent.java index 4adec3e76..3f8b80e0f 100644 --- a/src/main/java/org/scijava/platform/event/AppMenusCreatedEvent.java +++ b/src/main/java/org/scijava/platform/event/AppMenusCreatedEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppOpenFilesEvent.java b/src/main/java/org/scijava/platform/event/AppOpenFilesEvent.java index b74c97c66..cf42e0a73 100644 --- a/src/main/java/org/scijava/platform/event/AppOpenFilesEvent.java +++ b/src/main/java/org/scijava/platform/event/AppOpenFilesEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppPreferencesEvent.java b/src/main/java/org/scijava/platform/event/AppPreferencesEvent.java index f3e95ebda..f6cf5ce81 100644 --- a/src/main/java/org/scijava/platform/event/AppPreferencesEvent.java +++ b/src/main/java/org/scijava/platform/event/AppPreferencesEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppPrintEvent.java b/src/main/java/org/scijava/platform/event/AppPrintEvent.java index 21a377973..2cfd3a56e 100644 --- a/src/main/java/org/scijava/platform/event/AppPrintEvent.java +++ b/src/main/java/org/scijava/platform/event/AppPrintEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppQuitEvent.java b/src/main/java/org/scijava/platform/event/AppQuitEvent.java index 69293d35f..0b5f5f1ac 100644 --- a/src/main/java/org/scijava/platform/event/AppQuitEvent.java +++ b/src/main/java/org/scijava/platform/event/AppQuitEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppReOpenEvent.java b/src/main/java/org/scijava/platform/event/AppReOpenEvent.java index c0f168c91..a9db284a5 100644 --- a/src/main/java/org/scijava/platform/event/AppReOpenEvent.java +++ b/src/main/java/org/scijava/platform/event/AppReOpenEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppScreenSleepEvent.java b/src/main/java/org/scijava/platform/event/AppScreenSleepEvent.java index 1a5cdeb61..d499a62db 100644 --- a/src/main/java/org/scijava/platform/event/AppScreenSleepEvent.java +++ b/src/main/java/org/scijava/platform/event/AppScreenSleepEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppSleepEvent.java b/src/main/java/org/scijava/platform/event/AppSleepEvent.java index 8896089b7..5ba427317 100644 --- a/src/main/java/org/scijava/platform/event/AppSleepEvent.java +++ b/src/main/java/org/scijava/platform/event/AppSleepEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppSystemSleepEvent.java b/src/main/java/org/scijava/platform/event/AppSystemSleepEvent.java index e0b89a317..9e86cadf2 100644 --- a/src/main/java/org/scijava/platform/event/AppSystemSleepEvent.java +++ b/src/main/java/org/scijava/platform/event/AppSystemSleepEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppUserSessionEvent.java b/src/main/java/org/scijava/platform/event/AppUserSessionEvent.java index 9575d763c..fb202cbc3 100644 --- a/src/main/java/org/scijava/platform/event/AppUserSessionEvent.java +++ b/src/main/java/org/scijava/platform/event/AppUserSessionEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/AppVisibleEvent.java b/src/main/java/org/scijava/platform/event/AppVisibleEvent.java index d9826680d..1b6fd77d2 100644 --- a/src/main/java/org/scijava/platform/event/AppVisibleEvent.java +++ b/src/main/java/org/scijava/platform/event/AppVisibleEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/platform/event/ApplicationEvent.java b/src/main/java/org/scijava/platform/event/ApplicationEvent.java index 0e56d48e0..da0a2f3a1 100644 --- a/src/main/java/org/scijava/platform/event/ApplicationEvent.java +++ b/src/main/java/org/scijava/platform/event/ApplicationEvent.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/AbstractHandlerPlugin.java b/src/main/java/org/scijava/plugin/AbstractHandlerPlugin.java index 8a00c1878..2077a94c0 100644 --- a/src/main/java/org/scijava/plugin/AbstractHandlerPlugin.java +++ b/src/main/java/org/scijava/plugin/AbstractHandlerPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/AbstractHandlerService.java b/src/main/java/org/scijava/plugin/AbstractHandlerService.java index 2e13897a2..65873e6eb 100644 --- a/src/main/java/org/scijava/plugin/AbstractHandlerService.java +++ b/src/main/java/org/scijava/plugin/AbstractHandlerService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,22 +39,5 @@ public abstract class AbstractHandlerService> extends AbstractSingletonService implements HandlerService { - - // -- HandlerService methods -- - - @Override - public PT getHandler(final DT data) { - for (final PT handler : getInstances()) { - if (handler.supports(data)) return handler; - } - return null; - } - - // -- Typed methods -- - - @Override - public boolean supports(final DT data) { - return getHandler(data) != null; - } - + // NB: No implementation needed. } diff --git a/src/main/java/org/scijava/plugin/AbstractPTService.java b/src/main/java/org/scijava/plugin/AbstractPTService.java index 9c31eb2fc..5273d007b 100644 --- a/src/main/java/org/scijava/plugin/AbstractPTService.java +++ b/src/main/java/org/scijava/plugin/AbstractPTService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,8 +29,6 @@ package org.scijava.plugin; -import java.util.List; - import org.scijava.service.AbstractService; /** @@ -51,22 +47,7 @@ public abstract class AbstractPTService extends // -- PTService methods -- @Override - public PluginService getPluginService() { + public PluginService pluginService() { return pluginService; } - - @Override - public List> getPlugins() { - return pluginService.getPluginsOfType(getPluginType()); - } - - @Override - public

    P create(final Class

    pluginClass) { - final PluginInfo info = - pluginService.getPlugin(pluginClass, getPluginType()); - @SuppressWarnings("unchecked") - final P plugin = (P) pluginService.createInstance(info); - return plugin; - } - } diff --git a/src/main/java/org/scijava/plugin/AbstractRichPlugin.java b/src/main/java/org/scijava/plugin/AbstractRichPlugin.java index bc720e848..205d8ce8c 100644 --- a/src/main/java/org/scijava/plugin/AbstractRichPlugin.java +++ b/src/main/java/org/scijava/plugin/AbstractRichPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,9 +30,7 @@ package org.scijava.plugin; import org.scijava.AbstractContextual; -import org.scijava.Prioritized; import org.scijava.Priority; -import org.scijava.util.ClassUtils; /** * Abstract base class for {@link RichPlugin} implementations. @@ -46,7 +42,7 @@ public abstract class AbstractRichPlugin extends AbstractContextual implements { /** The priority of the plugin. */ - private double priority = Priority.NORMAL_PRIORITY; + private double priority = Priority.NORMAL; /** The metadata associated with the plugin. */ private PluginInfo info; @@ -83,18 +79,4 @@ public void setInfo(final PluginInfo info) { this.info = info; } - // -- Comparable methods -- - - @Override - public int compareTo(final Prioritized that) { - if (that == null) return 1; - - // compare priorities - final int priorityCompare = Priority.compare(this, that); - if (priorityCompare != 0) return priorityCompare; - - // compare classes - return ClassUtils.compare(getClass(), that.getClass()); - } - } diff --git a/src/main/java/org/scijava/plugin/AbstractSingletonService.java b/src/main/java/org/scijava/plugin/AbstractSingletonService.java index cc19bacab..b7f65c684 100644 --- a/src/main/java/org/scijava/plugin/AbstractSingletonService.java +++ b/src/main/java/org/scijava/plugin/AbstractSingletonService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,9 +35,11 @@ import java.util.List; import java.util.Map; +import org.scijava.event.EventHandler; import org.scijava.log.LogService; -import org.scijava.object.LazyObjects; import org.scijava.object.ObjectService; +import org.scijava.plugin.event.PluginsAddedEvent; +import org.scijava.plugin.event.PluginsRemovedEvent; /** * Abstract base class for {@link SingletonService}s. @@ -57,9 +57,6 @@ public abstract class AbstractSingletonService @Parameter private ObjectService objectService; - // TODO: Listen for PluginsAddedEvent and PluginsRemovedEvent - // and update the list of singletons accordingly. - /** List of singleton plugin instances. */ private List instances; @@ -67,10 +64,15 @@ public abstract class AbstractSingletonService // -- SingletonService methods -- + @Override + public ObjectService objectService() { + return objectService; + } + @Override public List getInstances() { if (instances == null) initInstances(); - return instances; + return Collections.unmodifiableList(instances); } @SuppressWarnings("unchecked") @@ -80,39 +82,47 @@ public

    P getInstance(final Class

    pluginClass) { return (P) instanceMap.get(pluginClass); } - // -- PTService methods -- +//-- Event handlers -- - @Override - public

    P create(final Class

    pluginClass) { - throw new UnsupportedOperationException( - "Cannot create singleton plugin instance. " - + "Use getInstance(Class) instead."); + @EventHandler + protected void onEvent(final PluginsRemovedEvent event) { + if (instanceMap == null) return; + for (final PluginInfo info : event.getItems()) { + final PT obj = instanceMap.remove(info.getPluginClass()); + if (obj != null) { // we actually removed a plugin + instances.remove(obj); + objectService.removeObject(obj); + } + } } - // -- Service methods -- - - @Override - public void initialize() { - // add singleton instances to the object index... IN THE FUTURE! - objectService.getIndex().addLater(new LazyObjects() { - - @Override - public ArrayList get() { - return new ArrayList(getInstances()); + @EventHandler + protected void onEvent(final PluginsAddedEvent event) { + if (instanceMap == null) return; + // collect singleton plugins + final List> singletons = new ArrayList<>(); + for (final PluginInfo pluginInfo : event.getItems()) { + if (getPluginType().isAssignableFrom(pluginInfo.getPluginType())) { + @SuppressWarnings("unchecked") + final PT plugin = pluginService().createInstance( + (PluginInfo) pluginInfo); + @SuppressWarnings("unchecked") + final Class pluginClass = (Class) plugin + .getClass(); + instanceMap.put(pluginClass, plugin); + instances.add(plugin); } - }); - } + } - // -- Internal methods -- + for (final PluginInfo pluginInfo : singletons) { + final PT plugin = pluginService().createInstance(pluginInfo); + @SuppressWarnings("unchecked") + final Class pluginClass = (Class) plugin + .getClass(); + instanceMap.put(pluginClass, plugin); + instances.add(plugin); + } - /** - * Allows subclasses to exclude instances. - * - * @param list the initial list of instances - * @return the filtered list of instances - */ - protected List filterInstances(final List list) { - return list; } // -- Helper methods -- @@ -120,16 +130,15 @@ protected List filterInstances(final List list) { private synchronized void initInstances() { if (instances != null) return; - final List list = - Collections.unmodifiableList(filterInstances(getPluginService() - .createInstancesOfType(getPluginType()))); + @SuppressWarnings("unchecked") + final List list = (List) filterInstances(pluginService() + .createInstancesOfType(getPluginType())); - final HashMap, PT> map = - new HashMap, PT>(); + final Map, PT> map = new HashMap<>(); for (final PT plugin : list) { @SuppressWarnings("unchecked") - final Class ptClass = + final Class ptClass = // (Class) plugin.getClass(); map.put(ptClass, plugin); } diff --git a/src/main/java/org/scijava/plugin/AbstractTypedPlugin.java b/src/main/java/org/scijava/plugin/AbstractTypedPlugin.java index 1498f6491..a9a12fb89 100644 --- a/src/main/java/org/scijava/plugin/AbstractTypedPlugin.java +++ b/src/main/java/org/scijava/plugin/AbstractTypedPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,12 +41,12 @@ public abstract class AbstractTypedPlugin extends AbstractRichPlugin implements TypedPlugin { - // -- Typed methods -- @Override public boolean supports(final D data) { - return true; + // NB: Overridden just for backwards compatibility, so that + // downstream classes which call super.supports continue to work. + return TypedPlugin.super.supports(data); } - } diff --git a/src/main/java/org/scijava/plugin/AbstractTypedService.java b/src/main/java/org/scijava/plugin/AbstractTypedService.java index cbdb3c4b5..c747223f3 100644 --- a/src/main/java/org/scijava/plugin/AbstractTypedService.java +++ b/src/main/java/org/scijava/plugin/AbstractTypedService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/AbstractWrapperPlugin.java b/src/main/java/org/scijava/plugin/AbstractWrapperPlugin.java index e50995bbd..4a8d11412 100644 --- a/src/main/java/org/scijava/plugin/AbstractWrapperPlugin.java +++ b/src/main/java/org/scijava/plugin/AbstractWrapperPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/AbstractWrapperService.java b/src/main/java/org/scijava/plugin/AbstractWrapperService.java index 85e2cea88..9bd224855 100644 --- a/src/main/java/org/scijava/plugin/AbstractWrapperService.java +++ b/src/main/java/org/scijava/plugin/AbstractWrapperService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,8 +29,6 @@ package org.scijava.plugin; -import org.scijava.log.LogService; - /** * Abstract base class for {@link WrapperService}s. * @@ -43,44 +39,5 @@ public abstract class AbstractWrapperService> extends AbstractTypedService implements WrapperService { - - @Parameter(required = false) - private LogService log; - - // -- WrapperService methods -- - - @Override - public PT create(final D data) { - final PT instance = findWrapper(data); - if (instance != null) instance.set(data); - return instance; - } - - // -- Service methods -- - - @Override - public void initialize() { - if (log != null) { - log.debug("Found " + getPlugins().size() + " " + - getPluginType().getSimpleName() + " plugins."); - } - } - - // -- Typed methods -- - - @Override - public boolean supports(final DT data) { - return findWrapper(data) != null; - } - - // -- Helper methods -- - - private PT findWrapper(final D data) { - for (final PluginInfo plugin : getPlugins()) { - final PT instance = getPluginService().createInstance(plugin); - if (instance.supports(data)) return instance; - } - return null; - } - + // NB: No implementation needed. } diff --git a/src/main/java/org/scijava/plugin/Attr.java b/src/main/java/org/scijava/plugin/Attr.java index 73e9156cd..1dce1f4d7 100644 --- a/src/main/java/org/scijava/plugin/Attr.java +++ b/src/main/java/org/scijava/plugin/Attr.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/DefaultPluginFinder.java b/src/main/java/org/scijava/plugin/DefaultPluginFinder.java index cb595ec76..bd3a0eb08 100644 --- a/src/main/java/org/scijava/plugin/DefaultPluginFinder.java +++ b/src/main/java/org/scijava/plugin/DefaultPluginFinder.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,9 +29,13 @@ package org.scijava.plugin; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import org.scijava.Context; import org.scijava.annotations.Index; import org.scijava.annotations.IndexItem; @@ -51,6 +53,8 @@ public class DefaultPluginFinder implements PluginFinder { /** Class loader to use when querying the annotation indexes. */ private final ClassLoader customClassLoader; + private final PluginBlocklist blocklist; + // -- Constructors -- public DefaultPluginFinder() { @@ -59,6 +63,7 @@ public DefaultPluginFinder() { public DefaultPluginFinder(final ClassLoader classLoader) { customClassLoader = classLoader; + blocklist = new SysPropBlocklist(); } // -- PluginFinder methods -- @@ -68,7 +73,7 @@ public HashMap findPlugins( final List> plugins) { final HashMap exceptions = - new HashMap(); + new HashMap<>(); // load the annotation indexes final ClassLoader classLoader = getClassLoader(); @@ -77,6 +82,7 @@ public HashMap findPlugins( // create a PluginInfo object for each item in the index for (final IndexItem item : annotationIndex) { + if (blocklist.contains(item.className())) continue; try { final PluginInfo info = createInfo(item, classLoader); plugins.add(info); @@ -101,12 +107,55 @@ private PluginInfo createInfo( final Class pluginType = (Class) plugin.type(); - return new PluginInfo(className, pluginType, plugin, classLoader); + return new PluginInfo<>(className, pluginType, plugin, classLoader); } private ClassLoader getClassLoader() { - if (customClassLoader != null) return customClassLoader; - return Thread.currentThread().getContextClassLoader(); + return customClassLoader != null ? // + customClassLoader : Context.getClassLoader(); + } + + // -- Helper classes -- + + private interface PluginBlocklist { + boolean contains(String className); + } + + /** + * A blocklist defined by the {@code scijava.plugin.blocklist} system + * property, formatted as a colon-separated list of regexes. + *

    + * If a plugin class matches any of the regexes, it is excluded from the + * plugin index. + *

    + */ + private class SysPropBlocklist implements PluginBlocklist { + private final List patterns; + + public SysPropBlocklist() { + final String sysProp = System.getProperty("scijava.plugin.blocklist"); + final String[] regexes = // + sysProp == null ? new String[0] : sysProp.split(":"); + patterns = new ArrayList<>(regexes.length); + for (final String regex : regexes) { + try { + patterns.add(Pattern.compile(regex)); + } + catch (final PatternSyntaxException exc) { + // NB: Ignore this malformed pattern. + } + } + } + + // -- PluginBlocklist methods -- + + @Override + public boolean contains(final String className) { + for (final Pattern pattern : patterns) { + if (pattern.matcher(className).matches()) return true; + } + return false; + } } } diff --git a/src/main/java/org/scijava/plugin/DefaultPluginService.java b/src/main/java/org/scijava/plugin/DefaultPluginService.java index cc7429149..096e75c92 100644 --- a/src/main/java/org/scijava/plugin/DefaultPluginService.java +++ b/src/main/java/org/scijava/plugin/DefaultPluginService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -183,7 +181,7 @@ public List> getPluginsOfType( public List> getPluginsOfClass(final Class

    pluginClass, final Class type) { - final ArrayList> result = new ArrayList>(); + final ArrayList> result = new ArrayList<>(); findPluginsOfClass(pluginClass, getPluginsOfType(type), result); filterNonmatchingClasses(pluginClass, result); return result; @@ -203,7 +201,7 @@ public List> getPluginsOfClass( getPluginsOfClass(final String className, final Class type) { final ArrayList> result = - new ArrayList>(); + new ArrayList<>(); findPluginsOfClass(className, getPluginsOfType(type), result); return result; } @@ -220,7 +218,7 @@ public List createInstancesOfType( public List createInstances( final List> infos) { - final ArrayList list = new ArrayList(); + final ArrayList list = new ArrayList<>(); for (final PluginInfo info : infos) { final PT p = createInstance(info); if (p != null) list.add(p); @@ -238,7 +236,10 @@ public List createInstances( return p; } catch (final Throwable t) { - log.error("Cannot create plugin: " + info, t); + final String errorMessage = // + "Cannot create plugin: " + info.getClassName(); + if (log.isDebug()) log.debug(errorMessage, t); + else log.error(errorMessage); } return null; } diff --git a/src/main/java/org/scijava/plugin/HandlerPlugin.java b/src/main/java/org/scijava/plugin/HandlerPlugin.java index 37bffe6ce..8abcba382 100644 --- a/src/main/java/org/scijava/plugin/HandlerPlugin.java +++ b/src/main/java/org/scijava/plugin/HandlerPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/HandlerService.java b/src/main/java/org/scijava/plugin/HandlerService.java index 53cd7d3d1..dec2d71b5 100644 --- a/src/main/java/org/scijava/plugin/HandlerService.java +++ b/src/main/java/org/scijava/plugin/HandlerService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,9 +52,17 @@ public interface HandlerService> extends * Gets the most appropriate handler for the given data object, or null if no * handler supports it. */ - PT getHandler(DT data); - - // NB: Javadoc overrides. + default PT getHandler(final DT data) { + for (final PT handler : getInstances()) { + try { + if (handler.supports(data)) return handler; + } + catch (final Throwable t) { + log().error("Malfunctioning plugin: " + handler.getClass().getName(), t); + } + } + return null; + } // -- SingletonService methods -- @@ -71,6 +77,7 @@ public interface HandlerService> extends /** Gets whether the given data object is supported. */ @Override - boolean supports(DT data); - + default boolean supports(final DT data) { + return getHandler(data) != null; + } } diff --git a/src/main/java/org/scijava/plugin/HasPluginInfo.java b/src/main/java/org/scijava/plugin/HasPluginInfo.java index 4915ca8d1..622429d98 100644 --- a/src/main/java/org/scijava/plugin/HasPluginInfo.java +++ b/src/main/java/org/scijava/plugin/HasPluginInfo.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/Menu.java b/src/main/java/org/scijava/plugin/Menu.java index abc746d36..ce861c6b6 100644 --- a/src/main/java/org/scijava/plugin/Menu.java +++ b/src/main/java/org/scijava/plugin/Menu.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/PTService.java b/src/main/java/org/scijava/plugin/PTService.java index 8abc4be7d..d0e61a997 100644 --- a/src/main/java/org/scijava/plugin/PTService.java +++ b/src/main/java/org/scijava/plugin/PTService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,7 +38,7 @@ *

    * There are many kinds of services, but most of them share one common * characteristic: they provide API specific to a particular type of plugin. A - * few examples from ImageJ: + * few examples: *

    *
      *
    • The {@link org.scijava.command.CommandService} works with @@ -87,15 +85,32 @@ public interface PTService extends Service { * Gets the service responsible for discovering and managing this service's * plugins. */ - PluginService getPluginService(); - - /** Gets the plugins managed by this service. */ - List> getPlugins(); + default PluginService pluginService() { + return context().getService(PluginService.class); + } /** Gets the type of plugins managed by this service. */ Class getPluginType(); + /** Gets the plugins managed by this service. */ + default List> getPlugins() { + return pluginService().getPluginsOfType(getPluginType()); + } + /** Creates an instance of the given plugin class. */ -

      P create(final Class

      pluginClass); + default

      P create(final Class

      pluginClass) { + final PluginInfo info = + pluginService().getPlugin(pluginClass, getPluginType()); + @SuppressWarnings("unchecked") + final P plugin = (P) pluginService().createInstance(info); + return plugin; + } + + // -- Deprecated methods -- + /** @deprecated Use {@link #pluginService()} instead. */ + @Deprecated + default PluginService getPluginService() { + return pluginService(); + } } diff --git a/src/main/java/org/scijava/plugin/Parameter.java b/src/main/java/org/scijava/plugin/Parameter.java index 05ccd809c..edc1da1af 100644 --- a/src/main/java/org/scijava/plugin/Parameter.java +++ b/src/main/java/org/scijava/plugin/Parameter.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,6 +36,7 @@ import org.scijava.ItemIO; import org.scijava.ItemVisibility; +import org.scijava.module.Module; /** * An annotation for indicating a field is an input or output parameter. This @@ -72,7 +71,7 @@ *

    */ // NB: We use the fully qualified name to work around a javac bug: - // http://bugs.sun.com/view_bug.do?bug_id=6512707 + // https://bugs.java.com/view_bug.do?bug_id=6512707 // See: // http://groups.google.com/group/project-lombok/browse_thread/thread/c5568eb659cab203 ItemIO type() default org.scijava.ItemIO.INPUT; @@ -95,11 +94,7 @@ * the user nor included as an input or output parameter. * */ - // NB: We use the fully qualified name to work around a javac bug: - // http://bugs.sun.com/view_bug.do?bug_id=6512707 - // See: - // http://groups.google.com/group/project-lombok/browse_thread/thread/c5568eb659cab203 - ItemVisibility visibility() default org.scijava.ItemVisibility.NORMAL; + ItemVisibility visibility() default ItemVisibility.NORMAL; /** * Defines whether the parameter value should be filled programmatically, if @@ -119,6 +114,14 @@ /** Defines a function that is called to initialize the parameter. */ String initializer() default ""; + /** + * Defines a function that is called to validate the parameter value after it + * is marked as resolved. + * + * @see Module#resolveInput(String) + */ + String validater() default ""; + /** * Defines a function that is called whenever this parameter changes. *

    @@ -149,12 +152,6 @@ /** Defines the step size to use (numeric parameters only). */ String stepSize() default ""; - /** - * Defines the width of the input field in characters (text field parameters - * only). - */ - int columns() default 6; - /** Defines the list of possible values (multiple choice text fields only). */ String[] choices() default {}; @@ -164,4 +161,7 @@ */ Attr[] attrs() default {}; + /** @deprecated Replaced by {@link #style()}. */ + @Deprecated + int columns() default 6; } diff --git a/src/main/java/org/scijava/plugin/Plugin.java b/src/main/java/org/scijava/plugin/Plugin.java index f4bca8fd8..cd1701881 100644 --- a/src/main/java/org/scijava/plugin/Plugin.java +++ b/src/main/java/org/scijava/plugin/Plugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -79,10 +77,10 @@ /** * Abbreviated menu path defining where the plugin is shown in the menu - * structure. Uses greater than signs (>) as a separator; e.g.: - * "Image > Overlay > Properties..." defines a "Properties..." menu item - * within the "Overlay" submenu of the "Image" menu. Use either - * {@link #menuPath} or {@link #menu} but not both. + * structure. Uses greater than signs ({@code >}) as a separator; e.g.: "Image + * > Overlay > Properties..." defines a "Properties..." menu item within + * the "Overlay" submenu of the "Image" menu. Use either {@link #menuPath} or + * {@link #menu} but not both. */ String menuPath() default ""; @@ -115,18 +113,18 @@ * Any double value is allowed, but for convenience, there are some presets: *

    *
      - *
    • {@link Priority#FIRST_PRIORITY}
    • - *
    • {@link Priority#VERY_HIGH_PRIORITY}
    • - *
    • {@link Priority#HIGH_PRIORITY}
    • - *
    • {@link Priority#NORMAL_PRIORITY}
    • - *
    • {@link Priority#LOW_PRIORITY}
    • - *
    • {@link Priority#VERY_LOW_PRIORITY}
    • - *
    • {@link Priority#LAST_PRIORITY}
    • + *
    • {@link Priority#FIRST}
    • + *
    • {@link Priority#VERY_HIGH}
    • + *
    • {@link Priority#HIGH}
    • + *
    • {@link Priority#NORMAL}
    • + *
    • {@link Priority#LOW}
    • + *
    • {@link Priority#VERY_LOW}
    • + *
    • {@link Priority#LAST}
    • *
    * * @see org.scijava.service.Service */ - double priority() default Priority.NORMAL_PRIORITY; + double priority() default Priority.NORMAL; /** * Whether the plugin can be selected in the user interface. A plugin's diff --git a/src/main/java/org/scijava/plugin/PluginFinder.java b/src/main/java/org/scijava/plugin/PluginFinder.java index 7267db536..9f193a90e 100644 --- a/src/main/java/org/scijava/plugin/PluginFinder.java +++ b/src/main/java/org/scijava/plugin/PluginFinder.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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/plugin/PluginIndex.java b/src/main/java/org/scijava/plugin/PluginIndex.java index 832cdbc8b..ea3aa8fdc 100644 --- a/src/main/java/org/scijava/plugin/PluginIndex.java +++ b/src/main/java/org/scijava/plugin/PluginIndex.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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: @@ -90,7 +88,7 @@ public PluginIndex() { */ @SuppressWarnings({ "rawtypes", "unchecked" }) public PluginIndex(final PluginFinder pluginFinder) { - // NB: See: http://stackoverflow.com/questions/4765520/ + // NB: See: https://stackoverflow.com/questions/4765520/ super((Class) PluginInfo.class); this.pluginFinder = pluginFinder; } @@ -103,7 +101,7 @@ public PluginIndex(final PluginFinder pluginFinder) { */ public void discover() { if (pluginFinder == null) return; - final ArrayList> plugins = new ArrayList>(); + final ArrayList> plugins = new ArrayList<>(); exceptions = pluginFinder.findPlugins(plugins); addAll(plugins); } diff --git a/src/main/java/org/scijava/plugin/PluginInfo.java b/src/main/java/org/scijava/plugin/PluginInfo.java index 2e67873d7..85bab5954 100644 --- a/src/main/java/org/scijava/plugin/PluginInfo.java +++ b/src/main/java/org/scijava/plugin/PluginInfo.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,8 @@ package org.scijava.plugin; import java.net.URL; +import java.util.Collection; +import java.util.Optional; import org.scijava.AbstractUIDetails; import org.scijava.Identifiable; @@ -44,8 +44,8 @@ import org.scijava.UIDetails; import org.scijava.Versioned; import org.scijava.input.Accelerator; -import org.scijava.util.ClassUtils; import org.scijava.util.StringMaker; +import org.scijava.util.Types; import org.scijava.util.VersionUtils; /** @@ -280,13 +280,15 @@ public String getClassName() { @Override public Class loadClass() throws InstantiableException { if (pluginClass == null) { - final Class c = ClassUtils.loadClass(className, classLoader); - if (c == null) { - throw new InstantiableException("Class not found: " + className); + try { + final Class c = Types.load(className, classLoader, false); + @SuppressWarnings("unchecked") + final Class typedClass = (Class) c; + pluginClass = typedClass; + } + catch (final IllegalArgumentException exc) { + throw new InstantiableException("Class not found: " + className, exc); } - @SuppressWarnings("unchecked") - final Class typedClass = (Class) c; - pluginClass = typedClass; } return pluginClass; @@ -316,12 +318,7 @@ public PT createInstance() throws InstantiableException { @Override public String getIdentifier() { - try { - return "plugin:" + loadClass(); - } - catch (final InstantiableException exc) { - return null; - } + return "plugin:" + getClassName(); } // -- Locatable methods -- @@ -329,7 +326,7 @@ public String getIdentifier() { @Override public String getLocation() { try { - return ClassUtils.getLocation(loadClass()).toExternalForm(); + return Types.location(loadClass()).toExternalForm(); } catch (InstantiableException exc) { return null; @@ -348,6 +345,143 @@ public String getVersion() { } } + // -- Utility methods -- + + /** + * Finds a {@link PluginInfo} of the given plugin class in the specified + * {@link PluginIndex}. Note that to avoid loading plugin classes, class + * identity is determined by class name equality only. + * + * @param pluginClass The concrete class of the plugin whose + * {@link PluginInfo} is desired. + * @param pluginIndex The {@link PluginIndex} to search for a matching + * {@link PluginInfo}. + * @return The matching {@link PluginInfo}, or null if none found. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static

    PluginInfo get( + final Class

    pluginClass, final PluginIndex pluginIndex) + { + return get(pluginClass, (Collection) pluginIndex.getAll()); + } + + /** + * Finds a {@link PluginInfo} of the given plugin class and plugin type in the + * specified {@link PluginIndex}. Note that to avoid loading plugin + * classes, class identity is determined by class name equality only. + * + * @param pluginClass The concrete class of the plugin whose + * {@link PluginInfo} is desired. + * @param pluginType The type of the plugin; see {@link #getPluginType()}. + * @param pluginIndex The {@link PluginIndex} to search for a matching + * {@link PluginInfo}. + * @return The matching {@link PluginInfo}, or null if none found. + */ + public static

    PluginInfo get( + final Class

    pluginClass, final Class pluginType, + final PluginIndex pluginIndex) + { + return get(pluginClass, pluginIndex.getPlugins(pluginType)); + } + + /** + * Finds a {@link PluginInfo} of the given plugin class in the specified list + * of plugins. Note that to avoid loading plugin classes, class identity + * is determined by class name equality only. + * + * @param pluginClass The concrete class of the plugin whose + * {@link PluginInfo} is desired. + * @param plugins The list of plugins to search for a match. + * @return The matching {@link PluginInfo}, or null if none found. + */ + public static

    PluginInfo get( + final Class

    pluginClass, + final Collection> plugins) + { + final String className = pluginClass.getName(); + final Optional> result = plugins.stream() // + .filter(info -> info.getClassName().equals(className)) // + .findFirst(); + return result.isPresent() ? result.get() : null; + } + + /** + * Creates a {@link PluginInfo} for the given plugin class. The class must be + * a concrete class annotated with the @{@link Plugin} annotation, from which + * the plugin type will be inferred. + * + * @param pluginClass The concrete class of the plugin for which a new + * {@link PluginInfo} is desired. + * @return A newly created {@link PluginInfo} for the given plugin class. + * @throws IllegalArgumentException if the given class is not annotated + * with @{@link Plugin}, or its annotated {@link Plugin#type() + * type()} is not a supertype of the plugin class. + */ + public static PluginInfo create( + final Class pluginClass) + { + @SuppressWarnings({ "rawtypes", "unchecked" }) + final PluginInfo info = new PluginInfo(pluginClass, // + pluginType(pluginClass)); + return info; + } + + /** + * Creates a {@link PluginInfo} for the given plugin class of the specified + * plugin type. + * + * @param pluginClass The concrete class of the plugin for which a new + * {@link PluginInfo} is desired. + * @param pluginType The type of the plugin; see {@link #getPluginType()}. + * @return A newly created {@link PluginInfo} for the given plugin class. + */ + public static

    PluginInfo create( + final Class

    pluginClass, final Class pluginType) + { + return new PluginInfo<>(pluginClass, pluginType); + } + + /** + * Obtains a {@link PluginInfo} for the given plugin class. If one already + * exists in the specified {@link PluginIndex}, it is retrieved (see + * {@link #get(Class, PluginIndex)}); otherwise, a new one is created (see + * {@link #create(Class)}) but not added to the index. + * + * @param pluginClass The concrete class of the plugin whose + * {@link PluginInfo} is desired. + * @param pluginIndex The {@link PluginIndex} to search for a matching + * {@link PluginInfo}. + * @throws IllegalArgumentException when creating a new {@link PluginInfo} if + * the associated plugin type cannot be inferred; see + * {@link #create(Class)}. + */ + public static

    PluginInfo getOrCreate( + final Class

    pluginClass, final PluginIndex pluginIndex) + { + final PluginInfo existing = get(pluginClass, pluginIndex); + return existing == null ? create(pluginClass) : existing; + } + + /** + * Obtains a {@link PluginInfo} for the given plugin class. If one already + * exists in the specified {@link PluginIndex}, it is retrieved (see + * {@link #get(Class, PluginIndex)}); otherwise, a new one is created (see + * {@link #create(Class)}) but not added to the index. + * + * @param pluginClass The concrete class of the plugin whose + * {@link PluginInfo} is desired. + * @param pluginType The type of the plugin; see {@link #getPluginType()}. + * @param pluginIndex The {@link PluginIndex} to search for a matching + * {@link PluginInfo}. + */ + public static

    PluginInfo + getOrCreate(final Class

    pluginClass, final Class pluginType, + final PluginIndex pluginIndex) + { + final PluginInfo existing = get(pluginClass, pluginType, pluginIndex); + return existing == null ? create(pluginClass, pluginType) : existing; + } + // -- Helper methods -- /** Populates the entry to match the associated @{@link Plugin} annotation. */ @@ -409,4 +543,23 @@ private MenuPath parseMenuPath(final Menu[] menu) { return menuPath; } + /** Extracts the plugin type from a class's @{@link Plugin} annotation. */ + private static

    Class pluginType( + final Class

    pluginClass) + { + final Plugin annotation = pluginClass.getAnnotation(Plugin.class); + if (annotation == null) { + throw new IllegalArgumentException( + "Cannot infer plugin type from class '" + pluginClass.getName() + + "' with no @Plugin annotation."); + } + final Class type = annotation.type(); + if (!type.isAssignableFrom(pluginClass)) { + throw new IllegalArgumentException("Invalid plugin type '" + // + type.getName() + "' for class '" + pluginClass.getName() + "'"); + } + @SuppressWarnings("unchecked") + final Class pluginType = (Class) type; + return pluginType; + } } diff --git a/src/main/java/org/scijava/plugin/PluginService.java b/src/main/java/org/scijava/plugin/PluginService.java index 6cbb5257e..1eab51413 100644 --- a/src/main/java/org/scijava/plugin/PluginService.java +++ b/src/main/java/org/scijava/plugin/PluginService.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,14 +31,20 @@ import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.scijava.Priority; import org.scijava.service.SciJavaService; /** * Interface for service that keeps track of available plugins. *

    - * The plugin service keeps a master index of all plugins known to the system. + * The plugin service keeps a central index of all plugins known to the system. * At heart, a plugin is a piece of functionality that extends a program's * capabilities. Plugins take many forms; see {@link SciJavaPlugin} for details. *

    @@ -253,4 +257,23 @@ public interface PluginService extends SciJavaService { */ PT createInstance(PluginInfo info); + /** + * Sorts the given list of plugin instances by priority. + * + * @param instances List of plugin instances to sort. + * @param type The type of plugin these instances represent. + */ + default void sort(final List instances, + final Class type) + { + // Create a mapping from plugin classes to priorities. + final List> plugins = getPluginsOfType(type); + final Map, PluginInfo> infos = plugins.stream().collect(// + Collectors.toMap(PluginInfo::getPluginClass, Function.identity())); + + // Compare plugin instances by priority via the mapping. + final Comparator comparator = (o1, o2) -> Priority.compare(// + infos.get(o1.getClass()), infos.get(o2.getClass())); + Collections.sort(instances, comparator); + } } diff --git a/src/main/java/org/scijava/plugin/RichPlugin.java b/src/main/java/org/scijava/plugin/RichPlugin.java index b64b91ed5..891cc7822 100644 --- a/src/main/java/org/scijava/plugin/RichPlugin.java +++ b/src/main/java/org/scijava/plugin/RichPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,12 @@ package org.scijava.plugin; import org.scijava.Contextual; +import org.scijava.Identifiable; +import org.scijava.Locatable; import org.scijava.Prioritized; +import org.scijava.Versioned; +import org.scijava.log.LogService; +import org.scijava.log.Logged; /** * Base interface for {@link Contextual}, {@link Prioritized} plugins that @@ -42,8 +45,21 @@ * * @author Curtis Rueden */ -public interface RichPlugin extends Contextual, Prioritized, HasPluginInfo, - SciJavaPlugin +public interface RichPlugin extends SciJavaPlugin, Contextual, Prioritized, + HasPluginInfo, Logged, Identifiable, Locatable, Versioned { - // NB: Marker interface. + + // -- Identifiable methods -- + + @Override + default String getIdentifier() { + return "plugin:" + getClass().getName(); + } + + // -- Logged methods -- + + @Override + default LogService log() { + return context().getService(LogService.class); + } } diff --git a/src/main/java/org/scijava/plugin/SciJavaPlugin.java b/src/main/java/org/scijava/plugin/SciJavaPlugin.java index 72bcb4133..97675e8bc 100644 --- a/src/main/java/org/scijava/plugin/SciJavaPlugin.java +++ b/src/main/java/org/scijava/plugin/SciJavaPlugin.java @@ -2,9 +2,7 @@ * #%L * SciJava Common shared library for SciJava software. * %% - * Copyright (C) 2009 - 2015 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,11 +40,17 @@ * The core types of plugins are as follows: *

    *