M8 komplett umgesetzt
This commit is contained in:
@@ -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;
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user