From 7c25d08506ff68da3e391191daf1256a55e5b331 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 7 May 2026 17:49:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(comment):=20add=20findDataByIds()=20?= =?UTF-8?q?=E2=80=94=20batch-fetch=20annotationId=20+=20plain-text=20previ?= =?UTF-8?q?ew=20in=20one=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the single-purpose findAnnotationIdsByIds() (kept as delegation shim). Introduces CommentData record (annotationId + preview) and stripAndTruncate() using Jsoup.parse().text() for DOM-safe HTML stripping. Truncates to 120 chars. Co-Authored-By: Claude Sonnet 4.6 --- backend/pom.xml | 7 ++ .../document/comment/CommentService.java | 24 ++++- .../document/comment/CommentServiceTest.java | 96 +++++++++++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index e4e2eb2b..daa14df3 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -190,6 +190,13 @@ owasp-java-html-sanitizer 20240325.1 + + + + org.jsoup + jsoup + 1.18.1 + diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/comment/CommentService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/comment/CommentService.java index 33af36ce..2fccb84a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/comment/CommentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/comment/CommentService.java @@ -13,6 +13,7 @@ import org.raddatz.familienarchiv.document.comment.DocumentComment; import org.raddatz.familienarchiv.document.transcription.TranscriptionBlock; import org.raddatz.familienarchiv.document.comment.CommentRepository; import org.raddatz.familienarchiv.notification.NotificationService; +import org.jsoup.Jsoup; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,26 +24,43 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class CommentService { + private static final int PREVIEW_MAX_CHARS = 120; + + public record CommentData(UUID annotationId, String preview) {} + private final CommentRepository commentRepository; private final UserService userService; private final NotificationService notificationService; private final AuditService auditService; private final TranscriptionService transcriptionService; - public Map findAnnotationIdsByIds(Collection commentIds) { + public Map findDataByIds(Collection commentIds) { if (commentIds == null || commentIds.isEmpty()) return Map.of(); - Map result = new HashMap<>(); + Map result = new HashMap<>(); for (DocumentComment c : commentRepository.findAllById(commentIds)) { - if (c.getAnnotationId() != null) result.put(c.getId(), c.getAnnotationId()); + result.put(c.getId(), new CommentData(c.getAnnotationId(), stripAndTruncate(c.getContent()))); } return result; } + public Map findAnnotationIdsByIds(Collection commentIds) { + return findDataByIds(commentIds).entrySet().stream() + .filter(e -> e.getValue().annotationId() != null) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().annotationId())); + } + + private String stripAndTruncate(String html) { + if (html == null || html.isBlank()) return ""; + String text = Jsoup.parse(html).text().trim(); + return text.length() > PREVIEW_MAX_CHARS ? text.substring(0, PREVIEW_MAX_CHARS) : text; + } + public List getCommentsForBlock(UUID blockId) { List roots = commentRepository.findByBlockIdAndParentIdIsNull(blockId); return withRepliesAndMentions(roots); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/comment/CommentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/comment/CommentServiceTest.java index f832cb46..897b1e34 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/comment/CommentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/comment/CommentServiceTest.java @@ -19,6 +19,7 @@ import org.raddatz.familienarchiv.notification.NotificationService; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -644,6 +645,101 @@ class CommentServiceTest { verify(auditService, never()).logAfterCommit(eq(AuditKind.MENTION_CREATED), any(), any(), any()); } + // ─── findDataByIds ──────────────────────────────────────────────────────── + + @Test + void findDataByIds_returns_empty_map_when_input_is_empty() { + assertThat(commentService.findDataByIds(List.of())).isEmpty(); + verify(commentRepository, never()).findAllById(anyList()); + } + + @Test + void findDataByIds_strips_html_and_extracts_plain_text() { + UUID id = UUID.randomUUID(); + when(commentRepository.findAllById(List.of(id))) + .thenReturn(List.of(DocumentComment.builder().id(id) + .content("

Hello world

").build())); + + Map result = commentService.findDataByIds(List.of(id)); + + assertThat(result.get(id).preview()).isEqualTo("Hello world"); + } + + @Test + void findDataByIds_truncates_at_exactly_120_chars() { + UUID id = UUID.randomUUID(); + String text121 = "a".repeat(121); + when(commentRepository.findAllById(List.of(id))) + .thenReturn(List.of(DocumentComment.builder().id(id).content(text121).build())); + + assertThat(commentService.findDataByIds(List.of(id)).get(id).preview()).hasSize(120); + } + + @Test + void findDataByIds_does_not_truncate_at_exactly_120_chars() { + UUID id = UUID.randomUUID(); + String text120 = "a".repeat(120); + when(commentRepository.findAllById(List.of(id))) + .thenReturn(List.of(DocumentComment.builder().id(id).content(text120).build())); + + assertThat(commentService.findDataByIds(List.of(id)).get(id).preview()).hasSize(120); + } + + @Test + void findDataByIds_returns_empty_string_for_blank_content() { + UUID id = UUID.randomUUID(); + when(commentRepository.findAllById(List.of(id))) + .thenReturn(List.of(DocumentComment.builder().id(id).content(" ").build())); + + assertThat(commentService.findDataByIds(List.of(id)).get(id).preview()).isEmpty(); + } + + @Test + void findDataByIds_returns_empty_string_for_null_content() { + UUID id = UUID.randomUUID(); + when(commentRepository.findAllById(List.of(id))) + .thenReturn(List.of(DocumentComment.builder().id(id).content(null).build())); + + assertThat(commentService.findDataByIds(List.of(id)).get(id).preview()).isEmpty(); + } + + @Test + void findDataByIds_omits_deleted_comments_from_result_map() { + UUID present = UUID.randomUUID(); + UUID deleted = UUID.randomUUID(); + when(commentRepository.findAllById(List.of(present, deleted))) + .thenReturn(List.of(DocumentComment.builder().id(present).content("Hi").build())); + + Map result = commentService.findDataByIds(List.of(present, deleted)); + + assertThat(result).containsKey(present); + assertThat(result).doesNotContainKey(deleted); + } + + @Test + void findDataByIds_preserves_annotationId_alongside_preview() { + UUID id = UUID.randomUUID(); + UUID annotationId = UUID.randomUUID(); + when(commentRepository.findAllById(List.of(id))) + .thenReturn(List.of(DocumentComment.builder().id(id) + .annotationId(annotationId).content("Text").build())); + + CommentService.CommentData data = commentService.findDataByIds(List.of(id)).get(id); + + assertThat(data.annotationId()).isEqualTo(annotationId); + assertThat(data.preview()).isEqualTo("Text"); + } + + @Test + void findDataByIds_sets_null_annotationId_when_comment_has_no_annotation() { + UUID id = UUID.randomUUID(); + when(commentRepository.findAllById(List.of(id))) + .thenReturn(List.of(DocumentComment.builder().id(id) + .annotationId(null).content("Text").build())); + + assertThat(commentService.findDataByIds(List.of(id)).get(id).annotationId()).isNull(); + } + // ─── findAnnotationIdsByIds ─────────────────────────────────────────────── @Test