Fixe SonarQube Reliability-Issues S2789, S3077 und S2184

S2789 (32 Stellen): null-Checks auf Optional-Feldern entfernt bzw. durch
Objects.requireNonNullElse(field, Optional.empty()) ersetzt. Die zuvor
defensive Behandlung von null-Optionals erfolgt jetzt ueber den
Bibliotheksaufruf, sodass das Verhalten unveraendert bleibt, aber die
direkte Null-Pruefung gegen Optional entfaellt.

S3077 (5 Stellen): volatile-Felder mit Objekt-Referenzen durch
AtomicReference ersetzt (ScheduledExecutorServiceSchedulerAdapter,
BootstrapRunner.guiApplicationRunContext, PdfPreviewPane.currentDocument/
currentRenderer/currentSourceFile, SingleInstanceGuard.socket). Die
PdfPreviewPane-Felder werden auf JavaFX- bzw. Worker-Thread genutzt;
AtomicReference bietet hier konsistente atomare Publikation ohne
Verhaltensaenderung.

S2184 (3 Stellen): Integer-Division SECONDARY_SPACING / 2 durch
SECONDARY_SPACING / 2.0 ersetzt, damit das Insets-Argument als double
ohne implizite Truncierung berechnet wird.
This commit is contained in:
2026-05-07 17:11:29 +02:00
parent 11eac074ef
commit 32e32a9b27
18 changed files with 76 additions and 80 deletions
@@ -4,6 +4,7 @@ import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -105,7 +106,7 @@ public final class GuiSchedulerTab {
public GuiSchedulerTab( public GuiSchedulerTab(
Optional<SchedulerControlUseCase> schedulerUseCase, Optional<SchedulerControlUseCase> schedulerUseCase,
Supplier<Boolean> isConfigDirty) { Supplier<Boolean> isConfigDirty) {
this.schedulerUseCase = schedulerUseCase == null ? Optional.empty() : schedulerUseCase; this.schedulerUseCase = Objects.requireNonNullElse(schedulerUseCase, Optional.empty());
this.isConfigDirty = isConfigDirty != null ? isConfigDirty : () -> false; this.isConfigDirty = isConfigDirty != null ? isConfigDirty : () -> false;
tab.setClosable(false); tab.setClosable(false);
buildUi(); buildUi();
@@ -146,8 +146,8 @@ public record GuiStartupContext(
*/ */
public GuiStartupContext { public GuiStartupContext {
initialState = Objects.requireNonNull(initialState, "initialState must not be null"); initialState = Objects.requireNonNull(initialState, "initialState must not be null");
startupNotice = startupNotice == null ? Optional.empty() : startupNotice; startupNotice = Objects.requireNonNullElse(startupNotice, Optional.empty());
applicationContextError = applicationContextError == null ? Optional.empty() : applicationContextError; applicationContextError = Objects.requireNonNullElse(applicationContextError, Optional.empty());
configurationFileLoader = Objects.requireNonNull(configurationFileLoader, configurationFileLoader = Objects.requireNonNull(configurationFileLoader,
"configurationFileLoader must not be null"); "configurationFileLoader must not be null");
configurationFileWriter = Objects.requireNonNull(configurationFileWriter, configurationFileWriter = Objects.requireNonNull(configurationFileWriter,
@@ -191,8 +191,8 @@ public record GuiStartupContext(
"promptEditorPortFactory must not be null"); "promptEditorPortFactory must not be null");
createNewDatabasePort = Objects.requireNonNull(createNewDatabasePort, createNewDatabasePort = Objects.requireNonNull(createNewDatabasePort,
"createNewDatabasePort must not be null"); "createNewDatabasePort must not be null");
schedulerControlUseCase = schedulerControlUseCase == null ? Optional.empty() : schedulerControlUseCase; schedulerControlUseCase = Objects.requireNonNullElse(schedulerControlUseCase, Optional.empty());
configurationFileLockPort = configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort; configurationFileLockPort = Objects.requireNonNullElse(configurationFileLockPort, Optional.empty());
applicationContextInitializer = applicationContextInitializer == null applicationContextInitializer = applicationContextInitializer == null
? GuiApplicationContextInitializer.noOp() : applicationContextInitializer; ? GuiApplicationContextInitializer.noOp() : applicationContextInitializer;
} }
@@ -276,7 +276,7 @@ public final class GuiBatchRunCoordinator {
this.historicalDocumentContextPort = Objects.requireNonNull( this.historicalDocumentContextPort = Objects.requireNonNull(
historicalDocumentContextPort, "historicalDocumentContextPort must not be null"); historicalDocumentContextPort, "historicalDocumentContextPort must not be null");
this.configurationFileLockPort = this.configurationFileLockPort =
configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort; Objects.requireNonNullElse(configurationFileLockPort, Optional.empty());
} }
/** /**
@@ -33,7 +33,7 @@ public record GuiBatchRunLaunchOutcome(
* Compact constructor normalising the failure message holder. * Compact constructor normalising the failure message holder.
*/ */
public GuiBatchRunLaunchOutcome { public GuiBatchRunLaunchOutcome {
failureMessage = failureMessage == null ? Optional.empty() : failureMessage; failureMessage = Objects.requireNonNullElse(failureMessage, Optional.empty());
} }
/** /**
@@ -88,16 +88,16 @@ public record GuiBatchRunResultRow(
} }
Objects.requireNonNull(fingerprint, "fingerprint must not be null"); Objects.requireNonNull(fingerprint, "fingerprint must not be null");
Objects.requireNonNull(status, "status must not be null"); Objects.requireNonNull(status, "status must not be null");
finalFileName = finalFileName == null ? Optional.empty() : finalFileName; finalFileName = Objects.requireNonNullElse(finalFileName, Optional.empty());
correctedFileName = correctedFileName == null ? Optional.empty() : correctedFileName; correctedFileName = Objects.requireNonNullElse(correctedFileName, Optional.empty());
resolvedDate = resolvedDate == null ? Optional.empty() : resolvedDate; resolvedDate = Objects.requireNonNullElse(resolvedDate, Optional.empty());
aiReasoning = aiReasoning == null ? Optional.empty() : aiReasoning; aiReasoning = Objects.requireNonNullElse(aiReasoning, Optional.empty());
aiFailureMessage = aiFailureMessage == null ? Optional.empty() : aiFailureMessage; aiFailureMessage = Objects.requireNonNullElse(aiFailureMessage, Optional.empty());
Objects.requireNonNull(processingDuration, "processingDuration must not be null"); Objects.requireNonNull(processingDuration, "processingDuration must not be null");
if (processingDuration.isNegative()) { if (processingDuration.isNegative()) {
throw new IllegalArgumentException("processingDuration must not be negative"); throw new IllegalArgumentException("processingDuration must not be negative");
} }
historicalContext = historicalContext == null ? Optional.empty() : historicalContext; historicalContext = Objects.requireNonNullElse(historicalContext, Optional.empty());
} }
/** /**
@@ -302,7 +302,7 @@ public final class GuiBatchRunTab {
targetFolderSupplier, "targetFolderSupplier must not be null"); targetFolderSupplier, "targetFolderSupplier must not be null");
Optional<ConfigurationFileLockPort> effectiveLockPort = Optional<ConfigurationFileLockPort> effectiveLockPort =
configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort; Objects.requireNonNullElse(configurationFileLockPort, Optional.empty());
this.coordinator = new GuiBatchRunCoordinator( this.coordinator = new GuiBatchRunCoordinator(
(configPath, observer, token) -> (configPath, observer, token) ->
launcherSupplier.get().launch(configPath, observer, token), launcherSupplier.get().launch(configPath, observer, token),
@@ -618,7 +618,7 @@ public final class GuiBatchRunTab {
HBox selectionButtonBar = new HBox(SECONDARY_SPACING, reprocessButton, resetStatusButton); HBox selectionButtonBar = new HBox(SECONDARY_SPACING, reprocessButton, resetStatusButton);
selectionButtonBar.setAlignment(Pos.CENTER_LEFT); selectionButtonBar.setAlignment(Pos.CENTER_LEFT);
selectionButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2, 0, SECONDARY_SPACING / 2, 0)); selectionButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2.0, 0, SECONDARY_SPACING / 2.0, 0));
// Meldungsbereich unterhalb der Selektions-Buttons (linke Spalte) // Meldungsbereich unterhalb der Selektions-Buttons (linke Spalte)
messageArea.setId("batch-run-message-area"); messageArea.setId("batch-run-message-area");
@@ -1246,7 +1246,7 @@ public final class GuiBatchRunTab {
HBox runButtonBar = new HBox(SECONDARY_SPACING, startButton, cancelButton); HBox runButtonBar = new HBox(SECONDARY_SPACING, startButton, cancelButton);
runButtonBar.setAlignment(Pos.CENTER_LEFT); runButtonBar.setAlignment(Pos.CENTER_LEFT);
runButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2, 0, 0, 0)); runButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2.0, 0, 0, 0));
return runButtonBar; return runButtonBar;
} }
@@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -170,18 +171,18 @@ public final class PdfPreviewPane {
/** /**
* Aktuell geöffnetes PDF-Dokument. Zugriff ausschließlich vom Worker-Thread. * Aktuell geöffnetes PDF-Dokument. Zugriff ausschließlich vom Worker-Thread.
* {@code null} wenn kein Dokument geöffnet ist. * Leerer Referenzwert wenn kein Dokument geöffnet ist.
*/ */
private volatile PDDocument currentDocument = null; private final AtomicReference<PDDocument> currentDocument = new AtomicReference<>();
/** /**
* Renderer für das aktuell geöffnete Dokument. Zugriff ausschließlich vom Worker-Thread. * Renderer für das aktuell geöffnete Dokument. Zugriff ausschließlich vom Worker-Thread.
* {@code null} wenn kein Dokument geöffnet ist. * Leerer Referenzwert wenn kein Dokument geöffnet ist.
*/ */
private volatile PDFRenderer currentRenderer = null; private final AtomicReference<PDFRenderer> currentRenderer = new AtomicReference<>();
/** Aktuell geladene Quelldatei; null wenn keine Selektion vorliegt. */ /** Aktuell geladene Quelldatei; leerer Referenzwert wenn keine Selektion vorliegt. */
private volatile Path currentSourceFile = null; private final AtomicReference<Path> currentSourceFile = new AtomicReference<>();
/** Aktuell angezeigte Seite (1-basiert; 0 wenn keine Datei geladen). */ /** Aktuell angezeigte Seite (1-basiert; 0 wenn keine Datei geladen). */
private volatile int currentPage = 0; private volatile int currentPage = 0;
@@ -303,7 +304,7 @@ public final class PdfPreviewPane {
clear(); clear();
return; return;
} }
currentSourceFile = sourceFile; currentSourceFile.set(sourceFile);
currentPage = 0; currentPage = 0;
totalPages = -1; totalPages = -1;
pageCache.clear(); pageCache.clear();
@@ -318,7 +319,7 @@ public final class PdfPreviewPane {
* Muss auf dem JavaFX Application Thread aufgerufen werden. * Muss auf dem JavaFX Application Thread aufgerufen werden.
*/ */
public void clear() { public void clear() {
currentSourceFile = null; currentSourceFile.set(null);
currentPage = 0; currentPage = 0;
totalPages = -1; totalPages = -1;
pageCache.clear(); pageCache.clear();
@@ -472,12 +473,13 @@ public final class PdfPreviewPane {
try { try {
PDDocument doc = Loader.loadPDF(ioFile); PDDocument doc = Loader.loadPDF(ioFile);
currentDocument = doc; currentDocument.set(doc);
currentRenderer = new PDFRenderer(doc); PDFRenderer renderer = new PDFRenderer(doc);
currentRenderer.set(renderer);
int pages = Math.max(1, doc.getNumberOfPages()); int pages = Math.max(1, doc.getNumberOfPages());
BufferedImage buffered = BufferedImage buffered =
currentRenderer.renderImageWithDPI(0, RENDER_DPI, ImageType.RGB); renderer.renderImageWithDPI(0, RENDER_DPI, ImageType.RGB);
Image fxImage = SwingFXUtils.toFXImage(buffered, null); Image fxImage = SwingFXUtils.toFXImage(buffered, null);
final int totalPagesFinal = pages; final int totalPagesFinal = pages;
@@ -513,7 +515,7 @@ public final class PdfPreviewPane {
* @param seq die Sequenznummer dieser Anforderung * @param seq die Sequenznummer dieser Anforderung
*/ */
private void renderPageOnWorker(int page, long seq) { private void renderPageOnWorker(int page, long seq) {
PDFRenderer renderer = currentRenderer; PDFRenderer renderer = currentRenderer.get();
if (renderer == null) { if (renderer == null) {
// Dokument wurde zwischenzeitlich geschlossen nichts zu tun // Dokument wurde zwischenzeitlich geschlossen nichts zu tun
return; return;
@@ -542,9 +544,8 @@ public final class PdfPreviewPane {
* auf dem Worker-Thread und ist idempotent. * auf dem Worker-Thread und ist idempotent.
*/ */
private void closeCurrentDocumentOnWorker() { private void closeCurrentDocumentOnWorker() {
PDDocument doc = currentDocument; PDDocument doc = currentDocument.getAndSet(null);
currentDocument = null; currentRenderer.set(null);
currentRenderer = null;
if (doc != null) { if (doc != null) {
try { try {
doc.close(); doc.close();
@@ -824,7 +825,7 @@ public final class PdfPreviewPane {
} }
private void updateNavigationButtons() { private void updateNavigationButtons() {
boolean canNavigate = enabled && currentSourceFile != null && totalPages > 0; boolean canNavigate = enabled && currentSourceFile.get() != null && totalPages > 0;
prevButton.setDisable(!canNavigate || currentPage <= 1); prevButton.setDisable(!canNavigate || currentPage <= 1);
nextButton.setDisable(!canNavigate || currentPage >= totalPages); nextButton.setDisable(!canNavigate || currentPage >= totalPages);
} }
@@ -29,10 +29,10 @@ public record GuiConfigurationEditorState(
* @param values current editable configuration values; must not be {@code null} * @param values current editable configuration values; must not be {@code null}
*/ */
public GuiConfigurationEditorState { public GuiConfigurationEditorState {
loadedFileSnapshot = loadedFileSnapshot == null ? Optional.empty() : loadedFileSnapshot; loadedFileSnapshot = Objects.requireNonNullElse(loadedFileSnapshot, Optional.empty());
baselineValues = Objects.requireNonNull(baselineValues, "baselineValues must not be null"); baselineValues = Objects.requireNonNull(baselineValues, "baselineValues must not be null");
values = Objects.requireNonNull(values, "values must not be null"); values = Objects.requireNonNull(values, "values must not be null");
pendingMigrationMessage = pendingMigrationMessage == null ? Optional.empty() : pendingMigrationMessage; pendingMigrationMessage = Objects.requireNonNullElse(pendingMigrationMessage, Optional.empty());
} }
/** /**
@@ -39,7 +39,7 @@ public record GuiMessageEntry(
Objects.requireNonNull(severity, "severity must not be null"); Objects.requireNonNull(severity, "severity must not be null");
Objects.requireNonNull(text, "text must not be null"); Objects.requireNonNull(text, "text must not be null");
Objects.requireNonNull(timestamp, "timestamp must not be null"); Objects.requireNonNull(timestamp, "timestamp must not be null");
source = source == null ? Optional.empty() : source; source = Objects.requireNonNullElse(source, Optional.empty());
} }
/** /**
@@ -54,7 +54,7 @@ public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {
*/ */
final AtomicReference<BatchRunTrigger> currentTrigger = new AtomicReference<>(); final AtomicReference<BatchRunTrigger> currentTrigger = new AtomicReference<>();
private volatile ScheduledExecutorService executor; private final AtomicReference<ScheduledExecutorService> executor = new AtomicReference<>();
/** /**
* Erstellt einen neuen Adapter. * Erstellt einen neuen Adapter.
@@ -85,7 +85,7 @@ public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {
public void startScheduler(SchedulerConfig config, BatchRunTrigger trigger) { public void startScheduler(SchedulerConfig config, BatchRunTrigger trigger) {
Objects.requireNonNull(config, "config darf nicht null sein"); Objects.requireNonNull(config, "config darf nicht null sein");
Objects.requireNonNull(trigger, "trigger darf nicht null sein"); Objects.requireNonNull(trigger, "trigger darf nicht null sein");
if (executor != null) { if (executor.get() != null) {
logger.debug("Scheduler ist bereits aktiv Start-Aufruf wird ignoriert."); logger.debug("Scheduler ist bereits aktiv Start-Aufruf wird ignoriert.");
return; return;
} }
@@ -105,7 +105,7 @@ public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {
0L, 0L,
config.intervalSeconds(), config.intervalSeconds(),
TimeUnit.SECONDS); TimeUnit.SECONDS);
executor = newExecutor; executor.set(newExecutor);
logger.info("Scheduler gestartet. Intervall: {} Sekunden.", config.intervalSeconds()); logger.info("Scheduler gestartet. Intervall: {} Sekunden.", config.intervalSeconds());
} }
@@ -118,12 +118,11 @@ public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {
*/ */
@Override @Override
public void stopScheduler() { public void stopScheduler() {
ScheduledExecutorService localExecutor = executor; ScheduledExecutorService localExecutor = executor.getAndSet(null);
if (localExecutor == null) { if (localExecutor == null) {
logger.debug("Scheduler ist bereits gestoppt Stop-Aufruf wird ignoriert."); logger.debug("Scheduler ist bereits gestoppt Stop-Aufruf wird ignoriert.");
return; return;
} }
executor = null;
currentTrigger.set(null); currentTrigger.set(null);
localExecutor.shutdown(); localExecutor.shutdown();
logger.info("Scheduler angehalten."); logger.info("Scheduler angehalten.");
@@ -1,6 +1,7 @@
package de.gecheckt.pdf.umbenenner.application.port.in; package de.gecheckt.pdf.umbenenner.application.port.in;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
/** /**
@@ -37,9 +38,9 @@ public record HistoricalDocumentContext(
* {@code lastFailureInstant} {@code null} sind * {@code lastFailureInstant} {@code null} sind
*/ */
public HistoricalDocumentContext { public HistoricalDocumentContext {
lastTargetFileName = lastTargetFileName == null ? Optional.empty() : lastTargetFileName; lastTargetFileName = Objects.requireNonNullElse(lastTargetFileName, Optional.empty());
lastSuccessInstant = lastSuccessInstant == null ? Optional.empty() : lastSuccessInstant; lastSuccessInstant = Objects.requireNonNullElse(lastSuccessInstant, Optional.empty());
lastFailureInstant = lastFailureInstant == null ? Optional.empty() : lastFailureInstant; lastFailureInstant = Objects.requireNonNullElse(lastFailureInstant, Optional.empty());
} }
/** /**
@@ -1,6 +1,7 @@
package de.gecheckt.pdf.umbenenner.application.port.in; package de.gecheckt.pdf.umbenenner.application.port.in;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary; import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
@@ -48,18 +49,10 @@ public record SchedulerStatus(
if (state == null) { if (state == null) {
throw new IllegalArgumentException("state darf nicht null sein"); throw new IllegalArgumentException("state darf nicht null sein");
} }
if (lastRunEndedAt == null) { Objects.requireNonNull(lastRunEndedAt, "lastRunEndedAt darf nicht null sein");
throw new IllegalArgumentException("lastRunEndedAt darf nicht null sein"); Objects.requireNonNull(lastRunSummary, "lastRunSummary darf nicht null sein");
} Objects.requireNonNull(nextTickAt, "nextTickAt darf nicht null sein");
if (lastRunSummary == null) { Objects.requireNonNull(lastError, "lastError darf nicht null sein");
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");
}
} }
/** /**
@@ -32,7 +32,7 @@ public record EffectiveApiKeyDescriptor(
*/ */
public EffectiveApiKeyDescriptor { public EffectiveApiKeyDescriptor {
Objects.requireNonNull(origin, "origin must not be null"); Objects.requireNonNull(origin, "origin must not be null");
envVarName = envVarName == null ? Optional.empty() : envVarName; envVarName = Objects.requireNonNullElse(envVarName, Optional.empty());
} }
/** /**
@@ -45,7 +45,7 @@ public record ModelCatalogRequest(
if (timeoutSeconds <= 0) { if (timeoutSeconds <= 0) {
throw new IllegalArgumentException("timeoutSeconds must be positive, was: " + timeoutSeconds); throw new IllegalArgumentException("timeoutSeconds must be positive, was: " + timeoutSeconds);
} }
baseUrl = baseUrl == null ? Optional.empty() : baseUrl; baseUrl = Objects.requireNonNullElse(baseUrl, Optional.empty());
apiKey = apiKey == null ? Optional.empty() : apiKey; apiKey = Objects.requireNonNullElse(apiKey, Optional.empty());
} }
} }
@@ -38,7 +38,7 @@ public record EditorValidationFinding(
public EditorValidationFinding { public EditorValidationFinding {
Objects.requireNonNull(severity, "severity must not be null"); Objects.requireNonNull(severity, "severity must not be null");
Objects.requireNonNull(message, "message must not be null"); Objects.requireNonNull(message, "message must not be null");
fieldKey = fieldKey == null ? Optional.empty() : fieldKey; fieldKey = Objects.requireNonNullElse(fieldKey, Optional.empty());
} }
/** /**
@@ -89,9 +89,7 @@ public sealed interface CheckpointResult
Objects.requireNonNull(checkpointId, "checkpointId must not be null"); Objects.requireNonNull(checkpointId, "checkpointId must not be null");
Objects.requireNonNull(severity, "severity must not be null"); Objects.requireNonNull(severity, "severity must not be null");
Objects.requireNonNull(message, "message must not be null"); Objects.requireNonNull(message, "message must not be null");
correctionSuggestion = correctionSuggestion == null correctionSuggestion = Objects.requireNonNullElse(correctionSuggestion, Optional.empty());
? Optional.empty()
: correctionSuggestion;
} }
/** /**
@@ -13,6 +13,7 @@ import java.util.Optional;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -260,10 +261,11 @@ public class BootstrapRunner {
* or via the {@link GuiApplicationContextInitializer} callback that the workspace invokes * or via the {@link GuiApplicationContextInitializer} callback that the workspace invokes
* on a background thread after each successful file open. Read by * on a background thread after each successful file open. Read by
* {@link #launchGuiBatchRun}, {@link #launchGuiMiniBatchRun}, and * {@link #launchGuiBatchRun}, {@link #launchGuiMiniBatchRun}, and
* {@link #resetDocumentStatusForGui}. {@code volatile} ensures visibility across threads * {@link #resetDocumentStatusForGui}. {@link AtomicReference} ensures atomic publication
* without explicit synchronisation on the happy path. * of the context across threads.
*/ */
private volatile Optional<ApplicationRunContext> guiApplicationRunContext = Optional.empty(); private final AtomicReference<Optional<ApplicationRunContext>> guiApplicationRunContext =
new AtomicReference<>(Optional.empty());
/** /**
* Der Scheduler-Use-Case, der beim GUI-Start mit einer gültigen Konfiguration * Der Scheduler-Use-Case, der beim GUI-Start mit einer gültigen Konfiguration
@@ -1083,7 +1085,7 @@ public class BootstrapRunner {
result -> LOG.debug("Scheduler-Tick-Ergebnis: {}", result -> LOG.debug("Scheduler-Tick-Ergebnis: {}",
result.getClass().getSimpleName())); result.getClass().getSimpleName()));
BatchRunTrigger batchRunTrigger = () -> { BatchRunTrigger batchRunTrigger = () -> {
Optional<ApplicationRunContext> ctxOpt = guiApplicationRunContext; Optional<ApplicationRunContext> ctxOpt = guiApplicationRunContext.get();
if (ctxOpt.isEmpty()) { if (ctxOpt.isEmpty()) {
return new BatchRunTriggerResult.Failed( return new BatchRunTriggerResult.Failed(
"Kein Anwendungskontext verfügbar.", "Kein Anwendungskontext verfügbar.",
@@ -1248,28 +1250,28 @@ public class BootstrapRunner {
migrateConfigurationIfNeeded(configFilePath); migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath); StartConfiguration config = loadAndValidateConfiguration(configFilePath);
initializeSchema(config); initializeSchema(config);
guiApplicationRunContext = Optional.of( guiApplicationRunContext.set(Optional.of(
new ApplicationRunContext(config, resolveActiveJdbcUrl(config))); new ApplicationRunContext(config, resolveActiveJdbcUrl(config))));
LOG.info("GUI-Anwendungskontext initialisiert für Konfiguration: {}", configFilePath); LOG.info("GUI-Anwendungskontext initialisiert für Konfiguration: {}", configFilePath);
return Optional.empty(); return Optional.empty();
} catch (ConfigurationLoadingException e) { } catch (ConfigurationLoadingException e) {
LOG.warn("GUI-Anwendungskontext: Konfiguration konnte nicht geladen werden: {}", LOG.warn("GUI-Anwendungskontext: Konfiguration konnte nicht geladen werden: {}",
e.getMessage()); e.getMessage());
guiApplicationRunContext = Optional.empty(); guiApplicationRunContext.set(Optional.empty());
return Optional.of(CONFIG_LOAD_FAILED_PREFIX + e.getMessage()); return Optional.of(CONFIG_LOAD_FAILED_PREFIX + e.getMessage());
} catch (InvalidStartConfigurationException e) { } catch (InvalidStartConfigurationException e) {
LOG.warn("GUI-Anwendungskontext: Konfiguration nicht lauffähig: {}", e.getMessage()); LOG.warn("GUI-Anwendungskontext: Konfiguration nicht lauffähig: {}", e.getMessage());
guiApplicationRunContext = Optional.empty(); guiApplicationRunContext.set(Optional.empty());
return Optional.of("Konfiguration nicht lauffähig: " + e.getMessage()); return Optional.of("Konfiguration nicht lauffähig: " + e.getMessage());
} catch (DocumentPersistenceException e) { } catch (DocumentPersistenceException e) {
LOG.warn("GUI-Anwendungskontext: SQLite-Initialisierung fehlgeschlagen: {}", LOG.warn("GUI-Anwendungskontext: SQLite-Initialisierung fehlgeschlagen: {}",
e.getMessage()); e.getMessage());
guiApplicationRunContext = Optional.empty(); guiApplicationRunContext.set(Optional.empty());
return Optional.of("SQLite konnte nicht initialisiert werden: " + e.getMessage()); return Optional.of("SQLite konnte nicht initialisiert werden: " + e.getMessage());
} catch (RuntimeException e) { } catch (RuntimeException e) {
LOG.warn("GUI-Anwendungskontext: Unerwarteter Fehler bei Initialisierung: {}", LOG.warn("GUI-Anwendungskontext: Unerwarteter Fehler bei Initialisierung: {}",
e.getMessage()); e.getMessage());
guiApplicationRunContext = Optional.empty(); guiApplicationRunContext.set(Optional.empty());
return Optional.of("Unerwarteter Fehler bei der Kontextinitialisierung: " return Optional.of("Unerwarteter Fehler bei der Kontextinitialisierung: "
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage())); + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
} }
@@ -1422,7 +1424,7 @@ public class BootstrapRunner {
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null"); Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath); LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext; Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) { if (ctx.isPresent()) {
LOG.debug("GUI-Verarbeitungslauf: Verwende vorbereiteten Anwendungskontext."); LOG.debug("GUI-Verarbeitungslauf: Verwende vorbereiteten Anwendungskontext.");
return executeRun(ctx.get(), progressObserver, cancellationToken); return executeRun(ctx.get(), progressObserver, cancellationToken);
@@ -1512,7 +1514,7 @@ public class BootstrapRunner {
LOG.info("GUI-Mini-Verarbeitungslauf: Startanforderung für {} Dokument(e), Konfiguration {}.", LOG.info("GUI-Mini-Verarbeitungslauf: Startanforderung für {} Dokument(e), Konfiguration {}.",
fingerprintFilter.size(), configFilePath); fingerprintFilter.size(), configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext; Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) { if (ctx.isPresent()) {
LOG.debug("GUI-Mini-Verarbeitungslauf: Verwende vorbereiteten Anwendungskontext."); LOG.debug("GUI-Mini-Verarbeitungslauf: Verwende vorbereiteten Anwendungskontext.");
return executeRunWithFilter(ctx.get(), fingerprintFilter, progressObserver, cancellationToken); return executeRunWithFilter(ctx.get(), fingerprintFilter, progressObserver, cancellationToken);
@@ -1595,7 +1597,7 @@ public class BootstrapRunner {
LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.", LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.",
fingerprints.size(), configFilePath); fingerprints.size(), configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext; Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) { if (ctx.isPresent()) {
LOG.debug("GUI-Status-Reset: Verwende vorbereiteten Anwendungskontext."); LOG.debug("GUI-Status-Reset: Verwende vorbereiteten Anwendungskontext.");
return executeResetWithContext(ctx.get(), fingerprints); return executeResetWithContext(ctx.get(), fingerprints);
@@ -2181,7 +2183,7 @@ public class BootstrapRunner {
* @return aktive JDBC-URL; nie {@code null} * @return aktive JDBC-URL; nie {@code null}
*/ */
private String resolveJdbcUrlForGui(Path configFilePath) { private String resolveJdbcUrlForGui(Path configFilePath) {
Optional<ApplicationRunContext> ctx = guiApplicationRunContext; Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) { if (ctx.isPresent()) {
return ctx.get().jdbcUrl(); return ctx.get().jdbcUrl();
} }
@@ -2204,7 +2206,7 @@ public class BootstrapRunner {
* @return validierte Konfiguration; nie {@code null} * @return validierte Konfiguration; nie {@code null}
*/ */
private StartConfiguration resolveStartConfigurationForGui(Path configFilePath) { private StartConfiguration resolveStartConfigurationForGui(Path configFilePath) {
Optional<ApplicationRunContext> ctx = guiApplicationRunContext; Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) { if (ctx.isPresent()) {
return ctx.get().startConfiguration(); return ctx.get().startConfiguration();
} }
@@ -5,6 +5,7 @@ import java.net.BindException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* Prozessweiter Einzelinstanz-Schutz auf Basis eines Loopback-ServerSocket-Binds. * Prozessweiter Einzelinstanz-Schutz auf Basis eines Loopback-ServerSocket-Binds.
@@ -44,7 +45,7 @@ public class SingleInstanceGuard implements AutoCloseable {
public static final int DEFAULT_PORT = 47832; public static final int DEFAULT_PORT = 47832;
private final int port; private final int port;
private volatile ServerSocket socket; private final AtomicReference<ServerSocket> socket = new AtomicReference<>();
private final AtomicBoolean closed = new AtomicBoolean(false); private final AtomicBoolean closed = new AtomicBoolean(false);
/** /**
@@ -85,7 +86,7 @@ public class SingleInstanceGuard implements AutoCloseable {
try { try {
InetAddress loopback = InetAddress.getLoopbackAddress(); InetAddress loopback = InetAddress.getLoopbackAddress();
ServerSocket serverSocket = new ServerSocket(port, 1, loopback); ServerSocket serverSocket = new ServerSocket(port, 1, loopback);
this.socket = serverSocket; this.socket.set(serverSocket);
Runtime.getRuntime().addShutdownHook(new Thread(() -> schliesseSilent(serverSocket), Runtime.getRuntime().addShutdownHook(new Thread(() -> schliesseSilent(serverSocket),
"single-instance-guard-shutdown")); "single-instance-guard-shutdown"));
} catch (BindException e) { } catch (BindException e) {
@@ -106,7 +107,7 @@ public class SingleInstanceGuard implements AutoCloseable {
@Override @Override
public void close() { public void close() {
if (closed.compareAndSet(false, true)) { if (closed.compareAndSet(false, true)) {
schliesseSilent(socket); schliesseSilent(socket.get());
} }
} }