1
0

Windows-Zeichenbehandlung im finalen Basis-Dateinamen explizit umgesetzt

This commit is contained in:
2026-04-07 13:59:18 +02:00
parent f81f30c7ea
commit 7e4201b651
2 changed files with 114 additions and 3 deletions

View File

@@ -97,6 +97,11 @@ public final class TargetFilenameBuildingService {
* If any rule is violated, the state is treated as an * If any rule is violated, the state is treated as an
* {@link InconsistentProposalState}. * {@link InconsistentProposalState}.
* <p> * <p>
* Windows compatibility: The final filename is cleaned of Windows-incompatible characters
* (e.g., {@code < > : " / \ | ? *}) to ensure the resulting filename can be created on
* Windows systems. This is a defensive measure; the validated title is already expected
* to contain only letters, digits, and spaces.
* <p>
* The 20-character limit applies exclusively to the base title. A duplicate-avoidance * The 20-character limit applies exclusively to the base title. A duplicate-avoidance
* suffix (e.g., {@code (1)}) may be appended by the target folder adapter after this * suffix (e.g., {@code (1)}) may be appended by the target folder adapter after this
* method returns and is not counted against the 20 characters. * method returns and is not counted against the 20 characters.
@@ -134,8 +139,16 @@ public final class TargetFilenameBuildingService {
+ title + "'"); + title + "'");
} }
// Defensive Windows compatibility: remove Windows-incompatible characters
String cleanedTitle = removeWindowsIncompatibleCharacters(title);
if (cleanedTitle.isBlank()) {
return new InconsistentProposalState(
"Title becomes empty after Windows-compatibility cleaning: '"
+ title + "'");
}
// Build: YYYY-MM-DD - Titel.pdf // Build: YYYY-MM-DD - Titel.pdf
String baseFilename = date + " - " + title + ".pdf"; String baseFilename = date + " - " + cleanedTitle + ".pdf";
return new BaseFilenameReady(baseFilename); return new BaseFilenameReady(baseFilename);
} }
@@ -156,4 +169,21 @@ public final class TargetFilenameBuildingService {
} }
return true; return true;
} }
/**
* Removes characters that are incompatible with Windows filenames.
* <p>
* Windows-incompatible characters are: {@code < > : " / \ | ? *}
* <p>
* This is a defensive measure for ensuring Windows compatibility. The characters are
* simply removed; no replacement is performed. Unicode letters (including Umlauts and ß)
* and spaces are retained.
*
* @param title the title to clean; must not be null
* @return the cleaned title with Windows-incompatible characters removed
*/
private static String removeWindowsIncompatibleCharacters(String title) {
// Windows-incompatible characters: < > : " / \ | ? *
return title.replaceAll("[<>:\"/\\\\|?*]", "");
}
} }

View File

@@ -21,8 +21,9 @@ import static org.assertj.core.api.Assertions.assertThatNullPointerException;
* Unit tests for {@link TargetFilenameBuildingService}. * Unit tests for {@link TargetFilenameBuildingService}.
* <p> * <p>
* Covers the verbindliches Zielformat {@code YYYY-MM-DD - Titel.pdf}, the 20-character * Covers the verbindliches Zielformat {@code YYYY-MM-DD - Titel.pdf}, the 20-character
* base-title rule, the fachliche Titelregel (only letters, digits, and spaces), and the * base-title rule, the fachliche Titelregel (only letters, digits, and spaces),
* detection of inconsistent persistence states. * Windows-compatibility character removal, and the detection of inconsistent persistence
* states.
*/ */
class TargetFilenameBuildingServiceTest { class TargetFilenameBuildingServiceTest {
@@ -218,6 +219,86 @@ class TargetFilenameBuildingServiceTest {
assertThat(result).isInstanceOf(InconsistentProposalState.class); assertThat(result).isInstanceOf(InconsistentProposalState.class);
} }
// -------------------------------------------------------------------------
// Windows compatibility removal of incompatible characters
// (defensive measure; characters should not appear in validated title)
// -------------------------------------------------------------------------
@Test
void buildBaseFilename_validTitleProducesWindowsCompatibleFilename() {
// Valid titles containing only letters, digits, and spaces should produce
// correct Windows-compatible filenames
ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 5, 20), "Versicherung");
BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt);
assertThat(result).isInstanceOf(BaseFilenameReady.class);
assertThat(((BaseFilenameReady) result).baseFilename())
.isEqualTo("2026-05-20 - Versicherung.pdf");
}
@Test
void buildBaseFilename_germanUmlautsAreRetainedInFilename() {
// German Umlauts (ä, ö, ü) and ß are valid filename characters on Windows
// and must be retained in the output filename
ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 6, 15), "Überprüfung");
BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt);
assertThat(result).isInstanceOf(BaseFilenameReady.class);
assertThat(((BaseFilenameReady) result).baseFilename())
.isEqualTo("2026-06-15 - Überprüfung.pdf");
}
@Test
void buildBaseFilename_germanSzligIsRetainedInFilename() {
// German ß is a valid filename character on Windows and must be retained
ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 3, 10), "Straße");
BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt);
assertThat(result).isInstanceOf(BaseFilenameReady.class);
assertThat(((BaseFilenameReady) result).baseFilename())
.isEqualTo("2026-03-10 - Straße.pdf");
}
@Test
void buildBaseFilename_completeFormatIsWindowsCompatible() {
// The complete filename format (YYYY-MM-DD - Titel.pdf) is Windows-compatible
// The hyphen in the date and the dot in the extension are valid Windows characters
ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 12, 31), "Bericht");
BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt);
assertThat(result).isInstanceOf(BaseFilenameReady.class);
String filename = ((BaseFilenameReady) result).baseFilename();
assertThat(filename).matches("\\d{4}-\\d{2}-\\d{2} - .+\\.pdf");
// Verify no Windows-incompatible characters: < > : " / \ | ? *
assertThat(filename).doesNotContain("<");
assertThat(filename).doesNotContain(">");
assertThat(filename).doesNotContain(":");
assertThat(filename).doesNotContain("\"");
assertThat(filename).doesNotContain("/");
assertThat(filename).doesNotContain("\\");
assertThat(filename).doesNotContain("|");
assertThat(filename).doesNotContain("?");
assertThat(filename).doesNotContain("*");
}
@Test
void buildBaseFilename_twentyCharacterRuleUnaffectedByWindowsCompatibility() {
// The 20-character rule applies to the base title only.
// Windows-compatibility cleaning does not change the length counting mechanism.
String title = "Stromabrechnung 2026"; // exactly 20 characters
ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 3, 31), title);
BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt);
assertThat(result).isInstanceOf(BaseFilenameReady.class);
assertThat(((BaseFilenameReady) result).baseFilename())
.startsWith("2026-03-31 - Stromabrechnung 2026");
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// InconsistentProposalState reason field is non-null // InconsistentProposalState reason field is non-null
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------