Skip to content

Commit 4517654

Browse files
ctruedenclaude
andcommitted
Add Windows URI scheme registration
Implements automatic registration of URI schemes with the Windows Registry, enabling users to click custom links (e.g., fiji://open) in browsers or other applications to launch the Java application. Key components: - SchemeInstaller interface for platform-independent scheme registration - WindowsSchemeInstaller using Windows reg commands (no JNA dependency) - LinkHandler.getSchemes() for handlers to declare supported schemes - DefaultLinkService auto-registers schemes on context initialization The launcher sets the scijava.app.executable system property to specify the executable path for registration. Registration uses HKEY_CURRENT_USER and requires no admin privileges. Includes comprehensive tests and documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 13ed115 commit 4517654

File tree

7 files changed

+857
-4
lines changed

7 files changed

+857
-4
lines changed

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,59 @@ to manage their integration with the system native desktop:
88

99
The scijava-desktop component requires Java 11 as a minimum, due
1010
to its use of java.awt.Desktop features not present in Java 8.
11+
12+
## Features
13+
14+
- **Link handling**: Register custom handlers for URI schemes through the `LinkHandler` plugin interface
15+
- **CLI integration**: Automatic handling of URI arguments passed on the command line via `ConsoleArgument`
16+
- **OS integration**: Automatic registration of URI schemes with the operating system (Windows supported, macOS/Linux planned)
17+
18+
## Usage
19+
20+
### Creating a Link Handler
21+
22+
Implement the `LinkHandler` interface to handle custom URI schemes:
23+
24+
```java
25+
@Plugin(type = LinkHandler.class)
26+
public class MyLinkHandler extends AbstractLinkHandler {
27+
28+
@Override
29+
public boolean supports(URI uri) {
30+
return "myapp".equals(uri.getScheme());
31+
}
32+
33+
@Override
34+
public void handle(URI uri) {
35+
// Handle the URI
36+
System.out.println("Handling: " + uri);
37+
}
38+
39+
@Override
40+
public List<String> getSchemes() {
41+
// Return schemes to register with the OS
42+
return Arrays.asList("myapp");
43+
}
44+
}
45+
```
46+
47+
### OS Registration
48+
49+
On Windows, URI schemes returned by `LinkHandler.getSchemes()` are automatically registered
50+
in the Windows Registry when the `LinkService` initializes. This allows users to click
51+
links like `myapp://action` in web browsers or other applications, which will launch your
52+
Java application with the URI as a command-line argument.
53+
54+
The registration uses `HKEY_CURRENT_USER` and requires no administrator privileges.
55+
56+
See [doc/WINDOWS.md](doc/WINDOWS.md) for details.
57+
58+
## Architecture
59+
60+
- `LinkService` - Service for routing URIs to appropriate handlers
61+
- `LinkHandler` - Plugin interface for implementing custom URI handlers
62+
- `LinkArgument` - Console argument plugin that recognizes URIs on the command line
63+
- `SchemeInstaller` - Interface for OS-specific URI scheme registration
64+
- `WindowsSchemeInstaller` - Windows implementation using registry commands
65+
66+
The launcher should set the `scijava.app.executable` system property to enable URI scheme registration.

doc/WINDOWS.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Windows URI Scheme Registration
2+
3+
This document describes how URI scheme registration works on Windows in scijava-links.
4+
5+
## Overview
6+
7+
When a SciJava application starts on Windows, the `DefaultLinkService` automatically:
8+
9+
1. Collects all URI schemes from registered `LinkHandler` plugins via `getSchemes()`
10+
2. Reads the executable path from the `scijava.app.executable` system property
11+
3. Registers each scheme in the Windows Registry under `HKEY_CURRENT_USER\Software\Classes`
12+
13+
## Registry Structure
14+
15+
For a scheme named `myapp`, the following registry structure is created:
16+
17+
```
18+
HKEY_CURRENT_USER\Software\Classes\myapp
19+
(Default) = "URL:myapp"
20+
URL Protocol = ""
21+
shell\
22+
open\
23+
command\
24+
(Default) = "C:\Path\To\App.exe" "%1"
25+
```
26+
27+
## Implementation Details
28+
29+
### SchemeInstaller Interface
30+
31+
The `SchemeInstaller` interface provides a platform-independent API for URI scheme registration:
32+
33+
- `isSupported()` - Checks if the installer works on the current platform
34+
- `install(scheme, executablePath)` - Registers a URI scheme
35+
- `isInstalled(scheme)` - Checks if a scheme is already registered
36+
- `getInstalledPath(scheme)` - Gets the executable path for a registered scheme
37+
- `uninstall(scheme)` - Removes a URI scheme registration
38+
39+
### WindowsSchemeInstaller
40+
41+
The Windows implementation uses the `reg` command-line tool to manipulate the registry:
42+
43+
- **No JNA dependency**: Uses native Windows `reg` commands via `ProcessBuilder`
44+
- **No admin rights**: Registers under `HKEY_CURRENT_USER` (not `HKEY_LOCAL_MACHINE`)
45+
- **Idempotent**: Safely handles re-registration with the same or different paths
46+
- **Robust error handling**: Proper timeouts, error logging, and validation
47+
48+
### Executable Path Configuration
49+
50+
The launcher must set the `scijava.app.executable` system property to the absolute path of the application's executable. This property is used by `DefaultLinkService` during URI scheme registration.
51+
52+
Example launcher configuration:
53+
```bash
54+
java -Dscijava.app.executable="C:\Program Files\MyApp\MyApp.exe" -jar myapp.jar
55+
```
56+
57+
On Windows, the launcher typically sets this to the `.exe` file path. On macOS, it would be the path inside the `.app` bundle. On Linux, it would be the shell script or executable.
58+
59+
## Example Handler
60+
61+
Here's a complete example of a `LinkHandler` that registers a custom scheme:
62+
63+
```java
64+
package com.example;
65+
66+
import org.scijava.links.AbstractLinkHandler;
67+
import org.scijava.links.LinkHandler;
68+
import org.scijava.log.LogService;
69+
import org.scijava.plugin.Parameter;
70+
import org.scijava.plugin.Plugin;
71+
72+
import java.net.URI;
73+
import java.util.Arrays;
74+
import java.util.List;
75+
76+
@Plugin(type = LinkHandler.class)
77+
public class ExampleLinkHandler extends AbstractLinkHandler {
78+
79+
@Parameter(required = false)
80+
private LogService log;
81+
82+
@Override
83+
public boolean supports(final URI uri) {
84+
return "example".equals(uri.getScheme());
85+
}
86+
87+
@Override
88+
public void handle(final URI uri) {
89+
if (log != null) {
90+
log.info("Handling example URI: " + uri);
91+
}
92+
93+
// Parse the URI and perform actions
94+
String operation = Links.operation(uri);
95+
Map<String, String> params = Links.query(uri);
96+
97+
// Your business logic here
98+
// ...
99+
}
100+
101+
@Override
102+
public List<String> getSchemes() {
103+
// This tells the system to register "example://" links on Windows
104+
return Arrays.asList("example");
105+
}
106+
}
107+
```
108+
109+
## Testing
110+
111+
The Windows scheme installation can be tested on Windows systems:
112+
113+
```bash
114+
mvn test -Dtest=WindowsSchemeInstallerTest
115+
```
116+
117+
Tests are automatically skipped on non-Windows platforms using JUnit's `Assume.assumeTrue()`.
118+
119+
To test with a specific executable path, set the system property:
120+
```bash
121+
mvn test -Dscijava.app.executable="C:\Path\To\App.exe"
122+
```
123+
124+
## Platform Notes
125+
126+
**macOS**: URI schemes are declared in the application's `Info.plist` within the `.app` bundle. This is configured at build/packaging time, not at runtime, since the bundle is typically code-signed and immutable.
127+
128+
**Linux**: URI schemes are declared in `.desktop` files, which is part of broader desktop integration (icons, MIME types, etc.). This functionality belongs in `scijava-plugins-platforms` rather than this component.
129+
130+
**Windows**: Runtime registration is appropriate because the Windows Registry is designed for runtime modifications, and registration under `HKEY_CURRENT_USER` requires no elevated privileges.
131+
132+
## Future Enhancements
133+
134+
- **Scheme validation**: Validate scheme names against RFC 3986
135+
- **User prompts**: Optional confirmation before registering schemes
136+
- **Uninstallation**: Automatic cleanup on application uninstall

src/main/java/org/scijava/desktop/links/DefaultLinkService.java

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@
3030

3131
import org.scijava.event.ContextCreatedEvent;
3232
import org.scijava.event.EventHandler;
33+
import org.scijava.desktop.links.SchemeInstaller;
34+
import org.scijava.desktop.platform.windows.WindowsSchemeInstaller;
35+
import org.scijava.log.LogService;
3336
import org.scijava.plugin.AbstractHandlerService;
37+
import org.scijava.plugin.Parameter;
3438
import org.scijava.plugin.Plugin;
3539
import org.scijava.service.Service;
3640

3741
import java.awt.Desktop;
3842
import java.net.URI;
43+
import java.util.HashSet;
44+
import java.util.Set;
3945

4046
/**
4147
* Default implementation of {@link LinkService}.
@@ -45,13 +51,84 @@
4551
@Plugin(type = Service.class)
4652
public class DefaultLinkService extends AbstractHandlerService<URI, LinkHandler> implements LinkService {
4753

54+
@Parameter(required = false)
55+
private LogService log;
56+
4857
@EventHandler
4958
private void onEvent(final ContextCreatedEvent evt) {
5059
// Register URI handler with the desktop system, if possible.
51-
if (!Desktop.isDesktopSupported()) return;
52-
final Desktop desktop = Desktop.getDesktop();
53-
if (!desktop.isSupported(Desktop.Action.APP_OPEN_URI)) return;
54-
desktop.setOpenURIHandler(event -> handle(event.getURI()));
60+
if (Desktop.isDesktopSupported()) {
61+
final Desktop desktop = Desktop.getDesktop();
62+
if (desktop.isSupported(Desktop.Action.APP_OPEN_URI)) {
63+
desktop.setOpenURIHandler(event -> handle(event.getURI()));
64+
}
65+
}
66+
67+
// Register URI schemes with the operating system (Windows only).
68+
installSchemes();
69+
}
70+
71+
/**
72+
* Installs URI schemes with the operating system.
73+
* <p>
74+
* This method collects all schemes supported by registered {@link LinkHandler}
75+
* plugins and registers them with the OS (currently Windows only).
76+
* </p>
77+
*/
78+
private void installSchemes() {
79+
// Create the appropriate installer for this platform
80+
final SchemeInstaller installer = createInstaller();
81+
if (installer == null || !installer.isSupported()) {
82+
if (log != null) log.debug("Scheme installation not supported on this platform");
83+
return;
84+
}
85+
86+
// Get executable path from system property
87+
final String executablePath = System.getProperty("scijava.app.executable");
88+
if (executablePath == null) {
89+
if (log != null) log.debug("No executable path set (scijava.app.executable property)");
90+
return;
91+
}
92+
93+
// Collect all schemes from registered handlers
94+
final Set<String> schemes = collectSchemes();
95+
if (schemes.isEmpty()) {
96+
if (log != null) log.debug("No URI schemes to register");
97+
return;
98+
}
99+
100+
// Install each scheme
101+
for (final String scheme : schemes) {
102+
try {
103+
installer.install(scheme, executablePath);
104+
}
105+
catch (final Exception e) {
106+
if (log != null) log.error("Failed to install URI scheme: " + scheme, e);
107+
}
108+
}
109+
}
110+
111+
/**
112+
* Creates the appropriate {@link SchemeInstaller} for the current platform.
113+
* <p>
114+
* Currently only Windows is supported. macOS uses Info.plist in the .app bundle
115+
* (configured at build time), and Linux .desktop file management belongs in
116+
* scijava-plugins-platforms.
117+
* </p>
118+
*/
119+
private SchemeInstaller createInstaller() {
120+
return new WindowsSchemeInstaller(log);
121+
}
122+
123+
/**
124+
* Collects all URI schemes from registered {@link LinkHandler} plugins.
125+
*/
126+
private Set<String> collectSchemes() {
127+
final Set<String> schemes = new HashSet<>();
128+
for (final LinkHandler handler : getInstances()) {
129+
schemes.addAll(handler.getSchemes());
130+
}
131+
return schemes;
55132
}
56133

57134
}

src/main/java/org/scijava/desktop/links/LinkHandler.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.scijava.plugin.HandlerPlugin;
3232

3333
import java.net.URI;
34+
import java.util.Collections;
35+
import java.util.List;
3436

3537
/**
3638
* A plugin for handling URI links.
@@ -46,6 +48,21 @@ public interface LinkHandler extends HandlerPlugin<URI> {
4648
*/
4749
void handle(URI uri);
4850

51+
/**
52+
* Gets the URI schemes that this handler supports.
53+
* <p>
54+
* This method is used for registering URI schemes with the operating system.
55+
* Handlers should return a list of scheme names (e.g., "fiji", "imagej")
56+
* that they can handle. Return an empty list if the handler does not
57+
* require OS-level scheme registration.
58+
* </p>
59+
*
60+
* @return List of URI schemes supported by this handler
61+
*/
62+
default List<String> getSchemes() {
63+
return Collections.emptyList();
64+
}
65+
4966
@Override
5067
default Class<URI> getType() {
5168
return URI.class;

0 commit comments

Comments
 (0)