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.io.Closeable;
020import java.io.IOException;
021
022import java.lang.reflect.Field;
023import java.lang.reflect.Method;
024
025import java.time.Duration;
026
027import java.time.temporal.ChronoUnit;
028
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.Objects;
033import java.util.Map;
034
035import java.util.concurrent.Executors;
036import java.util.concurrent.ScheduledExecutorService;
037import java.util.concurrent.ScheduledFuture;
038import java.util.concurrent.TimeUnit;
039
040import java.util.logging.Level;
041import java.util.logging.Logger;
042
043import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only
044import io.fabric8.kubernetes.client.KubernetesClientException;
045import io.fabric8.kubernetes.client.Watcher;
046
047import io.fabric8.kubernetes.client.dsl.base.BaseOperation;
048import io.fabric8.kubernetes.client.dsl.base.OperationSupport;
049
050import io.fabric8.kubernetes.client.dsl.Listable;
051import io.fabric8.kubernetes.client.dsl.Versionable;
052import io.fabric8.kubernetes.client.dsl.VersionWatchable;
053import io.fabric8.kubernetes.client.dsl.Watchable;
054
055import io.fabric8.kubernetes.client.dsl.internal.CustomResourceOperationsImpl;
056
057import io.fabric8.kubernetes.api.model.HasMetadata;
058import io.fabric8.kubernetes.api.model.ObjectMeta;
059import io.fabric8.kubernetes.api.model.KubernetesResourceList;
060import io.fabric8.kubernetes.api.model.ListMeta;
061
062import net.jcip.annotations.GuardedBy;
063import net.jcip.annotations.ThreadSafe;
064
065import okhttp3.OkHttpClient;
066
067import org.microbean.development.annotation.Issue;
068import org.microbean.development.annotation.NonBlocking;
069
070/**
071 * A pump of sorts that continuously "pulls" logical events out of
072 * Kubernetes and {@linkplain EventCache#add(Object, AbstractEvent.Type,
073 * HasMetadata) adds them} to an {@link EventCache} so as to logically
074 * "reflect" the contents of Kubernetes into the cache.
075 *
076 * <h2>Thread Safety</h2>
077 *
078 * <p>Instances of this class are safe for concurrent use by multiple
079 * {@link Thread}s.</p>
080 *
081 * @param <T> a type of Kubernetes resource
082 *
083 * @author <a href="https://about.me/lairdnelson"
084 * target="_parent">Laird Nelson</a>
085 *
086 * @see EventCache
087 */
088@ThreadSafe
089public class Reflector<T extends HasMetadata> implements Closeable {
090
091
092  /*
093   * Instance fields.
094   */
095
096
097  /**
098   * The operation that was supplied at construction time.
099   *
100   * <p>This field is never {@code null}.</p>
101   *
102   * <p>It is guaranteed that the value of this field may be
103   * assignable to a reference of type {@link Listable Listable&lt;?
104   * extends KubernetesResourceList&gt;} or to a reference of type
105   * {@link VersionWatchable VersionWatchable&lt;? extends Closeable,
106   * Watcher&lt;T&gt;&gt;}.</p>
107   *
108   * @see Listable
109   *
110   * @see VersionWatchable
111   */
112  private final Object operation;
113
114  /**
115   * The resource version 
116   */
117  private volatile Object lastSynchronizationResourceVersion;
118
119  private final ScheduledExecutorService synchronizationExecutorService;
120
121  @GuardedBy("this")
122  private ScheduledFuture<?> synchronizationTask;
123
124  private final boolean shutdownSynchronizationExecutorServiceOnClose;
125
126  private final long synchronizationIntervalInSeconds;
127
128  @GuardedBy("this")
129  private Closeable watch;
130
131  @GuardedBy("itself")
132  private final EventCache<T> eventCache;
133
134  /**
135   * A {@link Logger} for use by this {@link Reflector}.
136   *
137   * <p>This field is never {@code null}.</p>
138   *
139   * @see #createLogger()
140   */
141  protected final Logger logger;
142  
143
144  /*
145   * Constructors.
146   */
147
148
149  /**
150   * Creates a new {@link Reflector}.
151   *
152   * @param <X> a type that is both an appropriate kind of {@link
153   * Listable} and {@link VersionWatchable}, such as the kind of
154   * operation returned by {@link
155   * DefaultKubernetesClient#configMaps()} and the like
156   *
157   * @param operation a {@link Listable} and a {@link
158   * VersionWatchable} that can report information from a Kubernetes
159   * cluster; must not be {@code null}
160   *
161   * @param eventCache an {@link EventCache} <strong>that will be
162   * synchronized on</strong> and into which {@link Event}s will be
163   * logically "reflected"; must not be {@code null}
164   *
165   * @exception NullPointerException if {@code operation} or {@code
166   * eventCache} is {@code null}
167   *
168   * @exception IllegalStateException if the {@link #createLogger()}
169   * method returns {@code null}
170   *
171   * @see #Reflector(Listable, EventCache, ScheduledExecutorService,
172   * Duration)
173   *
174   * @see #start()
175   */
176  @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types
177  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation,
178                                                                                                                              final EventCache<T> eventCache) {
179    this(operation, eventCache, null, null);
180  }
181
182  /**
183   * Creates a new {@link Reflector}.
184   *
185   * @param <X> a type that is both an appropriate kind of {@link
186   * Listable} and {@link VersionWatchable}, such as the kind of
187   * operation returned by {@link
188   * DefaultKubernetesClient#configMaps()} and the like
189   *
190   * @param operation a {@link Listable} and a {@link
191   * VersionWatchable} that can report information from a Kubernetes
192   * cluster; must not be {@code null}
193   *
194   * @param eventCache an {@link EventCache} <strong>that will be
195   * synchronized on</strong> and into which {@link Event}s will be
196   * logically "reflected"; must not be {@code null}
197   *
198   * @param synchronizationInterval a {@link Duration} representing
199   * the time in between one {@linkplain EventCache#synchronize()
200   * synchronization operation} and another; interpreted with a
201   * granularity of seconds; may be {@code null} or semantically equal
202   * to {@code 0} seconds in which case no synchronization will occur
203   *
204   * @exception NullPointerException if {@code operation} or {@code
205   * eventCache} is {@code null}
206   *
207   * @exception IllegalStateException if the {@link #createLogger()}
208   * method returns {@code null}
209   *
210   * @see #Reflector(Listable, EventCache, ScheduledExecutorService,
211   * Duration)
212   *
213   * @see #start()
214   */
215  @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types
216  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation,
217                                                                                                                              final EventCache<T> eventCache,
218                                                                                                                              final Duration synchronizationInterval) {
219    this(operation, eventCache, null, synchronizationInterval);
220  }
221
222  /**
223   * Creates a new {@link Reflector}.
224   *
225   * @param <X> a type that is both an appropriate kind of {@link
226   * Listable} and {@link VersionWatchable}, such as the kind of
227   * operation returned by {@link
228   * DefaultKubernetesClient#configMaps()} and the like
229   *
230   * @param operation a {@link Listable} and a {@link
231   * VersionWatchable} that can report information from a Kubernetes
232   * cluster; must not be {@code null}
233   *
234   * @param eventCache an {@link EventCache} <strong>that will be
235   * synchronized on</strong> and into which {@link Event}s will be
236   * logically "reflected"; must not be {@code null}
237   *
238   * @param synchronizationExecutorService a {@link
239   * ScheduledExecutorService} to be used to tell the supplied {@link
240   * EventCache} to {@linkplain EventCache#synchronize() synchronize}
241   * on a schedule; may be {@code null} in which case no
242   * synchronization will occur
243   *
244   * @param synchronizationInterval a {@link Duration} representing
245   * the time in between one {@linkplain EventCache#synchronize()
246   * synchronization operation} and another; may be {@code null} in
247   * which case no synchronization will occur
248   *
249   * @exception NullPointerException if {@code operation} or {@code
250   * eventCache} is {@code null}
251   *
252   * @exception IllegalStateException if the {@link #createLogger()}
253   * method returns {@code null}
254   *
255   * @see #start()
256   */
257  @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types
258  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation,
259                                                                                                                              final EventCache<T> eventCache,
260                                                                                                                              final ScheduledExecutorService synchronizationExecutorService,
261                                                                                                                              final Duration synchronizationInterval) {
262    super();
263    this.logger = this.createLogger();
264    if (this.logger == null) {
265      throw new IllegalStateException("createLogger() == null");
266    }
267    final String cn = this.getClass().getName();
268    final String mn = "<init>";
269    if (this.logger.isLoggable(Level.FINER)) {
270      this.logger.entering(cn, mn, new Object[] { operation, eventCache, synchronizationExecutorService, synchronizationInterval });
271    }
272    Objects.requireNonNull(operation);
273    this.eventCache = Objects.requireNonNull(eventCache);
274    // TODO: research: maybe: operation.withField("metadata.resourceVersion", "0")?    
275    this.operation = withResourceVersion(operation, "0");
276    
277    if (synchronizationInterval == null) {
278      this.synchronizationIntervalInSeconds = 0L;
279    } else {
280      this.synchronizationIntervalInSeconds = synchronizationInterval.get(ChronoUnit.SECONDS);
281    }
282    if (this.synchronizationIntervalInSeconds <= 0L) {
283      this.synchronizationExecutorService = null;
284      this.shutdownSynchronizationExecutorServiceOnClose = false;
285    } else if (synchronizationExecutorService == null) {
286      this.synchronizationExecutorService = Executors.newScheduledThreadPool(1);
287      this.shutdownSynchronizationExecutorServiceOnClose = true;
288    } else {
289      this.synchronizationExecutorService = synchronizationExecutorService;
290      this.shutdownSynchronizationExecutorServiceOnClose = false;
291    }
292    if (this.logger.isLoggable(Level.FINER)) {
293      this.logger.exiting(cn, mn);
294    }
295  }
296
297
298  /*
299   * Instance methods.
300   */
301  
302
303  /**
304   * Returns a {@link Logger} that will be used for this {@link
305   * Reflector}.
306   *
307   * <p>This method never returns {@code null}.</p>
308   *
309   * <p>Overrides of this method must not return {@code null}.</p>
310   *
311   * @return a non-{@code null} {@link Logger}
312   */
313  protected Logger createLogger() {
314    return Logger.getLogger(this.getClass().getName());
315  }
316  
317  /**
318   * Notionally closes this {@link Reflector} by terminating any
319   * {@link Thread}s that it has started and invoking the {@link
320   * #onClose()} method while holding this {@link Reflector}'s
321   * monitor.
322   *
323   * @exception IOException if an error occurs
324   *
325   * @see #onClose()
326   */
327  @Override
328  public synchronized final void close() throws IOException {
329    final String cn = this.getClass().getName();
330    final String mn = "close";
331    if (this.logger.isLoggable(Level.FINER)) {
332      this.logger.entering(cn, mn);
333    }
334    try {
335      this.closeSynchronizationExecutorService();
336      if (this.watch != null) {
337        this.watch.close();
338      }
339    } finally {
340      this.onClose();
341    }
342    if (this.logger.isLoggable(Level.FINER)) {
343      this.logger.exiting(cn, mn);
344    }
345  }
346
347  private synchronized final void closeSynchronizationExecutorService() {
348    final String cn = this.getClass().getName();
349    final String mn = "closeSynchronizationExecutorService";
350    if (this.logger.isLoggable(Level.FINER)) {
351      this.logger.entering(cn, mn);
352    }
353    if (this.synchronizationExecutorService != null && this.shutdownSynchronizationExecutorServiceOnClose) {
354
355      // Stop accepting new tasks.  Not that any will be showing up
356      // anyway, but it's the right thing to do.
357      this.synchronizationExecutorService.shutdown();
358
359      // Nicely cancel our task.
360      final ScheduledFuture<?> synchronizationTask = this.synchronizationTask;
361      if (synchronizationTask != null) {
362        synchronizationTask.cancel(true /* interrupt the task */);
363      }
364
365      try {
366        if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) {
367          this.synchronizationExecutorService.shutdownNow();
368          if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) {
369            // TODO: log
370          }
371        }
372      } catch (final InterruptedException interruptedException) {
373        this.synchronizationExecutorService.shutdownNow();
374        Thread.currentThread().interrupt();
375      }
376    }
377    if (this.logger.isLoggable(Level.FINER)) {
378      this.logger.exiting(cn, mn);
379    }
380  }
381
382  private synchronized final void setUpSynchronization() {
383    final String cn = this.getClass().getName();
384    final String mn = "setUpSynchronization";
385    if (this.logger.isLoggable(Level.FINER)) {
386      this.logger.entering(cn, mn);
387    }
388
389    if (this.synchronizationExecutorService != null && this.synchronizationTask == null) {
390      assert this.synchronizationIntervalInSeconds > 0L;
391      
392      if (this.logger.isLoggable(Level.INFO)) {
393        this.logger.logp(Level.INFO, cn, mn, "Scheduling downstream synchronization every {0} seconds", Long.valueOf(this.synchronizationIntervalInSeconds));
394      }
395      final ScheduledFuture<?> job = this.synchronizationExecutorService.scheduleWithFixedDelay(() -> {
396          if (shouldSynchronize()) {
397            if (logger.isLoggable(Level.FINE)) {
398              logger.logp(Level.FINE, cn, mn, "Synchronizing event cache with its downstream consumers");
399            }
400            synchronized (eventCache) {
401              eventCache.synchronize();
402            }
403          }
404        }, 0L, this.synchronizationIntervalInSeconds, TimeUnit.SECONDS);
405      assert job != null;
406      this.synchronizationTask = job;
407    }
408
409    if (this.logger.isLoggable(Level.FINER)) {
410      this.logger.exiting(cn, mn);
411    }
412  }
413
414  /**
415   * Returns whether, at any given moment, this {@link Reflector}
416   * should cause its {@link EventCache} to {@linkplain
417   * EventCache#synchronize() synchronize}.
418   *
419   * <h2>Design Notes</h2>
420   *
421   * <p>This code follows the Go code in the Kubernetes {@code
422   * client-go/tools/cache} package.  One thing that becomes clear
423   * when looking at all of this through an object-oriented lens is
424   * that it is the {@link EventCache} (the {@code delta_fifo}, in the
425   * Go code) that is ultimately in charge of synchronizing.  It is
426   * not clear why this is a function of a reflector.  In an
427   * object-oriented world, perhaps the {@link EventCache} itself
428   * should be in charge of resynchronization schedules.</p>
429   *
430   * @return {@code true} if this {@link Reflector} should cause its
431   * {@link EventCache} to {@linkplain EventCache#synchronize()
432   * synchronize}; {@code false} otherwise
433   */
434  protected boolean shouldSynchronize() {
435    final String cn = this.getClass().getName();
436    final String mn = "shouldSynchronize";
437    if (this.logger.isLoggable(Level.FINER)) {
438      this.logger.entering(cn, mn);
439    }
440    final boolean returnValue = this.synchronizationExecutorService != null;
441    if (this.logger.isLoggable(Level.FINER)) {
442      this.logger.exiting(cn, mn, Boolean.valueOf(returnValue));
443    }
444    return returnValue;
445  }
446  
447  private final Object getLastSynchronizationResourceVersion() {
448    return this.lastSynchronizationResourceVersion;
449  }
450  
451  private final void setLastSynchronizationResourceVersion(final Object resourceVersion) {
452    this.lastSynchronizationResourceVersion = resourceVersion;
453  }
454
455  /**
456   * Using the {@code operation} supplied at construction time,
457   * {@linkplain Listable#list() lists} appropriate Kubernetes
458   * resources, and then, on a separate {@link Thread}, {@linkplain
459   * VersionWatchable sets up a watch} on them, calling {@link
460   * EventCache#replace(Collection, Object)} and {@link
461   * EventCache#add(Object, AbstractEvent.Type, HasMetadata)} methods
462   * as appropriate.
463   *
464   * <p>The calling {@link Thread} is not blocked by invocations of
465   * this method.</p>
466   *
467   * @see #close()
468   */
469  @NonBlocking
470  public final void start() {
471    final String cn = this.getClass().getName();
472    final String mn = "start";
473    if (this.logger.isLoggable(Level.FINER)) {
474      this.logger.entering(cn, mn);
475    }
476
477    synchronized (this) {
478      if (this.watch == null) {
479        
480        // Run a list operation, and get the resourceVersion of that list.
481        @SuppressWarnings("unchecked")
482        final KubernetesResourceList<? extends T> list = ((Listable<? extends KubernetesResourceList<? extends T>>)this.operation).list();
483        assert list != null;
484
485        final ListMeta metadata = list.getMetadata();
486        assert metadata != null;
487        final String resourceVersion = metadata.getResourceVersion();
488        assert resourceVersion != null;
489        
490        // Using the results of that list operation, do a full replace
491        // on the EventCache with them.
492        final Collection<? extends T> replacementItems;
493        final Collection<? extends T> items = list.getItems();
494        if (items == null || items.isEmpty()) {
495          replacementItems = Collections.emptySet();
496        } else {
497          replacementItems = Collections.unmodifiableCollection(new ArrayList<>(items));
498        }
499        synchronized (eventCache) {
500          this.eventCache.replace(replacementItems, resourceVersion);
501        }
502        
503        // Record the resource version we captured during our list
504        // operation.
505        this.setLastSynchronizationResourceVersion(resourceVersion);
506        
507        // Now that we've vetted that our list operation works (i.e. no
508        // syntax errors, no connectivity problems) we can schedule
509        // resynchronizations if necessary.
510        this.setUpSynchronization();
511        
512        // Now that we've taken care of our list() operation, set up our
513        // watch() operation.
514
515        @SuppressWarnings("unchecked")
516        final Versionable<? extends Watchable<? extends Closeable, Watcher<T>>> versionableOperation = (Versionable<? extends Watchable<? extends Closeable, Watcher<T>>>)this.operation;
517        this.watch = withResourceVersion(versionableOperation, resourceVersion).watch(new WatchHandler());
518        
519      }
520    }
521    
522    if (this.logger.isLoggable(Level.FINER)) {
523      this.logger.exiting(cn, mn);
524    }
525  }
526
527  /**
528   * Invoked when {@link #close()} is invoked.
529   *
530   * <p>The default implementation of this method does nothing.</p>
531   */
532  protected synchronized void onClose() {
533
534  }
535
536
537  /*
538   * Hacks.
539   */
540  
541
542  @Issue(
543    id = "kubernetes-client/1099",
544    uri = "https://github.com/fabric8io/kubernetes-client/issues/1099"
545  )
546  @SuppressWarnings({"rawtypes", "unchecked"})
547  private final Watchable<? extends Closeable, Watcher<T>> withResourceVersion(final Versionable<? extends Watchable<? extends Closeable, Watcher<T>>> operation, final String resourceVersion) {
548    Objects.requireNonNull(operation);
549    Objects.requireNonNull(resourceVersion);
550    Watchable<? extends Closeable, Watcher<T>> returnValue = null;
551    if (operation instanceof CustomResourceOperationsImpl) {
552      final CustomResourceOperationsImpl old = (CustomResourceOperationsImpl)operation;
553      try {
554        returnValue =
555          new CustomResourceOperationsImpl(getClient(old),
556                                           old.getConfig(),
557                                           getApiGroup(old),
558                                           old.getAPIVersion(),
559                                           getResourceT(old),
560                                           old.getNamespace(),
561                                           old.getName(),
562                                           old.isCascading(),
563                                           (T)old.getItem(),
564                                           resourceVersion,
565                                           old.isReloadingFromServer(),
566                                           old.getType(),
567                                           old.getListType(),
568                                           old.getDoneableType());
569      } catch (final ReflectiveOperationException reflectiveOperationException) {
570        throw new KubernetesClientException(reflectiveOperationException.getMessage(), reflectiveOperationException);
571      }
572    } else {
573      returnValue = operation.withResourceVersion(resourceVersion);
574    }
575    assert returnValue != null;
576    return returnValue;
577  }
578  
579  private static final OkHttpClient getClient(final OperationSupport operation) throws ReflectiveOperationException {
580    Objects.requireNonNull(operation);
581    final Field clientField = OperationSupport.class.getDeclaredField("client");
582    assert !clientField.isAccessible();
583    assert OkHttpClient.class.equals(clientField.getType());
584    OkHttpClient returnValue = null;
585    try {
586      clientField.setAccessible(true);
587      returnValue = (OkHttpClient)clientField.get(operation);
588    } finally {
589      clientField.setAccessible(false);
590    }
591    return returnValue;
592  }
593  
594  private static final String getApiGroup(final OperationSupport operation) throws ReflectiveOperationException {
595    Objects.requireNonNull(operation);
596    final Field apiGroupField = OperationSupport.class.getDeclaredField("apiGroup");
597    assert !apiGroupField.isAccessible();
598    assert String.class.equals(apiGroupField.getType());
599    String returnValue = null;
600    try {
601      apiGroupField.setAccessible(true);
602      returnValue = (String)apiGroupField.get(operation);
603    } finally {
604      apiGroupField.setAccessible(false);
605    }
606    return returnValue;
607  }
608
609  private static final String getResourceT(final OperationSupport operation) throws ReflectiveOperationException {
610    Objects.requireNonNull(operation);
611    final Field resourceTField = OperationSupport.class.getDeclaredField("resourceT");
612    assert !resourceTField.isAccessible();
613    assert String.class.equals(resourceTField.getType());
614    String returnValue = null;
615    try {
616      resourceTField.setAccessible(true);
617      returnValue = (String)resourceTField.get(operation);
618    } finally {
619      resourceTField.setAccessible(false);
620    }
621    return returnValue;
622  }
623  
624  private static final Map<String, String> getFields(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException {
625    Objects.requireNonNull(operation);
626    final Method getFieldsMethod = BaseOperation.class.getDeclaredMethod("getFields");
627    assert !getFieldsMethod.isAccessible();
628    assert Map.class.equals(getFieldsMethod.getReturnType());
629    Map<String, String> returnValue = null;
630    try {
631      getFieldsMethod.setAccessible(true);
632      @SuppressWarnings("unchecked")
633      final Map<String, String> temp = (Map<String, String>)getFieldsMethod.invoke(operation);
634      returnValue = temp;
635    } finally {
636      getFieldsMethod.setAccessible(false);
637    }
638    return returnValue;
639  }
640  
641  private static final Map<String, String> getLabels(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException {
642    Objects.requireNonNull(operation);
643    final Method getLabelsMethod = BaseOperation.class.getDeclaredMethod("getLabels");
644    assert !getLabelsMethod.isAccessible();
645    assert Map.class.equals(getLabelsMethod.getReturnType());
646    Map<String, String> returnValue = null;
647    try {
648      getLabelsMethod.setAccessible(true);
649      @SuppressWarnings("unchecked")
650      final Map<String, String> temp = (Map<String, String>)getLabelsMethod.invoke(operation);
651      returnValue = temp;
652    } finally {
653      getLabelsMethod.setAccessible(false);
654    }
655    return returnValue;
656  }
657
658  private static final Map<String, String[]> getLabelsIn(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException {
659    Objects.requireNonNull(operation);
660    final Method getLabelsInMethod = BaseOperation.class.getDeclaredMethod("getLabelsIn");
661    assert !getLabelsInMethod.isAccessible();
662    assert Map.class.equals(getLabelsInMethod.getReturnType());
663    Map<String, String[]> returnValue = null;
664    try {
665      getLabelsInMethod.setAccessible(true);
666      @SuppressWarnings("unchecked")
667      final Map<String, String[]> temp = (Map<String, String[]>)getLabelsInMethod.invoke(operation);
668      returnValue = temp;
669    } finally {
670      getLabelsInMethod.setAccessible(false);
671    }
672    return returnValue;
673  }
674
675  private static final Map<String, String> getLabelsNot(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException {
676    Objects.requireNonNull(operation);
677    final Method getLabelsNotMethod = BaseOperation.class.getDeclaredMethod("getLabelsNot");
678    assert !getLabelsNotMethod.isAccessible();
679    assert Map.class.equals(getLabelsNotMethod.getReturnType());
680    Map<String, String> returnValue = null;
681    try {
682      getLabelsNotMethod.setAccessible(true);
683      @SuppressWarnings("unchecked")
684      final Map<String, String> temp = (Map<String, String>)getLabelsNotMethod.invoke(operation);
685      returnValue = temp;
686    } finally {
687      getLabelsNotMethod.setAccessible(false);
688    }
689    return returnValue;
690  }
691  
692  private static final Map<String, String[]> getLabelsNotIn(final BaseOperation<?, ?, ?, ?> operation) throws ReflectiveOperationException {
693    Objects.requireNonNull(operation);
694    final Method getLabelsNotInMethod = BaseOperation.class.getDeclaredMethod("getLabelsNotIn");
695    assert !getLabelsNotInMethod.isAccessible();
696    assert Map.class.equals(getLabelsNotInMethod.getReturnType());
697    Map<String, String[]> returnValue = null;
698    try {
699      getLabelsNotInMethod.setAccessible(true);
700      @SuppressWarnings("unchecked")
701      final Map<String, String[]> temp = (Map<String, String[]>)getLabelsNotInMethod.invoke(operation);
702      returnValue = temp;
703    } finally {
704      getLabelsNotInMethod.setAccessible(false);
705    }
706    return returnValue;
707  }
708  
709
710  /*
711   * Inner and nested classes.
712   */
713  
714
715  /**
716   * A {@link Watcher} of Kubernetes resources.
717   *
718   * @author <a href="https://about.me/lairdnelson"
719   * target="_parent">Laird Nelson</a>
720   *
721   * @see Watcher
722   */
723  private final class WatchHandler implements Watcher<T> {
724
725
726    /*
727     * Constructors.
728     */
729
730    
731    /**
732     * Creates a new {@link WatchHandler}.
733     */
734    private WatchHandler() {
735      super();
736      final String cn = this.getClass().getName();
737      final String mn = "<init>";
738      if (logger.isLoggable(Level.FINER)) {
739        logger.entering(cn, mn);
740        logger.exiting(cn, mn);
741      }
742    }
743
744
745    /*
746     * Instance methods.
747     */
748    
749
750    /**
751     * Calls the {@link EventCache#add(Object, AbstractEvent.Type,
752     * HasMetadata)} method on the enclosing {@link Reflector}'s
753     * associated {@link EventCache} with information harvested from
754     * the supplied {@code resource}, and using an {@link Event.Type}
755     * selected appropriately given the supplied {@link
756     * Watcher.Action}.
757     *
758     * @param action the kind of Kubernetes event that happened; must
759     * not be {@code null}
760     *
761     * @param resource the {@link HasMetadata} object that was
762     * affected; must not be {@code null}
763     *
764     * @exception NullPointerException if {@code action} or {@code
765     * resource} was {@code null}
766     *
767     * @exception IllegalStateException if another error occurred
768     */
769    @Override
770    public final void eventReceived(final Watcher.Action action, final T resource) {
771      final String cn = this.getClass().getName();
772      final String mn = "eventReceived";
773      if (logger.isLoggable(Level.FINER)) {
774        logger.entering(cn, mn, new Object[] { action, resource });
775      }
776      Objects.requireNonNull(action);
777      Objects.requireNonNull(resource);
778
779      final ObjectMeta metadata = resource.getMetadata();
780      assert metadata != null;
781
782      final Event.Type eventType;
783
784      switch (action) {
785      case ADDED:
786        eventType = Event.Type.ADDITION;
787        break;
788      case MODIFIED:
789        eventType = Event.Type.MODIFICATION;
790        break;
791      case DELETED:
792        eventType = Event.Type.DELETION;
793        break;
794      case ERROR:        
795        // Uh...the Go code has:
796        //
797        //   if event.Type == watch.Error {
798        //     return apierrs.FromObject(event.Object)
799        //   }
800        //
801        // Now, apierrs.FromObject is here:
802        // https://github.com/kubernetes/apimachinery/blob/kubernetes-1.9.2/pkg/api/errors/errors.go#L80-L88
803        // This is looking for a Status object.  But
804        // WatchConnectionHandler will never forward on such a thing:
805        // https://github.com/fabric8io/kubernetes-client/blob/v3.1.8/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchConnectionManager.java#L246-L258
806        //
807        // So it follows that if by some chance we get here, resource
808        // will definitely be a HasMetadata.  We go back to the Go
809        // code again, and remember that if the type is Error, the
810        // equivalent of this watch handler simply returns and goes home.
811        //
812        // Now, if we were to throw a RuntimeException here, which is
813        // the idiomatic equivalent of returning and going home, this
814        // would cause a watch reconnect:
815        // https://github.com/fabric8io/kubernetes-client/blob/v3.1.8/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchConnectionManager.java#L159-L205
816        // ...up to the reconnect limit.
817        //
818        // ...which is fine, but I'm not sure that in an error case a
819        // WatchEvent will ever HAVE a HasMetadata as its payload.
820        // Which means MAYBE we'll never get here.  But if we do, all
821        // we can do is throw a RuntimeException...which ends up
822        // reducing to the same case as the default case below, so we
823        // fall through.
824      default:
825        eventType = null;
826        throw new IllegalStateException();
827      }
828
829      // Add an Event of the proper kind to our EventCache.
830      if (eventType != null) {
831        if (logger.isLoggable(Level.FINE)) {
832          logger.logp(Level.FINE, cn, mn, "Adding event to cache: {0} {1}", new Object[] { eventType, resource });
833        }
834        synchronized (eventCache) {
835          eventCache.add(Reflector.this, eventType, resource);
836        }
837      }
838
839      // Record the most recent resource version we're tracking to be
840      // that of this last successful watch() operation.  We set it
841      // earlier during a list() operation.
842      setLastSynchronizationResourceVersion(metadata.getResourceVersion());
843
844      if (logger.isLoggable(Level.FINER)) {
845        logger.exiting(cn, mn);
846      }
847    }
848
849    /**
850     * Invoked when the Kubernetes client connection closes.
851     *
852     * @param exception any {@link KubernetesClientException} that
853     * caused this closing to happen; may be {@code null}
854     */
855    @Override
856    public final void onClose(final KubernetesClientException exception) {
857      final String cn = this.getClass().getName();
858      final String mn = "onClose";
859      if (logger.isLoggable(Level.FINER)) {
860        logger.entering(cn, mn, exception);
861      }
862      if (exception != null && logger.isLoggable(Level.WARNING)) {
863        logger.logp(Level.WARNING, cn, mn, exception.getMessage(), exception);
864      }
865      if (logger.isLoggable(Level.FINER)) {
866        logger.exiting(cn, mn, exception);
867      }      
868    }
869    
870  }
871  
872}