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
|
* 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("[<>:\"/\\\\|?*]", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user