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..501f90a --- /dev/null +++ b/src/main/java/org/scijava/links/FijiURILink.java @@ -0,0 +1,131 @@ +/*- + * #%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, Marwan Zouinkhi + */ +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 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) { + try { + 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://"); + } + // 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(); + } +} 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/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..a4a4732 100644 --- a/src/test/java/org/scijava/links/LinksTest.java +++ b/src/test/java/org/scijava/links/LinksTest.java @@ -28,69 +28,95 @@ */ package org.scijava.links; - -import org.junit.Test; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; import java.util.Map; +import org.junit.Test; -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; + -/** - * Tests {@link Links}. - * - * @author Curtis Rueden - */ public class LinksTest { - private static final URI TEST_URI; + @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()); + } - 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 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 testPath() { - var actual = Links.path(TEST_URI); - assertEquals("op/sub/resource", actual); - } + @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 testOperation() { - var actual = Links.operation(TEST_URI); - assertEquals("op", actual); - } + @Test + public void rejectsWrongScheme() { + assertThrows(IllegalArgumentException.class, + () -> FijiURILink.parse("http://BDV/open?x=1")); + } - @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 rejectsInvalidUriSyntax() { + assertThrows(IllegalArgumentException.class, + () -> FijiURILink.parse("fiji://BDV/open?bad|query")); + } + + @Test + public void returnsObjectOnSuccess() { + FijiURILink ok = FijiURILink.parse("fiji://BDV/open?q=ok"); + assertNotNull(ok); + assertEquals("BDV", ok.getPlugin()); + assertEquals("open", ok.getSubPlugin()); + assertEquals("q=ok", ok.getQuery()); + } + } - @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); - } -}