001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2022–2023 microBean™. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with 006 * the License. You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 012 * specific language governing permissions and limitations under the License. 013 */ 014package org.microbean.qualifier; 015 016import java.lang.System.Logger; 017 018import java.lang.constant.ClassDesc; 019import java.lang.constant.Constable; 020import java.lang.constant.ConstantDesc; 021import java.lang.constant.DynamicConstantDesc; 022import java.lang.constant.MethodHandleDesc; 023 024import java.util.Iterator; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.SortedMap; 030import java.util.TreeMap; 031 032import org.microbean.constant.Constables; 033 034import static java.lang.System.Logger.Level.WARNING; 035 036import static java.lang.constant.ConstantDescs.BSM_INVOKE; 037import static java.lang.constant.ConstantDescs.CD_Map; 038import static java.lang.constant.ConstantDescs.CD_Object; 039import static java.lang.constant.ConstantDescs.CD_String; 040import static java.lang.constant.ConstantDescs.NULL; 041 042import static java.util.Collections.emptySortedMap; 043import static java.util.Collections.unmodifiableSortedMap; 044 045/** 046 * An abstract {@linkplain #attributes() attributed} {@linkplain #name() name}-{@linkplain #value() value} pair. 047 * 048 * @param <V> the type of a {@link Binding}'s {@linkplain #value() value} and of its {@linkplain #attributes() attribute 049 * values} 050 * 051 * @param <B> the type of the subclass 052 * 053 * @author <a href="https://about.me/lairdnelson" target="_parent">Laird Nelson</a> 054 */ 055public abstract class Binding<V, B extends Binding<V, B>> implements Comparable<B>, Constable { 056 057 058 /* 059 * Static fields. 060 */ 061 062 063 private static final Logger LOGGER = System.getLogger(Binding.class.getName()); 064 065 066 /* 067 * Instance fields. 068 */ 069 070 071 private final String name; 072 073 private final V value; 074 075 private final SortedMap<String, Object> attributes; 076 077 private final SortedMap<String, Object> info; 078 079 080 /* 081 * Constructors. 082 */ 083 084 085 /** 086 * Creates a new {@link Binding}. 087 * 088 * @param name the name; must not be {@code null} 089 * 090 * @param value the value; may be {@code null} 091 * 092 * @param attributes further describing this {@link Binding}; may be {@code null} 093 * 094 * @param info informational attributes further describing this {@link Binding} that are not considered by its {@link 095 * #equals(Object)} implementation; may be {@code null} 096 * 097 * @see #name() 098 * 099 * @see #value() 100 * 101 * @see #attributes() 102 * 103 * @see #info() 104 */ 105 @SuppressWarnings("unchecked") 106 protected Binding(final String name, 107 final V value, 108 final Map<? extends String, ?> attributes, 109 final Map<? extends String, ?> info) { 110 super(); 111 this.name = Objects.requireNonNull(name); 112 this.value = value; 113 this.attributes = 114 attributes == null || attributes.isEmpty() ? emptySortedMap() : unmodifiableSortedMap(new TreeMap<>(attributes)); 115 if (info == null || info.isEmpty()) { 116 this.info = emptySortedMap(); 117 } else { 118 final TreeMap<String, Object> map = new TreeMap<>(); 119 for (final String key : info.keySet()) { 120 if (!this.attributes.containsKey(key)) { 121 map.put(key, info.get(key)); 122 } 123 } 124 this.info = unmodifiableSortedMap(map); 125 } 126 } 127 128 129 /* 130 * Instance methods. 131 */ 132 133 134 /** 135 * Returns the name of this {@link Binding}. 136 * 137 * @return the name of this {@link Binding} 138 * 139 * @nullability This method never returns {@code null}. 140 * 141 * @idempotency This method is idempotent and deterministic. 142 * 143 * @threadsafety This method is safe for concurrent use by multiple threads. 144 */ 145 public final String name() { 146 return this.name; 147 } 148 149 /** 150 * Returns the value of this {@link Binding}, which may be {@code null}. 151 * 152 * @return the value of this {@link Binding} 153 * 154 * @nullability This method may return {@code null}. 155 * 156 * @idempotency This method is idempotent and deterministic. 157 * 158 * @threadsafety This method is safe for concurrent use by multiple threads. 159 */ 160 public final V value() { 161 return this.value; 162 } 163 164 /** 165 * Returns an immutable {@link SortedMap} representing any attributes further describing this {@link Binding}. 166 * 167 * <p>The attributes are considered by the {@link #equals(Object)} method.</p> 168 * 169 * @return the attributes of this {@link Binding} 170 * 171 * @nullability This method never returns {@code null}. 172 * 173 * @idempotency This method is idempotent and deterministic. 174 * 175 * @threadsafety This method is safe for concurrent use by multiple threads. 176 */ 177 public final SortedMap<String, Object> attributes() { 178 return this.attributes; 179 } 180 181 /** 182 * Returns an immutable {@link SortedMap} representing any informational-only attributes further describing this 183 * {@link Binding}. 184 * 185 * <p>The attributes are not considered by the {@link #equals(Object)} method.</p> 186 * 187 * @return the informational attributes of this {@link Binding} 188 * 189 * @nullability This method never returns {@code null}. 190 * 191 * @idempotency This method is idempotent and deterministic. 192 * 193 * @threadsafety This method is safe for concurrent use by multiple threads. 194 */ 195 public final SortedMap<String, Object> info() { 196 return this.info; 197 } 198 199 /** 200 * Returns an {@link Optional} housing a {@link ConstantDesc} describing this {@link Binding}, if this {@link Binding} 201 * is capable of being represented as a <a 202 * href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/package-summary.html#condycon">dynamic 203 * constant</a>, or an {@linkplain Optional#isEmpty() empty} {@link Optional} if not. 204 * 205 * @return an {@link Optional} housing a {@link ConstantDesc} describing this {@link Binding}, if this {@link Binding} 206 * is capable of being represented as a <a 207 * href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/package-summary.html#condycon">dynamic 208 * constant</a>, or an {@linkplain Optional#isEmpty() empty} {@link Optional} if not 209 * 210 * @nullability This method never returns {@code null}. 211 * 212 * @idempotency This method is idempotent and deterministic. 213 * 214 * @threadsafety This method is safe for concurrent use by multiple threads. 215 * 216 * @see #describeConstructor() 217 * 218 * @see <a 219 * href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/package-summary.html#condycon">Dynamically-computed 220 * constants</a> 221 */ 222 @Override // Constable 223 public Optional<? extends ConstantDesc> describeConstable() { 224 final MethodHandleDesc constructor = this.describeConstructor(); 225 if (constructor == null) { 226 return Optional.empty(); 227 } 228 final V value = this.value(); 229 final ConstantDesc valueCd; 230 if (value == null) { 231 valueCd = NULL; 232 } else if (value instanceof Constable c) { 233 valueCd = c.describeConstable().orElse(null); 234 } else if (value instanceof ConstantDesc cd) { 235 valueCd = cd; 236 } else { 237 return Optional.empty(); 238 } 239 final ConstantDesc attributesCd = Constables.describeConstable(this.attributes()).orElse(null); 240 if (attributesCd == null) { 241 return Optional.empty(); 242 } 243 final ConstantDesc infoCd = Constables.describeConstable(this.info()).orElse(null); 244 if (infoCd == null) { 245 return Optional.empty(); 246 } 247 return 248 Optional.of(DynamicConstantDesc.of(BSM_INVOKE, constructor, this.name(), valueCd, attributesCd, infoCd)); 249 } 250 251 /** 252 * Returns a {@link MethodHandleDesc} describing the constructor or {@code static} method that will be used to create 253 * a <a 254 * href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/package-summary.html#condycon">dynamic 255 * constant</a> representing this {@link Binding}. 256 * 257 * @return a {@link MethodHandleDesc} describing the constructor or {@code static} method that will be used to create 258 * a <a 259 * href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/package-summary.html#condycon">dynamic 260 * constant</a> representing this {@link Binding} 261 * 262 * @nullability This method does not, and its overrides must not, return {@code null}. 263 * 264 * @idempotency This method is, and its overrides must be, idempotent and deterministic. 265 * 266 * @threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads. 267 */ 268 protected MethodHandleDesc describeConstructor() { 269 return 270 MethodHandleDesc.ofConstructor(this.getClass().describeConstable().orElseThrow(), 271 new ClassDesc[] { CD_String, CD_Object, CD_Map, CD_Map }); 272 } 273 274 /** 275 * Returns a hashcode for this {@link Binding} that represents its {@linkplain #name() name}, {@linkplain #value() 276 * value} and {@linkplain #attributes() attributes}. 277 * 278 * <p>A subclass that overrides this method must also override the {@link #equals(Object)} and {@link 279 * #compareTo(Binding)} methods accordingly.</p> 280 * 281 * @return a hashcode for this {@link Binding} 282 * 283 * @idempotency This method is, and its overrides must be, idempotent and deterministic. 284 * 285 * @threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads. 286 * 287 * @see #equals(Object) 288 * 289 * @see #compareTo(Binding) 290 */ 291 @Override // Object 292 public int hashCode() { 293 int hashCode = 17; 294 295 Object v = this.name(); 296 int c = v == null ? 0 : v.hashCode(); 297 hashCode = 37 * hashCode + c; 298 299 v = this.value(); 300 c = v == null ? 0 : v.hashCode(); 301 hashCode = 37 * hashCode + c; 302 303 v = this.attributes(); 304 c = v == null ? 0 : v.hashCode(); 305 hashCode = 37 * hashCode + c; 306 307 return hashCode; 308 } 309 310 /** 311 * Returns an {@code int} representing a {@linkplain Comparable#compareTo(Object) comparison} of this {@link Binding} 312 * with the supplied {@link Binding}, by considering the {@linkplain #name() names}, {@linkplain #value() values} and 313 * {@linkplain #attributes() attributes} of both {@link Binding}s. 314 * 315 * <p>Any {@linkplain #attributes() attribute values} that are not {@link Comparable} instances will be compared based 316 * on their {@linkplain Object#toString() string representations}. It is strongly recommended that {@linkplain 317 * #attributes() attribute values} implement {@link Comparable}.</p> 318 * 319 * <p>This method is, and its overrides must be, {@linkplain Comparable consistent with equals}.</p> 320 * 321 * <p>A subclass that overrides this method must also override the {@link #hashCode()} and {@link #equals(Object)} 322 * methods accordingly.</p> 323 * 324 * @param other the other {@link Binding}; may be {@code null} in which case a negative value will be returned 325 * 326 * @return an {@code int} representing a {@linkplain Comparable#compareTo(Object) comparison} of this {@link Binding} 327 * with the supplied {@link Binding} 328 * 329 * @idempotency This method is, and its overrides must be, idempotent and deterministic. 330 * 331 * @threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads. 332 * 333 * @see #hashCode() 334 * 335 * @see #equals(Object) 336 */ 337 @Override // Comparable<B> 338 @SuppressWarnings("unchecked") 339 public int compareTo(final B other) { 340 if (other == null) { 341 return -1; 342 } else if (this.equals(other)) { 343 return 0; 344 } else { 345 346 int cmp = this.name().compareTo(other.name()); 347 if (cmp != 0) { 348 return cmp; 349 } 350 351 Object myValue = this.value(); 352 Object otherValue = other.value(); 353 if (value == null) { 354 if (otherValue != null) { 355 return 1; 356 } 357 } else if (otherValue == null) { 358 return -1; 359 } else if (myValue instanceof Comparable) { 360 try { 361 cmp = ((Comparable<Object>)myValue).compareTo(otherValue); 362 } catch (final ClassCastException ohWell) { 363 if (LOGGER.isLoggable(WARNING)) { 364 LOGGER.log(WARNING, ohWell); 365 } 366 } 367 } 368 if (cmp != 0) { 369 return cmp; 370 } 371 372 cmp = String.valueOf(this.value()).compareTo(String.valueOf(other.value())); // NOTE 373 if (cmp != 0) { 374 return cmp; 375 } 376 377 final SortedMap<String, Object> myAttributes = this.attributes(); 378 final SortedMap<String, Object> otherAttributes = other.attributes(); 379 cmp = Integer.compare(myAttributes.size(), otherAttributes.size()); 380 if (cmp != 0) { 381 return cmp; 382 } 383 384 final Iterator<Entry<String, Object>> myEntries = myAttributes.entrySet().iterator(); 385 final Iterator<Entry<String, Object>> otherEntries = otherAttributes.entrySet().iterator(); 386 while (myEntries.hasNext()) { 387 final Entry<String, ?> myEntry = myEntries.next(); 388 final Entry<String, ?> otherEntry = otherEntries.next(); 389 cmp = myEntry.getKey().compareTo(otherEntry.getKey()); 390 if (cmp != 0) { 391 return cmp; 392 } 393 394 myValue = myEntry.getValue(); 395 otherValue = otherEntry.getValue(); 396 if (myValue == null) { 397 if (otherValue != null) { 398 return 1; 399 } 400 } else if (otherValue == null) { 401 return -1; 402 } else if (myValue instanceof Comparable) { 403 try { 404 cmp = ((Comparable<Object>)myValue).compareTo(otherValue); 405 } catch (final ClassCastException ohWell) { 406 if (LOGGER.isLoggable(WARNING)) { 407 LOGGER.log(WARNING, ohWell); 408 } 409 } 410 } 411 if (cmp != 0) { 412 return cmp; 413 } 414 415 cmp = String.valueOf(myEntry.getValue()).compareTo(String.valueOf(otherEntry.getValue())); // NOTE 416 if (cmp != 0) { 417 return cmp; 418 } 419 } 420 421 assert cmp == 0 : "cmp: " + cmp; // really this means equals()/hashCode() were implemented badly 422 return cmp; 423 } 424 } 425 426 /** 427 * Returns {@code true} if the supplied {@link Object} is equal to this {@link Binding}. 428 * 429 * <p>The supplied {@link Object} is considered to be equal to this {@link Binding} if and only if:</p> 430 * 431 * <ul> 432 * 433 * <li>Its {@linkplain #getClass() class} is identical to this {@link Binding}'s {@linkplain #getClass() class}, 434 * and</li> 435 * 436 * <li>Its {@linkplain #name() name} {@linkplain String#equals(Object) is equal to} this {@link Binding}'s {@linkplain 437 * #name() name}, and</li> 438 * 439 * <li>Its {@linkplain #value() value} {@linkplain Object#equals(Object) is equal to} this {@link Binding}'s 440 * {@linkplain #value() value}, and</li> 441 * 442 * <li>Its {@linkplain #attributes() attributes} {@linkplain Map#equals(Object) are equal to} this {@link Binding}'s 443 * {@linkplain #attributes() attributes}</li> 444 * 445 * </ul> 446 * 447 * <p>A subclass that overrides this method must also override the {@link #hashCode()} and {@link #compareTo(Binding)} 448 * methods accordingly.</p> 449 * 450 * @param other the {@link Object} to test; may be {@code null} in which case {@code false} will be returned 451 * 452 * @return {@code true} if the supplied {@link Object} is equal to this {@link Binding} 453 * 454 * @idempotency This method is, and its overrides must be, idempotent and deterministic. 455 * 456 * @threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads. 457 * 458 * @see #hashCode() 459 * 460 * @see #compareTo(Binding) 461 */ 462 @Override // Object 463 public boolean equals(final Object other) { 464 if (this == other) { 465 return true; 466 } else if (other != null && other.getClass() == this.getClass()) { 467 final Binding<?, ?> her = (Binding<?, ?>)other; 468 return 469 Objects.equals(this.name(), her.name()) && 470 Objects.equals(this.value(), her.value()) && 471 Objects.equals(this.attributes(), her.attributes()); 472 } else { 473 return false; 474 } 475 } 476 477 /** 478 * Returns a {@link String} representation of this {@link Binding}. 479 * 480 * <p>The format of the returned {@link String} is deliberately undefined and may change between versions of this 481 * class without prior notice.</p> 482 * 483 * @return a {@link String} representation of this {@link Binding} 484 * 485 * @nullability This method does not, and its overrides must not, return {@code null}. 486 * 487 * @idempotency This method is, and its overrides must be, idempotent and deterministic. 488 * 489 * @threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads. 490 */ 491 @Override // Object 492 public String toString() { 493 final StringBuilder sb = new StringBuilder(this.name()).append('=').append(this.value()); 494 final Map<?, ?> attributes = this.attributes(); 495 if (!attributes.isEmpty()) { 496 sb.append(' ').append(attributes); 497 } 498 final Map<?, ?> info = this.info(); 499 if (!info.isEmpty()) { 500 sb.append(" (").append(info).append(')'); 501 } 502 return sb.toString(); 503 } 504 505}