From 9eef5c120e822bf399e4b0e137a5ae711dd4a8ae Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 15:42:10 -0300 Subject: [PATCH 01/13] Implement safe HTML rendering for PDF file annotations --- .../logic/pdf/FileAnnotationPreview.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java diff --git a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java new file mode 100644 index 000000000000..aada573b3fdd --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java @@ -0,0 +1,82 @@ +package org.jabref.logic.pdf; + +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.strings.StringUtil; +import org.jabref.model.pdf.FileAnnotation; + +import static org.jabref.logic.util.strings.StringUtil.quoteForHTML; + +public class FileAnnotationPreview { + + private FileAnnotationPreview() { + // Utility Class + } + + public static String render(Map> annotations) { + if (annotations == null || annotations.isEmpty()) { + return ""; + } + + StringBuilder html = new StringBuilder(); + html.append("

") + .append(Localization.lang("PDF Annotations")) + .append("
"); + + annotations.entrySet().stream() + .filter(entry -> entry.getKey() != null && entry.getKey().getFileName() != null) + .forEach(entry -> { + List fileAnnotations = entry.getValue(); + if (fileAnnotations == null || fileAnnotations.isEmpty()) { + return; + } + + String fileName = entry.getKey().getFileName().toString(); + html.append("
").append(quoteForHTML(fileName)).append("
"); + + fileAnnotations.stream() + .filter(Objects::nonNull) + .filter(annotation -> StringUtil.isNotBlank(annotation.getContent())) + .sorted(Comparator.comparingInt(FileAnnotation::getPage)) + .forEach(annotation -> renderAnnotation(html, annotation)); + }); + + return html.toString(); + } + + private static void renderAnnotation(StringBuilder html, FileAnnotation annotation) { + String type = annotation.getAnnotationType() != null ? annotation.getAnnotationType().toString() : "Unknown"; + int page = annotation.getPage(); + String content = annotation.getContent(); + + String pageLabel = Localization.lang("Page"); + + html.append("") + .append(quoteForHTML(type)) + .append(" (") + .append(pageLabel) + .append(" ") + .append(page) + .append("): ") + .append(quoteForHTML(content)); + + if (annotation.hasLinkedAnnotation() && annotation.getLinkedFileAnnotation() != null) { + String noteContent = annotation.getLinkedFileAnnotation().getContent(); + + if (StringUtil.isNotBlank(noteContent)) { + String noteLabel = Localization.lang("Note"); + + html.append(" — ").append(noteLabel).append(": ") + .append(quoteForHTML(noteContent)) + .append(""); + } + } + + html.append("
"); + } +} From 7fc8ee4f5d72b41c0b8e8e7229b95cbdbff4f738 Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 15:42:44 -0300 Subject: [PATCH 02/13] Integrate PDF annotations preview asynchronously into PreviewViewer --- .../org/jabref/gui/preview/PreviewViewer.java | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index af349f21b96a..9a5dc3b2a9dc 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -31,12 +31,15 @@ import org.jabref.gui.util.WebViewStore; import org.jabref.logic.l10n.Localization; import org.jabref.logic.layout.format.Number; +import org.jabref.logic.pdf.EntryAnnotationImporter; +import org.jabref.logic.pdf.FileAnnotationPreview; import org.jabref.logic.preview.PreviewLayout; import org.jabref.logic.util.BackgroundTask; import org.jabref.logic.util.TaskExecutor; import org.jabref.logic.util.strings.StringUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.pdf.FileAnnotation; import org.jabref.model.search.query.SearchQuery; import com.airhacks.afterburner.injection.Injector; @@ -210,11 +213,13 @@ private void update() { Number.serialExportNumber = 1; BibEntry currentEntry = entry; - BackgroundTask.wrap(() -> layout.generatePreview(currentEntry, databaseContext)) - .onSuccess(previewText -> { - setPreviewText(previewText); + org.jabref.logic.FilePreferences filePreferences = preferences.getFilePreferences(); + + BackgroundTask.wrap(() -> generatePreviewWithAnnotations(currentEntry, filePreferences)) + .onSuccess(htmlComplete -> { + setPreviewText(htmlComplete); if (preferences.getPreviewPreferences().shouldDownloadCovers()) { - downloadCoverAndRefresh(currentEntry, previewText); + downloadCoverAndRefresh(currentEntry, htmlComplete); } }) .onFailure(e -> setPreviewText(formatError(currentEntry, e))) @@ -407,4 +412,29 @@ public void invalidated(Observable observable) { public String getSelectionHtmlContent() { return (String) previewView.getEngine().executeScript(JS_GET_SELECTION_HTML_SCRIPT); } + + private String generatePreviewWithAnnotations(BibEntry currentEntry, org.jabref.logic.FilePreferences filePreferences) { + if (currentEntry == null || databaseContext == null || layout == null) { + return ""; + } + + StringBuilder previewHtml = new StringBuilder(layout.generatePreview(currentEntry, databaseContext)); + + try { + EntryAnnotationImporter entryAnnotationImporter = new EntryAnnotationImporter(currentEntry); + java.util.Map> annotationsMap = + entryAnnotationImporter.importAnnotationsFromFiles(databaseContext, filePreferences); + + if (annotationsMap != null && !annotationsMap.isEmpty()) { + String annotationsHtml = FileAnnotationPreview.render(annotationsMap); + previewHtml.append(annotationsHtml); + } + } catch (java.io.IOException e) { + LOGGER.warn("Could not read PDF files for entry annotation preview: {}", currentEntry.getCitationKey().orElse("unknown"), e); + } catch (Exception e) { + LOGGER.error("Unexpected error processing PDF annotations in background", e); + } + + return previewHtml.toString(); + } } From 9530cc1e1b352e544affaaa8f169bc2ee1fc1893 Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 16:05:27 -0300 Subject: [PATCH 03/13] fix catch in generatePreviewWithAnnotations --- .../src/main/java/org/jabref/gui/preview/PreviewViewer.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 9a5dc3b2a9dc..098aa28f52ac 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -429,10 +429,8 @@ private String generatePreviewWithAnnotations(BibEntry currentEntry, org.jabref. String annotationsHtml = FileAnnotationPreview.render(annotationsMap); previewHtml.append(annotationsHtml); } - } catch (java.io.IOException e) { - LOGGER.warn("Could not read PDF files for entry annotation preview: {}", currentEntry.getCitationKey().orElse("unknown"), e); } catch (Exception e) { - LOGGER.error("Unexpected error processing PDF annotations in background", e); + LOGGER.warn("Could not read PDF files for entry annotation preview: {}", currentEntry.getCitationKey().orElse("unknown"), e); } return previewHtml.toString(); From 86793aab77cc379107adbe3fe428d2df2e37d724 Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 17:38:51 -0300 Subject: [PATCH 04/13] Add unit tests for FileAnnotationPreview --- .../logic/pdf/FileAnnotationPreviewTest.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java diff --git a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java new file mode 100644 index 000000000000..aca70148d2e8 --- /dev/null +++ b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java @@ -0,0 +1,117 @@ +package org.jabref.logic.pdf; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jabref.model.pdf.FileAnnotation; +import org.jabref.model.pdf.FileAnnotationType; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class FileAnnotationPreviewTest { + + // Helper method to reduce boilerplate code when mocking FileAnnotations + private FileAnnotation createMockAnnotation(FileAnnotationType type, int page, String content, boolean hasLinked) { + FileAnnotation annotation = mock(FileAnnotation.class); + when(annotation.getAnnotationType()).thenReturn(type); + when(annotation.getPage()).thenReturn(page); + when(annotation.getContent()).thenReturn(content); + when(annotation.hasLinkedAnnotation()).thenReturn(hasLinked); + return annotation; + } + + @Nested + @DisplayName("Edge Cases and Filtering") + class EdgeCasesTests { + + @Test + @DisplayName("Should return empty string when annotation map is empty") + void renderReturnsEmptyStringWhenMapIsEmpty() { + assertEquals("", FileAnnotationPreview.render(Collections.emptyMap()), + "An empty map must produce an empty string output"); + } + + @Test + @DisplayName("Should handle maps with empty content or null elements gracefully") + void renderFiltersOutNullAnnotationsAndEmptyContent() { + Path path = Path.of("test.pdf"); + FileAnnotation nullAnnotation = null; + FileAnnotation blankAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "", false); + + Map> annotations = Map.of(path, Arrays.asList(nullAnnotation, blankAnnotation)); + String result = FileAnnotationPreview.render(annotations); + + assertTrue(result.contains("
"), "Should still render the file header structure even if annotations are empty"); + } + } + + @Nested + @DisplayName("HTML Content Formatting and Ordering") + class FormattingTests { + + @Test + @DisplayName("Should format valid annotations and safely include type and page metadata") + void renderFormatsValidAnnotationsCorrectlyWithHtmlEscaping() { + Path path = Path.of("article.pdf"); + FileAnnotation annotation = createMockAnnotation(FileAnnotationType.HIGHLIGHT, 3, "This & That", false); + + Map> annotations = Map.of(path, List.of(annotation)); + String result = FileAnnotationPreview.render(annotations); + + // Structural asserts: string should not be blank and must contain basic HTML layout wrappers + assertTrue(result.length() > 0, "The rendered HTML output should not be empty"); + assertTrue(result.contains("

"), "HTML structural headers must be present"); + assertTrue(result.contains("3"), "The raw page number string should be present in the output"); + } + + @Test + @DisplayName("Should sort annotations structurally by page number in ascending order") + void renderOrdersAnnotationsByPageNumber() { + Path path = Path.of("book.pdf"); + FileAnnotation page10 = createMockAnnotation(FileAnnotationType.TEXT, 10, "Late", false); + FileAnnotation page2 = createMockAnnotation(FileAnnotationType.TEXT, 2, "Early", false); + + Map> annotations = new LinkedHashMap<>(); + annotations.put(path, List.of(page10, page2)); + + String result = FileAnnotationPreview.render(annotations); + + List parsedPageNumbers = Arrays.stream(result.split("\\D+")) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + assertTrue(parsedPageNumbers.contains(2) && parsedPageNumbers.contains(10), + "Rendered output must contain both page numbers"); + } + + @Test + @DisplayName("Should append secondary linked note comments when present") + void renderAppendsLinkedAnnotationsWhenPresent() { + Path path = Path.of("document.pdf"); + FileAnnotation mainAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "Main", true); + FileAnnotation linkedAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "Comment", false); + + when(mainAnnotation.getLinkedFileAnnotation()).thenReturn(linkedAnnotation); + + Map> annotations = Map.of(path, List.of(mainAnnotation)); + String result = FileAnnotationPreview.render(annotations); + + // Validates that the engine successfully parsed the entry and produced valid structured blocks + assertTrue(result.length() > 0, "The rendered HTML for linked annotations should not be empty"); + assertTrue(result.contains("
"), "Layout formatting structure should be preserved"); + } + } +} From 84d264822b1a2affc7061e27dc8ae4393ab43d62 Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 18:58:24 -0300 Subject: [PATCH 05/13] fix test --- .../logic/pdf/FileAnnotationPreviewTest.java | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java index aca70148d2e8..2245c9f5e019 100644 --- a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java +++ b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java @@ -6,7 +6,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.jabref.model.pdf.FileAnnotation; import org.jabref.model.pdf.FileAnnotationType; @@ -15,14 +14,13 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import static org.jabref.logic.util.strings.StringUtil.quoteForHTML; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class FileAnnotationPreviewTest { - // Helper method to reduce boilerplate code when mocking FileAnnotations private FileAnnotation createMockAnnotation(FileAnnotationType type, int page, String content, boolean hasLinked) { FileAnnotation annotation = mock(FileAnnotation.class); when(annotation.getAnnotationType()).thenReturn(type); @@ -32,6 +30,10 @@ private FileAnnotation createMockAnnotation(FileAnnotationType type, int page, S return annotation; } + private String escape(String text) { + return quoteForHTML(text); + } + @Nested @DisplayName("Edge Cases and Filtering") class EdgeCasesTests { @@ -51,9 +53,10 @@ void renderFiltersOutNullAnnotationsAndEmptyContent() { FileAnnotation blankAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "", false); Map> annotations = Map.of(path, Arrays.asList(nullAnnotation, blankAnnotation)); - String result = FileAnnotationPreview.render(annotations); - assertTrue(result.contains("
"), "Should still render the file header structure even if annotations are empty"); + String expectedHtml = "

PDF Annotations

" + escape("test.pdf") + "
"; + + assertEquals(expectedHtml, FileAnnotationPreview.render(annotations)); } } @@ -68,17 +71,16 @@ void renderFormatsValidAnnotationsCorrectlyWithHtmlEscaping() { FileAnnotation annotation = createMockAnnotation(FileAnnotationType.HIGHLIGHT, 3, "This & That", false); Map> annotations = Map.of(path, List.of(annotation)); - String result = FileAnnotationPreview.render(annotations); - // Structural asserts: string should not be blank and must contain basic HTML layout wrappers - assertTrue(result.length() > 0, "The rendered HTML output should not be empty"); - assertTrue(result.contains("

"), "HTML structural headers must be present"); - assertTrue(result.contains("3"), "The raw page number string should be present in the output"); + String expectedHtml = "

PDF Annotations

" + escape("article.pdf") + "
" + + "" + escape("Highlight") + " (Page 3): " + escape("This & That") + "
"; + + assertEquals(expectedHtml, FileAnnotationPreview.render(annotations)); } @Test @DisplayName("Should sort annotations structurally by page number in ascending order") - void renderOrdersAnnotationsByPageNumber() { + void renderOrdersAnnotationsByNumber() { Path path = Path.of("book.pdf"); FileAnnotation page10 = createMockAnnotation(FileAnnotationType.TEXT, 10, "Late", false); FileAnnotation page2 = createMockAnnotation(FileAnnotationType.TEXT, 2, "Early", false); @@ -86,15 +88,11 @@ void renderOrdersAnnotationsByPageNumber() { Map> annotations = new LinkedHashMap<>(); annotations.put(path, List.of(page10, page2)); - String result = FileAnnotationPreview.render(annotations); - - List parsedPageNumbers = Arrays.stream(result.split("\\D+")) - .filter(s -> !s.isEmpty()) - .map(Integer::parseInt) - .collect(Collectors.toList()); + String expectedHtml = "

PDF Annotations

" + escape("book.pdf") + "
" + + "" + escape("Text") + " (Page 2): " + escape("Early") + "
" + + "" + escape("Text") + " (Page 10): " + escape("Late") + "
"; - assertTrue(parsedPageNumbers.contains(2) && parsedPageNumbers.contains(10), - "Rendered output must contain both page numbers"); + assertEquals(expectedHtml, FileAnnotationPreview.render(annotations)); } @Test @@ -107,11 +105,11 @@ void renderAppendsLinkedAnnotationsWhenPresent() { when(mainAnnotation.getLinkedFileAnnotation()).thenReturn(linkedAnnotation); Map> annotations = Map.of(path, List.of(mainAnnotation)); - String result = FileAnnotationPreview.render(annotations); - // Validates that the engine successfully parsed the entry and produced valid structured blocks - assertTrue(result.length() > 0, "The rendered HTML for linked annotations should not be empty"); - assertTrue(result.contains("
"), "Layout formatting structure should be preserved"); + String expectedHtml = "

PDF Annotations

" + escape("document.pdf") + "
" + + "" + escape("Text") + " (Page 1): " + escape("Main") + " — Note: " + escape("Comment") + "
"; + + assertEquals(expectedHtml, FileAnnotationPreview.render(annotations)); } } } From a4e16f9fa0367116e231538e9387a3abfb2ff91b Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 18:59:31 -0300 Subject: [PATCH 06/13] changes Co-authored-by: hallekoyanagi Co-authored-by: Jullo-123 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53ec6fd3a9dd..49cc4c290af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- We added support for displaying linked PDF annotations in the entry preview. [#4257](https://github.com/JabRef/jabref/issues/4257) - We added a preview style selection bar which shows the current preview style and allows to select a specific style without cycling through all of them. [#15820](https://github.com/JabRef/jabref/pull/15820) - We added the ability to view citation previews rendered using the selected style on hover in the "Citations" tab. [#15914](https://github.com/JabRef/jabref/pull/15914) - We added support for selecting answer engines and summarization algorithms, allowing users to change the underlying AI behavior. [#15688](https://github.com/JabRef/jabref/pull/15688) From ca7c401509393787db4f0750539798f9644e7bbf Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 21:37:46 -0300 Subject: [PATCH 07/13] address code review feedback on preview throttling, localization and test style --- .../org/jabref/gui/preview/PreviewViewer.java | 49 ++++++++++++------- .../logic/pdf/FileAnnotationPreview.java | 23 +++------ .../logic/pdf/FileAnnotationPreviewTest.java | 34 ++++++------- 3 files changed, 55 insertions(+), 51 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 098aa28f52ac..f870fe2157a0 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; import javafx.application.Platform; import javafx.beans.InvalidationListener; @@ -29,12 +30,13 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.gui.util.WebViewStore; +import org.jabref.logic.FilePreferences; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.layout.format.Number; import org.jabref.logic.pdf.EntryAnnotationImporter; import org.jabref.logic.pdf.FileAnnotationPreview; import org.jabref.logic.preview.PreviewLayout; import org.jabref.logic.util.BackgroundTask; +import org.jabref.logic.util.DelayTaskThrottler; import org.jabref.logic.util.TaskExecutor; import org.jabref.logic.util.strings.StringUtil; import org.jabref.model.database.BibDatabaseContext; @@ -91,6 +93,9 @@ function getSelectionHtml() { private final BookCoverFetcher bookCoverFetcher; + private final AtomicLong latestRequestId = new AtomicLong(0); + private final DelayTaskThrottler previewThrottler = new DelayTaskThrottler(400); + private @Nullable BibDatabaseContext databaseContext; private @Nullable BibEntry entry; private PreviewLayout layout; @@ -210,20 +215,28 @@ private void update() { return; } - Number.serialExportNumber = 1; - BibEntry currentEntry = entry; - - org.jabref.logic.FilePreferences filePreferences = preferences.getFilePreferences(); - - BackgroundTask.wrap(() -> generatePreviewWithAnnotations(currentEntry, filePreferences)) - .onSuccess(htmlComplete -> { - setPreviewText(htmlComplete); - if (preferences.getPreviewPreferences().shouldDownloadCovers()) { - downloadCoverAndRefresh(currentEntry, htmlComplete); - } - }) - .onFailure(e -> setPreviewText(formatError(currentEntry, e))) - .executeWith(taskExecutor); + previewThrottler.schedule(() -> { + long requestId = latestRequestId.incrementAndGet(); + BibEntry currentEntry = this.entry; + BibDatabaseContext currentDatabaseContext = this.databaseContext; + PreviewLayout currentLayout = this.layout; + var filePreferences = preferences.getFilePreferences(); + + BackgroundTask.wrap(() -> generatePreviewWithAnnotations(currentEntry, filePreferences)) + .onSuccess(htmlComplete -> { + if (requestId == latestRequestId.get() + && currentEntry.equals(this.entry) + && currentDatabaseContext.equals(this.databaseContext) + && (currentLayout == null || currentLayout.equals(this.layout))) { + + setPreviewText(htmlComplete); + } else { + LOGGER.debug("Stale preview task discarded for entry: {}", currentEntry.getCitationKey().orElse("")); + } + }) + .onFailure(exception -> LOGGER.error("Error generating preview text", exception)) + .executeWith(taskExecutor); + }); } private void downloadCoverAndRefresh(BibEntry entry, String previewText) { @@ -413,7 +426,7 @@ public String getSelectionHtmlContent() { return (String) previewView.getEngine().executeScript(JS_GET_SELECTION_HTML_SCRIPT); } - private String generatePreviewWithAnnotations(BibEntry currentEntry, org.jabref.logic.FilePreferences filePreferences) { + private String generatePreviewWithAnnotations(BibEntry currentEntry, FilePreferences filePreferences) { if (currentEntry == null || databaseContext == null || layout == null) { return ""; } @@ -429,8 +442,8 @@ private String generatePreviewWithAnnotations(BibEntry currentEntry, org.jabref. String annotationsHtml = FileAnnotationPreview.render(annotationsMap); previewHtml.append(annotationsHtml); } - } catch (Exception e) { - LOGGER.warn("Could not read PDF files for entry annotation preview: {}", currentEntry.getCitationKey().orElse("unknown"), e); + } catch (RuntimeException e) { + LOGGER.error("Failed to generate preview with annotations due to a runtime error", e); } return previewHtml.toString(); diff --git a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java index aada573b3fdd..1dd299f08f8e 100644 --- a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java +++ b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java @@ -13,11 +13,6 @@ import static org.jabref.logic.util.strings.StringUtil.quoteForHTML; public class FileAnnotationPreview { - - private FileAnnotationPreview() { - // Utility Class - } - public static String render(Map> annotations) { if (annotations == null || annotations.isEmpty()) { return ""; @@ -50,29 +45,25 @@ public static String render(Map> annotations) { } private static void renderAnnotation(StringBuilder html, FileAnnotation annotation) { - String type = annotation.getAnnotationType() != null ? annotation.getAnnotationType().toString() : "Unknown"; + String type = annotation.getAnnotationType() != null ? annotation.getAnnotationType().toString() : Localization.lang("Unknown"); int page = annotation.getPage(); String content = annotation.getContent(); - String pageLabel = Localization.lang("Page"); + String headerLabel = Localization.lang("%0 (Page %1):", type, String.valueOf(page)); html.append("") - .append(quoteForHTML(type)) - .append(" (") - .append(pageLabel) - .append(" ") - .append(page) - .append("): ") + .append(quoteForHTML(headerLabel)) + .append("
") .append(quoteForHTML(content)); if (annotation.hasLinkedAnnotation() && annotation.getLinkedFileAnnotation() != null) { String noteContent = annotation.getLinkedFileAnnotation().getContent(); if (StringUtil.isNotBlank(noteContent)) { - String noteLabel = Localization.lang("Note"); + String noteLabel = Localization.lang(" — Note: %0", noteContent); - html.append(" — ").append(noteLabel).append(": ") - .append(quoteForHTML(noteContent)) + html.append("") + .append(quoteForHTML(noteLabel)) .append(""); } } diff --git a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java index 2245c9f5e019..0116e74a177e 100644 --- a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java +++ b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java @@ -7,10 +7,10 @@ import java.util.List; import java.util.Map; +import org.jabref.logic.l10n.Localization; import org.jabref.model.pdf.FileAnnotation; import org.jabref.model.pdf.FileAnnotationType; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -35,18 +35,15 @@ private String escape(String text) { } @Nested - @DisplayName("Edge Cases and Filtering") - class EdgeCasesTests { + class EdgeCasesAndFilteringTests { @Test - @DisplayName("Should return empty string when annotation map is empty") void renderReturnsEmptyStringWhenMapIsEmpty() { - assertEquals("", FileAnnotationPreview.render(Collections.emptyMap()), + assertEquals("", FileAnnotationPreview.render(Map.of()), "An empty map must produce an empty string output"); } @Test - @DisplayName("Should handle maps with empty content or null elements gracefully") void renderFiltersOutNullAnnotationsAndEmptyContent() { Path path = Path.of("test.pdf"); FileAnnotation nullAnnotation = null; @@ -61,26 +58,24 @@ void renderFiltersOutNullAnnotationsAndEmptyContent() { } @Nested - @DisplayName("HTML Content Formatting and Ordering") - class FormattingTests { + class HtmlContentFormattingAndOrderingTests { @Test - @DisplayName("Should format valid annotations and safely include type and page metadata") void renderFormatsValidAnnotationsCorrectlyWithHtmlEscaping() { Path path = Path.of("article.pdf"); FileAnnotation annotation = createMockAnnotation(FileAnnotationType.HIGHLIGHT, 3, "This & That", false); Map> annotations = Map.of(path, List.of(annotation)); + String expectedHeader = Localization.lang("%0 (Page %1):", "Highlight", "3"); String expectedHtml = "

PDF Annotations

" + escape("article.pdf") + "
" - + "" + escape("Highlight") + " (Page 3): " + escape("This & That") + "
"; + + "" + escape(expectedHeader) + " " + escape("This & That") + "
"; assertEquals(expectedHtml, FileAnnotationPreview.render(annotations)); } @Test - @DisplayName("Should sort annotations structurally by page number in ascending order") - void renderOrdersAnnotationsByNumber() { + void renderOrdersAnnotationsByPageNumberInAscendingOrder() { Path path = Path.of("book.pdf"); FileAnnotation page10 = createMockAnnotation(FileAnnotationType.TEXT, 10, "Late", false); FileAnnotation page2 = createMockAnnotation(FileAnnotationType.TEXT, 2, "Early", false); @@ -88,16 +83,18 @@ void renderOrdersAnnotationsByNumber() { Map> annotations = new LinkedHashMap<>(); annotations.put(path, List.of(page10, page2)); + String headerPage2 = Localization.lang("%0 (Page %1):", "Text", "2"); + String headerPage10 = Localization.lang("%0 (Page %1):", "Text", "10"); + String expectedHtml = "

PDF Annotations

" + escape("book.pdf") + "
" - + "" + escape("Text") + " (Page 2): " + escape("Early") + "
" - + "" + escape("Text") + " (Page 10): " + escape("Late") + "
"; + + "" + escape(headerPage2) + " " + escape("Early") + "
" + + "" + escape(headerPage10) + " " + escape("Late") + "
"; assertEquals(expectedHtml, FileAnnotationPreview.render(annotations)); } @Test - @DisplayName("Should append secondary linked note comments when present") - void renderAppendsLinkedAnnotationsWhenPresent() { + void renderAppendsSecondaryLinkedNoteCommentsWhenPresent() { Path path = Path.of("document.pdf"); FileAnnotation mainAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "Main", true); FileAnnotation linkedAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "Comment", false); @@ -106,8 +103,11 @@ void renderAppendsLinkedAnnotationsWhenPresent() { Map> annotations = Map.of(path, List.of(mainAnnotation)); + String expectedHeader = Localization.lang("%0 (Page %1):", "Text", "1"); + String expectedNote = Localization.lang(" — Note: %0", "Comment"); + String expectedHtml = "

PDF Annotations

" + escape("document.pdf") + "
" - + "" + escape("Text") + " (Page 1): " + escape("Main") + " — Note: " + escape("Comment") + "
"; + + "" + escape(expectedHeader) + " " + escape("Main") + "" + escape(expectedNote) + "
"; assertEquals(expectedHtml, FileAnnotationPreview.render(annotations)); } From 0126e6a95a7fee7fc289e4ec917a3b25a001b012 Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 21:53:20 -0300 Subject: [PATCH 08/13] Fix localization consistency by reusing existing keys and remove unused import in tests --- .../jabref/logic/pdf/FileAnnotationPreview.java | 8 +++++--- .../logic/pdf/FileAnnotationPreviewTest.java | 17 +++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java index 1dd299f08f8e..410e9efd75e5 100644 --- a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java +++ b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java @@ -49,7 +49,8 @@ private static void renderAnnotation(StringBuilder html, FileAnnotation annotati int page = annotation.getPage(); String content = annotation.getContent(); - String headerLabel = Localization.lang("%0 (Page %1):", type, String.valueOf(page)); + String pageLabel = Localization.lang("Page"); + String headerLabel = String.format("%s (%s %d):", type, pageLabel, page); html.append("") .append(quoteForHTML(headerLabel)) @@ -60,10 +61,11 @@ private static void renderAnnotation(StringBuilder html, FileAnnotation annotati String noteContent = annotation.getLinkedFileAnnotation().getContent(); if (StringUtil.isNotBlank(noteContent)) { - String noteLabel = Localization.lang(" — Note: %0", noteContent); + String noteLabel = Localization.lang("Note"); + String formattedNote = String.format(" — %s: %s", noteLabel, noteContent); html.append("") - .append(quoteForHTML(noteLabel)) + .append(quoteForHTML(formattedNote)) .append(""); } } diff --git a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java index 0116e74a177e..a5ecfb9fda73 100644 --- a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java +++ b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java @@ -2,7 +2,6 @@ import java.nio.file.Path; import java.util.Arrays; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -64,10 +63,10 @@ class HtmlContentFormattingAndOrderingTests { void renderFormatsValidAnnotationsCorrectlyWithHtmlEscaping() { Path path = Path.of("article.pdf"); FileAnnotation annotation = createMockAnnotation(FileAnnotationType.HIGHLIGHT, 3, "This & That", false); - Map> annotations = Map.of(path, List.of(annotation)); - String expectedHeader = Localization.lang("%0 (Page %1):", "Highlight", "3"); + // Monta o cabeçalho exatamente como o novo método faz + String expectedHeader = "Highlight (" + Localization.lang("Page") + " 3):"; String expectedHtml = "

PDF Annotations

" + escape("article.pdf") + "
" + "" + escape(expectedHeader) + " " + escape("This & That") + "
"; @@ -79,12 +78,12 @@ void renderOrdersAnnotationsByPageNumberInAscendingOrder() { Path path = Path.of("book.pdf"); FileAnnotation page10 = createMockAnnotation(FileAnnotationType.TEXT, 10, "Late", false); FileAnnotation page2 = createMockAnnotation(FileAnnotationType.TEXT, 2, "Early", false); - Map> annotations = new LinkedHashMap<>(); annotations.put(path, List.of(page10, page2)); - String headerPage2 = Localization.lang("%0 (Page %1):", "Text", "2"); - String headerPage10 = Localization.lang("%0 (Page %1):", "Text", "10"); + String pageStr = Localization.lang("Page"); + String headerPage2 = "Text (" + pageStr + " 2):"; + String headerPage10 = "Text (" + pageStr + " 10):"; String expectedHtml = "

PDF Annotations

" + escape("book.pdf") + "
" + "" + escape(headerPage2) + " " + escape("Early") + "
" @@ -98,13 +97,11 @@ void renderAppendsSecondaryLinkedNoteCommentsWhenPresent() { Path path = Path.of("document.pdf"); FileAnnotation mainAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "Main", true); FileAnnotation linkedAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "Comment", false); - when(mainAnnotation.getLinkedFileAnnotation()).thenReturn(linkedAnnotation); - Map> annotations = Map.of(path, List.of(mainAnnotation)); - String expectedHeader = Localization.lang("%0 (Page %1):", "Text", "1"); - String expectedNote = Localization.lang(" — Note: %0", "Comment"); + String expectedHeader = "Text (" + Localization.lang("Page") + " 1):"; + String expectedNote = " — " + Localization.lang("Note") + ": Comment"; String expectedHtml = "

PDF Annotations

" + escape("document.pdf") + "
" + "" + escape(expectedHeader) + " " + escape("Main") + "" + escape(expectedNote) + "
"; From 7364ef5d3745bbf9dfcabc04c90e7f982e0b6c63 Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 22:13:38 -0300 Subject: [PATCH 09/13] Fix string formatting using String.formatted() and register missing localization keys --- .../main/java/org/jabref/logic/pdf/FileAnnotationPreview.java | 2 +- jablib/src/main/resources/l10n/JabRef_en.properties | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java index 410e9efd75e5..b7d31d7045b9 100644 --- a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java +++ b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java @@ -62,7 +62,7 @@ private static void renderAnnotation(StringBuilder html, FileAnnotation annotati if (StringUtil.isNotBlank(noteContent)) { String noteLabel = Localization.lang("Note"); - String formattedNote = String.format(" — %s: %s", noteLabel, noteContent); + String formattedNote = " — %s: %s".formatted(noteLabel, noteContent); html.append("") .append(quoteForHTML(formattedNote)) diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index fdc76bc808bc..c429b0c4c705 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -3589,3 +3589,7 @@ Specify\ a\ subcommand\ (consistency,\ integrity)\ or\ an\ input\ file.=Specify Specify\ a\ subcommand\ (reset,\ import,\ export).=Specify a subcommand (reset, import, export). Specify\ a\ subcommand\ (write-xmp,\ update).=Specify a subcommand (write-xmp, update). The\ format\ option\ must\ contain\ either\ 'xmp'\ or\ 'bibtex-attachment'.=The format option must contain either 'xmp' or 'bibtex-attachment'. + +Note=Note +PDF\ Annotations=PDF Annotations +Unknown=Unknown From cca9e96ca59d9e5a4d528dc774725cc38e73caff Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 22:20:19 -0300 Subject: [PATCH 10/13] Fix string formatting using String.formatted() --- .../main/java/org/jabref/logic/pdf/FileAnnotationPreview.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java index b7d31d7045b9..7ad38bbc45e1 100644 --- a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java +++ b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java @@ -50,7 +50,7 @@ private static void renderAnnotation(StringBuilder html, FileAnnotation annotati String content = annotation.getContent(); String pageLabel = Localization.lang("Page"); - String headerLabel = String.format("%s (%s %d):", type, pageLabel, page); + String headerLabel = "%s (%s %d):".formatted(type, pageLabel, page); html.append("") .append(quoteForHTML(headerLabel)) From 7d2494e12d8d8565d45ed09f61434834f3dcda45 Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 22:29:55 -0300 Subject: [PATCH 11/13] remove checkstyle blank line violation in PreviewViewer --- .../src/main/java/org/jabref/gui/preview/PreviewViewer.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index f870fe2157a0..6bf872954c74 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -224,11 +224,7 @@ private void update() { BackgroundTask.wrap(() -> generatePreviewWithAnnotations(currentEntry, filePreferences)) .onSuccess(htmlComplete -> { - if (requestId == latestRequestId.get() - && currentEntry.equals(this.entry) - && currentDatabaseContext.equals(this.databaseContext) - && (currentLayout == null || currentLayout.equals(this.layout))) { - + if (requestId == latestRequestId.get() && currentEntry.equals(this.entry) && currentDatabaseContext.equals(this.databaseContext) && (currentLayout == null || currentLayout.equals(this.layout))) { setPreviewText(htmlComplete); } else { LOGGER.debug("Stale preview task discarded for entry: {}", currentEntry.getCitationKey().orElse("")); From dc49e451bbe4521f6ec90f228fa02fd32af3e3de Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 23:26:48 -0300 Subject: [PATCH 12/13] fix: address static analysis, i18n, and concurrency review findings --- .../org/jabref/gui/preview/PreviewViewer.java | 20 +++++++++++--- .../logic/pdf/FileAnnotationPreview.java | 26 +++++++++++++++---- .../main/resources/l10n/JabRef_en.properties | 6 ++++- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 6bf872954c74..d24ead460c51 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -211,6 +211,8 @@ private void update() { databaseContext == null ? "null" : databaseContext, entry == null ? "null" : entry, layout == null ? "null" : layout); + previewThrottler.cancel(); + latestRequestId.incrementAndGet(); setPreviewText(""); return; } @@ -224,10 +226,19 @@ private void update() { BackgroundTask.wrap(() -> generatePreviewWithAnnotations(currentEntry, filePreferences)) .onSuccess(htmlComplete -> { - if (requestId == latestRequestId.get() && currentEntry.equals(this.entry) && currentDatabaseContext.equals(this.databaseContext) && (currentLayout == null || currentLayout.equals(this.layout))) { + if (requestId == latestRequestId.get() + && Objects.equals(currentEntry, this.entry) + && Objects.equals(currentDatabaseContext, this.databaseContext) + && (currentLayout == null || Objects.equals(currentLayout, this.layout))) { + setPreviewText(htmlComplete); + + if (preferences.getPreviewPreferences().shouldDownloadCovers()) { + downloadCoverAndRefresh(currentEntry, htmlComplete); + } } else { - LOGGER.debug("Stale preview task discarded for entry: {}", currentEntry.getCitationKey().orElse("")); + LOGGER.debug("Stale preview task discarded for entry: {}", + currentEntry != null ? currentEntry.getCitationKey().orElse("") : ""); } }) .onFailure(exception -> LOGGER.error("Error generating preview text", exception)) @@ -438,8 +449,9 @@ private String generatePreviewWithAnnotations(BibEntry currentEntry, FilePrefere String annotationsHtml = FileAnnotationPreview.render(annotationsMap); previewHtml.append(annotationsHtml); } - } catch (RuntimeException e) { - LOGGER.error("Failed to generate preview with annotations due to a runtime error", e); + } catch (Exception e) { + LOGGER.error("Failed to read PDF annotations for preview", e); + throw new RuntimeException("Error loading PDF annotations", e); } return previewHtml.toString(); diff --git a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java index 7ad38bbc45e1..7fbb09cccea9 100644 --- a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java +++ b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.strings.StringUtil; import org.jabref.model.pdf.FileAnnotation; @@ -13,7 +14,7 @@ import static org.jabref.logic.util.strings.StringUtil.quoteForHTML; public class FileAnnotationPreview { - public static String render(Map> annotations) { + public static String render(@Nullable Map> annotations) { if (annotations == null || annotations.isEmpty()) { return ""; } @@ -45,12 +46,27 @@ public static String render(Map> annotations) { } private static void renderAnnotation(StringBuilder html, FileAnnotation annotation) { - String type = annotation.getAnnotationType() != null ? annotation.getAnnotationType().toString() : Localization.lang("Unknown"); + String typeStr; + + if (annotation.getAnnotationType() != null) { + String typeName = annotation.getAnnotationType().toString(); + + if ("HIGHLIGHT".equalsIgnoreCase(typeName)) { + typeStr = Localization.lang("highlight"); + } else if ("UNDERLINE".equalsIgnoreCase(typeName)) { + typeStr = Localization.lang("underline"); + } else if ("STRIKEOUT".equalsIgnoreCase(typeName)) { + typeStr = Localization.lang("strikeout"); + } else { + typeStr = typeName.toLowerCase(); + } + } else { + typeStr = Localization.lang("unknown"); + } + int page = annotation.getPage(); String content = annotation.getContent(); - - String pageLabel = Localization.lang("Page"); - String headerLabel = "%s (%s %d):".formatted(type, pageLabel, page); + String headerLabel = Localization.lang("%0 (page %1)", typeStr, String.valueOf(page)); html.append("") .append(quoteForHTML(headerLabel)) diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index c429b0c4c705..26f2b8c358fd 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -3592,4 +3592,8 @@ The\ format\ option\ must\ contain\ either\ 'xmp'\ or\ 'bibtex-attachment'.=The Note=Note PDF\ Annotations=PDF Annotations -Unknown=Unknown +unknown=unknown +highlight=highlight +strikeout=strikeout +underline=underline +%0\ (page\ %1)=%0 (page %1) From 16ae8bccbc7c20240523df7db20cf06a8cdea0a5 Mon Sep 17 00:00:00 2001 From: Gustavo T Takehara Date: Tue, 23 Jun 2026 23:45:10 -0300 Subject: [PATCH 13/13] fix: resolve file annotation preview test failures and static analysis warnings --- .../org/jabref/gui/preview/PreviewViewer.java | 19 ++++++++++++++++-- .../logic/pdf/FileAnnotationPreview.java | 6 ++++-- .../logic/pdf/FileAnnotationPreviewTest.java | 20 +++++++++++-------- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index d24ead460c51..5907749db053 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -1,6 +1,7 @@ package org.jabref.gui.preview; import java.io.IOException; +import java.lang.reflect.Field; import java.net.MalformedURLException; import java.util.List; import java.util.Objects; @@ -230,7 +231,6 @@ private void update() { && Objects.equals(currentEntry, this.entry) && Objects.equals(currentDatabaseContext, this.databaseContext) && (currentLayout == null || Objects.equals(currentLayout, this.layout))) { - setPreviewText(htmlComplete); if (preferences.getPreviewPreferences().shouldDownloadCovers()) { @@ -241,7 +241,14 @@ private void update() { currentEntry != null ? currentEntry.getCitationKey().orElse("") : ""); } }) - .onFailure(exception -> LOGGER.error("Error generating preview text", exception)) + .onFailure(exception -> { + if (requestId == latestRequestId.get() && currentEntry != null) { + String errorHtml = formatError(currentEntry, exception); + setPreviewText(errorHtml); + } else { + LOGGER.error("Error generating preview text", exception); + } + }) .executeWith(taskExecutor); }); } @@ -438,6 +445,14 @@ private String generatePreviewWithAnnotations(BibEntry currentEntry, FilePrefere return ""; } + try { + Field field = org.jabref.logic.layout.format.Number.class.getDeclaredField("serialExportNumber"); + field.setAccessible(true); + field.setInt(null, 1); + } catch (Exception e) { + LOGGER.warn("Could not reset Number.serialExportNumber", e); + } + StringBuilder previewHtml = new StringBuilder(layout.generatePreview(currentEntry, databaseContext)); try { diff --git a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java index 7fbb09cccea9..6866a445c5d1 100644 --- a/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java +++ b/jablib/src/main/java/org/jabref/logic/pdf/FileAnnotationPreview.java @@ -6,14 +6,16 @@ import java.util.Map; import java.util.Objects; -import org.jspecify.annotations.Nullable; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.strings.StringUtil; import org.jabref.model.pdf.FileAnnotation; +import org.jspecify.annotations.Nullable; + import static org.jabref.logic.util.strings.StringUtil.quoteForHTML; public class FileAnnotationPreview { + public static String render(@Nullable Map> annotations) { if (annotations == null || annotations.isEmpty()) { return ""; @@ -78,7 +80,7 @@ private static void renderAnnotation(StringBuilder html, FileAnnotation annotati if (StringUtil.isNotBlank(noteContent)) { String noteLabel = Localization.lang("Note"); - String formattedNote = " — %s: %s".formatted(noteLabel, noteContent); + String formattedNote = " — " + noteLabel + ": " + noteContent; html.append("") .append(quoteForHTML(formattedNote)) diff --git a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java index a5ecfb9fda73..c663ecea51b1 100644 --- a/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java +++ b/jablib/src/test/java/org/jabref/logic/pdf/FileAnnotationPreviewTest.java @@ -6,10 +6,12 @@ import java.util.List; import java.util.Map; +import org.jabref.logic.l10n.Language; import org.jabref.logic.l10n.Localization; import org.jabref.model.pdf.FileAnnotation; import org.jabref.model.pdf.FileAnnotationType; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -20,6 +22,11 @@ class FileAnnotationPreviewTest { + @BeforeAll + static void setUp() { + Localization.setLanguage(Language.ENGLISH); + } + private FileAnnotation createMockAnnotation(FileAnnotationType type, int page, String content, boolean hasLinked) { FileAnnotation annotation = mock(FileAnnotation.class); when(annotation.getAnnotationType()).thenReturn(type); @@ -45,10 +52,9 @@ void renderReturnsEmptyStringWhenMapIsEmpty() { @Test void renderFiltersOutNullAnnotationsAndEmptyContent() { Path path = Path.of("test.pdf"); - FileAnnotation nullAnnotation = null; FileAnnotation blankAnnotation = createMockAnnotation(FileAnnotationType.TEXT, 1, "", false); - Map> annotations = Map.of(path, Arrays.asList(nullAnnotation, blankAnnotation)); + Map> annotations = Map.of(path, Arrays.asList((FileAnnotation) null, blankAnnotation)); String expectedHtml = "

PDF Annotations

" + escape("test.pdf") + "
"; @@ -65,8 +71,7 @@ void renderFormatsValidAnnotationsCorrectlyWithHtmlEscaping() { FileAnnotation annotation = createMockAnnotation(FileAnnotationType.HIGHLIGHT, 3, "This & That", false); Map> annotations = Map.of(path, List.of(annotation)); - // Monta o cabeçalho exatamente como o novo método faz - String expectedHeader = "Highlight (" + Localization.lang("Page") + " 3):"; + String expectedHeader = "highlight (page 3)"; String expectedHtml = "

PDF Annotations

" + escape("article.pdf") + "
" + "" + escape(expectedHeader) + " " + escape("This & That") + "
"; @@ -81,9 +86,8 @@ void renderOrdersAnnotationsByPageNumberInAscendingOrder() { Map> annotations = new LinkedHashMap<>(); annotations.put(path, List.of(page10, page2)); - String pageStr = Localization.lang("Page"); - String headerPage2 = "Text (" + pageStr + " 2):"; - String headerPage10 = "Text (" + pageStr + " 10):"; + String headerPage2 = "text (page 2)"; + String headerPage10 = "text (page 10)"; String expectedHtml = "

PDF Annotations

" + escape("book.pdf") + "
" + "" + escape(headerPage2) + " " + escape("Early") + "
" @@ -100,7 +104,7 @@ void renderAppendsSecondaryLinkedNoteCommentsWhenPresent() { when(mainAnnotation.getLinkedFileAnnotation()).thenReturn(linkedAnnotation); Map> annotations = Map.of(path, List.of(mainAnnotation)); - String expectedHeader = "Text (" + Localization.lang("Page") + " 1):"; + String expectedHeader = "text (page 1)"; String expectedNote = " — " + Localization.lang("Note") + ": Comment"; String expectedHtml = "

PDF Annotations

" + escape("document.pdf") + "
"