From 6224bcee62049aaf70958477bbc317e264c307cc Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 14:44:16 +0200 Subject: [PATCH 1/2] Attach each full text document as soon as its search finishes A multi-entry full text search collected every result and attached them only after the last entry was looked up. With the browser-extension provider taking up to minutes per entry, nothing appeared for a long time. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + docs/requirements/fetchers.md | 2 +- .../externalfiles/DownloadFullTextAction.java | 52 +++++++++--------- .../DownloadFullTextActionTest.java | 54 +++++++++++++------ 4 files changed, 68 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2da4e5c6734..839e802e5370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Fixed - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) +- We fixed an issue where full text documents for several selected entries were attached only after all searches finished. [TODO](TODO) ### Removed diff --git a/docs/requirements/fetchers.md b/docs/requirements/fetchers.md index 67b2fa4a1f3a..264ae661b873 100644 --- a/docs/requirements/fetchers.md +++ b/docs/requirements/fetchers.md @@ -51,7 +51,7 @@ For entries that contain a ScholarAPI identifier and have a PDF available, JabRe ## Full text search runs in the background `req~fetchers.fulltext-background-search~1` -The search for full text documents runs as a background task shown in the status bar with progress and a cancel option, so JabRef stays usable while it runs. Its results are applied to the library the entries were selected in, and are discarded if that library was closed meanwhile. +The search for full text documents runs as a background task shown in the status bar with progress and a cancel option, so JabRef stays usable while it runs. Each entry's result is applied as soon as its search finishes, without waiting for the remaining entries. Results are applied to the library the entries were selected in, and are discarded if that library was closed meanwhile. Needs: impl diff --git a/jabgui/src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java b/jabgui/src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java index eafb5adea773..c1afe63d5a05 100644 --- a/jabgui/src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java +++ b/jabgui/src/main/java/org/jabref/gui/externalfiles/DownloadFullTextAction.java @@ -1,8 +1,8 @@ package org.jabref.gui.externalfiles; -import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Function; import org.jabref.gui.DialogService; @@ -38,6 +38,7 @@ public class DownloadFullTextAction extends SimpleCommand { private final GuiPreferences preferences; private final UiTaskExecutor taskExecutor; private final Function> fullTextFinder; + private final Consumer uiThread; public DownloadFullTextAction(DialogService dialogService, StateManager stateManager, @@ -47,19 +48,22 @@ public DownloadFullTextAction(DialogService dialogService, stateManager, preferences, taskExecutor, - entry -> new FulltextFetchers(preferences.getImportFormatPreferences(), preferences.getImporterPreferences()).findFullTextPDF(entry)); + entry -> new FulltextFetchers(preferences.getImportFormatPreferences(), preferences.getImporterPreferences()).findFullTextPDF(entry), + UiTaskExecutor::runInJavaFXThread); } DownloadFullTextAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences, UiTaskExecutor taskExecutor, - Function> fullTextFinder) { + Function> fullTextFinder, + Consumer uiThread) { this.dialogService = dialogService; this.stateManager = stateManager; this.preferences = preferences; this.taskExecutor = taskExecutor; this.fullTextFinder = fullTextFinder; + this.uiThread = uiThread; this.executable.bind(ActionHelper.needsEntriesSelected(stateManager)); } @@ -92,10 +96,9 @@ private void execute(BibDatabaseContext databaseContext) { } } - BackgroundTask> findFullTextsTask = new BackgroundTask<>() { + BackgroundTask findFullTextsTask = new BackgroundTask<>() { @Override - public List call() { - List downloads = new ArrayList<>(entries.size()); + public Void call() { int count = 0; for (BibEntry entry : entries) { if (isCancelled()) { @@ -103,42 +106,41 @@ public List call() { } BibEntry lookupSnapshot = new BibEntry(entry); - downloads.add(new EntryDownload(entry, lookupSnapshot, fullTextFinder.apply(lookupSnapshot))); + EntryDownload download = new EntryDownload(entry, lookupSnapshot, fullTextFinder.apply(lookupSnapshot)); + // Attach right away: a lookup can take minutes, so the user should not wait for the whole selection + uiThread.accept(() -> downloadFullText(download, databaseContext)); updateProgress(++count, entries.size()); updateMessage(Localization.lang("%0/%1 entries", count, entries.size())); } - return downloads; + return null; } }; findFullTextsTask.setTitle(Localization.lang("Download full text documents")) .withInitialMessage(Localization.lang("Looking for full text document...")) .showToUser(true) - .onSuccess(downloads -> downloadFullTexts(downloads, databaseContext)) .executeWith(taskExecutor); } - private void downloadFullTexts(List downloads, BibDatabaseContext databaseContext) { + private void downloadFullText(EntryDownload download, BibDatabaseContext databaseContext) { if (!stateManager.getOpenDatabases().contains(databaseContext)) { - LOGGER.debug("Library closed before the full text search finished; skipping downloads."); + LOGGER.debug("Library closed before the full text search finished; skipping download."); return; } - for (EntryDownload download : downloads) { - BibEntry entry = download.entry(); - if (!databaseContext.getDatabase().getEntries().contains(entry)) { - continue; - } - if (!entry.equals(download.lookupSnapshot())) { - LOGGER.debug("Entry changed during full text search; skipping download."); - continue; - } - - download.result().ifPresentOrElse( - result -> addLinkedFileFromURL(databaseContext, result, entry), - () -> dialogService.notify(Localization.lang("No full text document found for entry %0.", - entry.getCitationKey().orElse(Localization.lang("undefined"))))); + BibEntry entry = download.entry(); + if (!databaseContext.getDatabase().getEntries().contains(entry)) { + return; } + if (!entry.equals(download.lookupSnapshot())) { + LOGGER.debug("Entry changed during full text search; skipping download."); + return; + } + + download.result().ifPresentOrElse( + result -> addLinkedFileFromURL(databaseContext, result, entry), + () -> dialogService.notify(Localization.lang("No full text document found for entry %0.", + entry.getCitationKey().orElse(Localization.lang("undefined"))))); } /// This method attaches a linked file from a URL (if not already linked) to an entry using the key and value pair diff --git a/jabgui/src/test/java/org/jabref/gui/externalfiles/DownloadFullTextActionTest.java b/jabgui/src/test/java/org/jabref/gui/externalfiles/DownloadFullTextActionTest.java index 7fc76d141ad1..2757c13a67fb 100644 --- a/jabgui/src/test/java/org/jabref/gui/externalfiles/DownloadFullTextActionTest.java +++ b/jabgui/src/test/java/org/jabref/gui/externalfiles/DownloadFullTextActionTest.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; import java.util.function.Function; import org.jabref.gui.DialogService; @@ -42,9 +41,11 @@ class DownloadFullTextActionTest { private BibDatabaseContext databaseContext; private BibEntry entry; private FetcherResult fetcherResult; + private List pendingUiActions; @BeforeEach void setUp() throws MalformedURLException { + pendingUiActions = new ArrayList<>(); dialogService = mock(DialogService.class); stateManager = new JabRefGuiStateManager(); preferences = mock(GuiPreferences.class); @@ -80,9 +81,9 @@ void skipsDownloadWhenEntryChangedAfterLookup() throws Exception { RecordingDownloadFullTextAction action = new RecordingDownloadFullTextAction(_ -> Optional.of(fetcherResult)); BackgroundTask task = captureTask(action); - Object downloads = task.call(); + task.call(); entry.withField(StandardField.TITLE, "Updated title"); - runSuccessHandler(task, downloads); + runPendingUiActions(); assertEquals(List.of(), action.downloadedEntries); } @@ -92,9 +93,9 @@ void skipsDownloadWhenEntryDeletedAfterLookup() throws Exception { RecordingDownloadFullTextAction action = new RecordingDownloadFullTextAction(_ -> Optional.of(fetcherResult)); BackgroundTask task = captureTask(action); - Object downloads = task.call(); + task.call(); databaseContext.getDatabase().removeEntry(entry); - runSuccessHandler(task, downloads); + runPendingUiActions(); assertEquals(List.of(), action.downloadedEntries); } @@ -113,6 +114,31 @@ void finderReceivesEntrySnapshot() throws Exception { assertEquals(List.of(entry), action.downloadedEntries); } + @Test + void attachesEachEntryBeforeLookingUpTheNext() throws Exception { + BibEntry secondEntry = new BibEntry().withField(StandardField.DOI, "10.1000/second"); + databaseContext.getDatabase().insertEntry(secondEntry); + stateManager.setSelectedEntries(List.of(entry, secondEntry)); + List attachedEntries = new ArrayList<>(); + List attachedBeforeLookup = new ArrayList<>(); + DownloadFullTextAction action = new DownloadFullTextAction(dialogService, stateManager, preferences, taskExecutor, _ -> { + runPendingUiActions(); + attachedBeforeLookup.add(attachedEntries.size()); + return Optional.of(fetcherResult); + }, pendingUiActions::add) { + @Override + void addLinkedFileFromURL(BibDatabaseContext databaseContext, FetcherResult result, BibEntry entry) { + attachedEntries.add(entry); + } + }; + + BackgroundTask task = captureTask(action); + completeTask(task); + + assertEquals(List.of(0, 1), attachedBeforeLookup); + assertEquals(List.of(entry, secondEntry), attachedEntries); + } + private BackgroundTask captureTask(DownloadFullTextAction action) { action.execute(); @@ -121,17 +147,15 @@ private BackgroundTask captureTask(DownloadFullTextAction action) { return taskCaptor.getValue(); } - private static void completeTask(BackgroundTask task) throws Exception { - Object downloads = task.call(); - runSuccessHandler(task, downloads); + private void completeTask(BackgroundTask task) throws Exception { + task.call(); + runPendingUiActions(); } - @SuppressWarnings({"rawtypes", "unchecked"}) - private static void runSuccessHandler(BackgroundTask task, Object downloads) { - Consumer onSuccess = task.getOnSuccess(); - if (onSuccess != null) { - onSuccess.accept(downloads); - } + private void runPendingUiActions() { + List actions = List.copyOf(pendingUiActions); + pendingUiActions.clear(); + actions.forEach(Runnable::run); } private class RecordingDownloadFullTextAction extends DownloadFullTextAction { @@ -139,7 +163,7 @@ private class RecordingDownloadFullTextAction extends DownloadFullTextAction { private final List downloadedResults = new ArrayList<>(); RecordingDownloadFullTextAction(Function> fullTextFinder) { - super(dialogService, stateManager, preferences, taskExecutor, fullTextFinder); + super(dialogService, stateManager, preferences, taskExecutor, fullTextFinder, pendingUiActions::add); } @Override From 7cbe4ddde466cb1c90a656e444c48df3366d1c0a Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 22 Sep 2026 14:51:45 +0200 Subject: [PATCH 2/2] Link CHANGELOG entry to PR Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839e802e5370..97d94eee42bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Fixed - We fixed an issue where the AI chat lost its scroll position when switching back to an entry. [#17172](https://github.com/JabRef/jabref/pull/17172) -- We fixed an issue where full text documents for several selected entries were attached only after all searches finished. [TODO](TODO) +- We fixed an issue where full text documents for several selected entries were attached only after all searches finished. [koppor#772](https://github.com/JabRef/jabref-koppor/pull/772) ### Removed