In this tutorial, we’ll demonstrate how to use the jDeploy desktop lib to add deep-linking support to a Swing desktop application. We’ll configure jDeploy by editing package.json directly.

Tip
This tutorial is also available in variants that show how to configure jDeploy using the GUI application or the MCP server with AI tools.

Background

This tutorial is a follow-up to The Desktop Companion App Pattern, which introduced the concept of using custom URL schemes to enable communication between web applications and desktop apps.

In that article, we demonstrated basic URL scheme handling, but there was a limitation: on Windows and Linux, opening a custom URL would launch a new process each time, with the URL passed as a command-line argument. This wasn’t ideal for most use cases, as it required managing multiple instances or manually implementing single-instance behavior.

jDeploy 6.0 introduces singleton mode, which ensures only one instance of your application runs at a time. When a custom URL is opened, the existing instance receives the URL via a callback — the same behavior macOS provides natively. This makes the companion app pattern much more practical, enabling simple inter-process communication between your web app and desktop app without spawning duplicate processes.

What is Deep Linking?

Deep linking allows users to navigate directly to specific content or functionality within an application using a URL. This is particularly useful for desktop applications, as it enables users to access specific features or data without having to navigate through the application’s interface.

Some examples of deep linking include:

  • A user clicking on a link in an email that opens a specific page within a desktop application.

  • A user sharing a link to a specific feature or content within a desktop application on social media.

  • A background service or web dashboard opening a specific view in the desktop app via a custom URL.

Prerequisites

This tutorial assumes you have:

  • A basic Swing application

  • jDeploy 6.0 or later installed

  • Familiarity with Maven or Gradle for dependency management

Overview

To add deep linking to your Swing application, you need to:

  1. Add the jdeploy-desktop-lib-swing dependency to your project

  2. Configure a custom URL scheme and singleton mode in your package.json

  3. Implement a JDeployOpenHandler to receive and handle deep link events

Add the Dependency

Add the jdeploy-desktop-lib-swing dependency to your project.

Gradle:

dependencies {
    implementation 'ca.weblite:jdeploy-desktop-lib-swing:1.0.2'
}

Maven:

<dependency>
    <groupId>ca.weblite</groupId>
    <artifactId>jdeploy-desktop-lib-swing</artifactId>
    <version>1.0.2</version>
</dependency>

Configure jDeploy

In your package.json, configure a custom URL scheme and enable singleton mode. Singleton mode ensures that when a user clicks a deep link, the existing application instance handles it rather than launching a new instance.

{
  "name": "my-app",
  "version": "1.0.0",
  "jdeploy": {
    "javaVersion": "11",
    "jar": "target/my-app.jar",
    "title": "My App",
    "singleton": true,
    "urlSchemes": ["myapp"]
  }
}

The urlSchemes array registers your custom URL schemes with the operating system. In this example, URLs like myapp://settings or myapp://user/123 will open your application.

Implement the Open Handler

The JDeployOpenHandler interface defines three callback methods that your application implements to handle external events:

  • openFiles(List<File> files) — Called when files should be opened (e.g., double-click, drag-drop, or "Open With")

  • openURIs(List<URI> uris) — Called when custom URL schemes are activated (deep links)

  • appActivated() — Called when the application should be brought to the foreground

Register your handler early in your application’s initialization using JDeploySwingApp.setOpenHandler(). All callbacks are dispatched on the Swing Event Dispatch Thread (EDT), so you can safely update your UI directly.

import ca.weblite.jdeploy.app.JDeployOpenHandler;
import ca.weblite.jdeploy.app.swing.JDeploySwingApp;
import javax.swing.*;
import java.io.File;
import java.net.URI;
import java.util.List;

public class MyApp {
    private JFrame mainWindow;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new MyApp().init();
        });
    }

    private void init() {
        mainWindow = new JFrame("My App");
        mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mainWindow.setSize(800, 600);

        // Register the deep link handler
        JDeploySwingApp.setOpenHandler(new JDeployOpenHandler() {
            @Override
            public void openFiles(List<File> files) {
                for (File file : files) {
                    openDocument(file);
                }
            }

            @Override
            public void openURIs(List<URI> uris) {
                for (URI uri : uris) {
                    handleDeepLink(uri);
                }
            }

            @Override
            public void appActivated() {
                // Bring window to front
                mainWindow.setState(JFrame.NORMAL);
                mainWindow.toFront();
                mainWindow.requestFocus();
            }
        });

        mainWindow.setVisible(true);
    }

    private void openDocument(File file) {
        System.out.println("Opening file: " + file.getAbsolutePath());
        // Load and display the file content
    }

    private void handleDeepLink(URI uri) {
        System.out.println("Deep link received: " + uri);
        // Route to the appropriate view based on the URI
    }
}

When a deep link is received, parse the URI to determine what action to take. The URI structure typically follows this pattern:

myapp://host/path?query=value#fragment

For example:

  • myapp://settings — Open the settings page

  • myapp://user/123 — Open user profile with ID 123

  • myapp://document?id=abc&mode=edit — Open document "abc" in edit mode

Here’s how to parse and route deep links:

