001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2024-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.common.context;
019
020import com.google.errorprone.annotations.ThreadSafe;
021
022import org.jspecify.annotations.Nullable;
023
024import java.util.Optional;
025
026/**
027 * TLC is the Thread Local {@link Context}. (Also known as "Tender Loving Care".)
028 *
029 * <p>This is useful to hold values that are user- or request-dependent.
030 *
031 * <p>For things which are "global", just use a {@link Singleton} instead.
032 *
033 * @author <a href="http://www.vorburger.ch">Michael Vorburger.ch</a>
034 */
035@ThreadSafe
036public final class TLC {
037
038    // TODO Use Java 21+ e JEP 446 --preview ScopedValue instead of ThreadLocal
039    // https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ScopedValue.html
040
041    private static final ThreadLocal<Context> threadLocalContext = new ThreadLocal<>();
042
043    /**
044     * Opens a new (!) {@link Context}, "stacked" over the current one (if any).
045     *
046     * <p>This is typically invoked from a try-with-resources, as the returned context must be
047     * closed again at some point; so the typical usage is: <code>try (var ctx = TLC.open()) {
048     * </code>.
049     */
050    public static Context open() {
051        Context next;
052        var previous = threadLocalContext.get();
053        if (previous != null) {
054            next = new Context(previous);
055        } else {
056            next = new Context();
057        }
058        setThreadLocalContext(next);
059        return next;
060    }
061
062    /* package local! */ static void setThreadLocalContext(Context context) {
063        threadLocalContext.set(context);
064    }
065
066    /**
067     * See {@link dev.enola.common.context.Context#get(Class)}.
068     *
069     * <p>Use {@link #optional(Enum)} to check if key is available in Context.
070     */
071    public static <K extends Enum<K> & Context.Key<T>, T> T get(K key) {
072        return context(key).get(key);
073    }
074
075    /** See {@link Context#optional(Enum)}. */
076    public static <K extends Enum<K> & Context.Key<T>, T> Optional<T> optional(K key) {
077        var tlc = threadLocalContext.get();
078        if (tlc == null) return Optional.empty();
079        return tlc.optional(key);
080    }
081
082    /** See {@link Context#get(java.lang.Class)}. */
083    public static <T> T get(Class<T> klass) {
084        return context(klass).get(klass);
085    }
086
087    public static <T> Optional<T> optional(Class<T> klass) {
088        var tlc = threadLocalContext.get();
089        if (tlc == null) return Optional.empty();
090        return tlc.optional(klass);
091    }
092
093    private static Context context(Object debug) {
094        var tlc = threadLocalContext.get();
095        if (tlc == null) {
096            throw new IllegalStateException(
097                    "Missing TLC.open() in call chain, can't get: " + debug);
098        }
099        return tlc;
100    }
101
102    /* package-local, always keep; never make public! */
103    static void reset(@Nullable Context context) {
104        threadLocalContext.set(context);
105    }
106
107    /* package-local, always keep; never make public! */
108    static @Nullable Context get() {
109        return threadLocalContext.get();
110    }
111
112    private TLC() {}
113}