001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017-2018 microBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.cdi;
018
019import java.lang.Thread.UncaughtExceptionHandler;
020
021import java.util.IdentityHashMap;
022import java.util.Objects;
023
024import java.util.concurrent.CountDownLatch;
025
026import javax.annotation.Priority;
027
028import javax.enterprise.context.ApplicationScoped;
029import javax.enterprise.context.BeforeDestroyed;
030
031import javax.enterprise.event.Observes;
032
033import javax.enterprise.inject.spi.Extension;
034
035import javax.interceptor.Interceptor;
036
037import java.util.logging.Level;
038import java.util.logging.Logger;
039
040/**
041 * An {@code abstract} {@link Extension} whose implementations can
042 * legally and politely prevent a CDI container from exiting.
043 *
044 * <p>The most common use case for such an implementation is an {@link
045 * Extension} that wishes to launch a server of some kind and to
046 * notionally block until someone interrupts the process.</p>
047 *
048 * @author <a href="https://about.me/lairdnelson"
049 * target="_parent">Laird Nelson</a>
050 *
051 * @see CountDownLatch
052 *
053 * @see CountDownLatch#countDown()
054 *
055 * @see CountDownLatch#await()
056 */
057public abstract class AbstractBlockingExtension implements Extension {
058
059
060  /*
061   * Static fields.
062   */
063
064
065  /**
066   * An {@link IdentityHashMap} tracking all known instances of the
067   * {@link AbstractBlockingExtension} class for {@linkplain
068   * #unblockAll() unblocking purposes}.
069   *
070   * @see #unblockAll()
071   *
072   * @see #unblock(boolean)
073   */
074  private static final IdentityHashMap<AbstractBlockingExtension, Void> instances = new IdentityHashMap<>(5);
075
076  /**
077   * Static initializer; decorates any {@linkplain
078   * Thread#getUncaughtExceptionHandler() existing
079   * <tt>UncaughtExceptionHandler</tt>} with one that calls {@link
080   * #unblockAll()}.
081   */
082  static {
083    try {
084      installExceptionHandler();
085    } catch (final RuntimeException securityExceptionProbably) {
086      // We take great care not to call anything with .class in it
087      // since we're still initializing the class!
088      final Logger logger = Logger.getLogger("org.microbean.cdi.AbstractBlockingExtension");
089      assert logger != null;
090      if (logger.isLoggable(Level.WARNING)) {
091        logger.logp(Level.WARNING, "org.microbean.cdi.AbstractBlockingExtension", "<static init>", securityExceptionProbably.getMessage(), securityExceptionProbably);
092      }
093    }
094  }
095  
096
097  /*
098   * Instance fields.
099   */
100
101
102  /**
103   * The {@link CountDownLatch} governing blocking behavior.
104   *
105   * <p>This field may be {@code null}.</p>
106   *
107   * @see #AbstractBlockingExtension(CountDownLatch)
108   */
109  private final CountDownLatch latch;
110
111  /**
112   * The {@link Logger} used by instances of this class.
113   *
114   * <p>This field is never {@code null}.</p>
115   *
116   * @see #createLogger()
117   */
118  protected final Logger logger;
119
120
121  /*
122   * Constructors.
123   */
124  
125
126  /**
127   * Creates a new {@link AbstractBlockingExtension} by calling the
128   * {@linkplain #AbstractBlockingExtension(CountDownLatch)
129   * appropriate constructor} with a new {@link CountDownLatch} with
130   * an {@linkplain CountDownLatch#getCount() initial count} of {@code
131   * 1}.
132   *
133   * @see #AbstractBlockingExtension(CountDownLatch)
134   */
135  protected AbstractBlockingExtension() {
136    this(new CountDownLatch(1));
137  }
138
139  /**
140   * Creates a new {@link AbstractBlockingExtension} that uses the
141   * supplied {@link CountDownLatch} for governing blocking behavior,
142   * and installs a {@linkplain Runtime#addShutdownHook(Thread)
143   * shutdown hook} that (indirectly) calls {@link
144   * CountDownLatch#countDown() countDown()} on the supplied {@code
145   * latch}.
146   *
147   * @param latch a {@link CountDownLatch} whose {@link
148   * CountDownLatch#countDown()} and {@link CountDownLatch#await()}
149   * methods will be used to control blocking behavior; may be {@code
150   * null} in which case no blocking behavior will take place
151   */
152  protected AbstractBlockingExtension(final CountDownLatch latch) {
153    super();
154    this.logger = this.createLogger();
155    if (this.logger == null) {
156      throw new IllegalStateException("createLogger() == null");
157    }
158    final String cn = this.getClass().getName();
159    final String mn = "<init>";
160    if (this.logger.isLoggable(Level.FINER)) {
161      this.logger.entering(cn, mn, latch);
162    }
163    this.latch = latch;
164    if (latch != null) {
165      Runtime.getRuntime().addShutdownHook(new ShutdownHook());
166      synchronized (instances) {
167        instances.put(this, null);
168      }
169    }
170    if (this.logger.isLoggable(Level.FINER)) {
171      this.logger.exiting(cn, mn);
172    }
173  }
174
175
176  /*
177   * Instance methods.
178   */
179
180
181  /**
182   * Returns a {@link Logger} suitable for use with this {@link
183   * AbstractBlockingExtension} implementation.
184   *
185   * <p>This method never returns {@code null}.</p>
186   *
187   * <p>Overrides of this method must not return {@code null}.</p>
188   *
189   * @return a non-{@code null} {@link Logger}
190   */
191  protected Logger createLogger() {
192    return Logger.getLogger(this.getClass().getName());
193  }
194
195  /**
196   * Blocks the main CDI thread immediately before any other recipient
197   * of the {@link BeforeDestroyed
198   * BeforeDestroyed(ApplicationScoped.class)} event is notified and
199   * waits for some other thread to call the {@link #unblock()}
200   * method.
201   *
202   * <p>Note that since the {@link #unblock()} method has no side
203   * effects, this very thread could have already called it, in which
204   * case no blocking behavior will be observed.</p>
205   *
206   * @param event the event in question; may be {@code null}; ignored
207   *
208   * @exception InterruptedException if the current {@link Thread} is
209   * {@linkplain Thread#interrupt() interrupted}
210   *
211   * @see #unblock()
212   *
213   * @see #unblockAll()
214   *
215   * @see Interceptor.Priority#PLATFORM_BEFORE
216   */
217  private final void block(@Observes
218                           @BeforeDestroyed(ApplicationScoped.class)
219                           @Priority(Interceptor.Priority.PLATFORM_BEFORE - 1)
220                           final Object event)
221    throws InterruptedException {
222    final String cn = this.getClass().getName();
223    final String mn = "block";
224    if (this.logger.isLoggable(Level.FINER)) {
225      this.logger.entering(cn, mn, event);
226    }
227    if (this.latch != null) {
228      this.latch.await();
229    }
230    if (this.logger.isLoggable(Level.FINER)) {
231      this.logger.exiting(cn, mn);
232    }
233  }
234
235  /**
236   * Calls {@link CountDownLatch#countDown()} on the {@link
237   * CountDownLatch} {@linkplain
238   * #AbstractBlockingExtension(CountDownLatch) supplied at
239   * construction time}.
240   *
241   * <p>This method may be invoked more than once without any side
242   * effects.</p>
243   *
244   * @see #unblockAll()
245   */
246  public void unblock() {
247    this.unblock(true);
248  }
249
250  /**
251   * Calls {@link CountDownLatch#countDown()} on the {@link
252   * CountDownLatch} {@linkplain
253   * #AbstractBlockingExtension(CountDownLatch) supplied at
254   * construction time}.
255   *
256   * <p>This method may be invoked more than once without any side
257   * effects.</p>
258   *
259   * @param remove whether to remove this {@link
260   * AbstractBlockingExtension} from {@linkplain #instances the
261   * internal set of <code>AbstractBlockingExtension</code> instances}
262   */
263  private final void unblock(final boolean remove) {
264    final String cn = this.getClass().getName();
265    final String mn = "unblock";
266    if (this.logger.isLoggable(Level.FINER)) {
267      this.logger.entering(cn, mn, Boolean.valueOf(remove));
268    }
269    if (this.latch != null) {
270      assert this.latch.getCount() == 0 || this.latch.getCount() == 1;
271      this.latch.countDown();
272      if (remove) {
273        synchronized (instances) {
274          instances.remove(this);
275        }
276      }
277    }
278    if (this.logger.isLoggable(Level.FINER)) {
279      this.logger.exiting(cn, mn);
280    }
281  }
282
283
284  /*
285   * Static methods.
286   */
287
288  
289  /**
290   * Unblocks all known instances of the {@link
291   * AbstractBlockingExtension} class by calling their associated
292   * {@link CountDownLatch}es {@linkplain
293   * #AbstractBlockingExtension(CountDownLatch) supplied at their
294   * construction time}.
295   *
296   * <p>This method may be invoked more than once without any side
297   * effects.</p>
298   *
299   * @see #unblock()
300   */
301  public static final void unblockAll() {
302    final String cn = AbstractBlockingExtension.class.getName();
303    final String mn = "unblockAll";
304    final Logger logger = Logger.getLogger(cn);
305    if (logger.isLoggable(Level.FINER)) {
306      logger.entering(cn, mn);
307    }
308
309    synchronized (instances) {
310      if (!instances.isEmpty()) {
311        instances.forEach((instance, ignored) -> {
312            if (instance != null) {
313              instance.unblock(false);
314            }
315          });
316        instances.clear();
317      }
318      assert instances.isEmpty();
319    }
320    if (logger.isLoggable(Level.FINER)) {
321      logger.exiting(cn, mn);
322    }
323  }
324
325  /**
326   * Decorates any {@linkplain Thread#getUncaughtExceptionHandler()
327   * existing <tt>UncaughtExceptionHandler</tt>} with one that first
328   * calls {@link #unblockAll()}.
329   */
330  private static final void installExceptionHandler() {
331    final Thread currentThread = Thread.currentThread();
332    final UncaughtExceptionHandler old = currentThread.getUncaughtExceptionHandler();
333    currentThread.setUncaughtExceptionHandler((thread, throwable) -> {
334        unblockAll();
335        if (old != null) {
336          old.uncaughtException(thread, throwable);
337        }
338      });
339  }
340
341
342  /*
343   * Inner and nested classes.
344   */
345
346
347  /**
348   * A {@link Thread} whose {@link Thread#run()} method calls the
349   * {@link #unblock()} method.
350   *
351   * @author <a href="https://about.me/lairdnelson"
352   * target="_parent">Laird Nelson</a>
353   *
354   * @see #unblock()
355   */
356  private final class ShutdownHook extends Thread {
357
358
359    /*
360     * Constructors.
361     */
362
363
364    /**
365     * Creates a new {@link ShutdownHook}.
366     */
367    private ShutdownHook() {
368      super();
369    }
370
371
372    /*
373     * Instance methods.
374     */
375    
376
377    /**
378     * Calls {@link #unblock()} when invoked.
379     */
380    @Override
381    public final void run() {
382      final String cn = this.getClass().getName();
383      final String mn = "run";
384      if (logger.isLoggable(Level.FINER)) {
385        logger.entering(cn, mn);
386      }
387      unblock();
388      if (logger.isLoggable(Level.FINER)) {
389        logger.exiting(cn, mn);
390      }
391    }
392    
393  }
394  
395}