private void handleDeepLink(URI uri) {
    String scheme = uri.getScheme();     // "myapp"
    String host = uri.getHost();         // "settings", "user", "document"
    String path = uri.getPath();         // "/123", "", etc.
    String query = uri.getQuery();       // "id=abc&mode=edit"

    switch (host) {
        case "settings":
            showSettingsPanel();
            break;
        case "user":
            if (path != null && path.length() > 1) {
                String userId = path.substring(1); // Remove leading "/"
                showUserProfile(userId);
            }
            break;
        case "document":
            Map<String, String> params = parseQueryString(query);
            String docId = params.get("id");
            String mode = params.getOrDefault("mode", "view");
            openDocument(docId, mode);
            break;
        default:
            System.out.println("Unknown deep link: " + uri);
    }
}

private Map<String, String> parseQueryString(String query) {
    Map<String, String> params = new HashMap<>();
    if (query != null && !query.isEmpty()) {
        for (String param : query.split("&")) {
            String[] keyValue = param.split("=", 2);
            if (keyValue.length == 2) {
                params.put(keyValue[0], keyValue[1]);
            }
        }
    }
    return params;
}

Complete Example

Here’s a complete example that demonstrates deep linking with a tabbed interface. Clicking a deep link switches to the corresponding tab.

import ca.weblite.jdeploy.app.JDeployOpenHandler;
import ca.weblite.jdeploy.app.swing.JDeploySwingApp;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.net.URI;
import java.util.List;

public class DeepLinkDemo {
    private JFrame frame;
    private JTabbedPane tabbedPane;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new DeepLinkDemo().init();
        });
    }

    private void init() {
        frame = new JFrame("Deep Link Demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(600, 400);

        // Create tabbed pane with sample tabs
        tabbedPane = new JTabbedPane();
        tabbedPane.addTab("Home", createPanel("Welcome to the Home tab"));
        tabbedPane.addTab("Settings", createPanel("Application Settings"));
        tabbedPane.addTab("Profile", createPanel("User Profile"));

        frame.add(tabbedPane, BorderLayout.CENTER);

        // Add instructions
        JLabel instructions = new JLabel(
            "<html>Try these deep links in your browser:<br>" +
            "&bull; demo://home<br>" +
            "&bull; demo://settings<br>" +
            "&bull; demo://profile</html>"
        );
        instructions.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        frame.add(instructions, BorderLayout.SOUTH);

        // Register deep link handler
        JDeploySwingApp.setOpenHandler(new JDeployOpenHandler() {
            @Override
            public void openFiles(List<File> files) {
                // Handle file opens if needed
            }

            @Override
            public void openURIs(List<URI> uris) {
                for (URI uri : uris) {
                    navigateTo(uri.getHost());
                }
            }

            @Override
            public void appActivated() {
                frame.setState(JFrame.NORMAL);
                frame.toFront();
                frame.requestFocus();
            }
        });

        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    private void navigateTo(String destination) {
        int tabIndex = switch (destination) {
            case "home" -> 0;
            case "settings" -> 1;
            case "profile" -> 2;
            default -> -1;
        };

        if (tabIndex >= 0) {
            tabbedPane.setSelectedIndex(tabIndex);
        }
    }

    private JPanel createPanel(String text) {
        JPanel panel = new JPanel(new BorderLayout());
        JLabel label = new JLabel(text, SwingConstants.CENTER);
        label.setFont(label.getFont().deriveFont(18f));
        panel.add(label, BorderLayout.CENTER);
        return panel;
    }
}

The package.json for this example:

{
  "name": "deep-link-demo",
  "version": "1.0.0",
  "jdeploy": {
    "javaVersion": "17",
    "jar": "target/deep-link-demo.jar",
    "title": "Deep Link Demo",
    "singleton": true,
    "urlSchemes": ["demo"]
  }
}

How It Works

Singleton Mode

When singleton mode is enabled, only one instance of your GUI application runs at a time. If the user launches the app again — whether by double-clicking a file, clicking a custom URL, or running the launcher command — the existing window is activated and the file or URI is forwarded to it.

On Windows and Linux, jDeploy uses file-based IPC to communicate between the secondary launch and the primary instance. On macOS, the operating system handles single-instance behavior natively through the Desktop API.

Event Queueing

If deep link events arrive before your handler is registered, they are queued and delivered as soon as you call setOpenHandler(). This ensures you never miss events during application startup.

Thread Safety

All callbacks from JDeploySwingApp are dispatched on the Swing Event Dispatch Thread (EDT). You can safely update your UI components directly in the handler methods without wrapping them in SwingUtilities.invokeLater().

Summary

Adding deep linking to a Swing application with jDeploy involves:

  1. Add the dependency — ca.weblite:jdeploy-desktop-lib-swing:1.0.2

  2. Configure jDeploy — Edit package.json to set singleton: true and define urlSchemes

  3. Implement JDeployOpenHandler — Register handlers for files, URIs, and app activation

  4. Parse and route URIs — Extract host, path, and query parameters to navigate your app

With these steps, your desktop application can respond to deep links from emails, web pages, other applications, and system tray helpers, providing a seamless user experience.

See Also