From 67a78a04aa650b339a3af8c58929f54e59e27794 Mon Sep 17 00:00:00 2001 From: Marwan Zouinkhi Date: Mon, 8 Sep 2025 15:49:58 -0400 Subject: [PATCH 1/3] update link uri handler --- .../java/org/scijava/links/FijiURILink.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/main/java/org/scijava/links/FijiURILink.java diff --git a/src/main/java/org/scijava/links/FijiURILink.java b/src/main/java/org/scijava/links/FijiURILink.java new file mode 100644 index 0000000..8df96ec --- /dev/null +++ b/src/main/java/org/scijava/links/FijiURILink.java @@ -0,0 +1,124 @@ +/*- + * #%L + * URL scheme handlers for SciJava. + * %% + * Copyright (C) 2023 - 2025 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.links; + +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Arrays; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * Utility class for working with {@link URI} objects. + * + * @author Curtis Rueden, Marwan Zouinkhi + */ +public final class FijiURILink { + + + private final String plugin; // e.g., "BDV" + private final String subPlugin; // e.g., "open" (nullable) + private final String query; // e.g., "a=1&b=2" (nullable) + private final String rawQuery; // e.g., "a=1&b=2" (nullable) + + private FijiURILink(String plugin, String subPlugin, String query, String rawQuery) { + this.plugin = plugin; + this.subPlugin = subPlugin; + this.query = query; + this.rawQuery = rawQuery; + } + + public static FijiURILink parse(String uriString) { + Objects.requireNonNull(uriString, "uriString"); + final URI uri; + try { + uri = new URI(uriString); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URI: " + uriString, e); + } + + if (!"fiji".equalsIgnoreCase(uri.getScheme())) { + throw new IllegalArgumentException("Scheme must be fiji://"); + } + // For opaque vs hierarchical handling: ensure it's hierarchical (has //) + String authority = uri.getAuthority(); // first segment after // + if (authority == null || authority.isEmpty()) { + throw new IllegalArgumentException("Missing plugin name after fiji://"); + } + String plugin = authority; + + String path = uri.getPath(); // includes leading '/' + String sub = null; + if (path != null && !path.isEmpty()) { + // normalize: "/open" -> "open"; "/" -> null + String trimmed = path.startsWith("/") ? path.substring(1) : path; + sub = trimmed.isEmpty() ? null : trimmed; + } + + // Raw query (no '?'), leave as-is; users can parse if they want. + String q = uri.getQuery(); + // Optional: decode percent-escapes (uncomment if desired) + // q = (q == null) ? null : java.net.URLDecoder.decode(q, StandardCharsets.UTF_8); + String raw = uri.getRawQuery(); + return new FijiURILink(plugin, sub, q, raw); + } + + public String getPlugin() { return plugin; } + public String getSubPlugin() { return subPlugin; } // may be null + public String getQuery() { return query; } // may be null + public String getRawQuery() { return rawQuery; } // may be null + + public Map getParsedQuery() { + final LinkedHashMap map = new LinkedHashMap<>(); + final String[] tokens = query == null ? new String[0] : query.split("&"); + for (final String token : tokens) { + final String[] kv = token.split("=", 2); + final String k = kv[0]; + final String v = kv.length > 1 ? kv[1] : null; + map.put(k, v); + } + return map; + } + + @Override public String toString() { + StringBuilder sb = new StringBuilder("fiji://").append(plugin); + if (subPlugin != null) sb.append('/').append(subPlugin); + if (query != null) sb.append('?').append(query); + return sb.toString(); + } + + // Convenience helper: returns null instead of throwing + public static FijiURILink tryParse(String uriString) { + try { return parse(uriString); } catch (RuntimeException e) { return null; } + } +} + \ No newline at end of file From 8d7e20e536f3773f22830b1e17edfd88c6ae3bd4 Mon Sep 17 00:00:00 2001 From: Marwan Zouinkhi Date: Mon, 8 Sep 2025 15:51:23 -0400 Subject: [PATCH 2/3] fix tests --- src/main/java/org/scijava/links/Links.java | 83 --------- .../java/org/scijava/links/LinksTest.java | 160 +++++++++++------- 2 files changed, 103 insertions(+), 140 deletions(-) delete mode 100644 src/main/java/org/scijava/links/Links.java diff --git a/src/main/java/org/scijava/links/Links.java b/src/main/java/org/scijava/links/Links.java deleted file mode 100644 index 76d2a50..0000000 --- a/src/main/java/org/scijava/links/Links.java +++ /dev/null @@ -1,83 +0,0 @@ -/*- - * #%L - * URL scheme handlers for SciJava. - * %% - * Copyright (C) 2023 - 2025 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.links; - -import java.net.URI; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Utility class for working with {@link URI} objects. - * - * @author Curtis Rueden - */ -public final class Links { - private Links() { - // NB: Prevent instantiation of utility class. - } - - public static String path(final URI uri) { - final String path = uri.getPath(); - if (path == null) return null; - return path.startsWith("/") ? path.substring(1) : path; - } - - public static String operation(final URI uri) { - final String path = path(uri); - if (path == null) return null; - final int slash = path.indexOf("/"); - return slash < 0 ? path : path.substring(0, slash); - } - - public static String[] pathFragments(final URI uri) { - final String path = path(uri); - if (path == null) return null; - return path.isEmpty() ? new String[0] : path.split("/"); - } - - public static String subPath(final URI uri) { - final String path = path(uri); - if (path == null) return null; - final int slash = path.indexOf("/"); - return slash < 0 ? "" : path.substring(slash + 1); - } - - public static Map query(final URI uri) { - final LinkedHashMap map = new LinkedHashMap<>(); - final String query = uri.getQuery(); - final String[] tokens = query == null ? new String[0] : query.split("&"); - for (final String token : tokens) { - final String[] kv = token.split("=", 2); - final String k = kv[0]; - final String v = kv.length > 1 ? kv[1] : null; - map.put(k, v); - } - return map; - } -} diff --git a/src/test/java/org/scijava/links/LinksTest.java b/src/test/java/org/scijava/links/LinksTest.java index 5ffc2f3..315ecf7 100644 --- a/src/test/java/org/scijava/links/LinksTest.java +++ b/src/test/java/org/scijava/links/LinksTest.java @@ -31,66 +31,112 @@ import org.junit.Test; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; + import java.util.Map; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + -/** - * Tests {@link Links}. - * - * @author Curtis Rueden - */ public class LinksTest { - private static final URI TEST_URI; - - static { - try { - TEST_URI = new URI( - "scijava://user:pass@example.com:8080/op/sub/resource?" + - "fruit=apple&veggie=beans#section" - ); - } - catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - @Test - public void testPath() { - var actual = Links.path(TEST_URI); - assertEquals("op/sub/resource", actual); - } - - @Test - public void testOperation() { - var actual = Links.operation(TEST_URI); - assertEquals("op", actual); - } - - @Test - public void testPathFragments() { - String[] expected = {"op", "sub", "resource"}; - var actual = Links.pathFragments(TEST_URI); - assertArrayEquals(expected, actual); - } - - @Test - public void testSubPath() { - var actual = Links.subPath(TEST_URI); - assertEquals("sub/resource", actual); - } - - @Test - public void testQuery() { - Map expected = new HashMap<>(); - expected.put("fruit", "apple"); - expected.put("veggie", "beans"); - var actual = Links.query(TEST_URI); - assertEquals(expected, actual); - } -} + @Test + public void parsesPluginSubAndQuery() { + FijiURILink link = FijiURILink.parse("fiji://BDV/open?source=s3&bucket=data"); + assertEquals("BDV", link.getPlugin()); + assertEquals("open", link.getSubPlugin()); + assertEquals("source=s3&bucket=data", link.getQuery()); + assertEquals("source=s3&bucket=data", link.getRawQuery()); // identical here + } + + @Test + public void parsesPluginOnly() { + FijiURILink link = FijiURILink.parse("fiji://BDV"); + assertEquals("BDV", link.getPlugin()); + assertNull(link.getSubPlugin()); + assertNull(link.getQuery()); + assertNull(link.getRawQuery()); + } + + @Test + public void parsesPluginAndEmptyPathSlash() { + FijiURILink link = FijiURILink.parse("fiji://BDV/?q=hello"); + assertEquals("BDV", link.getPlugin()); + assertNull(link.getSubPlugin()); // "/"" becomes no subplugin + assertEquals("q=hello", link.getQuery()); + assertEquals("q=hello", link.getRawQuery()); + } + + @Test + public void percentEncodedQuery_isPreservedInRawQuery() { + String u = "fiji://bdv?file=%2Ftmp%2Fdata.xml&flag"; + FijiURILink link = FijiURILink.parse(u); + assertEquals("bdv", link.getPlugin()); + assertNull(link.getSubPlugin()); + + // getQuery() returns decoded or not? Your class uses uri.getQuery() (decoded) + // and uri.getRawQuery() (raw). JDK behavior: getQuery() is decoded. + assertEquals("file=/tmp/data.xml&flag", link.getQuery()); + assertEquals("file=%2Ftmp%2Fdata.xml&flag", link.getRawQuery()); + } + + @Test + public void parsedQueryToMap_handlesMissingValues() { + FijiURILink link = FijiURILink.parse("fiji://BDV/open?a=1&b=2&flag"); + Map map = link.getParsedQuery(); + assertEquals(3, map.size()); + assertEquals("1", map.get("a")); + assertEquals("2", map.get("b")); + assertNull(map.get("flag")); // key present with no value + } + + @Test + public void toString_roundTrips_reasonably() { + String u = "fiji://BDV/open?x=1&y=2"; + FijiURILink link = FijiURILink.parse(u); + assertEquals(u, link.toString()); + } + + + @Test + public void rejectsWrongScheme() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> FijiURILink.parse("http://BDV/open?x=1")); + assertTrue(ex.getMessage().contains("Scheme must be fiji://")); + } + + @Test + public void rejectsMissingPlugin() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> FijiURILink.parse("fiji:///open?x=1")); + assertTrue(ex.getMessage().contains("Missing plugin name")); + } + + @Test + public void rejectsInvalidUriSyntax() { + assertThrows(IllegalArgumentException.class, + () -> FijiURILink.parse("fiji://BDV/open?bad|query")); + } + + + + @Test + public void returnsNullOnError() { + assertNull(FijiURILink.tryParse("not-a-uri")); + assertNull(FijiURILink.tryParse("http://BDV")); // wrong scheme + assertNull(FijiURILink.tryParse("fiji:///")); + } + + @Test + public void returnsObjectOnSuccess() { + FijiURILink ok = FijiURILink.tryParse("fiji://BDV/open?q=ok"); + assertNotNull(ok); + assertEquals("BDV", ok.getPlugin()); + assertEquals("open", ok.getSubPlugin()); + assertEquals("q=ok", ok.getQuery()); + } + } + From 25c6cc882e1d9b1acbfedb5a166e3f6a6c224c72 Mon Sep 17 00:00:00 2001 From: Marwan Zouinkhi Date: Mon, 8 Sep 2025 16:22:01 -0400 Subject: [PATCH 3/3] update link handling logic --- .../java/org/scijava/links/FijiURILink.java | 61 +++++++++++-------- .../java/org/scijava/links/LinkHandler.java | 8 ++- .../java/org/scijava/links/LinksTest.java | 26 +------- 3 files changed, 44 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/scijava/links/FijiURILink.java b/src/main/java/org/scijava/links/FijiURILink.java index 8df96ec..501f90a 100644 --- a/src/main/java/org/scijava/links/FijiURILink.java +++ b/src/main/java/org/scijava/links/FijiURILink.java @@ -31,11 +31,6 @@ import java.net.URI; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Arrays; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; /** * Utility class for working with {@link URI} objects. @@ -44,11 +39,12 @@ */ public final class FijiURILink { + public static final String FIJI_SCHEME = "fiji"; - private final String plugin; // e.g., "BDV" - private final String subPlugin; // e.g., "open" (nullable) - private final String query; // e.g., "a=1&b=2" (nullable) - private final String rawQuery; // e.g., "a=1&b=2" (nullable) + private final String plugin; // e.g., "BDV" + private final String subPlugin; // e.g., "open" (nullable) + private final String query; // e.g., "a=1&b=2" (nullable) + private final String rawQuery; // e.g., "a=1&b=2" (nullable) private FijiURILink(String plugin, String subPlugin, String query, String rawQuery) { this.plugin = plugin; @@ -58,13 +54,15 @@ private FijiURILink(String plugin, String subPlugin, String query, String rawQue } public static FijiURILink parse(String uriString) { - Objects.requireNonNull(uriString, "uriString"); - final URI uri; try { - uri = new URI(uriString); - } catch (URISyntaxException e) { + URI uri = URI.create(uriString); + return parse(uri); + } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid URI: " + uriString, e); } + } + + public static FijiURILink parse(URI uri) { if (!"fiji".equalsIgnoreCase(uri.getScheme())) { throw new IllegalArgumentException("Scheme must be fiji://"); @@ -87,15 +85,27 @@ public static FijiURILink parse(String uriString) { // Raw query (no '?'), leave as-is; users can parse if they want. String q = uri.getQuery(); // Optional: decode percent-escapes (uncomment if desired) - // q = (q == null) ? null : java.net.URLDecoder.decode(q, StandardCharsets.UTF_8); + // q = (q == null) ? null : java.net.URLDecoder.decode(q, + // StandardCharsets.UTF_8); String raw = uri.getRawQuery(); return new FijiURILink(plugin, sub, q, raw); } - public String getPlugin() { return plugin; } - public String getSubPlugin() { return subPlugin; } // may be null - public String getQuery() { return query; } // may be null - public String getRawQuery() { return rawQuery; } // may be null + public String getPlugin() { + return plugin; + } + + public String getSubPlugin() { + return subPlugin; + } // may be null + + public String getQuery() { + return query; + } // may be null + + public String getRawQuery() { + return rawQuery; + } // may be null public Map getParsedQuery() { final LinkedHashMap map = new LinkedHashMap<>(); @@ -109,16 +119,13 @@ public Map getParsedQuery() { return map; } - @Override public String toString() { + @Override + public String toString() { StringBuilder sb = new StringBuilder("fiji://").append(plugin); - if (subPlugin != null) sb.append('/').append(subPlugin); - if (query != null) sb.append('?').append(query); + if (subPlugin != null) + sb.append('/').append(subPlugin); + if (query != null) + sb.append('?').append(query); return sb.toString(); } - - // Convenience helper: returns null instead of throwing - public static FijiURILink tryParse(String uriString) { - try { return parse(uriString); } catch (RuntimeException e) { return null; } - } } - \ No newline at end of file diff --git a/src/main/java/org/scijava/links/LinkHandler.java b/src/main/java/org/scijava/links/LinkHandler.java index 5dfded8..a3603e1 100644 --- a/src/main/java/org/scijava/links/LinkHandler.java +++ b/src/main/java/org/scijava/links/LinkHandler.java @@ -35,7 +35,7 @@ /** * A plugin for handling URI links. * - * @author Curtis Rueden + * @author Curtis Rueden, Marwan Zouinkhi */ public interface LinkHandler extends HandlerPlugin { @@ -46,8 +46,14 @@ public interface LinkHandler extends HandlerPlugin { */ void handle(URI uri); + String getName(); + @Override default Class getType() { return URI.class; } + + default public boolean supports(final URI uri) { + return FijiURILink.parse(uri).getPlugin().toUpperCase().equals(getName().toUpperCase()); + } } diff --git a/src/test/java/org/scijava/links/LinksTest.java b/src/test/java/org/scijava/links/LinksTest.java index 315ecf7..a4a4732 100644 --- a/src/test/java/org/scijava/links/LinksTest.java +++ b/src/test/java/org/scijava/links/LinksTest.java @@ -28,17 +28,13 @@ */ package org.scijava.links; - -import org.junit.Test; - - import java.util.Map; +import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; public class LinksTest { @@ -103,17 +99,10 @@ public void toString_roundTrips_reasonably() { @Test public void rejectsWrongScheme() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + assertThrows(IllegalArgumentException.class, () -> FijiURILink.parse("http://BDV/open?x=1")); - assertTrue(ex.getMessage().contains("Scheme must be fiji://")); } - @Test - public void rejectsMissingPlugin() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, - () -> FijiURILink.parse("fiji:///open?x=1")); - assertTrue(ex.getMessage().contains("Missing plugin name")); - } @Test public void rejectsInvalidUriSyntax() { @@ -121,18 +110,9 @@ public void rejectsInvalidUriSyntax() { () -> FijiURILink.parse("fiji://BDV/open?bad|query")); } - - - @Test - public void returnsNullOnError() { - assertNull(FijiURILink.tryParse("not-a-uri")); - assertNull(FijiURILink.tryParse("http://BDV")); // wrong scheme - assertNull(FijiURILink.tryParse("fiji:///")); - } - @Test public void returnsObjectOnSuccess() { - FijiURILink ok = FijiURILink.tryParse("fiji://BDV/open?q=ok"); + FijiURILink ok = FijiURILink.parse("fiji://BDV/open?q=ok"); assertNotNull(ok); assertEquals("BDV", ok.getPlugin()); assertEquals("open", ok.getSubPlugin());