Fix #16: TitleCharacterRule um Punkt, Komma und Ampersand erweitern

- Erweitere TitleCharacterRule.isAllowed() um die Zeichen: . (Punkt), , (Komma), & (Ampersand)
- Passe JavaDoc-Kommentare auf Deutsch an
- Aktualisiere TitleCharacterRuleTest: ändere Punkt-Test von disallowed zu allowed
- Füge Tests für Komma und Ampersand hinzu
- Füge Tests hinzu, die Windows-Sonderzeichen (\ / : * ? " < > |) weiterhin als ungültig bestätigen
- Aktualisiere TargetFilenameBuildingServiceTest für den neuen Test-Fall
- Dokumentation: fachliche-anforderungen.md und CLAUDE.md aktualisiert

mvn clean verify erfolgreich bestanden

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 17:07:07 +02:00
parent 1df541d0f9
commit c46294159c
5 changed files with 67 additions and 26 deletions
+1 -1
View File
@@ -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`, ... - 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 - 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 - 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 - 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 - 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 - Mehrdeutige Dokumente liefern **kein** unsicheres Ergebnis, sondern einen Fehler
+1 -1
View File
@@ -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)** - maximal **konfigurierbare Anzahl Zeichen (Basistitel, Default 60, gültiger Bereich 10..120)**
- verständlich und eindeutig - verständlich und eindeutig
- keine Sonderzeichen außer Leerzeichen und Bindestrichen - keine Sonderzeichen außer Leerzeichen, Bindestrichen, Punkten, Kommas und Ampersands
--- ---
@@ -1,15 +1,15 @@
package de.gecheckt.pdf.umbenenner.application.service; package de.gecheckt.pdf.umbenenner.application.service;
/** /**
* Canonical rule for allowed title characters. * Kanonische Regel für erlaubte Titelzeichen.
* <p> * <p>
* A title may contain only Unicode letters (including German Umlauts and ß), * Ein Titel darf nur Unicode-Buchstaben (einschließlich deutscher Umlaute und ß),
* decimal digits, ASCII spaces, and hyphens ({@code -}). * Ziffern, ASCII-Leerzeichen, Bindestriche ({@code -}), Punkte ({@code .}),
* All other characters are considered disallowed. * Kommas ({@code ,}) und Ampersands ({@code &}) enthalten.
* Alle anderen Zeichen sind nicht erlaubt.
* <p> * <p>
* This class is the single authoritative implementation of the character-allowlist * Diese Klasse ist die einzige autoritative Implementierung der Zeichenerlaubnis-Regel.
* rule. All services that need to validate or verify title characters must delegate * Alle Services, die Titelzeichen validieren müssen, delegieren an {@link #isAllowed(String)}.
* to {@link #isAllowed(String)} instead of maintaining their own copies.
*/ */
public final class TitleCharacterRule { 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, * Gibt {@code true} zurück, wenn jedes Zeichen im {@code title} ein Buchstabe, eine Ziffer,
* ASCII space, or hyphen ({@code -}). * ein ASCII-Leerzeichen, ein Bindestrich ({@code -}), ein Punkt ({@code .}),
* ein Komma ({@code ,}) oder ein Ampersand ({@code &}) ist.
* <p> * <p>
* Unicode letters — including German Umlauts (ä, ö, ü, Ä, Ö, Ü) and ß — are * Unicode-Buchstabeneinschließlich deutscher Umlaute (ä, ö, ü, Ä, Ö, Ü) und ß — sind
* permitted. An empty string is considered allowed. * erlaubt. Eine leere Zeichenkette ist erlaubt.
* *
* @param title the title to check; must not be null * @param title der zu prüfende Titel; darf nicht null sein
* @return {@code true} if all characters pass the allowlist; {@code false} otherwise * @return {@code true} wenn alle Zeichen die Erlaubnisliste erfüllen; {@code false} andernfalls
* @throws NullPointerException if {@code title} is null * @throws NullPointerException wenn {@code title} null ist
*/ */
public static boolean isAllowed(String title) { public static boolean isAllowed(String title) {
for (int i = 0; i < title.length(); i++) { for (int i = 0; i < title.length(); i++) {
char c = title.charAt(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; return false;
} }
} }
@@ -250,14 +250,15 @@ class TargetFilenameBuildingServiceTest {
} }
@Test @Test
void buildBaseFilename_titleWithDot_returnsInconsistentProposalState() { void buildBaseFilename_titleWithDot_returnsBaseFilenameReady() {
// Dot (.) is NOT a Windows-incompatible character (as per our list < > : " / \ | ? *) // Punkt (.) ist nun ein erlaubtes Zeichen in Titeln
// So it remains in the cleaned title and causes validation to fail ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rechnung.Kopie");
ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rechnung.pdf");
BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH); 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");
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -7,8 +7,9 @@ import org.junit.jupiter.api.Test;
/** /**
* Unit tests for {@link TitleCharacterRule}. * Unit tests for {@link TitleCharacterRule}.
* <p> * <p>
* Verifies the canonical character-allowlist rule: letters (including German Umlauts * Prüft die kanonische Zeichenerlaubnis-Regel: Buchstaben (einschließlich deutscher Umlaute
* and ß), digits, ASCII spaces, and hyphens are permitted; everything else is not. * und ß), Ziffern, ASCII-Leerzeichen, Bindestriche, Punkte, Kommas und Ampersands sind
* erlaubt; alles andere ist nicht erlaubt.
*/ */
class TitleCharacterRuleTest { class TitleCharacterRuleTest {
@@ -46,6 +47,21 @@ class TitleCharacterRuleTest {
assertThat(TitleCharacterRule.isAllowed("Kfz-Haftpflicht 2026")).isTrue(); 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 @Test
void isAllowed_emptyString_returnsTrue() { void isAllowed_emptyString_returnsTrue() {
assertThat(TitleCharacterRule.isAllowed("")).isTrue(); assertThat(TitleCharacterRule.isAllowed("")).isTrue();
@@ -61,8 +77,8 @@ class TitleCharacterRuleTest {
} }
@Test @Test
void isAllowed_dot_returnsFalse() { void isAllowed_dot_returnsTrue() {
assertThat(TitleCharacterRule.isAllowed("Rechnung.pdf")).isFalse(); assertThat(TitleCharacterRule.isAllowed("Rechnung.Kopie")).isTrue();
} }
@Test @Test
@@ -104,4 +120,25 @@ class TitleCharacterRuleTest {
void isAllowed_underscore_returnsFalse() { void isAllowed_underscore_returnsFalse() {
assertThat(TitleCharacterRule.isAllowed("Rechnung_2026")).isFalse(); 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();
}
} }