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}