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.Serializable; // for javadoc only 020 021import java.time.Duration; 022import java.time.Instant; 023 024import java.util.EventListener; 025import java.util.EventObject; 026import java.util.Objects; 027 028import io.fabric8.kubernetes.api.model.HasMetadata; 029import io.fabric8.kubernetes.api.model.ObjectMeta; 030 031/** 032 * An {@code abstract} {@link EventObject} that represents another 033 * event that has occurred to a Kubernetes resource, usually as found 034 * in an {@link EventCache} implementation. 035 * 036 * @param <T> a type of Kubernetes resource 037 * 038 * @author <a href="https://about.me/lairdnelson" 039 * target="_parent">Laird Nelson</a> 040 * 041 * @see EventCache 042 */ 043public abstract class AbstractEvent<T extends HasMetadata> extends EventObject { 044 045 046 /* 047 * Static fields. 048 */ 049 050 051 /** 052 * The version of this class for {@linkplain Serializable 053 * serialization purposes}. 054 * 055 * @see Serializable 056 */ 057 private static final long serialVersionUID = 1L; 058 059 060 /* 061 * Instance fields. 062 */ 063 064 065 /** 066 * The key that identifies this {@link AbstractEvent}'s {@linkplain 067 * #getResource() resource} <strong>only when its final state is 068 * unknown</strong>. 069 * 070 * <p>This field can be—and often is—{@code null}.</p> 071 * 072 * @see #getKey() 073 * 074 * @see #setKey(Object) 075 */ 076 private volatile Object key; 077 078 /** 079 * The {@link Type} describing the type of this {@link 080 * AbstractEvent}. 081 * 082 * <p>This field is never {@code null}.</p> 083 * 084 * @see #getType() 085 */ 086 private final Type type; 087 088 /** 089 * A Kubernetes resource representing the <em>prior</em> state of 090 * the resource returned by this {@link AbstractEvent}'s {@link 091 * #getResource()} method. 092 * 093 * <p>This field may be {@code null}.</p> 094 * 095 * <p>The prior state of a given Kubernetes resource is often not 096 * known, so this field is often {@code null}.</p> 097 * 098 * @see #getResource() 099 */ 100 private final T priorResource; 101 102 /** 103 * A Kubernetes resource representing its state at the time of this 104 * event. 105 * 106 * <p>This field is never {@code null}.</p> 107 * 108 * @see #getResource() 109 */ 110 private final T resource; 111 112 113 /* 114 * Constructors. 115 */ 116 117 118 /** 119 * A private zero-argument constructor to reinforce to readers and 120 * subclassers alike that this is not only an {@code abstract} 121 * class, but one with a finite, known number of subclasses. 122 * 123 * @exception NullPointerException when invoked 124 * 125 * @see #AbstractEvent(Object, Type, HasMetadata, HasMetadata) 126 */ 127 private AbstractEvent() { 128 this(null, null, null, null); 129 } 130 131 /** 132 * Creates a new {@link AbstractEvent}. 133 * 134 * @param source the creator; must not be {@code null} 135 * 136 * @param type the {@link Type} of this {@link AbstractEvent}; must not be 137 * {@code null} 138 * 139 * @param priorResource a {@link HasMetadata} representing the 140 * <em>prior state</em> of the {@linkplain #getResource() Kubernetes 141 * resource this <code>AbstractEvent</code> primarily concerns}; may 142 * be—<strong>and often is</strong>—null 143 * 144 * @param resource a {@link HasMetadata} representing a Kubernetes 145 * resource; must not be {@code null} 146 * 147 * @exception NullPointerException if {@code source}, {@code type} 148 * or {@code resource} is {@code null} 149 * 150 * @exception IllegalStateException if somehow a subclass invoking 151 * this constructor manages illicitly to be neither an instance of 152 * {@link Event} nor an instance of {@link SynchronizationEvent} 153 * 154 * @see Type 155 * 156 * @see EventObject#getSource() 157 */ 158 AbstractEvent(final Object source, final Type type, final T priorResource, final T resource) { 159 super(source); 160 if (!(Event.class.isAssignableFrom(this.getClass()) || SynchronizationEvent.class.isAssignableFrom(this.getClass()))) { 161 throw new IllegalStateException("Unexpected subclass"); 162 } 163 this.type = Objects.requireNonNull(type); 164 this.priorResource = priorResource; 165 this.resource = Objects.requireNonNull(resource); 166 } 167 168 169 /* 170 * Instance methods. 171 */ 172 173 174 /** 175 * Returns a {@link Type} representing the type of this {@link 176 * AbstractEvent}. 177 * 178 * <p>This method never returns {@code null}.</p> 179 * 180 * @return a non-{@code null} {@link Type} 181 * 182 * @see Type 183 */ 184 public final Type getType() { 185 return this.type; 186 } 187 188 /** 189 * Returns a {@link HasMetadata} representing the <em>prior 190 * state</em> of the Kubernetes resource this {@link AbstractEvent} 191 * primarily concerns. 192 * 193 * <p>This method may return {@code null}, and often does.</p> 194 * 195 * <p>The prior state of a Kubernetes resource is often not known at 196 * {@link AbstractEvent} construction time so it is common for this method 197 * to return {@code null}. 198 * 199 * @return a {@link HasMetadata} representing the <em>prior 200 * state</em> of the {@linkplain #getResource() Kubernetes resource 201 * this <code>AbstractEvent</code> primarily concerns}, or {@code null} 202 * 203 * @see #getResource() 204 */ 205 public final T getPriorResource() { 206 return this.priorResource; 207 } 208 209 /** 210 * Returns a {@link HasMetadata} representing the Kubernetes 211 * resource this {@link AbstractEvent} concerns. 212 * 213 * <p>This method never returns {@code null}.</p> 214 * 215 * @return a non-{@code null} Kubernetes resource 216 */ 217 public final T getResource() { 218 return this.resource; 219 } 220 221 /** 222 * Returns {@code true} if this {@link AbstractEvent}'s {@linkplain 223 * #getResource() resource} is an accurate representation of its 224 * last known state. 225 * 226 * <p>This should only return {@code true} for some, but not all, 227 * deletion scenarios. Any other behavior should be considered to 228 * be an error.</p> 229 * 230 * @return {@code true} if this {@link AbstractEvent}'s {@linkplain 231 * #getResource() resource} is an accurate representation of its 232 * last known state; {@code false} otherwise 233 */ 234 public final boolean isFinalStateKnown() { 235 return this.key == null; 236 } 237 238 /** 239 * Sets the key identifying the Kubernetes resource this {@link 240 * AbstractEvent} describes. 241 * 242 * @param key the new key; may be {@code null} 243 * 244 * @see #getKey() 245 */ 246 final void setKey(final Object key) { 247 this.key = key; 248 } 249 250 /** 251 * Returns a key that can be used to unambiguously identify this 252 * {@link AbstractEvent}'s {@linkplain #getResource() resource}. 253 * 254 * <p>This method may return {@code null} in exceptional cases, but 255 * normally does not.</p> 256 * 257 * <p>Overrides of this method must not return {@code null} except 258 * in exceptional cases.</p> 259 * 260 * <p>The default implementation of this method returns the return 261 * value of the {@link HasMetadatas#getKey(HasMetadata)} method.</p> 262 * 263 * @return a key for this {@link AbstractEvent}, or {@code null} 264 * 265 * @see HasMetadatas#getKey(HasMetadata) 266 */ 267 public Object getKey() { 268 Object returnValue = this.key; 269 if (returnValue == null) { 270 returnValue = HasMetadatas.getKey(this.getResource()); 271 } 272 return returnValue; 273 } 274 275 /** 276 * Returns a hashcode for this {@link AbstractEvent}. 277 * 278 * @return a hashcode for this {@link AbstractEvent} 279 */ 280 @Override 281 public int hashCode() { 282 int hashCode = 37; 283 284 final Object source = this.getSource(); 285 int c = source == null ? 0 : source.hashCode(); 286 hashCode = hashCode * 17 + c; 287 288 final Object key = this.getKey(); 289 c = key == null ? 0 : key.hashCode(); 290 hashCode = hashCode * 17 + c; 291 292 final Object type = this.getType(); 293 c = type == null ? 0 : type.hashCode(); 294 hashCode = hashCode * 17 + c; 295 296 final Object resource = this.getResource(); 297 c = resource == null ? 0 : resource.hashCode(); 298 hashCode = hashCode * 17 + c; 299 300 final Object priorResource = this.getPriorResource(); 301 c = priorResource == null ? 0 : priorResource.hashCode(); 302 hashCode = hashCode * 17 + c; 303 304 return hashCode; 305 } 306 307 /** 308 * Returns {@code true} if the supplied {@link Object} is also an 309 * {@link AbstractEvent} and is equal in every respect to this one. 310 * 311 * @param other the {@link Object} to test; may be {@code null} in 312 * which case {@code false} will be returned 313 * 314 * @return {@code true} if the supplied {@link Object} is also an 315 * {@link AbstractEvent} and is equal in every respect to this one; {@code 316 * false} otherwise 317 */ 318 @Override 319 public boolean equals(final Object other) { 320 if (other == this) { 321 return true; 322 } else if (other instanceof AbstractEvent) { 323 324 final AbstractEvent<?> her = (AbstractEvent<?>)other; 325 326 final Object source = this.getSource(); 327 if (source == null) { 328 if (her.getSource() != null) { 329 return false; 330 } 331 } else if (!source.equals(her.getSource())) { 332 return false; 333 } 334 335 final Object key = this.getKey(); 336 if (key == null) { 337 if (her.getKey() != null) { 338 return false; 339 } 340 } else if (!key.equals(her.getKey())) { 341 return false; 342 } 343 344 final Object type = this.getType(); 345 if (type == null) { 346 if (her.getType() != null) { 347 return false; 348 } 349 } else if (!type.equals(her.getType())) { 350 return false; 351 } 352 353 final Object resource = this.getResource(); 354 if (resource == null) { 355 if (her.getResource() != null) { 356 return false; 357 } 358 } else if (!resource.equals(her.getResource())) { 359 return false; 360 } 361 362 final Object priorResource = this.getPriorResource(); 363 if (priorResource == null) { 364 if (her.getPriorResource() != null) { 365 return false; 366 } 367 } else if (!priorResource.equals(her.getPriorResource())) { 368 return false; 369 } 370 371 372 return true; 373 } else { 374 return false; 375 } 376 } 377 378 /** 379 * Returns a {@link String} representation of this {@link AbstractEvent}. 380 * 381 * <p>This method never returns {@code null}.</p> 382 * 383 * <p>Overrides of this method must not return {@code null}.</p> 384 * 385 * @return a non-{@code null} {@link String} representation of this 386 * {@link AbstractEvent} 387 */ 388 @Override 389 public String toString() { 390 final StringBuilder sb = new StringBuilder().append(this.getType()).append(": "); 391 final Object priorResource = this.getPriorResource(); 392 if (priorResource != null) { 393 sb.append(priorResource).append(" --> "); 394 } 395 sb.append(this.getResource()); 396 return sb.toString(); 397 } 398 399 400 /* 401 * Inner and nested classes. 402 */ 403 404 405 /** 406 * The type of an {@link AbstractEvent}. 407 * 408 * @author <a href="https://about.me/lairdnelson" 409 * target="_parent">Laird Nelson</a> 410 */ 411 public static enum Type { 412 413 /** 414 * A {@link Type} representing the addition of a resource. 415 */ 416 ADDITION, 417 418 /** 419 * A {@link Type} representing the modification of a resource. 420 */ 421 MODIFICATION, 422 423 /** 424 * A {@link Type} representing the deletion of a resource. 425 */ 426 DELETION 427 428 } 429 430}