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