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:
@@ -0,0 +1,54 @@
|
||||
package org.raddatz.familienarchiv.importing;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.tag.Tag;
|
||||
import org.raddatz.familienarchiv.tag.TagService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Loads {@code canonical-tag-tree.xlsx} into the tag domain via {@link TagService},
|
||||
* upserting each tag by its canonical {@code tag_path} (the source_ref). Parent links are
|
||||
* resolved by the parent's path, which is the child path with its last {@code /segment}
|
||||
* stripped. Rows are emitted parents-first by the normalizer, so a parent is always
|
||||
* resolved before any child references it.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TagTreeImporter {
|
||||
|
||||
static final List<String> REQUIRED_HEADERS = List.of("tag_path", "parent_name", "tag_name");
|
||||
private static final String PATH_SEPARATOR = "/";
|
||||
|
||||
private final TagService tagService;
|
||||
|
||||
public int load(File artifact) {
|
||||
List<CanonicalSheetReader.Row> rows = CanonicalSheetReader.readRows(artifact, REQUIRED_HEADERS);
|
||||
Map<String, UUID> idByPath = new HashMap<>();
|
||||
int processed = 0;
|
||||
for (CanonicalSheetReader.Row row : rows) {
|
||||
String path = row.get("tag_path");
|
||||
if (path.isBlank()) continue;
|
||||
UUID parentId = resolveParentId(path, idByPath);
|
||||
Tag tag = tagService.upsertBySourceRef(path, row.get("tag_name"), parentId);
|
||||
idByPath.put(path, tag.getId());
|
||||
processed++;
|
||||
}
|
||||
log.info("Imported {} tags from {}", processed, artifact.getName());
|
||||
return processed;
|
||||
}
|
||||
|
||||
private UUID resolveParentId(String path, Map<String, UUID> idByPath) {
|
||||
int lastSeparator = path.lastIndexOf(PATH_SEPARATOR);
|
||||
if (lastSeparator < 0) return null;
|
||||
String parentPath = path.substring(0, lastSeparator);
|
||||
return idByPath.get(parentPath);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user