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.kubernetes.controller; 018 019import java.beans.PropertyChangeEvent; 020import java.beans.PropertyChangeListener; 021import java.beans.PropertyChangeSupport; 022 023import java.io.Serializable; 024 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.Iterator; 029import java.util.LinkedHashMap; 030import java.util.Map; 031import java.util.Map.Entry; 032import java.util.NoSuchElementException; 033import java.util.Objects; 034import java.util.Set; 035import java.util.Spliterator; 036 037import java.util.concurrent.Executor; 038import java.util.concurrent.Executors; 039import java.util.concurrent.ExecutorService; 040import java.util.concurrent.Future; 041import java.util.concurrent.ScheduledExecutorService; 042import java.util.concurrent.ScheduledFuture; 043import java.util.concurrent.ScheduledThreadPoolExecutor; 044import java.util.concurrent.TimeUnit; 045 046import java.util.function.Consumer; 047import java.util.function.Supplier; 048 049import java.util.logging.Level; 050import java.util.logging.Logger; 051 052import io.fabric8.kubernetes.api.model.HasMetadata; 053import io.fabric8.kubernetes.api.model.ObjectMeta; 054 055import net.jcip.annotations.GuardedBy; 056import net.jcip.annotations.ThreadSafe; 057 058import org.microbean.development.annotation.Blocking; 059import org.microbean.development.annotation.NonBlocking; 060 061/** 062 * An {@link EventCache} that temporarily stores {@link Event}s in 063 * {@link EventQueue}s, one per named Kubernetes resource, and 064 * {@linkplain #start(Consumer) provides a means for processing those 065 * queues}. 066 * 067 * <h2>Thread Safety</h2> 068 * 069 * <p>This class is safe for concurrent use by multiple {@link 070 * Thread}s.</p> 071 * 072 * @param <T> a type of Kubernetes resource 073 * 074 * @author <a href="https://about.me/lairdnelson" 075 * target="_parent">Laird Nelson</a> 076 * 077 * @see #add(Object, AbstractEvent.Type, HasMetadata) 078 * 079 * @see #replace(Collection, Object) 080 * 081 * @see #synchronize() 082 * 083 * @see #start(Consumer) 084 * 085 * @see EventQueue 086 */ 087@ThreadSafe 088public class EventQueueCollection<T extends HasMetadata> implements EventCache<T>, Supplier<EventQueue<T>>, AutoCloseable { 089 090 091 /* 092 * Instance fields. 093 */ 094 095 096 /** 097 * A {@link PropertyChangeSupport} object that manages {@link 098 * PropertyChangeEvent}s on behalf of this {@link 099 * EventQueueCollection}. 100 * 101 * <p>This field is never {@code null}.</p> 102 * 103 * @see #addPropertyChangeListener(String, PropertyChangeListener) 104 */ 105 private final PropertyChangeSupport propertyChangeSupport; 106 107 /** 108 * Whether this {@link EventQueueCollection} is in the process of 109 * {@linkplain #close() closing}. 110 * 111 * @see #close() 112 */ 113 private volatile boolean closing; 114 115 /** 116 * Whether or not this {@link EventQueueCollection} has been 117 * populated via an invocation of the {@link #replace(Collection, 118 * Object)} method. 119 * 120 * <p>Mutations of this field must be synchronized on {@code 121 * this}.</p> 122 * 123 * @see #replace(Collection, Object) 124 */ 125 @GuardedBy("this") 126 private boolean populated; 127 128 /** 129 * The number of {@link EventQueue}s that this {@link 130 * EventQueueCollection} was initially {@linkplain 131 * #replace(Collection, Object) seeded with}. 132 * 133 * <p>Mutations of this field must be synchronized on {@code 134 * this}.</p> 135 * 136 * @see #replace(Collection, Object) 137 */ 138 @GuardedBy("this") 139 private int initialPopulationCount; 140 141 /** 142 * A {@link LinkedHashMap} of {@link EventQueue} instances, indexed 143 * by {@linkplain EventQueue#getKey() their keys}. 144 * 145 * <p>This field is never {@code null}.</p> 146 * 147 * <p>Mutations of the contents of this {@link LinkedHashMap} must 148 * be synchronized on {@code this}.</p> 149 * 150 * @see #add(Object, AbstractEvent.Type, HasMetadata) 151 */ 152 @GuardedBy("this") 153 private final LinkedHashMap<Object, EventQueue<T>> map; 154 155 /** 156 * A {@link Map} containing the last known state of Kubernetes 157 * resources this {@link EventQueueCollection} is caching events 158 * for. This field is used chiefly by the {@link #synchronize()} 159 * method, but by others as well. 160 * 161 * <p>This field may be {@code null}.</p> 162 * 163 * <p>Mutations of this field must be synchronized on this field's 164 * value.</p> 165 * 166 * @see #getKnownObjects() 167 * 168 * @see #synchronize() 169 */ 170 @GuardedBy("itself") 171 private final Map<?, ? extends T> knownObjects; 172 173 @GuardedBy("this") 174 private ScheduledExecutorService consumerExecutor; 175 176 private volatile Future<?> eventQueueConsumptionTask; 177 178 /** 179 * A {@link Logger} used by this {@link EventQueueCollection}. 180 * 181 * <p>This field is never {@code null}.</p> 182 * 183 * @see #createLogger() 184 */ 185 protected final Logger logger; 186 187 188 /* 189 * Constructors. 190 */ 191 192 193 /** 194 * Creates a new {@link EventQueueCollection} with an initial 195 * capacity of {@code 16} and a load factor of {@code 0.75} that is 196 * not interested in tracking Kubernetes resource deletions. 197 * 198 * @see #EventQueueCollection(Map, int, float) 199 */ 200 public EventQueueCollection() { 201 this(null, 16, 0.75f); 202 } 203 204 /** 205 * Creates a new {@link EventQueueCollection} with an initial 206 * capacity of {@code 16} and a load factor of {@code 0.75}. 207 * 208 * @param knownObjects a {@link Map} containing the last known state 209 * of Kubernetes resources this {@link EventQueueCollection} is 210 * caching events for; may be {@code null} if this {@link 211 * EventQueueCollection} is not interested in tracking deletions of 212 * objects; if non-{@code null} <strong>will be synchronized on by 213 * this class</strong> during retrieval and traversal operations 214 * 215 * @see #EventQueueCollection(Map, int, float) 216 */ 217 public EventQueueCollection(final Map<?, ? extends T> knownObjects) { 218 this(knownObjects, 16, 0.75f); 219 } 220 221 /** 222 * Creates a new {@link EventQueueCollection}. 223 * 224 * @param knownObjects a {@link Map} containing the last known state 225 * of Kubernetes resources this {@link EventQueueCollection} is 226 * caching events for; may be {@code null} if this {@link 227 * EventQueueCollection} is not interested in tracking deletions of 228 * objects; if non-{@code null} <strong>will be synchronized on by 229 * this class</strong> during retrieval and traversal operations 230 * 231 * @param initialCapacity the initial capacity of the internal data 232 * structure used to house this {@link EventQueueCollection}'s 233 * {@link EventQueue}s; must be an integer greater than {@code 0} 234 * 235 * @param loadFactor the load factor of the internal data structure 236 * used to house this {@link EventQueueCollection}'s {@link 237 * EventQueue}s; must be a positive number between {@code 0} and 238 * {@code 1} 239 */ 240 public EventQueueCollection(final Map<?, ? extends T> knownObjects, final int initialCapacity, final float loadFactor) { 241 super(); 242 final String cn = this.getClass().getName(); 243 final String mn = "<init>"; 244 this.logger = this.createLogger(); 245 if (logger == null) { 246 throw new IllegalStateException(); 247 } 248 if (this.logger.isLoggable(Level.FINER)) { 249 final String knownObjectsString; 250 if (knownObjects == null) { 251 knownObjectsString = null; 252 } else { 253 synchronized (knownObjects) { 254 knownObjectsString = knownObjects.toString(); 255 } 256 } 257 this.logger.entering(cn, mn, new Object[] { knownObjectsString, Integer.valueOf(initialCapacity), Float.valueOf(loadFactor) }); 258 } 259 this.propertyChangeSupport = new PropertyChangeSupport(this); 260 this.map = new LinkedHashMap<>(initialCapacity, loadFactor); 261 this.knownObjects = knownObjects; 262 if (this.logger.isLoggable(Level.FINER)) { 263 this.logger.exiting(cn, mn); 264 } 265 } 266 267 268 /* 269 * Instance methods. 270 */ 271 272 /** 273 * Returns a {@link Logger} for use by this {@link 274 * EventQueueCollection}. 275 * 276 * <p>This method never returns {@code null}.</p> 277 * 278 * <p>Overrides of this method must not return {@code null}.</p> 279 * 280 * @return a non-{@code null} {@link Logger} for use by this {@link 281 * EventQueueCollection} 282 */ 283 protected Logger createLogger() { 284 return Logger.getLogger(this.getClass().getName()); 285 } 286 287 private final Map<?, ? extends T> getKnownObjects() { 288 return this.knownObjects; 289 } 290 291 /** 292 * Returns {@code true} if this {@link EventQueueCollection} is empty. 293 * 294 * @return {@code true} if this {@link EventQueueCollection} is 295 * empty; {@code false} otherwise 296 */ 297 private synchronized final boolean isEmpty() { 298 return this.map.isEmpty(); 299 } 300 301 /** 302 * Returns {@code true} if this {@link EventQueueCollection} has 303 * been populated via a call to {@link #add(Object, AbstractEvent.Type, 304 * HasMetadata)} at some point, and if there are no {@link 305 * EventQueue}s remaining to be {@linkplain #start(Consumer) 306 * removed}. 307 * 308 * <p>This is a <a 309 * href="https://docs.oracle.com/javase/tutorial/javabeans/writing/properties.html#bound">bound 310 * property</a>.</p> 311 * 312 * @return {@code true} if this {@link EventQueueCollection} has 313 * been populated via a call to {@link #add(Object, AbstractEvent.Type, 314 * HasMetadata)} at some point, and if there are no {@link 315 * EventQueue}s remaining to be {@linkplain #start(Consumer) 316 * removed}; {@code false} otherwise 317 * 318 * @see #replace(Collection, Object) 319 * 320 * @see #add(Object, AbstractEvent.Type, HasMetadata) 321 * 322 * @see #synchronize() 323 */ 324 public synchronized final boolean isSynchronized() { 325 return this.populated && this.initialPopulationCount == 0; 326 } 327 328 /** 329 * <strong>Synchronizes on the {@code knownObjects} object</strong> 330 * {@linkplain #EventQueueCollection(Map, int, float) supplied at 331 * construction time}, if there is one, and, for every Kubernetes 332 * resource found within at the time of this call, adds a {@link 333 * SynchronizationEvent} for it with an {@link AbstractEvent.Type} 334 * of {@link AbstractEvent.Type#MODIFICATION}. 335 */ 336 @Override 337 public final void synchronize() { 338 final String cn = this.getClass().getName(); 339 final String mn = "synchronize"; 340 if (this.logger.isLoggable(Level.FINER)) { 341 this.logger.entering(cn, mn); 342 } 343 synchronized (this) { 344 final Map<?, ? extends T> knownObjects = this.getKnownObjects(); 345 if (knownObjects != null) { 346 synchronized (knownObjects) { 347 if (!knownObjects.isEmpty()) { 348 final Collection<? extends T> values = knownObjects.values(); 349 if (values != null && !values.isEmpty()) { 350 for (final T knownObject : values) { 351 if (knownObject != null) { 352 // We follow the Go code in that we use *our* key 353 // extraction logic, rather than relying on the 354 // known key in the knownObjects map. 355 final Object key = this.getKey(knownObject); 356 if (key != null) { 357 final EventQueue<T> eventQueue = this.map.get(key); 358 if (eventQueue == null || eventQueue.isEmpty()) { 359 // We make a SynchronizationEvent of type 360 // MODIFICATION. shared_informer.go checks in 361 // its HandleDeltas function to see if oldObj 362 // exists; if so, it's a modification. Here we 363 // take action *only if* the equivalent of 364 // oldObj exists, therefore this is a 365 // SynchronizationEvent of type MODIFICATION, 366 // not ADDITION. 367 this.synchronize(this, AbstractEvent.Type.MODIFICATION, knownObject, true /* yes, populate */); 368 } 369 } 370 } 371 } 372 } 373 } 374 } 375 } 376 } 377 if (this.logger.isLoggable(Level.FINER)) { 378 this.logger.exiting(cn, mn); 379 } 380 } 381 382 /** 383 * At a high level, fully replaces the internal state of this {@link 384 * EventQueueCollection} to reflect only the Kubernetes resources 385 * contained in the supplied {@link Collection}, notionally firing 386 * {@link SynchronizationEvent}s and {@link Event}s of type {@link 387 * AbstractEvent.Type#DELETION} as appropriate. 388 * 389 * <p>{@link EventQueue}s managed by this {@link 390 * EventQueueCollection} that have not yet {@linkplain 391 * #start(Consumer) been processed} are not removed by this 392 * operation.</p> 393 * 394 * <p><strong>This method synchronizes on the supplied {@code 395 * incomingResources} {@link Collection}</strong> while iterating 396 * over it.</p> 397 * 398 * @param incomingResources the {@link Collection} of Kubernetes 399 * resources with which to replace this {@link 400 * EventQueueCollection}'s internal state; may be {@code null} or 401 * {@linkplain Collection#isEmpty() empty}, which will be taken as 402 * an indication that this {@link EventQueueCollection} should 403 * effectively be emptied 404 * 405 * @param resourceVersion the version of the Kubernetes list 406 * resource that contained the incoming resources; currently 407 * ignored; may be {@code null} 408 * 409 * @exception IllegalStateException if the {@link 410 * #createEvent(Object, AbstractEvent.Type, HasMetadata)} method returns 411 * {@code null} for any reason 412 * 413 * @see SynchronizationEvent 414 * 415 * @see #createEvent(Object, AbstractEvent.Type, HasMetadata) 416 */ 417 @Override 418 public synchronized final void replace(final Collection<? extends T> incomingResources, final Object resourceVersion) { 419 final String cn = this.getClass().getName(); 420 final String mn = "replace"; 421 if (this.logger.isLoggable(Level.FINER)) { 422 final String incomingResourcesString; 423 if (incomingResources == null) { 424 incomingResourcesString = null; 425 } else { 426 synchronized (incomingResources) { 427 incomingResourcesString = incomingResources.toString(); 428 } 429 } 430 this.logger.entering(cn, mn, new Object[] { incomingResourcesString, resourceVersion }); 431 } 432 433 final boolean oldSynchronized = this.isSynchronized(); 434 final int size; 435 final Set<Object> replacementKeys; 436 437 if (incomingResources == null) { 438 size = 0; 439 replacementKeys = Collections.emptySet(); 440 } else { 441 synchronized (incomingResources) { 442 if (incomingResources.isEmpty()) { 443 size = 0; 444 replacementKeys = Collections.emptySet(); 445 } else { 446 size = incomingResources.size(); 447 assert size > 0; 448 replacementKeys = new HashSet<>(); 449 for (final T resource : incomingResources) { 450 if (resource != null) { 451 replacementKeys.add(this.getKey(resource)); 452 this.synchronize(this, AbstractEvent.Type.ADDITION, resource, false); 453 } 454 } 455 } 456 } 457 } 458 459 int queuedDeletions = 0; 460 461 final Map<?, ? extends T> knownObjects = this.getKnownObjects(); 462 if (knownObjects == null) { 463 464 for (final EventQueue<T> eventQueue : this.map.values()) { 465 assert eventQueue != null; 466 final Object key; 467 final AbstractEvent<? extends T> newestEvent; 468 synchronized (eventQueue) { 469 if (eventQueue.isEmpty()) { 470 newestEvent = null; 471 key = null; 472 assert false : "eventQueue.isEmpty(): " + eventQueue; 473 } else { 474 key = eventQueue.getKey(); 475 if (key == null) { 476 throw new IllegalStateException(); 477 } 478 if (replacementKeys.contains(key)) { 479 newestEvent = null; 480 } else { 481 // We have an EventQueue indexed under a key that 482 // identifies a resource that no longer exists in 483 // Kubernetes. Inform any consumers via a deletion 484 // event that this object was removed at some point from 485 // Kubernetes. The state of the object in question is 486 // indeterminate. 487 newestEvent = eventQueue.getLast(); 488 assert newestEvent != null; 489 } 490 } 491 } 492 if (newestEvent != null) { 493 assert key != null; 494 // We grab the last event in the queue in question and get 495 // its resource; this will serve as the state of the 496 // Kubernetes resource in question the last time we knew 497 // about it. This state is not necessarily, but could be, 498 // the true actual last state of the resource in question. 499 // The point is, the true state of the object when it was 500 // deleted is unknown. We build a new event to reflect all 501 // this. 502 // 503 // Astute readers will realize that this could result in two 504 // DELETION events enqueued, back to back, with identical 505 // payloads. See the deduplicate() method in EventQueue, 506 // which takes care of this situation. 507 final T resourceToBeDeleted = newestEvent.getResource(); 508 assert resourceToBeDeleted != null; 509 final Event<T> event = this.createEvent(this, AbstractEvent.Type.DELETION, resourceToBeDeleted); 510 if (event == null) { 511 throw new IllegalStateException("createEvent() == null"); 512 } 513 event.setKey(key); 514 this.add(event, false /* don't treat this as a population event */); 515 } 516 } 517 518 } else { 519 520 synchronized (knownObjects) { 521 if (!knownObjects.isEmpty()) { 522 final Collection<? extends Entry<?, ? extends T>> entrySet = knownObjects.entrySet(); 523 if (entrySet != null && !entrySet.isEmpty()) { 524 for (final Entry<?, ? extends T> entry : entrySet) { 525 if (entry != null) { 526 final Object knownKey = entry.getKey(); 527 if (!replacementKeys.contains(knownKey)) { 528 final Event<T> event = this.createEvent(this, AbstractEvent.Type.DELETION, entry.getValue()); 529 if (event == null) { 530 throw new IllegalStateException("createEvent() == null"); 531 } 532 event.setKey(knownKey); 533 this.add(event, false /* don't treat this as a population event */); 534 queuedDeletions++; 535 } 536 } 537 } 538 } 539 } 540 } 541 542 } 543 544 if (!this.populated) { 545 this.populated = true; 546 this.firePropertyChange("populated", false, true); 547 assert size >= 0; 548 assert queuedDeletions >= 0; 549 final int oldInitialPopulationCount = this.initialPopulationCount; 550 this.initialPopulationCount = size + queuedDeletions; 551 this.firePropertyChange("initialPopulationCount", oldInitialPopulationCount, this.initialPopulationCount); 552 if (this.initialPopulationCount == 0) { 553 this.firePropertyChange("synchronized", oldSynchronized, true); 554 } 555 } 556 557 if (this.logger.isLoggable(Level.FINER)) { 558 this.logger.exiting(cn, mn); 559 } 560 } 561 562 /** 563 * Returns an {@link Object} which will be used as the key that will 564 * uniquely identify the supplied {@code resource} to this {@link 565 * EventQueueCollection}. 566 * 567 * <p>This method may return {@code null}, but only if {@code 568 * resource} is {@code null} or is constructed in such a way that 569 * its {@link HasMetadata#getMetadata()} method returns {@code 570 * null}.</p> 571 * 572 * <p>Overrides of this method may return {@code null}, but only if 573 * {@code resource} is {@code null}. 574 * 575 * <p>The default implementation of this method returns the return 576 * value of the {@link HasMetadatas#getKey(HasMetadata)} method.</p> 577 * 578 * @param resource a {@link HasMetadata} for which a key should be 579 * returned; may be {@code null} in which case {@code null} may be 580 * returned 581 * 582 * @return a non-{@code null} key for the supplied {@code resource}; 583 * or {@code null} if {@code resource} is {@code null} 584 * 585 * @see HasMetadatas#getKey(HasMetadata) 586 */ 587 protected Object getKey(final T resource) { 588 final String cn = this.getClass().getName(); 589 final String mn = "getKey"; 590 if (this.logger.isLoggable(Level.FINER)) { 591 this.logger.entering(cn, mn, resource); 592 } 593 final Object returnValue = HasMetadatas.getKey(resource); 594 if (this.logger.isLoggable(Level.FINER)) { 595 this.logger.exiting(cn, mn, returnValue); 596 } 597 return returnValue; 598 } 599 600 /** 601 * Creates a new {@link EventQueue} suitable for holding {@link 602 * Event}s {@linkplain Event#getKey() matching} the supplied {@code 603 * key}. 604 * 605 * <p>This method never returns {@code null}.</p> 606 * 607 * <p>Overrides of this method must not return {@code null}.</p> 608 * 609 * @param key the key {@linkplain EventQueue#getKey() for the new 610 * <code>EventQueue</code>}; must not be {@code null} 611 * 612 * @return the new {@link EventQueue}; never {@code null} 613 * 614 * @exception NullPointerException if {@code key} is {@code null} 615 * 616 * @see EventQueue#EventQueue(Object) 617 */ 618 protected EventQueue<T> createEventQueue(final Object key) { 619 final String cn = this.getClass().getName(); 620 final String mn = "createEventQueue"; 621 if (this.logger.isLoggable(Level.FINER)) { 622 this.logger.entering(cn, mn, key); 623 } 624 final EventQueue<T> returnValue = new EventQueue<T>(key); 625 if (this.logger.isLoggable(Level.FINER)) { 626 this.logger.exiting(cn, mn, returnValue); 627 } 628 return returnValue; 629 } 630 631 /** 632 * Starts a new {@link Thread} that, until {@link #close()} is 633 * called, removes {@link EventQueue}s from this {@link 634 * EventQueueCollection} and supplies them to the supplied {@link 635 * Consumer}, and returns a {@link Future} representing this task. 636 * 637 * <p>This method may return {@code null}.</p> 638 * 639 * <p>Invoking this method does not block the calling {@link 640 * Thread}.</p> 641 * 642 * @param siphon the {@link Consumer} that will process each {@link 643 * EventQueue} as it becomes ready; must not be {@code null} 644 * 645 * @return a {@link Future} representing the task that is feeding 646 * {@link EventQueue}s to the supplied {@link Consumer}, or {@code 647 * null} if no task was started 648 * 649 * @exception NullPointerException if {@code siphon} is {@code null} 650 */ 651 @NonBlocking 652 public final Future<?> start(final Consumer<? super EventQueue<? extends T>> siphon) { 653 final String cn = this.getClass().getName(); 654 final String mn = "start"; 655 if (this.logger.isLoggable(Level.FINER)) { 656 this.logger.entering(cn, mn, siphon); 657 } 658 659 Objects.requireNonNull(siphon); 660 661 final Future<?> returnValue; 662 synchronized (this) { 663 if (this.consumerExecutor == null) { 664 this.consumerExecutor = this.createScheduledThreadPoolExecutor(); 665 if (this.consumerExecutor == null) { 666 throw new IllegalStateException(); 667 } 668 this.eventQueueConsumptionTask = this.consumerExecutor.scheduleWithFixedDelay(this.createEventQueueConsumptionTask(siphon), 0L, 1L, TimeUnit.SECONDS); 669 returnValue = this.eventQueueConsumptionTask; 670 } else { 671 returnValue = null; 672 } 673 } 674 675 if (this.logger.isLoggable(Level.FINER)) { 676 this.logger.exiting(cn, mn, returnValue); 677 } 678 return returnValue; 679 } 680 681 private final ScheduledThreadPoolExecutor createScheduledThreadPoolExecutor() { 682 final ScheduledThreadPoolExecutor returnValue = new ScheduledThreadPoolExecutor(1); 683 returnValue.setRemoveOnCancelPolicy(true); 684 return returnValue; 685 } 686 687 /** 688 * Semantically closes this {@link EventQueueCollection} by 689 * detaching any {@link Consumer} previously attached via the {@link 690 * #start(Consumer)} method. {@linkplain #add(Object, AbstractEvent.Type, 691 * HasMetadata) Additions}, {@linkplain #replace(Collection, Object) 692 * replacements} and {@linkplain #synchronize() synchronizations} 693 * are still possible, but there won't be anything consuming any 694 * events generated by or supplied to these operations. 695 * 696 * <p>A closed {@link EventQueueCollection} may be {@linkplain 697 * #start(Consumer) started} again.</p> 698 * 699 * @see #start(Consumer) 700 */ 701 @Override 702 public final void close() { 703 final String cn = this.getClass().getName(); 704 final String mn = "close"; 705 if (this.logger.isLoggable(Level.FINER)) { 706 this.logger.entering(cn, mn); 707 } 708 709 final ExecutorService consumerExecutor; 710 final Future<?> task; 711 synchronized (this) { 712 this.closing = true; 713 task = this.eventQueueConsumptionTask; 714 consumerExecutor = this.consumerExecutor; 715 } 716 717 if (consumerExecutor != null) { 718 // Stop accepting new tasks. 719 consumerExecutor.shutdown(); 720 721 if (task != null) { 722 task.cancel(true); 723 } 724 725 // Cancel all tasks firmly. 726 consumerExecutor.shutdownNow(); 727 728 try { 729 // Wait for termination to complete normally. 730 if (!consumerExecutor.awaitTermination(60, TimeUnit.SECONDS) && this.logger.isLoggable(Level.WARNING)) { 731 this.logger.logp(Level.WARNING, cn, mn, "consumerExecutor.awaitTermination() failed"); 732 } 733 } catch (final InterruptedException interruptedException) { 734 consumerExecutor.shutdownNow(); 735 Thread.currentThread().interrupt(); 736 } 737 } 738 739 if (this.logger.isLoggable(Level.FINER)) { 740 this.logger.exiting(cn, mn); 741 } 742 } 743 744 private final Runnable createEventQueueConsumptionTask(final Consumer<? super EventQueue<? extends T>> siphon) { 745 Objects.requireNonNull(siphon); 746 final Runnable returnValue = () -> { 747 while (!Thread.currentThread().isInterrupted()) { 748 @Blocking 749 final EventQueue<T> eventQueue = this.get(); 750 if (eventQueue != null) { 751 synchronized (eventQueue) { 752 try { 753 siphon.accept(eventQueue); 754 } catch (final TransientException transientException) { 755 this.map.putIfAbsent(eventQueue.getKey(), eventQueue); 756 } 757 } 758 } 759 } 760 }; 761 return returnValue; 762 } 763 764 /** 765 * Returns an {@link EventQueue} if one is available, 766 * <strong>blocking if one is not</strong> and returning {@code 767 * null} only if the {@linkplain Thread#interrupt() current thread 768 * is interrupted}. 769 * 770 * <p>This method may return {@code null} in which case the current 771 * {@link Thread} has been {@linkplain Thread#interrupt() 772 * interrupted}.</p> 773 * 774 * @return an {@link EventQueue}, or {@code null} 775 */ 776 @Blocking 777 @Override 778 public final EventQueue<T> get() { 779 final String cn = this.getClass().getName(); 780 final String mn = "get"; 781 if (this.logger.isLoggable(Level.FINER)) { 782 this.logger.entering(cn, mn); 783 } 784 785 EventQueue<T> returnValue = null; 786 try { 787 returnValue = this.take(); 788 } catch (final InterruptedException interruptedException) { 789 Thread.currentThread().interrupt(); 790 returnValue = null; 791 } 792 793 if (this.logger.isLoggable(Level.FINER)) { 794 this.logger.exiting(cn, mn, returnValue); 795 } 796 return returnValue; 797 } 798 799 @Blocking 800 private final EventQueue<T> take() throws InterruptedException { 801 final String cn = this.getClass().getName(); 802 final String mn = "take"; 803 if (this.logger.isLoggable(Level.FINER)) { 804 this.logger.entering(cn, mn); 805 } 806 807 final EventQueue<T> returnValue; 808 synchronized (this) { 809 while (this.isEmpty() && !this.closing) { 810 this.wait(); // blocks 811 } 812 assert this.populated : "this.populated == false"; 813 if (this.isEmpty()) { 814 assert this.closing : "this.isEmpty() && !this.closing"; 815 returnValue = null; 816 } else { 817 final Iterator<EventQueue<T>> iterator = this.map.values().iterator(); 818 assert iterator != null; 819 assert iterator.hasNext(); 820 returnValue = iterator.next(); 821 assert returnValue != null; 822 iterator.remove(); 823 if (this.initialPopulationCount > 0) { 824 // We know we're not populated and our 825 // initialPopulationCount is not 0, so therefore we are not 826 // synchronized. 827 assert !this.isSynchronized(); 828 final int oldInitialPopulationCount = this.initialPopulationCount; 829 this.initialPopulationCount--; 830 this.firePropertyChange("initialPopulationCount", oldInitialPopulationCount, this.initialPopulationCount); 831 this.firePropertyChange("synchronized", false, this.isSynchronized()); 832 } 833 this.firePropertyChange("empty", false, this.isEmpty()); 834 } 835 } 836 837 if (this.logger.isLoggable(Level.FINER)) { 838 final String eventQueueString; 839 synchronized (returnValue) { 840 eventQueueString = returnValue.toString(); 841 } 842 this.logger.exiting(cn, mn, eventQueueString); 843 } 844 return returnValue; 845 } 846 847 /** 848 * Creates an {@link Event} using the supplied raw materials and 849 * returns it. 850 * 851 * <p>This method never returns {@code null}.</p> 852 * 853 * <p>Implementations of this method must not return {@code 854 * null}.</p> 855 * 856 * <p>Implementations of this method must return a new {@link Event} 857 * with every invocation.</p> 858 * 859 * @param source the {@linkplain Event#getSource() source} of the 860 * {@link Event} that will be created; must not be null 861 * 862 * @param eventType the {@linkplain Event#getType() type} of {@link 863 * Event} that will be created; must not be {@code null} 864 * 865 * @param resource the {@linkplain Event#getResource() resource} of 866 * the {@link Event} that will be created; must not be 867 * {@code null} 868 * 869 * @return the created {@link Event}; never {@code null} 870 */ 871 protected Event<T> createEvent(final Object source, final AbstractEvent.Type eventType, final T resource) { 872 final String cn = this.getClass().getName(); 873 final String mn = "createEvent"; 874 if (this.logger.isLoggable(Level.FINER)) { 875 this.logger.entering(cn, mn, new Object[] { source, eventType, resource }); 876 } 877 878 Objects.requireNonNull(source); 879 Objects.requireNonNull(eventType); 880 Objects.requireNonNull(resource); 881 final Event<T> returnValue = new Event<>(source, eventType, null, resource); 882 883 if (this.logger.isLoggable(Level.FINER)) { 884 this.logger.exiting(cn, mn, returnValue); 885 } 886 return returnValue; 887 } 888 889 protected SynchronizationEvent<T> createSynchronizationEvent(final Object source, final AbstractEvent.Type eventType, final T resource) { 890 final String cn = this.getClass().getName(); 891 final String mn = "createSynchronizationEvent"; 892 if (this.logger.isLoggable(Level.FINER)) { 893 this.logger.entering(cn, mn, new Object[] { source, eventType, resource }); 894 } 895 896 Objects.requireNonNull(source); 897 Objects.requireNonNull(eventType); 898 Objects.requireNonNull(resource); 899 final SynchronizationEvent<T> returnValue = new SynchronizationEvent<>(source, eventType, null, resource); 900 901 if (this.logger.isLoggable(Level.FINER)) { 902 this.logger.exiting(cn, mn, returnValue); 903 } 904 return returnValue; 905 } 906 907 private final SynchronizationEvent<T> synchronize(final Object source, final AbstractEvent.Type eventType, final T resource, final boolean populate) { 908 final String cn = this.getClass().getName(); 909 final String mn = "synchronize"; 910 if (this.logger.isLoggable(Level.FINER)) { 911 this.logger.entering(cn, mn, new Object[] { source, eventType, resource }); 912 } 913 914 Objects.requireNonNull(source); 915 Objects.requireNonNull(eventType); 916 Objects.requireNonNull(resource); 917 918 if (!(eventType.equals(AbstractEvent.Type.ADDITION) || eventType.equals(AbstractEvent.Type.MODIFICATION))) { 919 throw new IllegalArgumentException("Illegal eventType: " + eventType); 920 } 921 922 final SynchronizationEvent<T> event = this.createSynchronizationEvent(source, eventType, resource); 923 if (event == null) { 924 throw new IllegalStateException("createSynchronizationEvent() == null"); 925 } 926 final SynchronizationEvent<T> returnValue = this.add(event, populate); 927 928 if (this.logger.isLoggable(Level.FINER)) { 929 this.logger.exiting(cn, mn, returnValue); 930 } 931 return returnValue; 932 } 933 934 /** 935 * Adds a new {@link Event} constructed out of the parameters 936 * supplied to this method to this {@link EventQueueCollection} and 937 * returns the {@link Event} that was added. 938 * 939 * <p>This method may return {@code null}.</p> 940 * 941 * <p>This implementation {@linkplain #createEventQueue(Object) 942 * creates an <code>EventQueue</code>} if necessary for the {@link 943 * Event} that will be added, and then adds the new {@link Event} to 944 * the queue.</p> 945 * 946 * @param source the {@linkplain Event#getSource() source} of the 947 * {@link Event} that will be created and added; must not be null 948 * 949 * @param eventType the {@linkplain Event#getType() type} of {@link 950 * Event} that will be created and added; must not be {@code null} 951 * 952 * @param resource the {@linkplain Event#getResource() resource} of 953 * the {@link Event} that will be created and added; must not be 954 * {@code null} 955 * 956 * @return the {@link Event} that was created and added, or {@code 957 * null} if no {@link Event} was actually added as a result of this 958 * method's invocation 959 * 960 * @exception NullPointerException if any of the parameters is 961 * {@code null} 962 * 963 * @see Event 964 */ 965 @Override 966 public final Event<T> add(final Object source, final AbstractEvent.Type eventType, final T resource) { 967 return this.add(source, eventType, resource, true); 968 } 969 970 /** 971 * Adds a new {@link Event} constructed out of the parameters 972 * supplied to this method to this {@link EventQueueCollection} and 973 * returns the {@link Event} that was added. 974 * 975 * <p>This method may return {@code null}.</p> 976 * 977 * <p>This implementation {@linkplain #createEventQueue(Object) 978 * creates an <code>EventQueue</code>} if necessary for the {@link 979 * Event} that will be added, and then adds the new {@link Event} to 980 * the queue.</p> 981 * 982 * @param source the {@linkplain Event#getSource() source} of the 983 * {@link Event} that will be created and added; must not be null 984 * 985 * @param eventType the {@linkplain Event#getType() type} of {@link 986 * Event} that will be created and added; must not be {@code null} 987 * 988 * @param resource the {@linkplain Event#getResource() resource} of 989 * the {@link Event} that will be created and added; must not be 990 * {@code null} 991 * 992 * @param populate if {@code true} then this {@link 993 * EventQueueCollection} will be internally marked as <em>initially 994 * populated</em> 995 * 996 * @return the {@link Event} that was created and added, or {@code 997 * null} if no {@link Event} was actually added as a result of this 998 * method's invocation 999 * 1000 * @exception NullPointerException if any of the parameters is 1001 * {@code null} 1002 * 1003 * @see Event 1004 */ 1005 private final Event<T> add(final Object source, final AbstractEvent.Type eventType, final T resource, final boolean populate) { 1006 final String cn = this.getClass().getName(); 1007 final String mn = "add"; 1008 if (this.logger.isLoggable(Level.FINER)) { 1009 this.logger.entering(cn, mn, new Object[] { source, eventType, resource, Boolean.valueOf(populate)}); 1010 } 1011 1012 final Event<T> event = this.createEvent(source, eventType, resource); 1013 if (event == null) { 1014 throw new IllegalStateException("createEvent() == null"); 1015 } 1016 final Event<T> returnValue = this.add(event, populate); 1017 1018 if (this.logger.isLoggable(Level.FINER)) { 1019 this.logger.exiting(cn, mn, returnValue); 1020 } 1021 return returnValue; 1022 } 1023 1024 /** 1025 * Adds the supplied {@link Event} to this {@link 1026 * EventQueueCollection} and returns the {@link Event} that was 1027 * added. 1028 * 1029 * <p>This method may return {@code null}.</p> 1030 * 1031 * <p>This implementation {@linkplain #createEventQueue(Object) 1032 * creates an <code>EventQueue</code>} if necessary for the {@link 1033 * Event} that will be added, and then adds the new {@link Event} to 1034 * the queue.</p> 1035 * 1036 * @param <E> an {@link AbstractEvent} type that is both consumed 1037 * and returned 1038 * 1039 * @param event the {@link Event} to add; must not be {@code null} 1040 * 1041 * @param populate if {@code true} then this {@link 1042 * EventQueueCollection} will be internally marked as <em>initially 1043 * populated</em> 1044 * 1045 * @return the {@link Event} that was created and added, or {@code 1046 * null} if no {@link Event} was actually added as a result of this 1047 * method's invocation 1048 * 1049 * @exception NullPointerException if any of the parameters is 1050 * {@code null} 1051 * 1052 * @see Event 1053 */ 1054 private final <E extends AbstractEvent<T>> E add(final E event, final boolean populate) { 1055 final String cn = this.getClass().getName(); 1056 final String mn = "add"; 1057 if (this.logger.isLoggable(Level.FINER)) { 1058 this.logger.entering(cn, mn, new Object[] { event, Boolean.valueOf(populate) }); 1059 } 1060 1061 if (this.closing) { 1062 throw new IllegalStateException(); 1063 } 1064 1065 Objects.requireNonNull(event); 1066 1067 final Object key = event.getKey(); 1068 if (key == null) { 1069 throw new IllegalArgumentException("event.getKey() == null"); 1070 } 1071 1072 E returnValue = null; 1073 1074 synchronized (this) { 1075 1076 if (populate) { 1077 final boolean old = this.populated; 1078 this.populated = true; 1079 this.firePropertyChange("populated", old, true); 1080 } 1081 1082 EventQueue<T> eventQueue = this.map.get(key); 1083 final boolean eventQueueExisted = eventQueue != null; 1084 if (!eventQueueExisted) { 1085 eventQueue = this.createEventQueue(key); 1086 if (eventQueue == null) { 1087 throw new IllegalStateException("createEventQueue(key) == null: " + key); 1088 } 1089 } 1090 assert eventQueue != null; 1091 1092 final boolean eventAdded; 1093 final boolean eventQueueIsEmpty; 1094 synchronized (eventQueue) { 1095 eventAdded = eventQueue.addEvent(event); 1096 // Adding an event to an EventQueue can result in compression, 1097 // which may result in the EventQueue becoming empty as a 1098 // result of the add operation. 1099 eventQueueIsEmpty = eventQueue.isEmpty(); 1100 } 1101 1102 if (eventAdded) { 1103 returnValue = event; 1104 } 1105 1106 if (eventQueueIsEmpty) { 1107 // Compression might have emptied the queue, so an add could 1108 // result in an empty queue. We don't permit empty queues. 1109 if (eventQueueExisted) { 1110 returnValue = null; 1111 final boolean old = this.isEmpty(); 1112 this.map.remove(key); 1113 this.firePropertyChange("empty", old, this.isEmpty()); 1114 } else { 1115 // Nothing to do; the queue we added the event to was 1116 // created here, and was never added to our internal map, so 1117 // we're done. 1118 } 1119 } else if (!eventQueueExisted) { 1120 // We created the EventQueue we just added to; now we need to 1121 // store it. 1122 final boolean old = this.isEmpty(); 1123 this.map.put(key, eventQueue); 1124 this.firePropertyChange("empty", old, this.isEmpty()); 1125 // Notify anyone blocked on our empty state that we're no 1126 // longer empty 1127 this.notifyAll(); 1128 } 1129 1130 } 1131 1132 if (this.logger.isLoggable(Level.FINER)) { 1133 this.logger.exiting(cn, mn, returnValue); 1134 } 1135 return returnValue; 1136 } 1137 1138 1139 /* 1140 * PropertyChangeListener support. 1141 */ 1142 1143 1144 /** 1145 * Adds the supplied {@link PropertyChangeListener} to this {@link 1146 * EventQueueCollection}'s collection of such listeners so that it 1147 * will be notified only when the bound property bearing the 1148 * supplied {@code name} changes. 1149 * 1150 * @param name the name of the bound property whose changes are of 1151 * interest; may be {@code null} in which case all property change 1152 * notifications will be dispatched to the supplied {@link 1153 * PropertyChangeListener} 1154 * 1155 * @param listener the {@link PropertyChangeListener} to add; may be 1156 * {@code null} in which case no action will be taken 1157 * 1158 * @see #addPropertyChangeListener(PropertyChangeListener) 1159 */ 1160 public final void addPropertyChangeListener(final String name, final PropertyChangeListener listener) { 1161 if (listener != null) { 1162 this.propertyChangeSupport.addPropertyChangeListener(name, listener); 1163 } 1164 } 1165 1166 /** 1167 * Adds the supplied {@link PropertyChangeListener} to this {@link 1168 * EventQueueCollection}'s collection of such listeners so that it 1169 * will be notified whenever any bound property of this {@link 1170 * EventQueueCollection} changes. 1171 * 1172 * @param listener the {@link PropertyChangeListener} to add; may be 1173 * {@code null} in which case no action will be taken 1174 * 1175 * @see #addPropertyChangeListener(String, PropertyChangeListener) 1176 */ 1177 public final void addPropertyChangeListener(final PropertyChangeListener listener) { 1178 if (listener != null) { 1179 this.propertyChangeSupport.addPropertyChangeListener(listener); 1180 } 1181 } 1182 1183 /** 1184 * Removes the supplied {@link PropertyChangeListener} from this 1185 * {@link EventQueueCollection} so that it will no longer be 1186 * notified of changes to bound properties bearing the supplied 1187 * {@code name}. 1188 * 1189 * @param name a bound property name; may be {@code null} 1190 * 1191 * @param listener the {@link PropertyChangeListener} to remove; may 1192 * be {@code null} in which case no action will be taken 1193 * 1194 * @see #addPropertyChangeListener(String, PropertyChangeListener) 1195 * 1196 * @see #removePropertyChangeListener(PropertyChangeListener) 1197 */ 1198 public final void removePropertyChangeListener(final String name, final PropertyChangeListener listener) { 1199 if (listener != null) { 1200 this.propertyChangeSupport.removePropertyChangeListener(name, listener); 1201 } 1202 } 1203 1204 /** 1205 * Removes the supplied {@link PropertyChangeListener} from this 1206 * {@link EventQueueCollection} so that it will no longer be 1207 * notified of any changes to bound properties. 1208 * 1209 * @param listener the {@link PropertyChangeListener} to remove; may 1210 * be {@code null} in which case no action will be taken 1211 * 1212 * @see #addPropertyChangeListener(PropertyChangeListener) 1213 * 1214 * @see #removePropertyChangeListener(String, PropertyChangeListener) 1215 */ 1216 public final void removePropertyChangeListener(final PropertyChangeListener listener) { 1217 if (listener != null) { 1218 this.propertyChangeSupport.removePropertyChangeListener(listener); 1219 } 1220 } 1221 1222 /** 1223 * Returns an array of {@link PropertyChangeListener}s that were 1224 * {@linkplain #addPropertyChangeListener(String, 1225 * PropertyChangeListener) registered} to receive notifications for 1226 * changes to bound properties bearing the supplied {@code name}. 1227 * 1228 * <p>This method never returns {@code null}.</p> 1229 * 1230 * @param name the name of a bound property; may be {@code null} in 1231 * which case an empty array will be returned 1232 * 1233 * @return a non-{@code null} array of {@link 1234 * PropertyChangeListener}s 1235 * 1236 * @see #getPropertyChangeListeners() 1237 * 1238 * @see #addPropertyChangeListener(String, PropertyChangeListener) 1239 * 1240 * @see #removePropertyChangeListener(String, 1241 * PropertyChangeListener) 1242 */ 1243 public final PropertyChangeListener[] getPropertyChangeListeners(final String name) { 1244 return this.propertyChangeSupport.getPropertyChangeListeners(name); 1245 } 1246 1247 /** 1248 * Returns an array of {@link PropertyChangeListener}s that were 1249 * {@linkplain #addPropertyChangeListener(String, 1250 * PropertyChangeListener) registered} to receive notifications for 1251 * changes to all bound properties. 1252 * 1253 * <p>This method never returns {@code null}.</p> 1254 * 1255 * @return a non-{@code null} array of {@link 1256 * PropertyChangeListener}s 1257 * 1258 * @see #getPropertyChangeListeners(String) 1259 * 1260 * @see #addPropertyChangeListener(PropertyChangeListener) 1261 * 1262 * @see #removePropertyChangeListener(PropertyChangeListener) 1263 */ 1264 public final PropertyChangeListener[] getPropertyChangeListeners() { 1265 return this.propertyChangeSupport.getPropertyChangeListeners(); 1266 } 1267 1268 /** 1269 * Fires a {@link PropertyChangeEvent} to {@linkplain 1270 * #addPropertyChangeListener(String, PropertyChangeListener) 1271 * registered <tt>PropertyChangeListener</tt>s} if the supplied 1272 * {@code old} and {@code newValue} objects are non-{@code null} and 1273 * not equal to each other. 1274 * 1275 * @param propertyName the name of the bound property that might 1276 * have changed; may be {@code null} (indicating that some unknown 1277 * set of bound properties has changed) 1278 * 1279 * @param old the old value of the bound property in question; may 1280 * be {@code null} 1281 * 1282 * @param newValue the new value of the bound property; may be 1283 * {@code null} 1284 */ 1285 protected final void firePropertyChange(final String propertyName, final Object old, final Object newValue) { 1286 final String cn = this.getClass().getName(); 1287 final String mn = "firePropertyChange"; 1288 if (this.logger.isLoggable(Level.FINER)) { 1289 this.logger.entering(cn, mn, new Object[] { propertyName, old, newValue }); 1290 } 1291 this.propertyChangeSupport.firePropertyChange(propertyName, old, newValue); 1292 if (this.logger.isLoggable(Level.FINER)) { 1293 this.logger.exiting(cn, mn); 1294 } 1295 } 1296 1297 /** 1298 * Fires a {@link PropertyChangeEvent} to {@linkplain 1299 * #addPropertyChangeListener(String, PropertyChangeListener) 1300 * registered <tt>PropertyChangeListener</tt>s} if the supplied 1301 * {@code old} and {@code newValue} objects are non-{@code null} and 1302 * not equal to each other. 1303 * 1304 * @param propertyName the name of the bound property that might 1305 * have changed; may be {@code null} (indicating that some unknown 1306 * set of bound properties has changed) 1307 * 1308 * @param old the old value of the bound property in question 1309 * 1310 * @param newValue the new value of the bound property 1311 */ 1312 protected final void firePropertyChange(final String propertyName, final int old, final int newValue) { 1313 final String cn = this.getClass().getName(); 1314 final String mn = "firePropertyChange"; 1315 if (this.logger.isLoggable(Level.FINER)) { 1316 this.logger.entering(cn, mn, new Object[] { propertyName, Integer.valueOf(old), Integer.valueOf(newValue) }); 1317 } 1318 this.propertyChangeSupport.firePropertyChange(propertyName, old, newValue); 1319 if (this.logger.isLoggable(Level.FINER)) { 1320 this.logger.exiting(cn, mn); 1321 } 1322 } 1323 1324 /** 1325 * Fires a {@link PropertyChangeEvent} to {@linkplain 1326 * #addPropertyChangeListener(String, PropertyChangeListener) 1327 * registered <tt>PropertyChangeListener</tt>s} if the supplied 1328 * {@code old} and {@code newValue} objects are non-{@code null} and 1329 * not equal to each other. 1330 * 1331 * @param name the name of the bound property that might 1332 * have changed; may be {@code null} (indicating that some unknown 1333 * set of bound properties has changed) 1334 * 1335 * @param old the old value of the bound property in question 1336 * 1337 * @param newValue the new value of the bound property 1338 */ 1339 protected final void firePropertyChange(final String name, final boolean old, final boolean newValue) { 1340 final String cn = this.getClass().getName(); 1341 final String mn = "firePropertyChange"; 1342 if (this.logger.isLoggable(Level.FINER)) { 1343 this.logger.entering(cn, mn, new Object[] { name, Boolean.valueOf(old), Boolean.valueOf(newValue) }); 1344 } 1345 this.propertyChangeSupport.firePropertyChange(name, old, newValue); 1346 if (this.logger.isLoggable(Level.FINER)) { 1347 this.logger.exiting(cn, mn); 1348 } 1349 } 1350 1351 1352 /* 1353 * Inner and nested classes. 1354 */ 1355 1356 1357 /** 1358 * A {@link RuntimeException} indicating that a {@link Consumer} 1359 * {@linkplain EventQueueCollection#start(Consumer) started} by an 1360 * {@link EventQueueCollection} has encountered an error that might 1361 * not happen if the consumption operation is retried. 1362 * 1363 * @author <a href="https://about.me/lairdnelson" 1364 * target="_parent">Laird Nelson</a> 1365 * 1366 * @see EventQueueCollection 1367 */ 1368 public static final class TransientException extends RuntimeException { 1369 1370 1371 /* 1372 * Static fields. 1373 */ 1374 1375 1376 /** 1377 * The version of this class for {@linkplain Serializable 1378 * serialization purposes}. 1379 * 1380 * @see Serializable 1381 */ 1382 private static final long serialVersionUID = 1L; 1383 1384 1385 /* 1386 * Constructors. 1387 */ 1388 1389 1390 /** 1391 * Creates a new {@link TransientException}. 1392 */ 1393 public TransientException() { 1394 super(); 1395 } 1396 1397 /** 1398 * Creates a new {@link TransientException}. 1399 * 1400 * @param message a detail message describing the error; may be 1401 * {@code null} 1402 */ 1403 public TransientException(final String message) { 1404 super(message); 1405 } 1406 1407 /** 1408 * Creates a new {@link TransientException}. 1409 * 1410 * @param cause the {@link Throwable} that caused this {@link 1411 * TransientException} to be created; may be {@code null} 1412 */ 1413 public TransientException(final Throwable cause) { 1414 super(cause); 1415 } 1416 1417 /** 1418 * Creates a new {@link TransientException}. 1419 * 1420 * @param message a detail message describing the error; may be 1421 * {@code null} 1422 * 1423 * @param cause the {@link Throwable} that caused this {@link 1424 * TransientException} to be created; may be {@code null} 1425 */ 1426 public TransientException(final String message, final Throwable cause) { 1427 super(message, cause); 1428 } 1429 1430 } 1431 1432}