ScanRunContext.java

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Egothor
// Copyright 2026 Accenture
package org.egothor.methodatlas;

import java.util.Optional;

/**
 * Thread-local holder for the current {@link ScanRun}, the JUL-friendly
 * equivalent of SLF4J's MDC.
 *
 * <p>
 * {@link MethodAtlasApp#run(String[], java.io.PrintWriter)} constructs a
 * {@link ScanRun} once at the top of every invocation and calls
 * {@link #set(ScanRun)} so that any code executed on the same thread for
 * the duration of the run can read the correlation id through
 * {@link #current()}. A custom {@code java.util.logging.Formatter} reads
 * the context and prepends the run id to every log record (introduced in
 * Item 20 of the architecture remediation plan).
 * </p>
 *
 * <h2>Thread safety</h2>
 *
 * <p>
 * Each thread sees its own value. The MethodAtlas CLI runs single-threaded
 * scans by default; AI provider calls are sequential. When concurrent
 * threads do appear (the {@link java.util.ServiceLoader} per-plugin
 * isolation), callers that want the run id on those threads must propagate
 * it explicitly.
 * </p>
 *
 * <h2>Cleanup</h2>
 *
 * <p>
 * Always pair {@link #set(ScanRun)} with a {@link #clear()} in a
 * {@code finally} block — without it, the thread-local reference outlives
 * the CLI invocation in container deployments that pool threads. The
 * standard MethodAtlas CLI exits the JVM at end-of-run, so cleanup matters
 * mainly when MethodAtlas is invoked programmatically.
 * </p>
 *
 * @since 1.0.0
 */
public final class ScanRunContext {

    private static final ThreadLocal<ScanRun> CURRENT = new ThreadLocal<>();

    private ScanRunContext() {
        // Utility class -- instantiation makes no sense.
    }

    /**
     * Sets the current scan run for the calling thread.
     *
     * @param run the run identifier; must not be {@code null}
     */
    public static void set(ScanRun run) {
        if (run == null) {
            throw new IllegalArgumentException("run must not be null; call clear() to remove the context");
        }
        CURRENT.set(run);
    }

    /**
     * Returns the current scan run for the calling thread, or
     * {@link Optional#empty()} when no run is currently set (the standard
     * case before {@link MethodAtlasApp#main(String[])} runs or after
     * {@link #clear()}).
     *
     * @return optional carrying the current run
     */
    public static Optional<ScanRun> current() {
        return Optional.ofNullable(CURRENT.get());
    }

    /**
     * Removes the current scan run from the calling thread. Always called in
     * a {@code finally} block paired with {@link #set(ScanRun)} so that the
     * thread-local reference does not outlive the CLI invocation.
     */
    public static void clear() {
        CURRENT.remove();
    }
}