From 167b80ba955d65ea551bd0be8cb9a95eb5cfd696 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 22 Jul 2026 18:20:15 +0200 Subject: [PATCH 1/7] Make directory libraries reachable through the REST API jabsrv identified every library by its .bib path, so directory libraries - which have no .bib path, only a root directory - were invisible to the whole HTTP API: absent from the library listing and the existence-check query, and un-addressable for reading or adding entries. ServerUtils now derives a library's id from getDatabasePath() or, for a directory library, its root directory (the same identity the GUI session store uses), so listing, the batch query, entry reads, and appends all resolve it. The GUI append matcher gains the same fallback, so an append targets the open directory-library tab by its root; the entries then flow through the normal write-back into sidecars and the mirror. Standalone-server mode still serves only .bib files (adds require the GUI regardless). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DDcHNMt9fPWnpYaHheFvry --- docs/requirements/directory-library.md | 13 ++++ .../gui/frame/JabRefFrameViewModel.java | 4 ++ .../http/server/services/ServerUtils.java | 36 +++++++--- .../http/server/services/ServerUtilsTest.java | 69 +++++++++++++++++++ 4 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 jabsrv/src/test/java/org/jabref/http/server/services/ServerUtilsTest.java diff --git a/docs/requirements/directory-library.md b/docs/requirements/directory-library.md index e79d5ae9326d..883053544a32 100644 --- a/docs/requirements/directory-library.md +++ b/docs/requirements/directory-library.md @@ -125,4 +125,17 @@ metadata), and the root is reopened as a directory library. Needs: impl, utest +## Directory libraries are reachable through the REST API +`req~directory-library.rest-api~1` + +An open directory library must be addressable through JabRef's HTTP API (jabsrv) exactly like a +`.bib` library. Because a directory library has no `.bib` path, it is identified by its root +directory (the same identity the session store uses): the library listing includes it, and the +per-library endpoints — reading entries, the batch existence-check query, and appending entries +— resolve it by that root-derived id. In GUI mode an append targets the open directory-library +tab (matched by its root) so the added entries flow through the normal write-back into sidecars +and the mirror; standalone-server mode still serves only `.bib` files. + +Needs: impl, utest + diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java index 39669e6ee4ac..88d46281f2c4 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java @@ -271,7 +271,11 @@ private void importFromFileAndOpen(String location) { private void selectLibraryTab(Optional library) { library.map(path -> path.toAbsolutePath().normalize()).ifPresent(normalized -> tabContainer.getLibraryTabs().stream() + // A directory library has no .bib path; it is identified by its root + // directory, the same path the server derives its id from + // [impl->req~directory-library.rest-api~1] .filter(tab -> tab.getBibDatabaseContext().getDatabasePath() + .or(tab.getBibDatabaseContext()::getDirectoryLibraryRoot) .map(path -> path.toAbsolutePath().normalize().equals(normalized)) .orElse(false)) .findFirst() diff --git a/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java b/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java index 4d088109053d..0318a68c7a7c 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java +++ b/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java @@ -30,27 +30,43 @@ public class ServerUtils { + /// The on-disk path that identifies a library: the `.bib` file of a regular library, or the + /// root directory of a directory library (which has no `.bib` path of its own). This is the + /// same identity the GUI session store uses to remember open libraries, so a directory + /// library keeps a stable id across everything. + /// [impl->req~directory-library.rest-api~1] + public static Optional libraryIdentifyingPath(BibDatabaseContext context) { + return context.getDatabasePath().or(context::getDirectoryLibraryRoot); + } + + /// The stable id string used in URLs for a library at the given identifying path. + private static String libraryId(Path path) { + return path.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(path); + } + /// Returns ids of all libraries the state manager currently considers /// open. Used by every resource that operates across the open /// collection (libraries listing, batch query, ...). public static List openLibraryIds(SrvStateManager srvStateManager) { return srvStateManager.getOpenDatabases().stream() - .map(BibDatabaseContext::getDatabasePath) + .map(ServerUtils::libraryIdentifyingPath) .flatMap(Optional::stream) - .map(path -> path.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(path)) + .map(ServerUtils::libraryId) .toList(); } /// Returns the on-disk path of the library with the given id, looking it up in the - /// state manager's open databases (the same source as [#getBibDatabaseContext]). + /// state manager's open databases (the same source as [#getBibDatabaseContext]). For a + /// directory library this is its root directory, which the GUI append command routes back + /// to the open directory-library tab. /// /// @throws NotFoundException if no library with the given id is found public static @NonNull Path getLibraryPath(String id, SrvStateManager srvStateManager) { return srvStateManager.getOpenDatabases() .stream() - .map(BibDatabaseContext::getDatabasePath) - .flatMap(java.util.Optional::stream) - .filter(p -> (p.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(p)).equals(id)) + .map(ServerUtils::libraryIdentifyingPath) + .flatMap(Optional::stream) + .filter(p -> libraryId(p).equals(id)) .findAny() .orElseThrow(NotFoundException::new); } @@ -77,11 +93,9 @@ public static List openLibraryIds(SrvStateManager srvStateManager) { return srvStateManager.getActiveDatabase().orElseThrow(NotFoundException::new); } return srvStateManager.getOpenDatabases().stream() - .filter(context -> context.getDatabasePath().isPresent()) - .filter(context -> { - Path p = context.getDatabasePath().get(); - return (p.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(p)).equals(id); - }) + .filter(context -> libraryIdentifyingPath(context) + .map(p -> libraryId(p).equals(id)) + .orElse(false)) .findFirst() .orElseThrow(() -> new NotFoundException("No library with id " + HtmlEscapers.htmlEscaper().escape(id) + " found")); } diff --git a/jabsrv/src/test/java/org/jabref/http/server/services/ServerUtilsTest.java b/jabsrv/src/test/java/org/jabref/http/server/services/ServerUtilsTest.java new file mode 100644 index 000000000000..91a6dad1b2d7 --- /dev/null +++ b/jabsrv/src/test/java/org/jabref/http/server/services/ServerUtilsTest.java @@ -0,0 +1,69 @@ +package org.jabref.http.server.services; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import javafx.collections.FXCollections; + +import org.jabref.http.SrvStateManager; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.util.io.BackupFileUtil; +import org.jabref.model.database.BibDatabaseContext; + +import jakarta.ws.rs.NotFoundException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/// A directory library carries no `.bib` path, only a root directory. These tests pin that +/// jabsrv identifies it by that root — so the library listing, the existence-check query, and +/// the add endpoints can all address it. +/// [utest->req~directory-library.rest-api~1] +class ServerUtilsTest { + + @TempDir + Path root; + + private BibDatabaseContext directoryLibrary() { + BibDatabaseContext context = new BibDatabaseContext(); + context.convertToDirectoryLibrary(root); + return context; + } + + private String rootId() { + return root.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(root); + } + + private SrvStateManager stateManagerWith(BibDatabaseContext context) { + SrvStateManager stateManager = mock(SrvStateManager.class); + when(stateManager.getOpenDatabases()).thenReturn(FXCollections.observableArrayList(context)); + return stateManager; + } + + @Test + void directoryLibraryIsListedByItsRootDerivedId() { + assertEquals(List.of(rootId()), ServerUtils.openLibraryIds(stateManagerWith(directoryLibrary()))); + } + + @Test + void directoryLibraryResolvesByIdToItsRoot() { + assertEquals(root, ServerUtils.getLibraryPath(rootId(), stateManagerWith(directoryLibrary()))); + } + + @Test + void directoryLibraryContextResolvesById() throws IOException { + BibDatabaseContext context = directoryLibrary(); + assertSame(context, ServerUtils.getBibDatabaseContext(rootId(), stateManagerWith(context), mock(ImportFormatPreferences.class))); + } + + @Test + void unknownIdIsNotFound() { + assertThrows(NotFoundException.class, () -> ServerUtils.getLibraryPath("does-not-exist", stateManagerWith(directoryLibrary()))); + } +} From e71ac295f90503678c667feaa734d142b2aa533c Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 3 Sep 2026 05:35:35 +0200 Subject: [PATCH 2/7] Remove duplicated CHANGELOG entries left by the resync merge Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vr3E1Gg5DRU4LQDDVnhPys --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18d8b199912c..0592a4a8cbc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,6 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - Directory libraries now save into their sidecar files: edits are written back automatically (debounced until typing pauses; Ctrl+S forces the write and no longer creates a `.bib`), the first edit of a PDF-only entry creates a Markdown sidecar (`X.md` with the Hayagriva data as frontmatter and the comment fields as notes body), renaming a citation key renames the YAML key, and deleting an entry removes it from its file (the file is trashed once empty, the PDF stays). Hand-written content that JabRef does not understand survives rewrites. [#739](https://github.com/JabRef/jabref-koppor/pull/739) - Directory libraries now stay in sync with external file changes: creating, editing, deleting, or renaming `.yml`/`.md`/`.pdf` files in the opened folder updates the open library live, and renames keep the affected entries (selection and undo history survive). [#738](https://github.com/JabRef/jabref-koppor/pull/738) - We added "Open folder as library" (File menu): JabRef opens a directory as a library, filling the main table from the Hayagriva `.yml` files, the Markdown notes files with a Hayagriva YAML frontmatter (`.md`; the notes body maps to the entry's comment fields), and the PDFs found in the folder tree. A PDF next to a sidecar of the same name is linked to the sidecar's entry; PDFs without a sidecar appear immediately and are enriched in the background with metadata extracted from the PDF itself and a generated citation key; a missing DOI is looked up online and its metadata fills the remaining empty fields. Directory libraries that were open on shutdown are reopened on the next start. [#737](https://github.com/JabRef/jabref-koppor/pull/737) -- We added "Open folder as library" (File menu): JabRef opens a directory as a library, filling the main table from the Hayagriva `.yml` files and PDFs found in the folder tree. A PDF next to a `.yml` sidecar of the same name is linked to the sidecar's entry; PDFs without a sidecar appear as stub entries titled after the file. Edits are not yet written back to the files. [#737](https://github.com/JabRef/jabref-koppor/pull/737) -- We added "Open folder as library" (File menu): JabRef opens a directory as a library, filling the main table from the Hayagriva `.yml` files and PDFs found in the folder tree. A PDF next to a `.yml` sidecar of the same name is linked to the sidecar's entry; PDFs without a sidecar appear as entries with metadata extracted from the PDF itself (falling back to the file name). Edits are not yet written back to the files. [#737](https://github.com/JabRef/jabref-koppor/pull/737) -- We added "Open folder as library" (File menu): JabRef opens a directory as a library, filling the main table from the Hayagriva `.yml` files and PDFs found in the folder tree. A PDF next to a `.yml` sidecar of the same name is linked to the sidecar's entry; PDFs without a sidecar appear immediately and are enriched in the background with metadata extracted from the PDF itself and a generated citation key. Edits are not yet written back to the files. [#737](https://github.com/JabRef/jabref-koppor/pull/737) -- We added "Open folder as library" (File menu): JabRef opens a directory as a library, filling the main table from the Hayagriva `.yml` files and PDFs found in the folder tree. A PDF next to a `.yml` sidecar of the same name is linked to the sidecar's entry; PDFs without a sidecar appear immediately and are enriched in the background with metadata extracted from the PDF itself and a generated citation key; a missing DOI is looked up online and its metadata fills the remaining empty fields. Edits are not yet written back to the files. [#737](https://github.com/JabRef/jabref-koppor/pull/737) -- We added "Open folder as library" (File menu): JabRef opens a directory as a library, filling the main table from the Hayagriva `.yml` files and PDFs found in the folder tree. A PDF next to a `.yml` sidecar of the same name is linked to the sidecar's entry; PDFs without a sidecar appear immediately and are enriched in the background with metadata extracted from the PDF itself and a generated citation key; a missing DOI is looked up online and its metadata fills the remaining empty fields. Directory libraries that were open on shutdown are reopened on the next start. Edits are not yet written back to the files. [#737](https://github.com/JabRef/jabref-koppor/pull/737) -- We added "Open folder as library" (File menu): JabRef opens a directory as a library, filling the main table from the Hayagriva `.yml` files, the Markdown notes files with a Hayagriva YAML frontmatter (`.md`; the notes body maps to the entry's comment fields), and the PDFs found in the folder tree. A PDF next to a sidecar of the same name is linked to the sidecar's entry; PDFs without a sidecar appear immediately and are enriched in the background with metadata extracted from the PDF itself and a generated citation key; a missing DOI is looked up online and its metadata fills the remaining empty fields. Directory libraries that were open on shutdown are reopened on the next start. Edits are not yet written back to the files. [#737](https://github.com/JabRef/jabref-koppor/pull/737) - We added a "Commit and push" button which allows to commit and then push in one go for Git operations. [#16339](https://github.com/JabRef/jabref/issues/16339) - We added the option to close and reopen the PDF preview in the unlinked files dialog. [#16159](https://github.com/JabRef/jabref/issues/16159) - We added the ability for LibreOffice BST citations to use style-defined labels. [forum#3764]([https://github.com/JabRef/jabref/issues/16357](https://discourse.jabref.org/t/feature-request-custom-citation-styles-from-bst/3764)) From fe6cbd29914f52eea35c89906dd5be4bae21f2c5 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 3 Sep 2026 05:37:23 +0200 Subject: [PATCH 3/7] Identify directory libraries consistently across the REST API The id derives from the library's location on disk in one place; endpoints that need BibTeX bytes read a directory library's mirror file, and the select command matches directory libraries too. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vr3E1Gg5DRU4LQDDVnhPys --- .../gui/frame/JabRefFrameViewModel.java | 9 +++-- .../server/command/SelectEntriesCommand.java | 9 ++--- .../server/resources/LibraryResource.java | 2 +- .../http/server/resources/MapResource.java | 2 +- .../http/server/services/ServerUtils.java | 33 +++++++++++-------- .../http/server/services/ServerUtilsTest.java | 14 ++++++-- 6 files changed, 40 insertions(+), 29 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java index a0a8ae0ee497..dbc250e17cf4 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java @@ -264,17 +264,16 @@ private void importFromFileAndOpen(String location) { /// unchanged. If the library is not open yet, it is opened in a new (raised) tab; the actual /// loading happens in the background, so callers must run the append via /// [#waitForLoadingFinished(Runnable)] to let the new tab finish loading first. A path - /// that does not exist (or is not a .bib file) is silently ignored by + /// that does not exist (or is neither a .bib file nor a directory) is silently ignored by /// [OpenDatabaseAction#openFile(Path)] and the current tab is kept (the server side /// already rejects unknown ids with 404, so this is only a defensive fallback). private void selectLibraryTab(Optional library) { library.map(path -> path.toAbsolutePath().normalize()).ifPresent(normalized -> tabContainer.getLibraryTabs().stream() - // A directory library has no .bib path; it is identified by its root - // directory, the same path the server derives its id from + // A directory library is identified by its root directory, the same + // path the server derives its id from // [impl->req~directory-library.rest-api~1] - .filter(tab -> tab.getBibDatabaseContext().getDatabasePath() - .or(tab.getBibDatabaseContext()::getDirectoryLibraryRoot) + .filter(tab -> tab.getBibDatabaseContext().getPathOnDisk() .map(path -> path.toAbsolutePath().normalize().equals(normalized)) .orElse(false)) .findFirst() diff --git a/jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java b/jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java index 25260d390ce5..7280518261dd 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java +++ b/jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java @@ -7,9 +7,9 @@ import org.jabref.http.JabRefSrvStateManager; import org.jabref.http.SrvStateManager; +import org.jabref.http.server.services.ServerUtils; import org.jabref.logic.UiCommand; import org.jabref.logic.command.CommandSelectionTab; -import org.jabref.logic.util.io.BackupFileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -61,7 +61,7 @@ public Response execute() { CommandSelectionTab commandSelectionTab = activeTab.get(); - if (!getLibraryIdFromContext(commandSelectionTab.getBibDatabaseContext()).equals(libraryId)) { + if (ServerUtils.libraryId(commandSelectionTab.getBibDatabaseContext()).filter(libraryId::equals).isEmpty()) { return Response.status(Response.Status.BAD_REQUEST) .entity("This command cannot be executed because the libraryId does not match the active selection tab.") .build(); @@ -76,9 +76,4 @@ public Response execute() { return Response.ok().build(); } - private String getLibraryIdFromContext(BibDatabaseContext bibDatabaseContext) { - return bibDatabaseContext.getDatabasePath() - .map(path -> path.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(path)) - .orElse(""); - } } diff --git a/jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java b/jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java index 0c85954561f9..209642b133da 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java +++ b/jabsrv/src/main/java/org/jabref/http/server/resources/LibraryResource.java @@ -87,7 +87,7 @@ public Response getBibtex(@PathParam("id") String id) { .build(); } - java.nio.file.Path library = ServerUtils.getLibraryPath(id, srvStateManager); + java.nio.file.Path library = ServerUtils.getLibraryFile(id, srvStateManager); String libraryAsString; try { libraryAsString = Files.readString(library); diff --git a/jabsrv/src/main/java/org/jabref/http/server/resources/MapResource.java b/jabsrv/src/main/java/org/jabref/http/server/resources/MapResource.java index b4afdea422b1..2bec79f192e5 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/resources/MapResource.java +++ b/jabsrv/src/main/java/org/jabref/http/server/resources/MapResource.java @@ -86,7 +86,7 @@ public void updateJabMapJson(@PathParam("id") String id, String fileContent) thr } private java.nio.file.Path getJabMapPath(String id) { - java.nio.file.Path libraryPath = ServerUtils.getLibraryPath(id, srvStateManager); + java.nio.file.Path libraryPath = ServerUtils.getLibraryFile(id, srvStateManager); String newName = libraryPath.getFileName().toString().replaceFirst("\\.bib$", ".jmp"); return libraryPath.getParent().resolve(newName); } diff --git a/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java b/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java index 87ebca15b736..a8782d9f0afe 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java +++ b/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Optional; @@ -12,6 +13,7 @@ import org.jabref.http.SrvStateManager; import org.jabref.logic.ai.chatting.ChatModel; import org.jabref.logic.ai.chatting.util.ChatModelFactory; +import org.jabref.logic.directorylibrary.DirectoryLibrarySynchronizer; import org.jabref.logic.importer.FetcherException; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.fileformat.BibtexImporter; @@ -30,28 +32,35 @@ public class ServerUtils { - /// The on-disk path that identifies a library: the `.bib` file of a regular library, or the - /// root directory of a directory library (which has no `.bib` path of its own). This is the - /// same identity the GUI session store uses to remember open libraries, so a directory - /// library keeps a stable id across everything. + /// The stable id used in URLs: derived from the `.bib` file of a regular library, or from + /// the root directory of a directory library (which has no `.bib` path of its own) — the + /// same identity the GUI session store uses, so a directory library keeps one id across + /// everything. Empty for unsaved libraries. /// [impl->req~directory-library.rest-api~1] - public static Optional libraryIdentifyingPath(BibDatabaseContext context) { - return context.getDatabasePath().or(context::getDirectoryLibraryRoot); + public static Optional libraryId(BibDatabaseContext context) { + return context.getPathOnDisk().map(ServerUtils::libraryId); } - /// The stable id string used in URLs for a library at the given identifying path. private static String libraryId(Path path) { return path.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(path); } + /// The file holding the library's BibTeX for endpoints that read or derive files: the + /// `.bib` itself, or, for a directory library, its `.bib` mirror inside the root. + /// + /// @throws NotFoundException if no library with the given id is found + public static @NonNull Path getLibraryFile(String id, SrvStateManager srvStateManager) { + Path path = getLibraryPath(id, srvStateManager); + return Files.isDirectory(path) ? path.resolve(DirectoryLibrarySynchronizer.mirrorFileName(path)) : path; + } + /// Returns ids of all libraries the state manager currently considers /// open. Used by every resource that operates across the open /// collection (libraries listing, batch query, ...). public static List openLibraryIds(SrvStateManager srvStateManager) { return srvStateManager.getOpenDatabases().stream() - .map(ServerUtils::libraryIdentifyingPath) - .flatMap(Optional::stream) .map(ServerUtils::libraryId) + .flatMap(Optional::stream) .toList(); } @@ -64,7 +73,7 @@ public static List openLibraryIds(SrvStateManager srvStateManager) { public static @NonNull Path getLibraryPath(String id, SrvStateManager srvStateManager) { return srvStateManager.getOpenDatabases() .stream() - .map(ServerUtils::libraryIdentifyingPath) + .map(BibDatabaseContext::getPathOnDisk) .flatMap(Optional::stream) .filter(p -> libraryId(p).equals(id)) .findAny() @@ -93,9 +102,7 @@ public static List openLibraryIds(SrvStateManager srvStateManager) { return srvStateManager.getActiveDatabase().orElseThrow(NotFoundException::new); } return srvStateManager.getOpenDatabases().stream() - .filter(context -> libraryIdentifyingPath(context) - .map(p -> libraryId(p).equals(id)) - .orElse(false)) + .filter(context -> libraryId(context).filter(id::equals).isPresent()) .findFirst() .orElseThrow(() -> new NotFoundException("No library with id " + HtmlEscapers.htmlEscaper().escape(id) + " found")); } diff --git a/jabsrv/src/test/java/org/jabref/http/server/services/ServerUtilsTest.java b/jabsrv/src/test/java/org/jabref/http/server/services/ServerUtilsTest.java index 91a6dad1b2d7..31f62e205354 100644 --- a/jabsrv/src/test/java/org/jabref/http/server/services/ServerUtilsTest.java +++ b/jabsrv/src/test/java/org/jabref/http/server/services/ServerUtilsTest.java @@ -3,12 +3,12 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.Optional; import javafx.collections.FXCollections; import org.jabref.http.SrvStateManager; import org.jabref.logic.importer.ImportFormatPreferences; -import org.jabref.logic.util.io.BackupFileUtil; import org.jabref.model.database.BibDatabaseContext; import jakarta.ws.rs.NotFoundException; @@ -37,7 +37,7 @@ private BibDatabaseContext directoryLibrary() { } private String rootId() { - return root.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(root); + return ServerUtils.libraryId(directoryLibrary()).orElseThrow(); } private SrvStateManager stateManagerWith(BibDatabaseContext context) { @@ -62,6 +62,16 @@ void directoryLibraryContextResolvesById() throws IOException { assertSame(context, ServerUtils.getBibDatabaseContext(rootId(), stateManagerWith(context), mock(ImportFormatPreferences.class))); } + @Test + void directoryLibraryFileIsItsMirror() { + assertEquals(root.resolve(root.getFileName() + ".bib"), ServerUtils.getLibraryFile(rootId(), stateManagerWith(directoryLibrary()))); + } + + @Test + void unsavedLibraryHasNoId() { + assertEquals(Optional.empty(), ServerUtils.libraryId(new BibDatabaseContext())); + } + @Test void unknownIdIsNotFound() { assertThrows(NotFoundException.class, () -> ServerUtils.getLibraryPath("does-not-exist", stateManagerWith(directoryLibrary()))); From 82dc8e9cdfabb51a67f9c54690a86b392aeb4123 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 3 Sep 2026 05:39:40 +0200 Subject: [PATCH 4/7] Fix checkstyle findings in the select command Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vr3E1Gg5DRU4LQDDVnhPys --- .../org/jabref/http/server/command/SelectEntriesCommand.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java b/jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java index 7280518261dd..33efdcdbc3d7 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java +++ b/jabsrv/src/main/java/org/jabref/http/server/command/SelectEntriesCommand.java @@ -10,7 +10,6 @@ import org.jabref.http.server.services.ServerUtils; import org.jabref.logic.UiCommand; import org.jabref.logic.command.CommandSelectionTab; -import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -75,5 +74,4 @@ public Response execute() { return Response.ok().build(); } - } From 9b9391d199ce62e456d12c30865789b4fde96b96 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 7 Sep 2026 00:16:56 +0200 Subject: [PATCH 5/7] Apply IntelliJ formatter after upstream resync Fixes the CI format check. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DDcHNMt9fPWnpYaHheFvry --- .../directorylibrary/DirectoryLibrarySynchronizer.java | 3 ++- .../org/jabref/logic/exporter/HayagrivaEntryWriter.java | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/jablib/src/main/java/org/jabref/logic/directorylibrary/DirectoryLibrarySynchronizer.java b/jablib/src/main/java/org/jabref/logic/directorylibrary/DirectoryLibrarySynchronizer.java index 4fc4d5ed313c..48b947d061a8 100644 --- a/jablib/src/main/java/org/jabref/logic/directorylibrary/DirectoryLibrarySynchronizer.java +++ b/jablib/src/main/java/org/jabref/logic/directorylibrary/DirectoryLibrarySynchronizer.java @@ -259,7 +259,8 @@ Path sidecarOf(BibEntry entry) { /// Waits until every event queued so far has been handled (tests). void awaitPendingEvents() throws InterruptedException, ExecutionException { - syncExecutor.submit(() -> { }).get(); + syncExecutor.submit(() -> { + }).get(); } /// Stops watching and writes what is still pending. Events already queued (the last diff --git a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java index 3d6ac8edaf77..876fd667275a 100644 --- a/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java +++ b/jablib/src/main/java/org/jabref/logic/exporter/HayagrivaEntryWriter.java @@ -352,9 +352,12 @@ private ObjectNode parentNode(String type, String title) { private String booktitleParentType(EntryType entryType) { return switch (entryType) { - case StandardEntryType.InProceedings -> "proceedings"; - case StandardEntryType.InBook -> "book"; - default -> "anthology"; + case StandardEntryType.InProceedings -> + "proceedings"; + case StandardEntryType.InBook -> + "book"; + default -> + "anthology"; }; } From 6b2ca7f96d119160fd08caa276abcf62f3718aa1 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 7 Sep 2026 11:14:36 +0200 Subject: [PATCH 6/7] Renumber the directory-as-library ADR to 0072 Upstream added a 0071 ADR after this stack claimed the number, so the MADR duplicate-ID check failed. Co-Authored-By: Claude Opus 4.8 --- ....md => 0072-directory-as-library-with-hayagriva-sidecars.md} | 2 +- docs/requirements/directory-library.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/decisions/{0071-directory-as-library-with-hayagriva-sidecars.md => 0072-directory-as-library-with-hayagriva-sidecars.md} (99%) diff --git a/docs/decisions/0071-directory-as-library-with-hayagriva-sidecars.md b/docs/decisions/0072-directory-as-library-with-hayagriva-sidecars.md similarity index 99% rename from docs/decisions/0071-directory-as-library-with-hayagriva-sidecars.md rename to docs/decisions/0072-directory-as-library-with-hayagriva-sidecars.md index 3a1db01cbb2b..6ffd7c03e355 100644 --- a/docs/decisions/0071-directory-as-library-with-hayagriva-sidecars.md +++ b/docs/decisions/0072-directory-as-library-with-hayagriva-sidecars.md @@ -1,5 +1,5 @@ --- -nav_order: 0071 +nav_order: 0072 parent: Decision Records --- diff --git a/docs/requirements/directory-library.md b/docs/requirements/directory-library.md index 6c076fbda0a5..1c402919d1ad 100644 --- a/docs/requirements/directory-library.md +++ b/docs/requirements/directory-library.md @@ -19,7 +19,7 @@ online and the metadata behind it fills only the fields the PDF did not provide. and `.md` files without a Hayagriva frontmatter are skipped; unparseable Hayagriva files are reported as warnings without aborting the scan. Scanning must not write or modify any file in the directory. -See [ADR 71](../decisions/0071-directory-as-library-with-hayagriva-sidecars.md) for more details. +See [ADR 72](../decisions/0072-directory-as-library-with-hayagriva-sidecars.md) for more details. Needs: impl From 8ee71de0e6c6a61adb3d8536246b6c748b7f0cc9 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 13 Sep 2026 02:01:10 +0200 Subject: [PATCH 7/7] Identify directory libraries by their root in getLibraryId Main's new library id ignored directory libraries, so in-app entry links could not find them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CajtxjcCEjS87f4hoTBTJD --- .../java/org/jabref/model/database/BibDatabaseContext.java | 3 ++- .../main/java/org/jabref/http/server/services/ServerUtils.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jablib/src/main/java/org/jabref/model/database/BibDatabaseContext.java b/jablib/src/main/java/org/jabref/model/database/BibDatabaseContext.java index 770740cd991f..bf0626037f79 100644 --- a/jablib/src/main/java/org/jabref/model/database/BibDatabaseContext.java +++ b/jablib/src/main/java/org/jabref/model/database/BibDatabaseContext.java @@ -134,9 +134,10 @@ public void clearDatabasePath() { /// The id used to address this library from the outside: the REST API (`/libraries/{id}/...`), /// cite-as-you-write (`libraryid=`), JabMap and in-app links (`jabref://libraries/{id}/entries/{key}`). + /// A directory library is identified by its root directory. /// Empty for libraries that have not been saved to disk yet. public Optional getLibraryId() { - return getDatabasePath().map(path -> path.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(path)); + return getPathOnDisk().map(path -> path.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(path)); } public BibDatabase getDatabase() { diff --git a/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java b/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java index a8782d9f0afe..8a330b343ed4 100644 --- a/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java +++ b/jabsrv/src/main/java/org/jabref/http/server/services/ServerUtils.java @@ -38,7 +38,7 @@ public class ServerUtils { /// everything. Empty for unsaved libraries. /// [impl->req~directory-library.rest-api~1] public static Optional libraryId(BibDatabaseContext context) { - return context.getPathOnDisk().map(ServerUtils::libraryId); + return context.getLibraryId(); } private static String libraryId(Path path) {