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}