Windows-Zeichenbehandlung im finalen Basis-Dateinamen explizit umgesetzt
This commit is contained in:
@@ -97,6 +97,11 @@ public final class TargetFilenameBuildingService {
|
||||
* If any rule is violated, the state is treated as an
|
||||
* {@link InconsistentProposalState}.
|
||||
* <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
|
||||
* 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.
|
||||
@@ -134,8 +139,16 @@ public final class TargetFilenameBuildingService {
|
||||
+ 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
|
||||
String baseFilename = date + " - " + title + ".pdf";
|
||||
String baseFilename = date + " - " + cleanedTitle + ".pdf";
|
||||
return new BaseFilenameReady(baseFilename);
|
||||
}
|
||||
|
||||
@@ -156,4 +169,21 @@ public final class TargetFilenameBuildingService {
|
||||
}
|
||||
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("[<>:\"/\\\\|?*]", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||
* Unit tests for {@link TargetFilenameBuildingService}.
|
||||
* <p>
|
||||
* 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
|
||||
* detection of inconsistent persistence states.
|
||||
* base-title rule, the fachliche Titelregel (only letters, digits, and spaces),
|
||||
* Windows-compatibility character removal, and the detection of inconsistent persistence
|
||||
* states.
|
||||
*/
|
||||
class TargetFilenameBuildingServiceTest {
|
||||
|
||||
@@ -218,6 +219,86 @@ class TargetFilenameBuildingServiceTest {
|
||||
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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user