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}