feat(importing): add TagTreeImporter loader

First of four canonical loaders. Reads canonical-tag-tree.xlsx by header
name, upserts each tag via TagService.upsertBySourceRef (never the
repository — layering rule), and resolves parent links by stripping the
last /segment of the canonical tag_path. Idempotent by source_ref.

Refs #669

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-27 10:26:05 +02:00
parent 3501382ff5
commit bcd928f12d
2 changed files with 157 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
package org.raddatz.familienarchiv.importing;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.tag.TagService;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TagTreeImporterTest {
@Test
void load_upsertsRootTagWithNullParent(@TempDir Path tempDir) throws Exception {
TagService tagService = mock(TagService.class);
when(tagService.upsertBySourceRef(any(), any(), any()))
.thenAnswer(inv -> tagOf(inv.getArgument(0), inv.getArgument(1), inv.getArgument(2)));
Path xlsx = writeTagTree(tempDir, List.<String[]>of(
new String[]{"Themen", "", "Themen"}));
new TagTreeImporter(tagService).load(xlsx.toFile());
verify(tagService).upsertBySourceRef("Themen", "Themen", null);
}
@Test
void load_resolvesParentByPath_forChildTag(@TempDir Path tempDir) throws Exception {
TagService tagService = mock(TagService.class);
UUID rootId = UUID.randomUUID();
when(tagService.upsertBySourceRef(eq("Themen"), eq("Themen"), isNull()))
.thenReturn(tagOf("Themen", "Themen", null, rootId));
when(tagService.upsertBySourceRef(eq("Themen/Brautbriefe"), eq("Brautbriefe"), eq(rootId)))
.thenReturn(tagOf("Themen/Brautbriefe", "Brautbriefe", rootId));
Path xlsx = writeTagTree(tempDir, List.<String[]>of(
new String[]{"Themen", "", "Themen"},
new String[]{"Themen/Brautbriefe", "Themen", "Brautbriefe"}));
new TagTreeImporter(tagService).load(xlsx.toFile());
verify(tagService).upsertBySourceRef("Themen/Brautbriefe", "Brautbriefe", rootId);
}
@Test
void load_returnsCountOfProcessedRows(@TempDir Path tempDir) throws Exception {
TagService tagService = mock(TagService.class);
when(tagService.upsertBySourceRef(any(), any(), any()))
.thenAnswer(inv -> tagOf(inv.getArgument(0), inv.getArgument(1), inv.getArgument(2)));
Path xlsx = writeTagTree(tempDir, List.<String[]>of(
new String[]{"Themen", "", "Themen"},
new String[]{"Themen/Brautbriefe", "Themen", "Brautbriefe"}));
int processed = new TagTreeImporter(tagService).load(xlsx.toFile());
assertThat(processed).isEqualTo(2);
}
private static Tag tagOf(String sourceRef, String name, UUID parentId) {
return tagOf(sourceRef, name, parentId, UUID.randomUUID());
}
private static Tag tagOf(String sourceRef, String name, UUID parentId, UUID id) {
return Tag.builder().id(id).sourceRef(sourceRef).name(name).parentId(parentId).build();
}
private Path writeTagTree(Path dir, List<String[]> rows) throws Exception {
Path xlsx = dir.resolve("canonical-tag-tree.xlsx");
try (XSSFWorkbook wb = new XSSFWorkbook()) {
Sheet sheet = wb.createSheet("Sheet1");
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("tag_path");
header.createCell(1).setCellValue("parent_name");
header.createCell(2).setCellValue("tag_name");
for (int r = 0; r < rows.size(); r++) {
Row row = sheet.createRow(r + 1);
String[] values = rows.get(r);
for (int c = 0; c < values.length; c++) {
row.createCell(c).setCellValue(values[c]);
}
}
try (OutputStream out = Files.newOutputStream(xlsx)) {
wb.write(out);
}
}
return xlsx;
}
}