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}