Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We fixed an issue where closing a library did not free its memory, so a session that opened and closed several libraries kept all of them in memory. [#16948](https://github.com/JabRef/jabref/pull/16948)
- We fixed an issue where case-sensitive search (`=!`, `==!`, `=~!`) in linked files ignored the casing and matched text in any casing. [#13048](https://github.com/JabRef/jabref/issues/13048)
- We fixed linked-file full-text search results missing from the entry editor. [#17367](https://github.com/JabRef/jabref/pull/17367)
- We fixed an issue on Windows where linked files with very long paths opened the file explorer, not the file. [TODO](TODO)
- We fixed an issue where accepting a backup that adds groups to a library without groups failed with an error. [#17165](https://github.com/JabRef/jabref/pull/17165)
- We fixed an issue where JabRef closed when a client of the integrated language server disconnected. [#17303](https://github.com/JabRef/jabref/pull/17303)
- We fixed menu items not showing why they are disabled when a menu was opened for the first time. [#17200](https://github.com/JabRef/jabref/pull/17200)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
nav_order: 81
parent: Decision Records
status: accepted
date: 2026-11-04
---
# Open long Windows paths with the URL protocol handler
`adr~open-long-windows-paths-with-the-url-protocol-handler~1`

Needs: impl

## Context and Problem Statement

On Windows, JabRef opens a linked file by starting `explorer.exe` with the file as its argument, and reveals a file with `explorer.exe /select,`.
Explorer truncates that argument at `MAX_PATH` (260 characters, the terminating NUL included), so from 260 characters on the argument names nothing and Explorer silently shows the user's Documents folder instead.
Nothing fails and nothing is logged: the user clicks the PDF icon and gets the wrong window.

JabRef reaches that length on its own.
The default file name pattern builds names from author, year and title, and a library stored a few directories deep passes 260 characters without the user doing anything unusual — in the library that uncovered this, 50 of 269 linked PDFs are affected.

Check failure on line 19 in docs/decisions/0081-open-long-windows-paths-with-the-url-protocol-handler.md

View workflow job for this annotation

GitHub Actions / Spellling

TextLint [write-good]

"few" is a weasel word | 19:96

[Microsoft's documentation on the path length limitation](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation) describes the opt-in out of that limit: the `LongPathsEnabled` registry value plus a `longPathAware` application manifest.
The question is whether JabRef can use it here.

## Decision Drivers

* The user must reach the file, with no silent wrong result.
* JabRef must not depend on a machine-wide setting the user has to change.
* The fix must stay inside JabRef's own process and its own invocations.

## Considered Options

* Hand arguments of `MAX_PATH` characters or more to `rundll32.exe url.dll,FileProtocolHandler`
* Declare `longPathAware` in the application manifest and require `LongPathsEnabled`
* Prefix the path with `\?\`
* Pass the 8.3 short name of the path to Explorer
* Open the file through `java.awt.Desktop#open`

## Decision Outcome

Chosen option: "Hand arguments of `MAX_PATH` characters or more to `rundll32.exe url.dll,FileProtocolHandler`", because it is the only option that works without changing anything outside JabRef.
The protocol handler takes its argument unquoted and without the limit, and it resolves the file association the same way Explorer does, so the user's PDF viewer opens as before.
Revealing a file has no such substitute — the protocol handler cannot select a file in its folder — so beyond the limit JabRef opens the containing folder without the selection, which at least arrives where the file is.

The manifest option does not apply.
`longPathAware` governs the process that declares it, and the process that fails here is `explorer.exe`, which JabRef starts and whose manifest is Microsoft's.
The behaviour was measured on a machine with `LongPathsEnabled` set to 1, where Explorer still truncated at exactly 260 characters.
JabRef's own file access is not affected either way, because the JDK prefixes long absolute paths with `\?\` in its Win32 calls.

### Consequences

* Good, because linked files open regardless of their path length, without the user configuring Windows.
* Good, because it changes one method: everything else keeps calling Explorer.
* Bad, because a revealed file beyond the limit is no longer selected in its folder.
* Bad, because the limit is now encoded in JabRef and has to be kept in step should Explorer ever lift it.

### Confirmation

`WindowsTest` pins both command builders around the limit: 259 characters still go to Explorer, 260 go to the protocol handler, and a path beyond the limit reveals its parent folder instead of selecting the file.

## Pros and Cons of the Options

### Declare `longPathAware` in the application manifest and require `LongPathsEnabled`

* Good, because it is the mechanism Microsoft documents for the limit.
* Bad, because it applies to the declaring process, and the truncation happens in Explorer.
* Bad, because `LongPathsEnabled` is machine-wide and off by default, so the fix would not reach most users.
* Bad, because the launcher's manifest comes from `jpackage` and is not JabRef's to write.

### Prefix the path with `\?\`

* Good, because it lifts the limit for the Win32 file functions.
* Bad, because the prefix addresses the file system directly and the shell does not accept it, so Explorer is no better off.

### Pass the 8.3 short name of the path to Explorer

* Good, because the short name usually stays below the limit and Explorer accepts it.
* Bad, because 8.3 name creation can be switched off per volume, and then no short name exists.
* Bad, because the shortened path is unreadable in error messages and in the folder Explorer opens.

### Open the file through `java.awt.Desktop#open`

* Good, because it is one call and already used as the fallback of the other platforms.
* Bad, because it is unverified at the limit — it goes through the same shell API as Explorer.
* Bad, because the GUI avoids AWT on Windows, where it is only the fallback path.

<!-- markdownlint-disable-file MD022 -->
2 changes: 1 addition & 1 deletion jabgui/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@

// requires net.harawata.appdirs;
// requires com.sun.jna;
requires com.sun.jna.platform;
// requires com.sun.jna.platform;

requires org.eclipse.jgit;
// uses org.eclipse.jgit.transport.SshSessionFactory;
Expand Down
2 changes: 2 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import javafx.stage.WindowEvent;

import org.jabref.gui.clipboard.ClipBoardManager;
import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.frame.JabRefFrame;
import org.jabref.gui.help.VersionWorker;
import org.jabref.gui.icon.IconTheme;
Expand Down Expand Up @@ -128,6 +129,7 @@ public void start(Stage stage) {
// Load JavaFX stylesheet now instead of loading it later when the first Control is initialized.
setUserAgentStylesheet(null);
Injector.setModelOrService(Stage.class, mainStage);
Injector.setModelOrService(NativeDesktop.class, NativeDesktop.create(getHostServices(), preferences));

initialize();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import javafx.scene.control.Hyperlink;
import javafx.scene.layout.StackPane;

import org.jabref.gui.DialogService;
import org.jabref.gui.FXDialog;
import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.frame.ExternalApplicationsPreferences;
Expand All @@ -27,7 +28,7 @@ public class BackupResolverDialog extends FXDialog {

private static final Logger LOGGER = LoggerFactory.getLogger(BackupResolverDialog.class);

public BackupResolverDialog(Path originalPath, Path backupDir, ExternalApplicationsPreferences externalApplicationsPreferences) {
public BackupResolverDialog(Path originalPath, Path backupDir, ExternalApplicationsPreferences externalApplicationsPreferences, DialogService dialogService) {
super(AlertType.CONFIRMATION, Localization.lang("Backup found"), true);
setHeaderText(null);
getDialogPane().getButtonTypes().setAll(RESTORE_FROM_BACKUP, REVIEW_BACKUP, IGNORE_BACKUP);
Expand All @@ -53,7 +54,7 @@ public BackupResolverDialog(Path originalPath, Path backupDir, ExternalApplicati
String clickedLinkText = ((Hyperlink) (e.getSource())).getText();
if (backupFilename.equals(clickedLinkText)) {
try {
NativeDesktop.openFolderAndSelectFile(backupPathOpt.get(), externalApplicationsPreferences, null);
NativeDesktop.openFolderAndSelectFile(backupPathOpt.get(), externalApplicationsPreferences, dialogService);
} catch (IOException ex) {
LOGGER.error("Could not open backup folder", ex);
}
Expand Down
25 changes: 17 additions & 8 deletions jabgui/src/main/java/org/jabref/gui/desktop/os/DefaultDesktop.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,53 @@
import java.io.IOException;
import java.nio.file.Path;

import javafx.application.HostServices;

import org.jabref.architecture.AllowedToUseAwt;
import org.jabref.gui.DialogService;
import org.jabref.gui.frame.ExternalApplicationsPreferences;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.util.Directories;

import org.jspecify.annotations.NullMarked;
import org.slf4j.LoggerFactory;

/// This class contains some default implementations (if OS is neither linux, windows or osx) file directories and file/application open handling methods.
///
/// We cannot use a static logger instance here in this class as the Logger first needs to be configured in the [JabKit#initLogging].
/// The configuration of tinylog will become immutable as soon as the first log entry is issued.
/// https://tinylog.org/v2/configuration/
@NullMarked
@AllowedToUseAwt("Requires AWT to open a file")
public class DefaultDesktop extends NativeDesktop {

public DefaultDesktop(HostServices hostServices, GuiPreferences preferences) {
super(hostServices, preferences);
}

@Override
public void openFile(String filePath, String fileType, ExternalApplicationsPreferences externalApplicationsPreferences) throws IOException {
protected void openFileWithDefaultApplication(String filePath) throws IOException {
Desktop.getDesktop().open(Path.of(filePath).toFile());
}

@Override
public void openUrlWithSystemHandler(String url) throws IOException {
throw new IOException("No URL handler known for this platform, cannot open " + url);
public void openUrlWithSystemHandler(String url) {
// No native URL handler known for this platform
showDocument(url);
}

@Override
public void openFileWithApplication(String filePath, String application) throws IOException {
Desktop.getDesktop().open(Path.of(filePath).toFile());
protected void openFileWithCustomApplication(String filePath, String application) throws IOException {
new ProcessBuilder(application, filePath).start();
}

@Override
public void openFolderAndSelectFile(Path filePath) throws IOException {
protected void openFolderAndSelectFileWithDefaultFileBrowser(Path filePath) throws IOException {
File file = filePath.toAbsolutePath().getParent().toFile();
Desktop.getDesktop().open(file);
}

@Override
public void openConsole(String absolutePath, DialogService dialogService) throws IOException {
protected void openConsoleWithDefaultTerminal(String absolutePath, DialogService dialogService) throws IOException {
LoggerFactory.getLogger(DefaultDesktop.class).error("This feature is not supported by your Operating System.");
}

Expand Down
Loading
Loading