diff --git a/CLAUDE.md b/CLAUDE.md index 5e98ef1..86dac9b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -123,7 +123,7 @@ Ein Arbeitspaket ist erst fertig, wenn die betroffenen öffentlichen Klassen und - Bei Namenskollisionen: `YYYY-MM-DD - Titel(1).pdf`, `YYYY-MM-DD - Titel(2).pdf`, ... - Die **konfigurierte maximale Titellänge** gilt nur für den **Basistitel**; das Dubletten-Suffix zählt nicht mit - Das Dubletten-Suffix wird unmittelbar vor `.pdf` angehängt -- Titel sind **deutsch**, verständlich, eindeutig und enthalten keine Sonderzeichen außer Leerzeichen und Bindestrichen +- Titel sind **deutsch**, verständlich, eindeutig und enthalten keine Sonderzeichen außer Leerzeichen, Bindestrichen, Punkten, Kommas und Ampersands - Eigennamen bleiben unverändert - Datumsermittlung mit Priorität aus den fachlichen Anforderungen; wenn kein belastbares Datum eindeutig ableitbar ist, ist das **aktuelle Datum** als Fallback erlaubt - Mehrdeutige Dokumente liefern **kein** unsicheres Ergebnis, sondern einen Fehler diff --git a/docs/specs/fachliche-anforderungen.md b/docs/specs/fachliche-anforderungen.md index eae44aa..a3472f1 100644 --- a/docs/specs/fachliche-anforderungen.md +++ b/docs/specs/fachliche-anforderungen.md @@ -68,7 +68,7 @@ Fallback auf aktuelles Datum ist erlaubt, wenn kein belastbares Datum eindeutig - maximal **konfigurierbare Anzahl Zeichen (Basistitel, Default 60, gültiger Bereich 10..120)** - verständlich und eindeutig -- keine Sonderzeichen außer Leerzeichen und Bindestrichen +- keine Sonderzeichen außer Leerzeichen, Bindestrichen, Punkten, Kommas und Ampersands --- diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/TitleCharacterRule.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/TitleCharacterRule.java index 07d2928..9fe0158 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/TitleCharacterRule.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/TitleCharacterRule.java @@ -1,15 +1,15 @@ package de.gecheckt.pdf.umbenenner.application.service; /** - * Canonical rule for allowed title characters. + * Kanonische Regel für erlaubte Titelzeichen. *

- * A title may contain only Unicode letters (including German Umlauts and ß), - * decimal digits, ASCII spaces, and hyphens ({@code -}). - * All other characters are considered disallowed. + * Ein Titel darf nur Unicode-Buchstaben (einschließlich deutscher Umlaute und ß), + * Ziffern, ASCII-Leerzeichen, Bindestriche ({@code -}), Punkte ({@code .}), + * Kommas ({@code ,}) und Ampersands ({@code &}) enthalten. + * Alle anderen Zeichen sind nicht erlaubt. *

- * This class is the single authoritative implementation of the character-allowlist - * rule. All services that need to validate or verify title characters must delegate - * to {@link #isAllowed(String)} instead of maintaining their own copies. + * Diese Klasse ist die einzige autoritative Implementierung der Zeichenerlaubnis-Regel. + * Alle Services, die Titelzeichen validieren müssen, delegieren an {@link #isAllowed(String)}. */ public final class TitleCharacterRule { @@ -18,20 +18,23 @@ public final class TitleCharacterRule { } /** - * Returns {@code true} if every character in {@code title} is a letter, digit, - * ASCII space, or hyphen ({@code -}). + * Gibt {@code true} zurück, wenn jedes Zeichen im {@code title} ein Buchstabe, eine Ziffer, + * ein ASCII-Leerzeichen, ein Bindestrich ({@code -}), ein Punkt ({@code .}), + * ein Komma ({@code ,}) oder ein Ampersand ({@code &}) ist. *

- * Unicode letters — including German Umlauts (ä, ö, ü, Ä, Ö, Ü) and ß — are - * permitted. An empty string is considered allowed. + * Unicode-Buchstaben — einschließlich deutscher Umlaute (ä, ö, ü, Ä, Ö, Ü) und ß — sind + * erlaubt. Eine leere Zeichenkette ist erlaubt. * - * @param title the title to check; must not be null - * @return {@code true} if all characters pass the allowlist; {@code false} otherwise - * @throws NullPointerException if {@code title} is null + * @param title der zu prüfende Titel; darf nicht null sein + * @return {@code true} wenn alle Zeichen die Erlaubnisliste erfüllen; {@code false} andernfalls + * @throws NullPointerException wenn {@code title} null ist */ public static boolean isAllowed(String title) { for (int i = 0; i < title.length(); i++) { char c = title.charAt(i); - if (!Character.isLetter(c) && !Character.isDigit(c) && c != ' ' && c != '-') { + // Erlaubt: Buchstaben, Ziffern, Leerzeichen, Bindestrich, Punkt, Komma, Ampersand + if (!Character.isLetter(c) && !Character.isDigit(c) && c != ' ' && c != '-' + && c != '.' && c != ',' && c != '&') { return false; } } diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/TargetFilenameBuildingServiceTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/TargetFilenameBuildingServiceTest.java index 5109652..8c7bb4e 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/TargetFilenameBuildingServiceTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/TargetFilenameBuildingServiceTest.java @@ -250,14 +250,15 @@ class TargetFilenameBuildingServiceTest { } @Test - void buildBaseFilename_titleWithDot_returnsInconsistentProposalState() { - // Dot (.) is NOT a Windows-incompatible character (as per our list < > : " / \ | ? *) - // So it remains in the cleaned title and causes validation to fail - ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rechnung.pdf"); + void buildBaseFilename_titleWithDot_returnsBaseFilenameReady() { + // Punkt (.) ist nun ein erlaubtes Zeichen in Titeln + ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rechnung.Kopie"); BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH); - assertThat(result).isInstanceOf(InconsistentProposalState.class); + assertThat(result).isInstanceOf(BaseFilenameReady.class); + assertThat((BaseFilenameReady) result).extracting("baseFilename") + .isEqualTo("2026-01-01 - Rechnung.Kopie.pdf"); } // ------------------------------------------------------------------------- diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/TitleCharacterRuleTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/TitleCharacterRuleTest.java index 3a592ef..e665e51 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/TitleCharacterRuleTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/TitleCharacterRuleTest.java @@ -7,8 +7,9 @@ import org.junit.jupiter.api.Test; /** * Unit tests for {@link TitleCharacterRule}. *

- * Verifies the canonical character-allowlist rule: letters (including German Umlauts - * and ß), digits, ASCII spaces, and hyphens are permitted; everything else is not. + * Prüft die kanonische Zeichenerlaubnis-Regel: Buchstaben (einschließlich deutscher Umlaute + * und ß), Ziffern, ASCII-Leerzeichen, Bindestriche, Punkte, Kommas und Ampersands sind + * erlaubt; alles andere ist nicht erlaubt. */ class TitleCharacterRuleTest { @@ -46,6 +47,21 @@ class TitleCharacterRuleTest { assertThat(TitleCharacterRule.isAllowed("Kfz-Haftpflicht 2026")).isTrue(); } + @Test + void isAllowed_comma_returnsTrue() { + assertThat(TitleCharacterRule.isAllowed("Müller, Hans")).isTrue(); + } + + @Test + void isAllowed_ampersand_returnsTrue() { + assertThat(TitleCharacterRule.isAllowed("Müller & Söhne")).isTrue(); + } + + @Test + void isAllowed_dotCommaAmpersand_returnsTrue() { + assertThat(TitleCharacterRule.isAllowed("Firma Dr. Meyer, Müller & Co.")).isTrue(); + } + @Test void isAllowed_emptyString_returnsTrue() { assertThat(TitleCharacterRule.isAllowed("")).isTrue(); @@ -61,8 +77,8 @@ class TitleCharacterRuleTest { } @Test - void isAllowed_dot_returnsFalse() { - assertThat(TitleCharacterRule.isAllowed("Rechnung.pdf")).isFalse(); + void isAllowed_dot_returnsTrue() { + assertThat(TitleCharacterRule.isAllowed("Rechnung.Kopie")).isTrue(); } @Test @@ -104,4 +120,25 @@ class TitleCharacterRuleTest { void isAllowed_underscore_returnsFalse() { assertThat(TitleCharacterRule.isAllowed("Rechnung_2026")).isFalse(); } + + // Windows-Sonderzeichen müssen weiterhin abgelehnt werden + @Test + void isAllowed_doubleQuote_returnsFalse() { + assertThat(TitleCharacterRule.isAllowed("Rechnung \"2026\"")).isFalse(); + } + + @Test + void isAllowed_lessThan_returnsFalse() { + assertThat(TitleCharacterRule.isAllowed("Rg < 2026")).isFalse(); + } + + @Test + void isAllowed_greaterThan_returnsFalse() { + assertThat(TitleCharacterRule.isAllowed("Rg > 2026")).isFalse(); + } + + @Test + void isAllowed_pipe_returnsFalse() { + assertThat(TitleCharacterRule.isAllowed("Rg | Strom")).isFalse(); + } }