Konsolidiere Titelzeichen-Validierung in TitleCharacterRule
Doppelte private isAllowedTitleCharacters-Methoden in AiResponseValidator und TargetFilenameBuildingService werden durch eine kanonische TitleCharacterRule.isAllowed()-Methode ersetzt. Beide Services delegieren jetzt dorthin statt eigene Kopien zu pflegen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-20
@@ -100,7 +100,7 @@ public final class AiResponseValidator {
|
||||
AiErrorClassification.FUNCTIONAL);
|
||||
}
|
||||
|
||||
if (!isAllowedTitleCharacters(title)) {
|
||||
if (!TitleCharacterRule.isAllowed(title)) {
|
||||
return AiValidationResult.invalid(
|
||||
"Title contains disallowed characters (only letters, digits, spaces, and hyphens are permitted): '"
|
||||
+ title + "'",
|
||||
@@ -137,25 +137,6 @@ public final class AiResponseValidator {
|
||||
return AiValidationResult.valid(proposal);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns {@code true} if every character in the title is a letter, digit, space, or hyphen.
|
||||
* <p>
|
||||
* Permits Unicode letters including German Umlauts (ä, ö, ü, Ä, Ö, Ü) and ß.
|
||||
*/
|
||||
private static boolean isAllowedTitleCharacters(String title) {
|
||||
for (int i = 0; i < title.length(); i++) {
|
||||
char c = title.charAt(i);
|
||||
if (!Character.isLetter(c) && !Character.isDigit(c) && c != ' ' && c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the title is a known generic placeholder.
|
||||
* Comparison is case-insensitive.
|
||||
|
||||
+1
-15
@@ -149,7 +149,7 @@ public final class TargetFilenameBuildingService {
|
||||
}
|
||||
|
||||
// After cleaning, verify that only letters, digits, spaces, and hyphens remain
|
||||
if (!isAllowedTitleCharacters(cleanedTitle)) {
|
||||
if (!TitleCharacterRule.isAllowed(cleanedTitle)) {
|
||||
return new InconsistentProposalState(
|
||||
"After Windows-compatibility cleaning, title contains disallowed characters "
|
||||
+ "(only letters, digits, spaces, and hyphens are permitted): '"
|
||||
@@ -165,20 +165,6 @@ public final class TargetFilenameBuildingService {
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns {@code true} if every character in the title is a letter, a digit, space, or hyphen.
|
||||
* Unicode letters (including German Umlauts and ß) are permitted.
|
||||
*/
|
||||
private static boolean isAllowedTitleCharacters(String title) {
|
||||
for (int i = 0; i < title.length(); i++) {
|
||||
char c = title.charAt(i);
|
||||
if (!Character.isLetter(c) && !Character.isDigit(c) && c != ' ' && c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes characters that are incompatible with Windows filenames.
|
||||
* <p>
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.service;
|
||||
|
||||
/**
|
||||
* Canonical rule for allowed title characters.
|
||||
* <p>
|
||||
* A title may contain only Unicode letters (including German Umlauts and ß),
|
||||
* decimal digits, ASCII spaces, and hyphens ({@code -}).
|
||||
* All other characters are considered disallowed.
|
||||
* <p>
|
||||
* 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.
|
||||
*/
|
||||
public final class TitleCharacterRule {
|
||||
|
||||
private TitleCharacterRule() {
|
||||
// utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if every character in {@code title} is a letter, digit,
|
||||
* ASCII space, or hyphen ({@code -}).
|
||||
* <p>
|
||||
* Unicode letters — including German Umlauts (ä, ö, ü, Ä, Ö, Ü) and ß — are
|
||||
* permitted. An empty string is considered allowed.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
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 != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+2
@@ -22,6 +22,8 @@
|
||||
* two-level persistence</li>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.service.AiRequestComposer}
|
||||
* — Deterministic composition of AI request representations from prompt and document text</li>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.service.TitleCharacterRule}
|
||||
* — Canonical character-allowlist rule for document titles (single authoritative implementation)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Document processing flow ({@code DocumentProcessingCoordinator})</h2>
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
* Unit tests for {@link TargetFilenameBuildingService}.
|
||||
* <p>
|
||||
* Covers the verbindliches Zielformat {@code YYYY-MM-DD - Titel.pdf}, the 60-character
|
||||
* base-title rule, the fachliche Titelregel (only letters, digits, and spaces),
|
||||
* base-title rule, the fachliche Titelregel (only letters, digits, spaces, and hyphens),
|
||||
* Windows-compatibility character removal, and the detection of inconsistent persistence
|
||||
* states.
|
||||
*/
|
||||
@@ -267,7 +267,7 @@ class TargetFilenameBuildingServiceTest {
|
||||
|
||||
@Test
|
||||
void buildBaseFilename_validTitleProducesWindowsCompatibleFilename() {
|
||||
// Valid titles containing only letters, digits, and spaces should produce
|
||||
// Valid titles containing only letters, digits, spaces, and hyphens should produce
|
||||
// correct Windows-compatible filenames
|
||||
ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 5, 20), "Versicherung");
|
||||
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link TitleCharacterRule}.
|
||||
* <p>
|
||||
* Verifies the canonical character-allowlist rule: letters (including German Umlauts
|
||||
* and ß), digits, ASCII spaces, and hyphens are permitted; everything else is not.
|
||||
*/
|
||||
class TitleCharacterRuleTest {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Allowed characters
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void isAllowed_lettersOnly_returnsTrue() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Stromabrechnung")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_lettersAndDigits_returnsTrue() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Rechnung 2026")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_germanUmlauts_returnsTrue() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Müller Überprüfung")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_germanSzlig_returnsTrue() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Straße")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_hyphen_returnsTrue() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Strom-Rechnung")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_hyphenWithSpaces_returnsTrue() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Kfz-Haftpflicht 2026")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_emptyString_returnsTrue() {
|
||||
assertThat(TitleCharacterRule.isAllowed("")).isTrue();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Disallowed characters
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void isAllowed_exclamationMark_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Rechnung!")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_dot_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Rechnung.pdf")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_colon_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Betreff: Rechnung")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_slash_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Rg/Strom")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_backslash_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Rg\\Strom")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_questionMark_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Was?")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_asterisk_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Rech*nung")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_atSign_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("user@mail")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_parenthesis_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Rechnung (2026)")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAllowed_underscore_returnsFalse() {
|
||||
assertThat(TitleCharacterRule.isAllowed("Rechnung_2026")).isFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user