001package com.echothree.util.server.cdi; 002 003import java.lang.annotation.Annotation; 004import java.util.ArrayDeque; 005import java.util.Deque; 006import java.util.HashMap; 007import java.util.Map; 008import javax.enterprise.context.ContextNotActiveException; 009import javax.enterprise.context.spi.Context; 010import javax.enterprise.context.spi.Contextual; 011import javax.enterprise.context.spi.CreationalContext; 012import org.slf4j.Logger; 013import org.slf4j.LoggerFactory; 014 015public class CommandScopeContext implements Context { 016 017 private static final Logger log = LoggerFactory.getLogger(CommandScopeContext.class); 018 019 /** 020 * Per-thread stack of context layers. 021 * Each layer holds the contextual instances for that "sub-scope". 022 */ 023 private final ThreadLocal<Deque<ContextLayer>> layers = ThreadLocal.withInitial(ArrayDeque::new); 024 025 @Override 026 public Class<? extends Annotation> getScope() { 027 log.debug("CommandScopeContext.getScope()"); 028 return CommandScope.class; 029 } 030 031 @Override 032 public boolean isActive() { 033 final var isActive = !layers.get().isEmpty(); 034 035 log.debug("CommandScopeContext.isActive() = {}", isActive); 036 037 return isActive; 038 } 039 040 // -------------------------------------------------------------------------------- 041 // Public Lifecycle API 042 // -------------------------------------------------------------------------------- 043 044 /** 045 * Activate the root scope for the current thread if not already active. 046 * Typically called at the start of a request. 047 */ 048 public void activate() { 049 log.debug("CommandScopeContext.activate()"); 050 051 final var deque = layers.get(); 052 if(deque.isEmpty()) { 053 deque.push(new ContextLayer()); 054 } 055 } 056 057 /** 058 * Deactivate the scope for the current thread, destroying all layers. 059 * Typically called at the end of a request. 060 */ 061 public void deactivate() { 062 log.debug("CommandScopeContext.deactivate()"); 063 064 final var deque = layers.get(); 065 while (!deque.isEmpty()) { 066 destroyLayer(deque.pop()); 067 } 068 069 log.debug("layers empty, removing ThreadLocal"); 070 layers.remove(); 071 } 072 073 /** 074 * Push a new nested layer on top of the current one. 075 * Returns an AutoCloseable handle so you can use try-with-resources. 076 */ 077 public ScopeHandle push() { 078 log.debug("CommandScopeContext.push()"); 079 080 if(!isActive()) { 081 // You can choose to implicitly activate() here instead 082 throw new ContextNotActiveException("@StackedRequestScoped context is not active; call activate() first"); 083 } 084 085 layers.get().push(new ContextLayer()); 086 return new ScopeHandle(); 087 } 088 089 /** 090 * Pop the current layer, destroying its beans. 091 */ 092 public void pop() { 093 log.debug("CommandScopeContext.pop()"); 094 095 final var deque = layers.get(); 096 if(deque.isEmpty()) { 097 throw new ContextNotActiveException("No active @StackedRequestScoped layer to pop"); 098 } 099 100 final var layer = deque.pop(); 101 destroyLayer(layer); 102 103 if(deque.isEmpty()) { 104 log.debug("layers empty, removing ThreadLocal"); 105 106 // Optionally clean up thread-local completely 107 layers.remove(); 108 } else { 109 log.debug("{} layers remaining", deque.size()); 110 } 111 } 112 113 // -------------------------------------------------------------------------------- 114 // CDI Context Methods 115 // -------------------------------------------------------------------------------- 116 117 @Override 118 @SuppressWarnings("unchecked") 119 public <T> T get(final Contextual<T> contextual, final CreationalContext<T> creationalContext) { 120 log.debug("CommandScopeContext.get(Contextual<T> contextual, CreationalContext<T> creationalContext)"); 121 122 final var layer = currentLayer(); 123 var handle = (InstanceHandle<T>) layer.instances.get(contextual); 124 if(handle != null) { 125 return handle.instance; 126 } 127 128 if(creationalContext == null) { 129 return null; 130 } 131 132 final var instance = contextual.create(creationalContext); 133 handle = new InstanceHandle<>(instance, creationalContext); 134 layer.instances.put(contextual, handle); 135 136 return instance; 137 } 138 139 @Override 140 @SuppressWarnings("unchecked") 141 public <T> T get(final Contextual<T> contextual) { 142 log.debug("CommandScopeContext.get(Contextual<T> contextual)"); 143 144 final var layer = currentLayer(); 145 final var handle = (InstanceHandle<T>) layer.instances.get(contextual); 146 147 return handle != null ? handle.instance : null; 148 } 149 150 // -------------------------------------------------------------------------------- 151 // Internal Helpers 152 // -------------------------------------------------------------------------------- 153 154 private ContextLayer currentLayer() { 155 final var deque = layers.get(); 156 157 if(deque.isEmpty()) { 158 throw new ContextNotActiveException("@Command context is not active"); 159 } 160 161 return deque.peek(); 162 } 163 164 private void destroyLayer(final ContextLayer layer) { 165 for (var entry : layer.instances.entrySet()) { 166 final var contextual = entry.getKey(); 167 final var handle = entry.getValue(); 168 169 try { 170 // Generics on Contextual#destroy require matching <T> for both the instance and the 171 // CreationalContext<T>. Since we store them in wildcarded holders, cast safely here. 172 @SuppressWarnings("unchecked") 173 Contextual<Object> typedContextual = (Contextual<Object>) contextual; 174 @SuppressWarnings("unchecked") 175 CreationalContext<Object> cc = (CreationalContext<Object>) handle.creationalContext; 176 177 log.debug("destroying {}", handle.instance.getClass().getName()); 178 typedContextual.destroy(handle.instance, cc); 179 } catch (Exception e) { 180 // Log and continue; don't stop destroying other beans 181 log.warn("Exception destroying bean {}", handle.instance.getClass().getName(), e); 182 } 183 } 184 layer.instances.clear(); 185 } 186 187 private static final class ContextLayer { 188 final Map<Contextual<?>, InstanceHandle<?>> instances = new HashMap<>(); 189 } 190 191 private record InstanceHandle<T>(T instance, CreationalContext<T> creationalContext) { 192 } 193 194 /** 195 * Handle used for try-with-resources push/pop. 196 */ 197 public final class ScopeHandle implements AutoCloseable { 198 private boolean closed; 199 200 private ScopeHandle() { 201 } 202 203 @Override 204 public void close() { 205 if(!closed) { 206 pop(); 207 closed = true; 208 } 209 } 210 } 211 212}