diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/lock/FilesystemRunLockPortAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/lock/FilesystemRunLockPortAdapter.java index 757e98b..54002e8 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/lock/FilesystemRunLockPortAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/lock/FilesystemRunLockPortAdapter.java @@ -4,22 +4,26 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException; /** - * File-based implementation of {@link RunLockPort} that uses a lock file to prevent concurrent runs. + * Dateibasierte Implementierung von {@link RunLockPort}. *

- * Creates an exclusive lock file on acquire and deletes it on release. - * If the lock file already exists, {@link #acquire()} throws {@link RunLockUnavailableException} - * to signal that another instance is already running. + * Verwendet eine Lock-Datei, um parallele Läufe zu verhindern. + * Beim Erwerb wird die Lock-Datei angelegt; bei der Freigabe wird sie gelöscht. + * Existiert die Datei bereits, ist der Lock belegt. *

- * The lock file contains the PID of the acquiring process. Release is best-effort: a failure - * to delete the lock file is logged as a warning but does not throw. + * Die Lock-Datei enthält die PID des erwerbenden Prozesses. + * Die Freigabe ist best-effort: Ein Fehler beim Löschen wird als Warnung + * geloggt, wirft aber keine Ausnahme. */ public class FilesystemRunLockPortAdapter implements RunLockPort { @@ -28,27 +32,31 @@ public class FilesystemRunLockPortAdapter implements RunLockPort { private final Path lockFile; /** - * Creates a new FilesystemRunLockPortAdapter for the given lock file path. + * Erstellt einen neuen {@code FilesystemRunLockPortAdapter} für den + * angegebenen Lock-Datei-Pfad. * - * @param lockFile path of the lock file to create on acquire and delete on release + * @param lockFile Pfad der Lock-Datei, die beim Erwerb angelegt und + * bei der Freigabe gelöscht wird */ public FilesystemRunLockPortAdapter(Path lockFile) { this.lockFile = lockFile; } /** - * Acquires the run lock by creating the lock file. + * Erwirbt den Run-Lock durch Anlegen der Lock-Datei (blockierend). *

- * If the lock file already exists, throws {@link RunLockUnavailableException}. - * If the parent directory does not exist, it is created before attempting file creation. + * Existiert die Lock-Datei bereits, wird eine + * {@link RunLockUnavailableException} geworfen. Das übergeordnete + * Verzeichnis wird bei Bedarf angelegt. * - * @throws RunLockUnavailableException if the lock file already exists or cannot be created + * @throws RunLockUnavailableException wenn die Lock-Datei bereits existiert + * oder nicht angelegt werden kann */ @Override public void acquire() { if (Files.exists(lockFile)) { throw new RunLockUnavailableException( - "Run lock file already exists - another instance may be running: " + lockFile); + "Run-Lock-Datei existiert bereits – eine andere Instanz könnte laufen: " + lockFile); } try { Path parent = lockFile.getParent(); @@ -57,26 +65,83 @@ public class FilesystemRunLockPortAdapter implements RunLockPort { } long pid = ProcessHandle.current().pid(); Files.writeString(lockFile, String.valueOf(pid), StandardOpenOption.CREATE_NEW); - LOG.debug("Run lock acquired: {} (PID {})", lockFile, pid); + LOG.debug("Run-Lock erworben: {} (PID {})", lockFile, pid); } catch (IOException e) { - throw new RunLockUnavailableException("Failed to acquire run lock file: " + lockFile, e); + throw new RunLockUnavailableException("Run-Lock-Datei konnte nicht angelegt werden: " + lockFile, e); } } /** - * Releases the run lock by deleting the lock file. + * Gibt den Run-Lock durch Löschen der Lock-Datei frei. *

- * If deletion fails, a warning is logged but no exception is thrown. + * Schlägt das Löschen fehl, wird eine Warnung geloggt; keine Ausnahme + * wird geworfen. */ @Override public void release() { try { boolean deleted = Files.deleteIfExists(lockFile); if (deleted) { - LOG.debug("Run lock released: {}", lockFile); + LOG.debug("Run-Lock freigegeben: {}", lockFile); } } catch (IOException e) { - LOG.warn("Failed to release run lock file: {} — manual cleanup may be required", lockFile, e); + LOG.warn("Run-Lock-Datei konnte nicht gelöscht werden: {} – manuelle Bereinigung erforderlich", + lockFile, e); + } + } + + /** + * Versucht nicht-blockierend, den Run-Lock zu erwerben. + *

+ * Existiert die Lock-Datei bereits, wird sofort {@link Optional#empty()} + * zurückgegeben. Andernfalls wird die Datei atomar mit + * {@link StandardOpenOption#CREATE_NEW} angelegt. Schlägt das Anlegen + * aufgrund einer Race-Condition fehl (z.B. gleichzeitiger Erwerb durch + * eine andere Instanz), wird ebenfalls {@link Optional#empty()} zurückgegeben. + *

+ * Das zurückgegebene {@link RunLockHandle} gibt den Lock idempotent frei. + * + * @return Handle mit dem erworbenen Lock, oder {@link Optional#empty()} + * wenn der Lock nicht verfügbar ist + */ + @Override + public Optional tryAcquire() { + if (Files.exists(lockFile)) { + LOG.debug("Run-Lock nicht verfügbar (Datei existiert): {}", lockFile); + return Optional.empty(); + } + try { + Path parent = lockFile.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + long pid = ProcessHandle.current().pid(); + Files.writeString(lockFile, String.valueOf(pid), StandardOpenOption.CREATE_NEW); + LOG.debug("Run-Lock (tryAcquire) erworben: {} (PID {})", lockFile, pid); + return Optional.of(new FilesystemRunLockHandle()); + } catch (IOException e) { + // CREATE_NEW schlägt mit FileAlreadyExistsException fehl wenn eine + // Race-Condition vorliegt – kein Fehler, sondern normaler Busy-Zustand + LOG.debug("Run-Lock (tryAcquire) nicht verfügbar: {} – {}", lockFile, e.getMessage()); + return Optional.empty(); + } + } + + /** + * Handle für einen über {@link #tryAcquire()} erworbenen Run-Lock. + *

+ * Gibt den Lock idempotent frei. Mehrfaches Aufrufen von {@link #close()} + * hat nach dem ersten Aufruf keine Wirkung. + */ + private class FilesystemRunLockHandle implements RunLockHandle { + + private final AtomicBoolean released = new AtomicBoolean(false); + + @Override + public void close() { + if (released.compareAndSet(false, true)) { + FilesystemRunLockPortAdapter.this.release(); + } } } } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerControlUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerControlUseCase.java new file mode 100644 index 0000000..e17b1ca --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerControlUseCase.java @@ -0,0 +1,63 @@ +package de.gecheckt.pdf.umbenenner.application.port.in; + +/** + * Inbound-Port zur Steuerung des automatischen Schedulers. + *

+ * Dieser Use Case kapselt den vollständigen Scheduler-Lifecycle: + * Starten, Stoppen und Abfragen des aktuellen Zustands. Die Steuerung + * erfolgt ausschließlich über dieses Interface – GUI-Komponenten kennen + * weder den {@code SchedulerPort} noch Bootstrap-interne Typen. + *

+ * Alle Operationen sind idempotent: Ein {@link #start()} auf einem + * bereits laufenden Scheduler ist ein No-op; ein {@link #stop()} auf + * einem bereits gestoppten Scheduler ebenso. + *

+ * Implementierungen verwalten den Zustand threadsicher über eine + * {@code AtomicReference}. + */ +public interface SchedulerControlUseCase { + + /** + * Startet den automatischen Scheduler. + *

+ * Ist der Scheduler bereits aktiv ({@code state != STOPPED}), hat + * dieser Aufruf keine Wirkung. Andernfalls wird der Scheduler über + * folgende Sequenz gestartet: + *

    + *
  1. Zustand auf {@code STARTING} setzen
  2. + *
  3. {@code scheduler.enabled=true} persistieren
  4. + *
  5. Exklusiven OS-Lock auf Konfigurationsdatei erwerben
  6. + *
  7. Scheduler-Adapter starten (erster Tick sofort)
  8. + *
  9. Zustand auf {@code RUNNING_IDLE} setzen
  10. + *
+ * Schlägt ein Schritt fehl, wird ein vollständiger Rollback + * durchgeführt und der Zustand auf {@code STOPPED} zurückgesetzt. + * + * @throws SchedulerStartException wenn der Start fehlschlägt und + * kein Rollback möglich ist; enthält eine deutsche Meldung + * für die GUI-Anzeige + */ + void start() throws SchedulerStartException; + + /** + * Stoppt den automatischen Scheduler. + *

+ * Ist der Scheduler bereits gestoppt, hat dieser Aufruf keine Wirkung. + * Läuft gerade ein Tick, wechselt der Zustand zu + * {@code STOPPING_BATCH_ACTIVE}; der laufende Batch wird regulär + * zu Ende geführt. Danach werden {@code scheduler.enabled=false} + * persistiert und der OS-Lock freigegeben. + */ + void stop(); + + /** + * Gibt den aktuellen Scheduler-Zustand als unveränderlichen Snapshot zurück. + *

+ * Der Snapshot kann von beliebigen Threads gelesen werden. Die GUI + * ruft diese Methode regelmäßig über eine zentrale Status-Refresh-Timeline + * auf und aktualisiert alle betroffenen Tabs entsprechend. + * + * @return aktueller Scheduler-Status; nie {@code null} + */ + SchedulerStatus getStatus(); +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerStartException.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerStartException.java new file mode 100644 index 0000000..6b7bbe6 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerStartException.java @@ -0,0 +1,34 @@ +package de.gecheckt.pdf.umbenenner.application.port.in; + +/** + * Wird geworfen, wenn der Start des automatischen Schedulers fehlschlägt. + *

+ * Mögliche Ursachen sind: Fehler beim Erwerb des Konfigurations-Datei-Locks, + * Fehler beim Persistieren von {@code scheduler.enabled=true} oder + * technische Fehler beim Starten des Scheduler-Adapters. + *

+ * Diese Ausnahme ist ungeprüft (extends {@link RuntimeException}) und + * wird in der Callchain bis zum GUI-Layer weitergeleitet, der eine + * benutzerfreundliche deutsche Fehlermeldung anzeigt. + */ +public class SchedulerStartException extends RuntimeException { + + /** + * Erstellt eine neue {@code SchedulerStartException} mit der angegebenen Nachricht. + * + * @param message benutzerlesbare deutsche Fehlerbeschreibung + */ + public SchedulerStartException(String message) { + super(message); + } + + /** + * Erstellt eine neue {@code SchedulerStartException} mit Nachricht und Ursache. + * + * @param message benutzerlesbare deutsche Fehlerbeschreibung + * @param cause technische Ursache + */ + public SchedulerStartException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerState.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerState.java new file mode 100644 index 0000000..8dcb5f1 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerState.java @@ -0,0 +1,72 @@ +package de.gecheckt.pdf.umbenenner.application.port.in; + +/** + * Zustandsautomat des automatischen Schedulers. + *

+ * Beschreibt den aktuellen Lebenszykluszustand des Schedulers und + * steuert, welche Aktionen (Starten, Stoppen, manuelle Läufe) + * in der GUI erlaubt sind. + */ +public enum SchedulerState { + + /** + * Scheduler ist gestoppt. + *

+ * Manuelle Läufe sind erlaubt. Der Scheduler-Start-Button ist + * aktiv, sofern weitere Voraussetzungen erfüllt sind. + */ + STOPPED, + + /** + * Scheduler befindet sich im Startvorgang. + *

+ * Lock-Erwerb und initiale Einrichtung laufen. Manuelle Starts + * sind in diesem Übergangszustand deterministisch gesperrt. + */ + STARTING, + + /** + * Scheduler läuft und wartet auf den nächsten Tick. + *

+ * Kein Batch läuft gerade. Der Countdown bis zum nächsten Tick + * ist sichtbar. Manuelle Läufe sind gesperrt. + */ + RUNNING_IDLE, + + /** + * Scheduler läuft und ein Tick verarbeitet gerade einen Batch. + *

+ * Manuelle Läufe sind gesperrt. Der Stop-Button bleibt aktiv, + * bricht den laufenden Batch jedoch nicht ab. + */ + RUNNING_BATCH_ACTIVE, + + /** + * Stopp wurde angefordert, aber der laufende Batch läuft noch zu Ende. + *

+ * Nach Abschluss des Batches wechselt der Zustand zu {@link #STOPPED}. + * Der Status-Indikator zeigt „Gestoppt – aktueller Lauf läuft noch". + */ + STOPPING_BATCH_ACTIVE; + + /** + * Prüft, ob der Scheduler in diesem Zustand als aktiv gilt. + *

+ * Als aktiv gelten alle Zustände außer {@link #STOPPED}. + * + * @return {@code true}, wenn der Scheduler nicht gestoppt ist + */ + public boolean isActive() { + return this != STOPPED; + } + + /** + * Prüft, ob in diesem Zustand ein Batch verarbeitet wird. + * + * @return {@code true} bei {@link #RUNNING_BATCH_ACTIVE} oder + * {@link #STOPPING_BATCH_ACTIVE} + */ + public boolean isBatchRunning() { + return this == RUNNING_BATCH_ACTIVE || this == STOPPING_BATCH_ACTIVE; + } +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerStatus.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerStatus.java new file mode 100644 index 0000000..1f65eac --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerStatus.java @@ -0,0 +1,80 @@ +package de.gecheckt.pdf.umbenenner.application.port.in; + +import java.time.Instant; +import java.util.Optional; + +import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary; + +/** + * Unveränderlicher Snapshot des aktuellen Scheduler-Zustands. + *

+ * Instanzen dieses Records sind threadsicher, da alle Felder final sind. + * Im {@code DefaultSchedulerControlUseCase} werden Snapshots atomar über + * eine {@code AtomicReference} ausgetauscht. + *

+ * Die GUI liest diesen Snapshot regelmäßig über eine zentrale + * Status-Refresh-Timeline (1 Hz) und aktualisiert Scheduler-Tab, + * Batch-Tab und Konfig-Tab entsprechend. + * + * @param state aktueller Lebenszykluszustand des Schedulers + * @param lastRunEndedAt Endzeitpunkt des letzten abgeschlossenen Laufs; + * leer vor dem ersten Lauf + * @param lastRunSummary Zusammenfassung des letzten abgeschlossenen Laufs; + * leer vor dem ersten Lauf + * @param nextTickAt geplanter Zeitpunkt des nächsten Ticks; + * nur befüllt wenn {@code state == RUNNING_IDLE} + * @param lastError letzte aufgetretene deutsche Fehlermeldung; + * wird bei erfolgreichem Lauf gelöscht, + * bei {@code SkippedBusy} unverändert gelassen + * @param autostartFailed {@code true}, wenn ein konfigurierter Autostart + * beim Programmstart fehlgeschlagen ist + */ +public record SchedulerStatus( + SchedulerState state, + Optional lastRunEndedAt, + Optional lastRunSummary, + Optional nextTickAt, + Optional lastError, + boolean autostartFailed +) { + + /** + * Validiert, dass Pflichtfelder und Optional-Felder nicht null sind. + */ + public SchedulerStatus { + if (state == null) { + throw new IllegalArgumentException("state darf nicht null sein"); + } + if (lastRunEndedAt == null) { + throw new IllegalArgumentException("lastRunEndedAt darf nicht null sein"); + } + if (lastRunSummary == null) { + throw new IllegalArgumentException("lastRunSummary darf nicht null sein"); + } + if (nextTickAt == null) { + throw new IllegalArgumentException("nextTickAt darf nicht null sein"); + } + if (lastError == null) { + throw new IllegalArgumentException("lastError darf nicht null sein"); + } + } + + /** + * Erzeugt den initialen Scheduler-Status beim Programmstart. + *

+ * Zustand ist {@link SchedulerState#STOPPED}, alle optionalen Felder + * sind leer und {@code autostartFailed} ist {@code false}. + * + * @return initialer Scheduler-Status + */ + public static SchedulerStatus initial() { + return new SchedulerStatus( + SchedulerState.STOPPED, + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + false + ); + } +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/BatchRunTrigger.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/BatchRunTrigger.java new file mode 100644 index 0000000..4b74950 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/BatchRunTrigger.java @@ -0,0 +1,36 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Funktionales Interface zum Auslösen eines Verarbeitungslaufs. + *

+ * Dieses Interface entkoppelt den Scheduler-Adapter von konkreten + * Bootstrap- oder GUI-Klassen. Das Bootstrap-Modul erzeugt beim Start + * eine Implementierung als Lambda und übergibt sie beim Starten des + * Schedulers an {@link SchedulerPort#startScheduler(SchedulerConfig, BatchRunTrigger)}. + *

+ * Ausführungsmodell: + *

+ */ +@FunctionalInterface +public interface BatchRunTrigger { + + /** + * Löst synchron einen Verarbeitungslauf aus. + *

+ * Ist der RunLock nicht verfügbar, kehrt diese Methode sofort mit + * {@link BatchRunTriggerResult.SkippedBusy} zurück, ohne einen Lauf + * zu starten. Wird der Lauf gestartet, kehrt die Methode erst nach + * vollständigem Abschluss zurück. + * + * @return Ergebnis des Laufs; nie {@code null} + */ + BatchRunTriggerResult triggerRun(); +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/BatchRunTriggerResult.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/BatchRunTriggerResult.java new file mode 100644 index 0000000..a62d074 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/BatchRunTriggerResult.java @@ -0,0 +1,87 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +import java.time.Instant; + +/** + * Ergebnis eines über {@link BatchRunTrigger#triggerRun()} ausgelösten + * Verarbeitungslaufs. + *

+ * Die sealed Hierarchie ermöglicht erschöpfendes Pattern-Matching: + *

+ *

+ * {@link Throwable}-Instanzen werden nicht im Result-Objekt transportiert. + * Stacktraces werden im Adapter geloggt; die GUI erhält ausschließlich + * benutzerfreundliche deutsche Meldungen. + */ +public sealed interface BatchRunTriggerResult + permits BatchRunTriggerResult.Started, + BatchRunTriggerResult.SkippedBusy, + BatchRunTriggerResult.Failed { + + /** + * Der Lauf wurde erfolgreich gestartet und abgeschlossen. + *

+ * Das Ergebnis enthält den Endzeitpunkt des Laufs sowie eine + * {@link RunSummary} mit den aggregierten Verarbeitungszählern. + * Ein No-op-Lauf (keine Kandidaten) ist ebenfalls {@code Started} + * und liefert eine {@link RunSummary} mit allen Zählern gleich null. + * + * @param endedAt Zeitpunkt, zu dem der Lauf abgeschlossen wurde + * @param summary Zusammenfassung der Verarbeitungsergebnisse + */ + record Started(Instant endedAt, RunSummary summary) + implements BatchRunTriggerResult { + + /** + * Validiert, dass Pflichtfelder nicht null sind. + */ + public Started { + if (endedAt == null) { + throw new IllegalArgumentException("endedAt darf nicht null sein"); + } + if (summary == null) { + throw new IllegalArgumentException("summary darf nicht null sein"); + } + } + } + + /** + * Der Lauf wurde übersprungen, weil bereits ein anderer Lauf aktiv war. + *

+ * {@link BatchRunTrigger#triggerRun()} kehrt sofort zurück, ohne + * einen neuen Lauf zu starten. Dieser Zustand ist kein Fehler. + */ + record SkippedBusy() implements BatchRunTriggerResult {} + + /** + * Der Lauf ist mit einem technischen Fehler beendet worden. + *

+ * Enthält eine benutzerlesbare deutsche Meldung für die GUI-Anzeige + * sowie eine technische Meldung für Diagnose-Logging. Der zugehörige + * Stacktrace wurde bereits im Adapter auf ERROR geloggt und wird hier + * nicht transportiert. + * + * @param userMessage deutsche, GUI-taugliche Fehlermeldung für den Endanwender + * @param technicalMessage technische Detailmeldung für Diagnose und Logging + */ + record Failed(String userMessage, String technicalMessage) + implements BatchRunTriggerResult { + + /** + * Validiert, dass Pflichtfelder nicht null sind. + */ + public Failed { + if (userMessage == null) { + throw new IllegalArgumentException("userMessage darf nicht null sein"); + } + if (technicalMessage == null) { + throw new IllegalArgumentException("technicalMessage darf nicht null sein"); + } + } + } +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ConfigurationFileLockException.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ConfigurationFileLockException.java new file mode 100644 index 0000000..5ac14be --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ConfigurationFileLockException.java @@ -0,0 +1,37 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Wird geworfen, wenn der exklusive OS-Lock auf die Konfigurationsdatei + * nicht erworben werden kann. + *

+ * Typische Ursachen sind: ein externer Prozess hält die Datei bereits + * gesperrt, ein Netzlaufwerk reagiert nicht innerhalb der Deadline, + * oder die Datei ist nicht zugänglich. + *

+ * Diese Ausnahme ist ungeprüft und wird vom Aufrufer + * ({@link ConfigurationFileLockPort#acquireLock()}) in eine + * benutzerfreundliche GUI-Meldung umgewandelt. + */ +public class ConfigurationFileLockException extends RuntimeException { + + /** + * Erstellt eine neue {@code ConfigurationFileLockException} mit der + * angegebenen Nachricht. + * + * @param message deutsche Fehlerbeschreibung + */ + public ConfigurationFileLockException(String message) { + super(message); + } + + /** + * Erstellt eine neue {@code ConfigurationFileLockException} mit + * Nachricht und Ursache. + * + * @param message deutsche Fehlerbeschreibung + * @param cause technische Ursache + */ + public ConfigurationFileLockException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ConfigurationFileLockPort.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ConfigurationFileLockPort.java new file mode 100644 index 0000000..186dc4d --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/ConfigurationFileLockPort.java @@ -0,0 +1,51 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Outbound-Port für den exklusiven OS-Lock auf die Konfigurationsdatei. + *

+ * Solange der Scheduler läuft oder ein Verarbeitungslauf aktiv ist, hält + * die Anwendung einen exklusiven Lock auf die {@code .properties}-Datei. + * Dieser Lock schützt vor konkurrierenden Schreibzugriffen durch externe + * Prozesse (z.B. Texteditoren). + *

+ * Der Lock wird als bestmöglicher OS-Level-Schreibschutz betrachtet und + * erhebt keinen Anspruch darauf, alle externen Schreibstrategien zu + * verhindern (z.B. Delete-Rename durch manche Editoren). + *

+ * Alle Operationen müssen im Worker-Thread ausgeführt werden, + * niemals auf dem JavaFX Application Thread. + */ +public interface ConfigurationFileLockPort { + + /** + * Erwirbt den exklusiven OS-Lock auf die Konfigurationsdatei. + *

+ * Falls die Datei bereits durch einen anderen Prozess gesperrt ist, + * wird der Erwerb mit einer konfigurierbaren Deadline-Schleife + * versucht. Schlägt der Erwerb nach Ablauf der Deadline fehl, + * wird eine {@link ConfigurationFileLockException} geworfen. + *

+ * Ist der Lock bereits durch diese Instanz gehalten, hat dieser + * Aufruf keine Wirkung. + * + * @throws ConfigurationFileLockException wenn der Lock nicht innerhalb + * der Deadline erworben werden kann + */ + void acquireLock() throws ConfigurationFileLockException; + + /** + * Gibt den exklusiven Lock frei. + *

+ * Ist kein Lock aktiv, hat dieser Aufruf keine Wirkung (idempotent). + * Implementierungen dürfen bei der Freigabe keine geprüfte Ausnahme + * werfen; Fehler werden geloggt und still übergangen. + */ + void releaseLock(); + + /** + * Prüft, ob der Lock aktuell von dieser Instanz gehalten wird. + * + * @return {@code true}, wenn der Lock aktiv ist + */ + boolean isLocked(); +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockHandle.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockHandle.java new file mode 100644 index 0000000..1f96bfb --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockHandle.java @@ -0,0 +1,26 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Handle für einen erworbenen Run-Lock. + *

+ * Dieses Interface ermöglicht die Nutzung des Run-Locks in einem + * try-with-resources-Block. Das Schließen des Handles gibt den Lock + * idempotent frei – mehrfaches Aufrufen von {@link #close()} ist sicher + * und hat nach dem ersten Aufruf keine Wirkung. + *

+ * Instanzen dieses Typs werden ausschließlich von + * {@link RunLockPort#tryAcquire()} erzeugt und dürfen nicht + * weitergegeben oder gecacht werden. + */ +public interface RunLockHandle extends AutoCloseable { + + /** + * Gibt den Run-Lock frei. + *

+ * Diese Methode ist idempotent: Mehrfaches Aufrufen hat nach dem + * ersten Aufruf keine weitere Wirkung. Implementierungen dürfen + * keine geprüfte Ausnahme werfen. + */ + @Override + void close(); +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockPort.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockPort.java index b8ba21a..d5088ab 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockPort.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunLockPort.java @@ -1,54 +1,76 @@ package de.gecheckt.pdf.umbenenner.application.port.out; +import java.util.Optional; + /** - * Outbound port for exclusive run locking. + * Outbound-Port für den exklusiven Run-Lock. *

- * This port abstracts the mechanism for ensuring that only one instance of the PDF Umbenenner - * is executing at any given time. The port defines the contract without prescribing the - * implementation (e.g., file-based locks, OS-level locks, distributed locks). + * Stellt sicher, dass zu einem Zeitpunkt nur eine Instanz des PDF-Umbenenners + * einen Verarbeitungslauf ausführt. Der Port abstrahiert den Mechanismus + * (z.B. dateibasierter Lock) ohne eine konkrete Implementierung vorzuschreiben. *

- * Responsibilities: + * Verantwortlichkeiten: *

*

- * Lock Lifecycle: - *

- * + * Lock-Lifecycle (blockierender Pfad – headless und manueller GUI-Lauf): + *
    + *
  1. Lock beim Laufstart erwerben ({@link #acquire()})
  2. + *
  3. Lock für die gesamte Dauer des Laufs halten
  4. + *
  5. Lock am Laufende freigeben ({@link #release()}), auch bei Fehler
  6. + *
+ * Lock-Lifecycle (nicht-blockierender Pfad – Scheduler-Tick): + *
    + *
  1. Lock nicht-blockierend versuchen ({@link #tryAcquire()})
  2. + *
  3. Bei leerem Optional sofort mit {@code SkippedBusy} abbrechen
  4. + *
  5. Bei vorhandenem Handle in try-with-resources verwenden
  6. + *
*/ public interface RunLockPort { /** - * Acquires an exclusive lock for the batch run. + * Erwirbt den exklusiven Run-Lock (blockierend). *

- * This method blocks or throws an exception if the lock cannot be acquired - * (e.g., another instance already holds it). The behavior depends on the implementation. - *

- * If this method returns normally, the caller holds the lock and must ensure - * {@link #release()} is called to free it, typically in a finally block. + * Wenn der Lock nicht erworben werden kann (z.B. weil eine andere + * Instanz ihn bereits hält), wird eine {@link RunLockUnavailableException} + * geworfen. Bei normalem Rücksprung hält der Aufrufer den Lock und muss + * {@link #release()} in einem {@code finally}-Block aufrufen. * - * @throws RunLockUnavailableException if the lock cannot be acquired - * (e.g., another instance already holds it or system error prevents acquiring) - * @throws RuntimeException for other critical lock-related failures + * @throws RunLockUnavailableException wenn der Lock nicht erworben werden kann + * @throws RuntimeException bei anderen kritischen Fehlern */ void acquire(); /** - * Releases the exclusive lock held by this batch run. + * Gibt den exklusiven Run-Lock frei. *

- * This method is called after batch processing completes (successfully or not) - * to allow other instances to run. - *

- * Implementations should handle the case where release is called multiple times - * or when no lock is currently held, avoiding exceptions if possible. + * Wird nach Abschluss des Laufs (erfolgreich oder fehlerhaft) aufgerufen. + * Implementierungen sollen keinen Fehler werfen, wenn der Lock bereits + * freigegeben wurde oder gar nicht gehalten wird. * - * @throws RuntimeException if lock release fails critically + * @throws RuntimeException wenn die Freigabe kritisch fehlschlägt */ void release(); + + /** + * Versucht nicht-blockierend, den Run-Lock zu erwerben. + *

+ * Gibt ein {@link RunLockHandle} zurück, wenn der Lock erfolgreich + * erworben wurde. Das Handle kann in einem try-with-resources-Block + * verwendet werden; {@link RunLockHandle#close()} gibt den Lock + * idempotent frei. + *

+ * Ist der Lock bereits durch eine andere Instanz gehalten, wird sofort + * {@link Optional#empty()} zurückgegeben – ohne zu warten oder zu queuen. + * Diese Methode ist race-condition-sicher und frei von check-then-act-Mustern. + * + * @return Handle mit dem erworbenen Lock, oder {@link Optional#empty()} + * wenn der Lock nicht verfügbar ist + * @throws RuntimeException bei kritischen technischen Fehlern beim + * Lock-Versuch (nicht bei normaler Nicht-Verfügbarkeit) + */ + Optional tryAcquire(); } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunSummary.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunSummary.java new file mode 100644 index 0000000..4fc2f71 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/RunSummary.java @@ -0,0 +1,37 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Zusammenfassung eines abgeschlossenen Verarbeitungslaufs. + *

+ * Enthält die aggregierten Zähler für erfolgreich verarbeitete, + * fehlgeschlagene und übersprungene Dokumente. Ein No-op-Lauf – + * bei dem keine Kandidaten im Quellordner gefunden wurden – wird + * durch {@code RunSummary(0, 0, 0)} repräsentiert. + * + * @param successCount Anzahl der in diesem Lauf erfolgreich verarbeiteten Dokumente + * @param failedCount Anzahl der in diesem Lauf fehlgeschlagenen Dokumente + * (retryable und final zusammengefasst) + * @param skippedCount Anzahl der in diesem Lauf übersprungenen Dokumente + * (bereits verarbeitet oder dauerhaft fehlgeschlagen) + */ +public record RunSummary(int successCount, int failedCount, int skippedCount) { + + /** + * Prüft, ob dieser Lauf ein No-op-Lauf war, d.h. keine Dokumente + * verarbeitet, fehlgeschlagen oder übersprungen wurden. + * + * @return {@code true}, wenn alle Zähler null sind + */ + public boolean isNoOp() { + return successCount == 0 && failedCount == 0 && skippedCount == 0; + } + + /** + * Erzeugt eine {@code RunSummary} für einen No-op-Lauf. + * + * @return {@code RunSummary} mit allen Zählern gleich null + */ + public static RunSummary noOp() { + return new RunSummary(0, 0, 0); + } +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerConfig.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerConfig.java new file mode 100644 index 0000000..009da28 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerConfig.java @@ -0,0 +1,26 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Betriebskonfiguration für den automatischen Scheduler. + *

+ * Enthält ausschließlich die für den Scheduler-Betrieb relevanten + * Laufzeitparameter. Die Konfiguration wird beim Start des Schedulers + * an {@link SchedulerPort#startScheduler(SchedulerConfig, BatchRunTrigger)} + * übergeben und bleibt für die Dauer des Scheduler-Betriebs unverändert. + * + * @param intervalSeconds Wartezeit in Sekunden zwischen dem Ende eines + * Laufs und dem Beginn des nächsten Ticks; + * muss mindestens 30 betragen + */ +public record SchedulerConfig(int intervalSeconds) { + + /** + * Validiert, dass das Intervall mindestens 30 Sekunden beträgt. + */ + public SchedulerConfig { + if (intervalSeconds < 30) { + throw new IllegalArgumentException( + "Scheduler-Intervall muss mindestens 30 Sekunden betragen, war: " + intervalSeconds); + } + } +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerPort.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerPort.java new file mode 100644 index 0000000..942e64c --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerPort.java @@ -0,0 +1,42 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Outbound-Port zur Steuerung des technischen Scheduler-Mechanismus. + *

+ * Kapselt die Infrastruktur für das periodische Polling (z.B. einen + * {@code ScheduledExecutorService}). Die fachliche Scheduler-Steuerung + * liegt im Use Case {@code SchedulerControlUseCase}; dieser Port + * delegiert ausschließlich den technischen Lifecycle-Start und -Stop. + *

+ * Abhängigkeitsrichtung: Application → Adapter (hexagonal outbound). + */ +public interface SchedulerPort { + + /** + * Startet den periodischen Scheduler-Mechanismus. + *

+ * Der erste Tick startet sofort (Initial Delay 0). Nachfolgende Ticks + * starten jeweils {@link SchedulerConfig#intervalSeconds()} Sekunden + * nach dem Ende des vorigen Ticks ({@code scheduleWithFixedDelay}). + *

+ * Der bereitgestellte {@link BatchRunTrigger} wird bei jedem Tick + * synchron aufgerufen. Der Scheduler-Adapter darf keine weiteren + * Entscheidungen treffen – er ruft {@code trigger.triggerRun()} auf + * und aktualisiert den Zustand anhand des Ergebnisses. + * + * @param config Betriebskonfiguration mit Intervall in Sekunden + * @param trigger Auslöser für den Verarbeitungslauf pro Tick + * @throws RuntimeException wenn der Scheduler-Mechanismus nicht + * gestartet werden kann + */ + void startScheduler(SchedulerConfig config, BatchRunTrigger trigger); + + /** + * Stoppt den periodischen Scheduler-Mechanismus. + *

+ * Laufende Ticks werden nicht abgebrochen; es werden lediglich keine + * weiteren Ticks geplant. Ist der Scheduler bereits gestoppt, hat + * dieser Aufruf keine Wirkung (idempotent). + */ + void stopScheduler(); +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettings.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettings.java new file mode 100644 index 0000000..2263659 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettings.java @@ -0,0 +1,35 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Persistierte Scheduler-Einstellungen aus der {@code .properties}-Datei. + *

+ * Dieses DTO repräsentiert die beiden Scheduler-Properties + * {@code scheduler.enabled} und {@code scheduler.interval.seconds}, + * wie sie aus der Konfigurationsdatei gelesen werden. Es wird von + * {@link SchedulerSettingsPort#loadSettings()} zurückgegeben und + * dient als Eingabe für die Autostart-Entscheidung und die + * Scheduler-Tab-Anzeige. + * + * @param enabled {@code true}, wenn der Scheduler beim nächsten + * Programmstart automatisch gestartet werden soll + * @param intervalSeconds konfigurierte Wartezeit in Sekunden zwischen + * Läufen; entspricht dem gelesenen Rohwert + * ohne weitere Validierung + */ +public record SchedulerSettings(boolean enabled, int intervalSeconds) { + + /** Standardwert für {@code scheduler.enabled}, wenn der Key fehlt oder leer ist. */ + public static final boolean DEFAULT_ENABLED = false; + + /** Standardwert für {@code scheduler.interval.seconds}, wenn der Key fehlt oder leer ist. */ + public static final int DEFAULT_INTERVAL_SECONDS = 180; + + /** + * Erzeugt eine {@code SchedulerSettings}-Instanz mit Standardwerten. + * + * @return Instanz mit {@code enabled=false} und {@code intervalSeconds=180} + */ + public static SchedulerSettings defaults() { + return new SchedulerSettings(DEFAULT_ENABLED, DEFAULT_INTERVAL_SECONDS); + } +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettingsPort.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettingsPort.java new file mode 100644 index 0000000..5ce60ff --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettingsPort.java @@ -0,0 +1,57 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Outbound-Port zum Lesen und Schreiben der Scheduler-Einstellungen + * in der {@code .properties}-Konfigurationsdatei. + *

+ * Schreiboperationen aktualisieren ausschließlich die beiden + * Scheduler-Keys ({@code scheduler.enabled} und + * {@code scheduler.interval.seconds}). Alle übrigen Zeilen, Kommentare + * und unbekannten Properties bleiben unverändert erhalten. + *

+ * Schreibvorgänge sind atomar: Sie erfolgen über eine temporäre Datei, + * die erst nach vollständigem Schreiben an den Zielort verschoben wird. + * Bei einem Fehler bleibt die Originaldatei unverändert. + *

+ * Die Implementierung teilt sich einen {@code FileChannel} mit dem + * {@link ConfigurationFileLockPort}-Adapter, damit Settings auch + * während eines aktiven OS-Locks geschrieben werden können. + */ +public interface SchedulerSettingsPort { + + /** + * Liest die aktuellen Scheduler-Einstellungen aus der + * Konfigurationsdatei. + *

+ * Fehlt ein Key oder ist er leer, wird der jeweilige Standardwert + * zurückgegeben (siehe {@link SchedulerSettings#defaults()}). + * Ungültige Werte (z.B. nicht-numerisches Intervall) werden als + * Fehler in {@code SchedulerSettings} signalisiert und führen nicht + * zu einer Exception in diesem Port. + * + * @return gelesene Scheduler-Einstellungen; nie {@code null} + */ + SchedulerSettings loadSettings(); + + /** + * Schreibt den Wert von {@code scheduler.enabled} in die + * Konfigurationsdatei. + *

+ * Alle übrigen Inhalte der Datei bleiben unverändert. + * + * @param enabled neuer Wert für {@code scheduler.enabled} + * @throws SchedulerSettingsWriteException wenn der Schreibvorgang fehlschlägt + */ + void saveEnabled(boolean enabled) throws SchedulerSettingsWriteException; + + /** + * Schreibt den Wert von {@code scheduler.interval.seconds} in die + * Konfigurationsdatei. + *

+ * Alle übrigen Inhalte der Datei bleiben unverändert. + * + * @param seconds neues Intervall in Sekunden; muss mindestens 30 betragen + * @throws SchedulerSettingsWriteException wenn der Schreibvorgang fehlschlägt + */ + void saveIntervalSeconds(int seconds) throws SchedulerSettingsWriteException; +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettingsWriteException.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettingsWriteException.java new file mode 100644 index 0000000..00bc374 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/out/SchedulerSettingsWriteException.java @@ -0,0 +1,37 @@ +package de.gecheckt.pdf.umbenenner.application.port.out; + +/** + * Wird geworfen, wenn das Schreiben der Scheduler-Einstellungen in die + * {@code .properties}-Datei fehlschlägt. + *

+ * Der Schreibvorgang ist atomar (über eine temporäre Datei), sodass die + * Konfigurationsdatei bei einem Fehler nicht in einem korrupten Zustand + * hinterlassen wird. Diese Ausnahme signalisiert, dass weder der neue + * noch ein partieller Stand geschrieben wurde. + *

+ * Ursachen können sein: fehlende Schreibrechte, Netzlaufwerksfehler, + * Festplatte voll oder ein aktiver exklusiver Lock durch einen anderen Prozess. + */ +public class SchedulerSettingsWriteException extends RuntimeException { + + /** + * Erstellt eine neue {@code SchedulerSettingsWriteException} mit der + * angegebenen Nachricht. + * + * @param message deutsche Fehlerbeschreibung + */ + public SchedulerSettingsWriteException(String message) { + super(message); + } + + /** + * Erstellt eine neue {@code SchedulerSettingsWriteException} mit + * Nachricht und Ursache. + * + * @param message deutsche Fehlerbeschreibung + * @param cause technische Ursache + */ + public SchedulerSettingsWriteException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java index 50cacdf..f022044 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java @@ -1121,6 +1121,11 @@ class BatchRunProcessingUseCaseTest { @Override public void release() { releaseCalled = true; } + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } + boolean wasAcquireCalled() { return acquireCalled; } boolean wasReleaseCalled() { return releaseCalled; } } @@ -1142,6 +1147,11 @@ class BatchRunProcessingUseCaseTest { @Override public void release() { releaseCount++; } + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } + int acquireCallCount() { return acquireCount; } int releaseCallCount() { return releaseCount; } } @@ -1157,6 +1167,11 @@ class BatchRunProcessingUseCaseTest { @Override public void release() { releaseCalled = true; } + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } + boolean wasAcquireCalled() { return acquireCalled; } boolean wasReleaseCalled() { return releaseCalled; } } diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java index 5108cf6..a742790 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java @@ -302,6 +302,10 @@ class BatchRunProgressObservationTest { private static final class NoOpLock implements RunLockPort { @Override public void acquire() { } @Override public void release() { } + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } } private static final class EmptyCandidatesPort implements SourceDocumentCandidatesPort { diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerConfigPathSemanticsTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerConfigPathSemanticsTest.java index c4a6e74..97d7266 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerConfigPathSemanticsTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerConfigPathSemanticsTest.java @@ -452,6 +452,10 @@ class BootstrapRunnerConfigPathSemanticsTest { private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort { @Override public void acquire() {} @Override public void release() {} + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } } private static class MockSchemaInitializationPort diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerEdgeCasesTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerEdgeCasesTest.java index 71abecb..87b578b 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerEdgeCasesTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerEdgeCasesTest.java @@ -571,6 +571,11 @@ class BootstrapRunnerEdgeCasesTest { @Override public void release() { } + + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } } private static class MockSchemaInitializationPort implements PersistenceSchemaInitializationPort { diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerStartupDispatchTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerStartupDispatchTest.java index a50e86a..f947333 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerStartupDispatchTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerStartupDispatchTest.java @@ -258,6 +258,10 @@ class BootstrapRunnerStartupDispatchTest { private static class MockRunLockPort implements de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort { @Override public void acquire() {} @Override public void release() {} + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } } private static class MockSchemaInitializationPort diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java index e02a597..1d8c471 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java @@ -550,6 +550,11 @@ class BootstrapRunnerTest { @Override public void release() { } + + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } } private static class MockSchemaInitializationPort implements PersistenceSchemaInitializationPort { diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapSmokeTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapSmokeTest.java index b279a75..b316d41 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapSmokeTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapSmokeTest.java @@ -191,6 +191,10 @@ class BootstrapSmokeTest { private static class NoOpRunLockPort implements RunLockPort { @Override public void acquire() { } @Override public void release() { } + @Override + public java.util.Optional tryAcquire() { + return java.util.Optional.empty(); + } } private static class NoOpSchemaInitializationPort implements PersistenceSchemaInitializationPort {