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