From 310bb5b2d5f0c80f736f3f11b1736128a98f5970 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 5 May 2026 07:36:20 +0200 Subject: [PATCH] refactor(training-export): route export services through owning services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SegmentationTrainingExportService and TrainingDataExportService each injected TranscriptionBlockRepository, AnnotationRepository and DocumentRepository directly. They now go through: - TranscriptionBlockQueryService (extended) for the three eligible-block queries — used over TranscriptionService to keep SenderModelService → TrainingDataExportService → TranscriptionService cycle-free. - AnnotationService.findById (new) — read API on the annotation domain. - DocumentService.findById (already added in #417 commit 3). The TrainingDataExportServiceTest @DataJpaTest delegates the new service reads to the real JPA repositories via Mockito stubs in the new makeService helper, so the integration coverage stays unchanged. Refs #417 (C6.2 violations #6 and #7). Co-Authored-By: Claude Opus 4.7 --- .../service/AnnotationService.java | 5 +++ .../SegmentationTrainingExportService.java | 15 +++---- .../service/TrainingDataExportService.java | 17 ++++---- .../TranscriptionBlockQueryService.java | 13 ++++++ .../TrainingDataExportServiceTest.java | 42 ++++++++++++++----- 5 files changed, 63 insertions(+), 29 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java index 1c6da350..1484c796 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java @@ -17,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; @Slf4j @@ -32,6 +33,10 @@ public class AnnotationService { return annotationRepository.findByDocumentId(documentId); } + public Optional findById(UUID id) { + return annotationRepository.findById(id); + } + @Transactional public DocumentAnnotation createAnnotation(UUID documentId, CreateAnnotationDTO dto, UUID userId, String fileHash) { DocumentAnnotation annotation = DocumentAnnotation.builder() diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/SegmentationTrainingExportService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/SegmentationTrainingExportService.java index 3b2a1428..c41e2abf 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/SegmentationTrainingExportService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/SegmentationTrainingExportService.java @@ -8,9 +8,6 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentAnnotation; import org.raddatz.familienarchiv.model.TranscriptionBlock; -import org.raddatz.familienarchiv.repository.AnnotationRepository; -import org.raddatz.familienarchiv.repository.DocumentRepository; -import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @@ -27,13 +24,13 @@ import java.util.zip.ZipOutputStream; @Slf4j public class SegmentationTrainingExportService { - private final TranscriptionBlockRepository blockRepository; - private final AnnotationRepository annotationRepository; - private final DocumentRepository documentRepository; + private final TranscriptionBlockQueryService transcriptionBlockQueryService; + private final AnnotationService annotationService; + private final DocumentService documentService; private final FileService fileService; public List querySegmentationBlocks() { - return blockRepository.findSegmentationBlocks(); + return transcriptionBlockQueryService.findSegmentationBlocks(); } public StreamingResponseBody exportToZip() { @@ -51,14 +48,14 @@ public class SegmentationTrainingExportService { // Pre-fetch annotations keyed by id Map annotations = new HashMap<>(); for (TranscriptionBlock b : blocks) { - annotationRepository.findById(b.getAnnotationId()) + annotationService.findById(b.getAnnotationId()) .ifPresent(a -> annotations.put(a.getId(), a)); } // Pre-fetch documents keyed by id Map documents = new HashMap<>(); for (UUID docId : byDoc.keySet()) { - documentRepository.findById(docId).ifPresent(d -> documents.put(d.getId(), d)); + documentService.findById(docId).ifPresent(d -> documents.put(d.getId(), d)); } return out -> { diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/TrainingDataExportService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/TrainingDataExportService.java index 86c81053..89f69223 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/TrainingDataExportService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/TrainingDataExportService.java @@ -8,9 +8,6 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.DocumentAnnotation; import org.raddatz.familienarchiv.model.TranscriptionBlock; -import org.raddatz.familienarchiv.repository.AnnotationRepository; -import org.raddatz.familienarchiv.repository.DocumentRepository; -import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @@ -28,13 +25,13 @@ import java.util.zip.ZipOutputStream; @Slf4j public class TrainingDataExportService { - private final TranscriptionBlockRepository blockRepository; - private final AnnotationRepository annotationRepository; - private final DocumentRepository documentRepository; + private final TranscriptionBlockQueryService transcriptionBlockQueryService; + private final AnnotationService annotationService; + private final DocumentService documentService; private final FileService fileService; public List queryEligibleBlocks() { - return blockRepository.findEligibleKurrentBlocks(); + return transcriptionBlockQueryService.findEligibleKurrentBlocks(); } public StreamingResponseBody exportToZip() { @@ -42,7 +39,7 @@ public class TrainingDataExportService { } public List queryBlocksForSender(UUID personId) { - return blockRepository.findManualKurrentBlocksByPerson(personId); + return transcriptionBlockQueryService.findManualKurrentBlocksByPerson(personId); } public StreamingResponseBody exportForSender(UUID personId) { @@ -63,14 +60,14 @@ public class TrainingDataExportService { // Pre-fetch annotations keyed by id Map annotations = new HashMap<>(); for (TranscriptionBlock b : blocks) { - annotationRepository.findById(b.getAnnotationId()) + annotationService.findById(b.getAnnotationId()) .ifPresent(a -> annotations.put(a.getId(), a)); } // Pre-fetch documents keyed by id Map documents = new HashMap<>(); for (UUID docId : byDoc.keySet()) { - documentRepository.findById(docId).ifPresent(d -> documents.put(d.getId(), d)); + documentService.findById(docId).ifPresent(d -> documents.put(d.getId(), d)); } return out -> { diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionBlockQueryService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionBlockQueryService.java index 945b45d7..591a8f83 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionBlockQueryService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionBlockQueryService.java @@ -1,6 +1,7 @@ package org.raddatz.familienarchiv.service; import lombok.RequiredArgsConstructor; +import org.raddatz.familienarchiv.model.TranscriptionBlock; import org.raddatz.familienarchiv.repository.CompletionStatsRow; import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; import org.springframework.stereotype.Service; @@ -24,4 +25,16 @@ public class TranscriptionBlockQueryService { } return result; } + + public List findSegmentationBlocks() { + return blockRepository.findSegmentationBlocks(); + } + + public List findEligibleKurrentBlocks() { + return blockRepository.findEligibleKurrentBlocks(); + } + + public List findManualKurrentBlocksByPerson(UUID personId) { + return blockRepository.findManualKurrentBlocksByPerson(personId); + } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/TrainingDataExportServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/TrainingDataExportServiceTest.java index cce70601..ffe60ba8 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/TrainingDataExportServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/TrainingDataExportServiceTest.java @@ -27,6 +27,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @@ -60,7 +61,7 @@ class TrainingDataExportServiceTest { blockRepository.save(manualBlock(docId, annotId, "Liebe Mutter")); FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); StreamingResponseBody body = service.exportToZip(); byte[] zipBytes = stream(body); @@ -79,7 +80,7 @@ class TrainingDataExportServiceTest { blockRepository.save(block); FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); StreamingResponseBody body = service.exportToZip(); assertThat(zipEntryNames(stream(body))).isEmpty(); @@ -92,7 +93,7 @@ class TrainingDataExportServiceTest { blockRepository.save(manualBlock(docId, annotId, "Liebe Tante")); FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); StreamingResponseBody body = service.exportToZip(); byte[] zipBytes = stream(body); @@ -110,7 +111,7 @@ class TrainingDataExportServiceTest { blockRepository.save(block); FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); StreamingResponseBody body = service.exportToZip(); assertThat(zipEntryNames(stream(body))).isNotEmpty(); @@ -127,7 +128,7 @@ class TrainingDataExportServiceTest { blockRepository.save(block); FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); StreamingResponseBody body = service.exportToZip(); assertThat(zipEntryNames(stream(body))).isEmpty(); @@ -143,7 +144,7 @@ class TrainingDataExportServiceTest { blockRepository.save(manualBlock(docId, annotId, "Zweite Zeile")); FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); byte[] zipBytes = stream(service.exportToZip()); var names = zipEntryNames(zipBytes); @@ -160,7 +161,7 @@ class TrainingDataExportServiceTest { blockRepository.save(manualBlock(docId, annotId, expectedText)); FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); byte[] zipBytes = stream(service.exportToZip()); String xmlContent = readZipEntry(zipBytes, ".xml"); @@ -174,7 +175,7 @@ class TrainingDataExportServiceTest { blockRepository.save(manualBlock(docId, annotId, "A & B < C > D")); FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); byte[] zipBytes = stream(service.exportToZip()); String xmlContent = readZipEntry(zipBytes, ".xml"); @@ -196,7 +197,7 @@ class TrainingDataExportServiceTest { when(fileService.downloadFileBytes("fail.pdf")).thenThrow(new FileService.StorageFileNotFoundException("missing")); when(fileService.downloadFileBytes("ok.pdf")).thenReturn(minimalPdfBytes); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); byte[] zipBytes = stream(service.exportToZip()); var names = zipEntryNames(zipBytes); @@ -209,13 +210,34 @@ class TrainingDataExportServiceTest { @Test void queryEligibleBlocks_returnsEmpty_whenNoEnrolledDocuments() { FileService fileService = mockFileService(); - TrainingDataExportService service = new TrainingDataExportService(blockRepository, annotationRepository, documentRepository, fileService); + TrainingDataExportService service = makeService(fileService); assertThat(service.queryEligibleBlocks()).isEmpty(); } // ─── helpers ───────────────────────────────────────────────────────────── + /** + * Builds the export service with mocked owning services that transparently + * delegate every read to the real JPA repositories provided by {@code @DataJpaTest}. + * Keeps the integration test green after #417's layering refactor without + * pulling the full transcription/annotation/document service trees into scope. + */ + private TrainingDataExportService makeService(FileService fileService) { + TranscriptionBlockQueryService blockQueryService = mock(TranscriptionBlockQueryService.class); + AnnotationService annotationService = mock(AnnotationService.class); + DocumentService documentService = mock(DocumentService.class); + when(blockQueryService.findEligibleKurrentBlocks()) + .thenAnswer(inv -> blockRepository.findEligibleKurrentBlocks()); + when(blockQueryService.findManualKurrentBlocksByPerson(any(UUID.class))) + .thenAnswer(inv -> blockRepository.findManualKurrentBlocksByPerson(inv.getArgument(0))); + when(annotationService.findById(any(UUID.class))) + .thenAnswer(inv -> annotationRepository.findById(inv.getArgument(0))); + when(documentService.findById(any(UUID.class))) + .thenAnswer(inv -> documentRepository.findById(inv.getArgument(0))); + return new TrainingDataExportService(blockQueryService, annotationService, documentService, fileService); + } + private UUID enrolledDoc(String filename) { Document doc = documentRepository.save(Document.builder() .title(filename).originalFilename(filename).filePath(filename)