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