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.util.AbstractCollection;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Iterator;
024import java.util.LinkedList;
025import java.util.NoSuchElementException; // for javadoc only
026import java.util.Objects;
027import java.util.Queue;
028
029import java.util.function.Consumer;
030
031import java.util.logging.Level;
032import java.util.logging.Logger;
033
034import io.fabric8.kubernetes.api.model.HasMetadata;
035
036import net.jcip.annotations.GuardedBy;
037import net.jcip.annotations.ThreadSafe;
038
039/**
040 * A publicly-unmodifiable {@link AbstractCollection} of {@link
041 * AbstractEvent}s produced by an {@link EventQueueCollection}.
042 *
043 * <p>All {@link AbstractEvent}s in an {@link EventQueue} describe the
044 * life of a single {@linkplain HasMetadata resource} in
045 * Kubernetes.</p>
046 *
047 * <h2>Thread Safety</h2>
048 *
049 * <p>This class is safe for concurrent use by multiple {@link
050 * Thread}s.  Some operations, like the usage of the {@link
051 * #iterator()} method, require that callers synchronize on the {@link
052 * EventQueue} directly.  This class' internals synchronize on {@code
053 * this} when locking is needed.</p>
054 *
055 * <p>Overrides of this class must also be safe for concurrent use by
056 * multiple {@link Thread}s.</p>
057 *
058 * @param <T> the type of a Kubernetes resource
059 *
060 * @author <a href="https://about.me/lairdnelson"
061 * target="_parent">Laird Nelson</a>
062 *
063 * @see EventQueueCollection
064 */
065@ThreadSafe
066public class EventQueue<T extends HasMetadata> extends AbstractCollection<AbstractEvent<T>> {
067
068  
069  /*
070   * Instance fields.
071   */
072
073
074  /**
075   * A {@link Logger} for use by this {@link EventQueue}.
076   *
077   * <p>This field is never {@code null}.</p>
078   *
079   * @see #createLogger()
080   */
081  protected final Logger logger;
082
083  /**
084   * The key identifying the Kubernetes resource to which all of the
085   * {@link AbstractEvent}s managed by this {@link EventQueue} apply.
086   *
087   * <p>This field is never {@code null}.</p>
088   */
089  private final Object key;
090
091  /**
092   * The actual underlying queue of {@link AbstractEvent}s.
093   *
094   * <p>This field is never {@code null}.</p>
095   */
096  @GuardedBy("this")
097  private final LinkedList<AbstractEvent<T>> events;
098
099
100  /*
101   * Constructors.
102   */
103
104
105  /**
106   * Creates a new {@link EventQueue}.
107   *
108   * @param key the key identifying the Kubernetes resource to which
109   * all of the {@link AbstractEvent}s managed by this {@link
110   * EventQueue} apply; must not be {@code null}
111   *
112   * @exception NullPointerException if {@code key} is {@code null}
113   *
114   * @exception IllegalStateException if the {@link #createLogger()}
115   * method returns {@code null}
116   */
117  protected EventQueue(final Object key) {
118    super();
119    this.logger = this.createLogger();
120    if (this.logger == null) {
121      throw new IllegalStateException("createLogger() == null");
122    }
123    final String cn = this.getClass().getName();
124    final String mn = "<init>";
125    if (this.logger.isLoggable(Level.FINER)) {
126      this.logger.entering(cn, mn, key);
127    }
128    this.key = Objects.requireNonNull(key);
129    this.events = new LinkedList<>();
130    if (this.logger.isLoggable(Level.FINER)) {
131      this.logger.exiting(cn, mn);
132    }
133  }
134
135
136  /*
137   * Instance methods.
138   */
139
140
141  /**
142   * Returns a {@link Logger} for use by this {@link EventQueue}.
143   *
144   * <p>This method never returns {@code null}.</p>
145   *
146   * <p>Overrides of this method must not return {@code null}.</p>
147   *
148   * @return a non-{@code null} {@link Logger}
149   */
150  protected Logger createLogger() {
151    return Logger.getLogger(this.getClass().getName());
152  }
153
154  /**
155   * Returns the key identifying the Kubernetes resource to which all
156   * of the {@link AbstractEvent}s managed by this {@link EventQueue}
157   * apply.
158   *
159   * <p>This method never returns {@code null}.</p>
160   *
161   * @return a non-{@code null} {@link Object}
162   *
163   * @see #EventQueue(Object)
164   */
165  public final Object getKey() {
166    final String cn = this.getClass().getName();
167    final String mn = "getKey";
168    if (this.logger.isLoggable(Level.FINER)) {
169      this.logger.entering(cn, mn);
170    }
171    final Object returnValue = this.key;
172    if (this.logger.isLoggable(Level.FINER)) {
173      this.logger.entering(cn, mn, returnValue);
174    }
175    return returnValue;
176  }
177
178  /**
179   * Returns {@code true} if this {@link EventQueue} is empty.
180   *
181   * @return {@code true} if this {@link EventQueue} is empty; {@code
182   * false} otherwise
183   *
184   * @see #size()
185   */
186  public synchronized final boolean isEmpty() {
187    final String cn = this.getClass().getName();
188    final String mn = "isEmpty";
189    if (this.logger.isLoggable(Level.FINER)) {
190      this.logger.entering(cn, mn);
191    }
192    final boolean returnValue = this.events.isEmpty();
193    if (this.logger.isLoggable(Level.FINER)) {
194      this.logger.exiting(cn, mn, Boolean.valueOf(returnValue));
195    }
196    return returnValue;
197  }
198
199  /**
200   * Returns the size of this {@link EventQueue}.
201   *
202   * <p>This method never returns an {@code int} less than {@code
203   * 0}.</p>
204   *
205   * @return the size of this {@link EventQueue}; never negative
206   *
207   * @see #isEmpty()
208   */
209  @Override
210  public synchronized final int size() {
211    final String cn = this.getClass().getName();
212    final String mn = "size";
213    if (this.logger.isLoggable(Level.FINER)) {
214      this.logger.entering(cn, mn);
215    }
216    final int returnValue = this.events.size();
217    if (this.logger.isLoggable(Level.FINER)) {
218      this.logger.exiting(cn, mn, Integer.valueOf(returnValue));
219    }
220    return returnValue;
221  }
222
223  /**
224   * Adds the supplied {@link AbstractEvent} to this {@link
225   * EventQueue} under certain conditions.
226   *
227   * <p>The supplied {@link AbstractEvent} is added to this {@link
228   * EventQueue} if:</p>
229   *
230   * <ul>
231   *
232   * <li>its {@linkplain AbstractEvent#getKey() key} is equal to this
233   * {@link EventQueue}'s {@linkplain #getKey() key}</li>
234   *
235   * <li>it is either not a {@linkplain SynchronizationEvent}
236   * synchronization event}, or it <em>is</em> a {@linkplain
237   * SynchronizationEvent synchronization event} and this {@link
238   * EventQueue} does not represent a sequence of events that
239   * {@linkplain #resultsInDeletion() describes a deletion}, and</li>
240   *
241   * <li>optional {@linkplain #compress(Collection) compression} does
242   * not result in this {@link EventQueue} being empty</li>
243   *
244   * </ul>
245   *
246   * @param event the {@link AbstractEvent} to add; must not be {@code
247   * null}
248   *
249   * @return {@code true} if an addition took place and {@linkplain
250   * #compress(Collection) optional compression} did not result in
251   * this {@link EventQueue} {@linkplain #isEmpty() becoming empty};
252   * {@code false} otherwise
253   *
254   * @exception NullPointerException if {@code event} is {@code null}
255   *
256   * @exception IllegalArgumentException if {@code event}'s
257   * {@linkplain AbstractEvent#getKey() key} is not equal to this
258   * {@link EventQueue}'s {@linkplain #getKey() key}
259   *
260   * @see #compress(Collection)
261   *
262   * @see SynchronizationEvent
263   *
264   * @see #resultsInDeletion()
265   */
266  final boolean addEvent(final AbstractEvent<T> event) {
267    final String cn = this.getClass().getName();
268    final String mn = "addEvent";
269    if (this.logger.isLoggable(Level.FINER)) {
270      this.logger.entering(cn, mn, event);
271    }
272    
273    Objects.requireNonNull(event);
274
275    final Object key = this.getKey();
276    if (!key.equals(event.getKey())) {
277      throw new IllegalArgumentException("!this.getKey().equals(event.getKey()): " + key + ", " + event.getKey());
278    }
279
280    boolean returnValue = false;
281
282    final AbstractEvent.Type eventType = event.getType();
283    assert eventType != null;
284
285    synchronized (this) {
286      if (!(event instanceof SynchronizationEvent) || !this.resultsInDeletion()) {
287        // If the event is NOT a synchronization event (so it's an
288        // addition, modification, or deletion)...
289        // ...OR if it IS a synchronization event AND we are NOT
290        // already going to delete this queue...
291        returnValue = this.events.add(event);
292        if (returnValue) {
293          this.deduplicate();
294          final Collection<AbstractEvent<T>> readOnlyEvents = Collections.unmodifiableCollection(this.events);
295          final Collection<AbstractEvent<T>> newEvents = this.compress(readOnlyEvents);
296          if (newEvents != readOnlyEvents) {
297            this.events.clear();
298            if (newEvents != null && !newEvents.isEmpty()) {
299              this.events.addAll(newEvents);
300            }
301          }
302          returnValue = !this.isEmpty();
303        }
304      }
305    }
306    
307    if (this.logger.isLoggable(Level.FINER)) {
308      this.logger.exiting(cn, mn, Boolean.valueOf(returnValue));
309    }
310    return returnValue;
311  }
312
313  /**
314   * Returns the last (and definitionally newest) {@link
315   * AbstractEvent} in this {@link EventQueue}.
316   *
317   * <p>This method never returns {@code null}.</p>
318   *
319   * @return the last {@link AbstractEvent} in this {@link
320   * EventQueue}; never {@code null}
321   *
322   * @exception NoSuchElementException if this {@link EventQueue} is
323   * {@linkplain #isEmpty() empty}
324   */
325  synchronized final AbstractEvent<T> getLast() {
326    final String cn = this.getClass().getName();
327    final String mn = "getLast";
328    if (this.logger.isLoggable(Level.FINER)) {
329      this.logger.entering(cn, mn);
330    }
331    final AbstractEvent<T> returnValue = this.events.getLast();
332    if (this.logger.isLoggable(Level.FINER)) {
333      this.logger.exiting(cn, mn, returnValue);
334    }
335    return returnValue;
336  }
337
338  /**
339   * Synchronizes on this {@link EventQueue} and, while holding its
340   * monitor, invokes the {@link Consumer#accept(Object)} method on
341   * the supplied {@link Consumer} for every {@link AbstractEvent} in
342   * this {@link EventQueue}.
343   *
344   * @param action the {@link Consumer} in question; must not be
345   * {@code null}
346   *
347   * @exception NullPointerException if {@code action} is {@code null}
348   */
349  @Override
350  public synchronized final void forEach(final Consumer<? super AbstractEvent<T>> action) {
351    super.forEach(action);
352  }
353  
354  /**
355   * Synchronizes on this {@link EventQueue} and, while holding its
356   * monitor, returns an unmodifiable {@link Iterator} over its
357   * contents.
358   *
359   * <p>This method never returns {@code null}.</p>
360   *
361   * @return a non-{@code null} unmodifiable {@link Iterator} of
362   * {@link AbstractEvent}s
363   */
364  @Override
365  public synchronized final Iterator<AbstractEvent<T>> iterator() {
366    return Collections.unmodifiableCollection(this.events).iterator();
367  }
368
369  /**
370   * If this {@link EventQueue}'s {@linkplain #size() size} is greater
371   * than {@code 2}, and if its last two {@link AbstractEvent}s are
372   * {@linkplain AbstractEvent.Type#DELETION deletions}, and if the
373   * next-to-last deletion {@link AbstractEvent}'s {@linkplain
374   * AbstractEvent#isFinalStateKnown() state is known}, then this method
375   * causes that {@link AbstractEvent} to replace the two under consideration.
376   *
377   * <p>This method is called only by the {@link #addEvent(AbstractEvent)}
378   * method.</p>
379   *
380   * @see #addEvent(AbstractEvent)
381   */
382  private synchronized final void deduplicate() {
383    final String cn = this.getClass().getName();
384    final String mn = "deduplicate";
385    if (this.logger.isLoggable(Level.FINER)) {
386      this.logger.entering(cn, mn);
387    }
388    final int size = this.size();
389    if (size > 2) {
390      final AbstractEvent<T> lastEvent = this.events.get(size - 1);
391      final AbstractEvent<T> nextToLastEvent = this.events.get(size - 2);
392      final AbstractEvent<T> event;
393      if (lastEvent != null && nextToLastEvent != null && AbstractEvent.Type.DELETION.equals(lastEvent.getType()) && AbstractEvent.Type.DELETION.equals(nextToLastEvent.getType())) {
394        event = nextToLastEvent.isFinalStateKnown() ? nextToLastEvent : lastEvent;
395      } else {
396        event = null;
397      }
398      if (event != null) {
399        this.events.set(size - 2, event);
400        this.events.remove(size - 1);
401      }
402    }
403    if (this.logger.isLoggable(Level.FINER)) {
404      this.logger.exiting(cn, mn);
405    }
406  }
407
408  /**
409   * Returns {@code true} if this {@link EventQueue} is {@linkplain
410   * #isEmpty() not empty} and the {@linkplain #getLast() last
411   * <code>AbstractEvent</code> in this <code>EventQueue</code>} is a
412   * {@linkplain AbstractEvent.Type#DELETION deletion event}.
413   *
414   * @return {@code true} if this {@link EventQueue} currently
415   * logically represents the deletion of a resource, {@code false}
416   * otherwise
417   */
418  synchronized final boolean resultsInDeletion() {
419    final String cn = this.getClass().getName();
420    final String mn = "resultsInDeletion";
421    if (this.logger.isLoggable(Level.FINER)) {
422      this.logger.entering(cn, mn);
423    }
424    final boolean returnValue = !this.isEmpty() && this.getLast().getType().equals(AbstractEvent.Type.DELETION);
425    if (this.logger.isLoggable(Level.FINER)) {
426      this.logger.exiting(cn, mn, Boolean.valueOf(returnValue));
427    }
428    return returnValue;
429  }
430
431  /**
432   * Performs a compression operation on the supplied {@link
433   * Collection} of {@link AbstractEvent}s and returns the result of that
434   * operation.
435   *
436   * <p>This method may return {@code null}, which will result in the
437   * emptying of this {@link EventQueue}.</p>
438   *
439   * <p>This method is called while holding this {@link EventQueue}'s
440   * monitor.</p>
441   *
442   * <p>This method is called when an {@link EventQueueCollection} (or
443   * some other {@link AbstractEvent} producer with access to
444   * package-protected methods of this class) adds an {@link AbstractEvent} to
445   * this {@link EventQueue} and provides the {@link EventQueue}
446   * implementation with the ability to eliminate duplicates or
447   * otherwise compress the event stream it represents.</p>
448   *
449   * <p>This implementation simply returns the supplied {@code events}
450   * {@link Collection}; i.e. no compression is performed.</p>
451   *
452   * @param events an {@link
453   * Collections#unmodifiableCollection(Collection) unmodifiable
454   * <tt>Collection</tt>} of {@link AbstractEvent}s representing the
455   * current state of this {@link EventQueue}; will never be {@code
456   * null}
457   *
458   * @return the new state that this {@link EventQueue} should assume;
459   * may be {@code null}; may simply be the supplied {@code events}
460   * {@link Collection} if compression is not desired or implemented
461   */
462  protected Collection<AbstractEvent<T>> compress(final Collection<AbstractEvent<T>> events) {
463    return events;
464  }
465
466  /**
467   * Returns a hashcode for this {@link EventQueue}.
468   *
469   * @return a hashcode for this {@link EventQueue}
470   *
471   * @see #equals(Object)
472   */
473  @Override
474  public final int hashCode() {
475    int hashCode = 17;
476
477    Object value = this.getKey();
478    int c = value == null ? 0 : value.hashCode();
479    hashCode = 37 * hashCode + c;
480
481    synchronized (this) {
482      value = this.events;
483      c = value == null ? 0 : value.hashCode();
484    }
485    hashCode = 37 * hashCode + c;
486
487    return hashCode;
488  }
489
490  /**
491   * Returns {@code true} if the supplied {@link Object} is also an
492   * {@link EventQueue} and is equal in all respects to this one.
493   *
494   * @param other the {@link Object} to test; may be {@code null} in
495   * which case {@code null} will be returned
496   *
497   * @return {@code true} if the supplied {@link Object} is also an
498   * {@link EventQueue} and is equal in all respects to this one;
499   * {@code false} otherwise
500   *
501   * @see #hashCode()
502   */
503  @Override
504  public final boolean equals(final Object other) {
505    if (other == this) {
506      return true;
507    } else if (other instanceof EventQueue) {
508      final EventQueue<?> her = (EventQueue<?>)other;
509
510      final Object key = this.getKey();
511      if (key == null) {
512        if (her.getKey() != null) {
513          return false;
514        }
515      } else if (!key.equals(her.getKey())) {
516        return false;
517      }
518
519      synchronized (this) {
520        final Object events = this.events;
521        if (events == null) {
522          synchronized (her) {
523            if (her.events != null) {
524              return false;
525            }
526          }
527        } else {
528          synchronized (her) {
529            if (!events.equals(her.events)) {
530              return false;
531            }
532          }
533        }
534      }
535
536      return true;
537    } else {
538      return false;
539    }
540  }
541
542  /**
543   * Returns a {@link String} representation of this {@link
544   * EventQueue}.
545   *
546   * <p>This method never returns {@code null}.</p>
547   *
548   * @return a non-{@code null} {@link String} representation of this
549   * {@link EventQueue}
550   */
551  @Override
552  public synchronized final String toString() {
553    return new StringBuilder().append(this.getKey()).append(": ").append(this.events).toString();
554  }
555
556}