feat(importing): add PersonTreeImporter loader
Third canonical loader. Reads canonical-persons-tree.json, upserts tree persons via PersonService keyed on the shared personId slug (#670 now emits it into the tree, so the tree reconciles with the register rather than duplicating it). Relationships are resolved from local rowIds to the upserted person UUIDs and created via RelationshipService (never the repository). A duplicate/circular relationship on re-import is swallowed for idempotency; unresolved rowIds are skipped with a warning. Refs #669 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package org.raddatz.familienarchiv.importing;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||
import org.raddatz.familienarchiv.person.Person;
|
||||
import org.raddatz.familienarchiv.person.PersonService;
|
||||
import org.raddatz.familienarchiv.person.PersonType;
|
||||
import org.raddatz.familienarchiv.person.PersonUpsertCommand;
|
||||
import org.raddatz.familienarchiv.person.relationship.RelationType;
|
||||
import org.raddatz.familienarchiv.person.relationship.RelationshipService;
|
||||
import org.raddatz.familienarchiv.person.relationship.dto.CreateRelationshipRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Loads {@code canonical-persons-tree.json} into the person + relationship domains.
|
||||
* Tree persons are upserted via {@link PersonService} keyed on the shared
|
||||
* {@code personId} slug (which Phase 1 #670 now emits into the tree), so they reconcile
|
||||
* with the register rather than duplicating it. Relationships reference persons by the
|
||||
* tree's local {@code rowId}; each side is mapped to the upserted person's UUID and
|
||||
* created through {@link RelationshipService} (never the relationship repository —
|
||||
* layering rule). A duplicate relationship on re-import is swallowed for idempotency.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PersonTreeImporter {
|
||||
|
||||
private final PersonService personService;
|
||||
private final RelationshipService relationshipService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public int load(File artifact) {
|
||||
JsonNode root = readTree(artifact);
|
||||
Map<String, UUID> idByRowId = upsertPersons(root.path("persons"));
|
||||
int relationships = createRelationships(root.path("relationships"), idByRowId);
|
||||
log.info("Imported {} tree persons and {} relationships from {}",
|
||||
idByRowId.size(), relationships, artifact.getName());
|
||||
return idByRowId.size();
|
||||
}
|
||||
|
||||
private JsonNode readTree(File artifact) {
|
||||
try {
|
||||
return objectMapper.readTree(artifact);
|
||||
} catch (Exception e) {
|
||||
throw DomainException.badRequest(ErrorCode.IMPORT_ARTIFACT_INVALID,
|
||||
"Unreadable canonical artifact: " + artifact.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, UUID> upsertPersons(JsonNode persons) {
|
||||
Map<String, UUID> idByRowId = new HashMap<>();
|
||||
for (JsonNode node : persons) {
|
||||
String personId = text(node, "personId");
|
||||
if (personId.isBlank()) continue;
|
||||
Person person = personService.upsertBySourceRef(toCommand(node, personId));
|
||||
idByRowId.put(text(node, "rowId"), person.getId());
|
||||
}
|
||||
return idByRowId;
|
||||
}
|
||||
|
||||
private PersonUpsertCommand toCommand(JsonNode node, String personId) {
|
||||
return PersonUpsertCommand.builder()
|
||||
.sourceRef(personId)
|
||||
.lastName(blankToNull(text(node, "lastName")))
|
||||
.firstName(blankToNull(text(node, "firstName")))
|
||||
.maidenName(blankToNull(text(node, "maidenName")))
|
||||
.notes(blankToNull(text(node, "notes")))
|
||||
.birthYear(intOrNull(node, "birthYear"))
|
||||
.deathYear(intOrNull(node, "deathYear"))
|
||||
.familyMember(node.path("familyMember").asBoolean(false))
|
||||
.personType(PersonType.PERSON)
|
||||
.provisional(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
private int createRelationships(JsonNode relationships, Map<String, UUID> idByRowId) {
|
||||
int created = 0;
|
||||
for (JsonNode node : relationships) {
|
||||
UUID person = idByRowId.get(text(node, "personId"));
|
||||
UUID related = idByRowId.get(text(node, "relatedPersonId"));
|
||||
if (person == null || related == null) {
|
||||
log.warn("Skipping tree relationship with unresolved rowId: {} -> {}",
|
||||
text(node, "personId"), text(node, "relatedPersonId"));
|
||||
continue;
|
||||
}
|
||||
if (addRelationshipIdempotently(person, related, text(node, "type"))) {
|
||||
created++;
|
||||
}
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
private boolean addRelationshipIdempotently(UUID person, UUID related, String type) {
|
||||
try {
|
||||
relationshipService.addRelationship(person,
|
||||
new CreateRelationshipRequest(related, RelationType.valueOf(type), null, null, null));
|
||||
return true;
|
||||
} catch (DomainException e) {
|
||||
if (e.getCode() == ErrorCode.DUPLICATE_RELATIONSHIP
|
||||
|| e.getCode() == ErrorCode.CIRCULAR_RELATIONSHIP) {
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(JsonNode node, String field) {
|
||||
JsonNode value = node.get(field);
|
||||
return value == null || value.isNull() ? "" : value.asText();
|
||||
}
|
||||
|
||||
private static Integer intOrNull(JsonNode node, String field) {
|
||||
JsonNode value = node.get(field);
|
||||
return value == null || value.isNull() ? null : value.asInt();
|
||||
}
|
||||
|
||||
private static String blankToNull(String s) {
|
||||
return (s == null || s.isBlank()) ? null : s;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user