1
0

M8 komplett umgesetzt

This commit is contained in:
2026-04-08 16:30:13 +02:00
parent a3f47ba560
commit d61316c699
21 changed files with 2377 additions and 89 deletions

View File

@@ -0,0 +1,18 @@
/**
* Outbound adapter for system time access.
* <p>
* Components:
* <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.adapter.out.clock.SystemClockAdapter}
* — Production implementation of {@link de.gecheckt.pdf.umbenenner.application.port.out.ClockPort}
* that delegates to the JVM system clock ({@code Instant.now()}).</li>
* </ul>
* <p>
* The {@link de.gecheckt.pdf.umbenenner.application.port.out.ClockPort} abstraction ensures that
* all application-layer and domain-layer code obtains the current instant through the port,
* enabling deterministic time injection in tests without coupling to wall-clock time.
* <p>
* No date/time logic or formatting is performed in this package; that responsibility
* belongs to the application layer.
*/
package de.gecheckt.pdf.umbenenner.adapter.out.clock;

View File

@@ -247,6 +247,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
* @return the most recent {@code PROPOSAL_READY} attempt, or {@code null}
* @throws DocumentPersistenceException if the query fails
*/
@Override
public ProcessingAttempt findLatestProposalReadyAttempt(DocumentFingerprint fingerprint) {
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
@@ -259,7 +260,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
final_target_file_name
FROM processing_attempt
WHERE fingerprint = ?
AND status = 'PROPOSAL_READY'
AND status = ?
ORDER BY attempt_number DESC
LIMIT 1
""";
@@ -270,6 +271,7 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
pragmaStmt.execute(PRAGMA_FOREIGN_KEYS_ON);
statement.setString(1, fingerprint.sha256Hex());
statement.setString(2, ProcessingStatus.PROPOSAL_READY.name());
try (ResultSet rs = statement.executeQuery()) {
if (rs.next()) {

View File

@@ -1,5 +1,7 @@
package de.gecheckt.pdf.umbenenner.adapter.out.sqlite;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@@ -93,53 +95,70 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
}
}
/**
* Wraps a shared transaction connection so that {@code close()} becomes a no-op.
* <p>
* Repository adapters manage their own connection lifecycle via try-with-resources,
* which would close the shared transaction connection prematurely if not wrapped.
* All other {@link Connection} methods are delegated unchanged to the underlying connection.
*
* @param underlying the real shared connection; must not be null
* @return a proxy connection that ignores {@code close()} calls
*/
private static Connection nonClosingWrapper(Connection underlying) {
return (Connection) Proxy.newProxyInstance(
Connection.class.getClassLoader(),
new Class<?>[] { Connection.class },
(proxy, method, args) -> {
if ("close".equals(method.getName())) {
return null;
}
try {
return method.invoke(underlying, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
});
}
private class TransactionOperationsImpl implements TransactionOperations {
private final Connection connection;
TransactionOperationsImpl(Connection connection) {
this.connection = connection;
}
@Override
public void saveProcessingAttempt(ProcessingAttempt attempt) {
// Repository methods declare DocumentPersistenceException as the only thrown exception.
// Any other exception (NullPointerException, etc.) will propagate to the outer try-catch
// and be caught there.
SqliteProcessingAttemptRepositoryAdapter repo =
new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl) {
@Override
protected Connection getConnection() throws SQLException {
return connection;
return nonClosingWrapper(connection);
}
};
repo.save(attempt);
}
@Override
public void createDocumentRecord(DocumentRecord record) {
// Repository methods declare DocumentPersistenceException as the only thrown exception.
// Any other exception (NullPointerException, etc.) will propagate to the outer try-catch
// and be caught there.
SqliteDocumentRecordRepositoryAdapter repo =
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl) {
@Override
protected Connection getConnection() throws SQLException {
return connection;
return nonClosingWrapper(connection);
}
};
repo.create(record);
}
@Override
public void updateDocumentRecord(DocumentRecord record) {
// Repository methods declare DocumentPersistenceException as the only thrown exception.
// Any other exception (NullPointerException, etc.) will propagate to the outer try-catch
// and be caught there.
SqliteDocumentRecordRepositoryAdapter repo =
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl) {
@Override
protected Connection getConnection() throws SQLException {
return connection;
return nonClosingWrapper(connection);
}
};
repo.update(record);

View File

@@ -0,0 +1,24 @@
/**
* Outbound adapter for writing the target file copy.
* <p>
* Components:
* <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.adapter.out.targetcopy.FilesystemTargetFileCopyAdapter}
* — Filesystem-based implementation of
* {@link de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopyPort}.</li>
* </ul>
* <p>
* The adapter uses a two-step write pattern: the source is first copied to a temporary
* file ({@code resolvedFilename + ".tmp"}) in the target folder, then renamed/moved to
* the final filename. An atomic move is attempted first; a standard move is used as a
* fallback when the filesystem does not support atomic cross-directory moves.
* <p>
* <strong>Source integrity:</strong> The source file is never modified, moved, or deleted.
* Only a copy is created in the target folder.
* <p>
* <strong>Architecture boundary:</strong> All NIO ({@code Path}, {@code Files}) operations
* are strictly confined to this package. The port interface
* {@link de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopyPort} contains no
* filesystem types, preserving the hexagonal architecture boundary.
*/
package de.gecheckt.pdf.umbenenner.adapter.out.targetcopy;

View File

@@ -0,0 +1,26 @@
/**
* Outbound adapter for target folder management and unique filename resolution.
* <p>
* Components:
* <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.adapter.out.targetfolder.FilesystemTargetFolderAdapter}
* — Filesystem-based implementation of
* {@link de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort}.</li>
* </ul>
* <p>
* <strong>Duplicate resolution:</strong> Given a base name such as
* {@code 2024-01-15 - Rechnung.pdf}, the adapter checks whether the file exists in the
* target folder and appends a numeric suffix ({@code (1)}, {@code (2)}, …) directly
* before {@code .pdf} until a free name is found. The 20-character base-title limit
* does not apply to the suffix.
* <p>
* <strong>Rollback support:</strong> The adapter provides a best-effort deletion method
* used by the application layer to remove a successfully written target copy when
* subsequent persistence fails, preventing orphaned target files.
* <p>
* <strong>Architecture boundary:</strong> All NIO ({@code Path}, {@code Files}) operations
* are strictly confined to this package. The port interface
* {@link de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort} contains no
* filesystem types, preserving the hexagonal architecture boundary.
*/
package de.gecheckt.pdf.umbenenner.adapter.out.targetfolder;