feat(document): regenerate auto-title on save when date/location change (#726)
updateDocument now captures the machine title from the persisted state before any setter runs, and rebuilds it from the new state only when the submitted title still equals that machine value — an exact comparison that relies on the edit form round-tripping an untouched title verbatim. A hand-written or freshly-typed title is kept; a blank submission falls back to the rebuilt auto-title (title is always present); a file-replaced document no longer matches its import-time title and is treated as manual. projectedState mirrors the setter asymmetry exactly (date/location overwrite incl. null-clear; precision/end/raw skip-null from the entity). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,7 @@ import static org.raddatz.familienarchiv.document.DocumentSpecifications.*;
|
||||
public class DocumentService {
|
||||
|
||||
private final DocumentRepository documentRepository;
|
||||
private final DocumentTitleFactory documentTitleFactory;
|
||||
private final PersonService personService;
|
||||
private final FileService fileService;
|
||||
private final TagService tagService;
|
||||
@@ -379,8 +380,14 @@ public class DocumentService {
|
||||
|
||||
DocumentStatus statusBefore = doc.getStatus();
|
||||
|
||||
// Auto-title sync (#726): capture the machine title from the CURRENTLY-persisted state
|
||||
// BEFORE any setter runs — the setters below overwrite date/location and applyDatePrecision
|
||||
// skips nulls, so the old state must be read first. The submitted title is the catalog
|
||||
// auto-title iff it equals this; only then does it follow date/location forward.
|
||||
String autoTitleBefore = documentTitleFactory.build(doc);
|
||||
|
||||
// 1. Einfache Felder Update
|
||||
doc.setTitle(dto.getTitle());
|
||||
doc.setTitle(resolveTitle(dto.getTitle(), autoTitleBefore, doc, dto));
|
||||
doc.setDocumentDate(dto.getDocumentDate());
|
||||
applyDatePrecision(doc, dto);
|
||||
validateDateRange(doc); // guard before any save (updateDocumentTags below persists)
|
||||
@@ -452,6 +459,47 @@ public class DocumentService {
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides the title to persist on an edit (#726). The submitted title is the catalog
|
||||
* auto-title only when it equals {@code autoBefore} (built from the stored state) — an exact
|
||||
* comparison with no heuristic, relying on the edit form round-tripping the stored title
|
||||
* verbatim when untouched. A machine title is rebuilt from the new state so a corrected
|
||||
* date/location flows into it; a hand-written or freshly-typed title is kept verbatim. A blank
|
||||
* submission is never persisted (title is always present) — it falls back to the rebuilt
|
||||
* auto-title, which always carries at least the index.
|
||||
*/
|
||||
private String resolveTitle(String submitted, String autoBefore, Document doc, DocumentUpdateDTO dto) {
|
||||
if (submitted == null || submitted.isBlank()) {
|
||||
return documentTitleFactory.build(projectedState(doc, dto));
|
||||
}
|
||||
if (!Objects.equals(submitted, autoBefore)) {
|
||||
return submitted;
|
||||
}
|
||||
return documentTitleFactory.build(projectedState(doc, dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* The document state the regenerated title is built from, mirroring {@code updateDocument}'s
|
||||
* setter asymmetry exactly: {@code documentDate}/{@code location} are overwritten from the DTO
|
||||
* (a null value clears the field), while precision/end/raw are taken from the DTO only when
|
||||
* non-null and otherwise kept from the stored entity (see {@link #applyDatePrecision}). The
|
||||
* index ({@code originalFilename}) is never touched by a metadata edit. A mismatch here would
|
||||
* silently produce a wrong label, so it is kept lock-step with the real setters.
|
||||
*/
|
||||
private Document projectedState(Document doc, DocumentUpdateDTO dto) {
|
||||
return Document.builder()
|
||||
.originalFilename(doc.getOriginalFilename())
|
||||
.documentDate(dto.getDocumentDate())
|
||||
.location(dto.getLocation())
|
||||
.metaDatePrecision(dto.getMetaDatePrecision() != null
|
||||
? dto.getMetaDatePrecision() : doc.getMetaDatePrecision())
|
||||
.metaDateEnd(dto.getMetaDateEnd() != null
|
||||
? dto.getMetaDateEnd() : doc.getMetaDateEnd())
|
||||
.metaDateRaw(dto.getMetaDateRaw() != null
|
||||
? dto.getMetaDateRaw() : doc.getMetaDateRaw())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the three date-precision fields only when the DTO carries them.
|
||||
* A null field means "not submitted" — overwriting the stored value with null
|
||||
|
||||
@@ -23,7 +23,9 @@ public class DocumentTitleFactory {
|
||||
* location segment is dropped when blank.
|
||||
*/
|
||||
public String build(Document doc) {
|
||||
StringBuilder title = new StringBuilder(doc.getOriginalFilename());
|
||||
// originalFilename is NOT NULL in production; guard only so a synthetic/partial entity
|
||||
// never trips StringBuilder(null) with an opaque NPE.
|
||||
StringBuilder title = new StringBuilder(doc.getOriginalFilename() == null ? "" : doc.getOriginalFilename());
|
||||
if (doc.getDocumentDate() != null && doc.getMetaDatePrecision() != DatePrecision.UNKNOWN) {
|
||||
title.append(SEPARATOR).append(DocumentTitleFormatter.formatTitleDate(
|
||||
doc.getDocumentDate(), doc.getMetaDatePrecision(),
|
||||
|
||||
Reference in New Issue
Block a user