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