001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 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.Constable;
019import java.lang.constant.DynamicConstantDesc;
020import java.lang.constant.MethodHandleDesc;
021import java.lang.constant.MethodTypeDesc;
022
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Objects;
031import java.util.Optional;
032import java.util.SortedMap;
033import java.util.TreeMap;
034
035import org.microbean.constant.Constables;
036
037import org.microbean.invoke.ContentHashable;
038
039import static java.lang.System.Logger.Level.DEBUG;
040import static java.lang.System.Logger.Level.WARNING;
041
042import static java.lang.constant.ConstantDescs.BSM_INVOKE;
043import static java.lang.constant.ConstantDescs.CD_Collection;
044import static java.lang.constant.ConstantDescs.CD_Map;
045import static java.lang.constant.ConstantDescs.CD_String;
046import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
047
048import static org.microbean.qualifier.ConstantDescs.CD_NamedAttributeMap;
049
050public record NamedAttributeMap<V>(String name,
051                                   Map<String, V> attributes,
052                                   Map<String, V> info,
053                                   Collection<NamedAttributeMap<V>> metadata)
054  implements Comparable<NamedAttributeMap<V>>, Constable, ContentHashable {
055
056  private static final Logger LOGGER = System.getLogger(NamedAttributeMap.class.getName());
057
058  public NamedAttributeMap(final String name) {
059    this(name, Map.of(), Map.of(), List.of());
060  }
061
062  public NamedAttributeMap(final String name, final V value) {
063    this(name, Map.of("value", value), Map.of(), List.of());
064  }
065
066  public NamedAttributeMap(final String name, final Map<String, V> attributes) {
067    this(name, attributes, Map.of(), List.of());
068  }
069
070  @SuppressWarnings("unchecked")
071  public NamedAttributeMap {
072    Objects.requireNonNull(name, "name");
073    if (attributes.isEmpty()) {
074      attributes = Collections.emptySortedMap();
075    } else {
076      final SortedMap<String, V> m =
077        attributes instanceof TreeMap<String, V> cloneMe ?
078        (SortedMap<String, V>)cloneMe.clone() :
079        new TreeMap<>(attributes instanceof SortedMap<String, V> sm ? sm.comparator() : null);
080      m.putAll(attributes);
081      attributes = Collections.unmodifiableSortedMap(m);
082    }
083    if (info.isEmpty()) {
084      info = Collections.emptySortedMap();
085    } else {
086      final SortedMap<String, V> m =
087        info instanceof TreeMap<String, V> cloneMe ?
088        (TreeMap<String, V>)cloneMe.clone() :
089        new TreeMap<>(info instanceof SortedMap<String, V> sm ? sm.comparator() : null);
090      m.putAll(info);
091      info = Collections.unmodifiableSortedMap(m);
092    }
093    if (metadata.isEmpty()) {
094      metadata = List.of();
095    } else {
096      final List<NamedAttributeMap<V>> l =
097        metadata instanceof ArrayList<NamedAttributeMap<V>> cloneMe ?
098        (List<NamedAttributeMap<V>>)cloneMe.clone() :
099        new ArrayList<>(metadata);
100      Collections.sort(l);
101      metadata = Collections.unmodifiableList(l);
102    }
103  }
104
105  public final boolean isEmpty() {
106    return this.attributes().isEmpty();
107  }
108
109  public final boolean containsKey(final String k) {
110    return this.attributes().containsKey(k);
111  }
112
113  public final V get(final String k) {
114    return this.attributes().get(k);
115  }
116
117  @Override // ContentHashable
118  public final Optional<String> contentHashInput() {
119    final StringBuilder sb = new StringBuilder(this.name());
120    for (final Entry<String, ?> e : this.attributes().entrySet()) {
121      sb.append(e.getKey());
122      final Object value = e.getValue();
123      if (value instanceof ContentHashable ch) {
124        final CharSequence cs = ch.contentHashInput().orElse(null);
125        if (cs == null) {
126          return Optional.empty();
127        }
128        sb.append(cs);
129      } else if (value != null) {
130        sb.append(value);
131      }
132    }
133    return Optional.of(sb.toString());
134  }
135
136  // Consistent with equals().
137  @Override // Comparable
138  @SuppressWarnings("unchecked")
139  public final int compareTo(final NamedAttributeMap<V> other) {
140    if (other == null) {
141      return -1;
142    }
143    int cmp = this.name().compareTo(other.name());
144    if (cmp != 0) {
145      return cmp;
146    }
147    final Map<String, V> attributes = this.attributes();
148    final Map<String, V> herAttributes = other.attributes();
149    cmp = Integer.signum(attributes.size() - herAttributes.size());
150    if (cmp != 0) {
151      return cmp;
152    }
153    final Iterator<Entry<String, V>> myIterator = attributes.entrySet().iterator();
154    final Iterator<Entry<String, V>> herIterator = herAttributes.entrySet().iterator();
155    while (myIterator.hasNext()) {
156      final Entry<String, V> myEntry = myIterator.next();
157      final Entry<String, V> herEntry = herIterator.next();
158      final String myKey = myEntry.getKey();
159      final String herKey = herEntry.getKey();
160      if (myKey == null) {
161        if (herKey != null) {
162          return 1;
163        }
164      } else if (herKey == null) {
165        return -1;
166      }
167      cmp = myKey.compareTo(herKey);
168      if (cmp != 0) {
169        return cmp;
170      }
171      final V myValue = myEntry.getValue();
172      final V herValue = herEntry.getValue();
173      if (myValue == null) {
174        if (herValue != null) {
175          return 1;
176        }
177      } else if (herValue == null) {
178        return -1;
179      } else if (myValue instanceof Comparable) {
180        try {
181          cmp = ((Comparable<V>)myValue).compareTo(herValue);
182        } catch (final ClassCastException ohWell) {
183          if (LOGGER.isLoggable(WARNING)) {
184            LOGGER.log(WARNING, ohWell);
185          }
186        }
187      } else if (!myValue.equals(herValue)) {
188        if (LOGGER.isLoggable(DEBUG)) {
189          LOGGER.log(DEBUG, "Using toString() for value comparison: {0} <=> {1}", myValue.toString(), herValue.toString());
190        }
191        cmp = myValue.toString().compareTo(herValue.toString());
192      }
193      if (cmp != 0) {
194        return cmp;
195      }
196    }
197    // Note: we do not consider info or metadata on purpose
198    assert cmp == 0;
199    return 0;
200  }
201
202  @Override
203  public final boolean equals(final Object other) {
204    if (other == this) {
205      return true;
206    } else if (other != null && other.getClass() == this.getClass()) {
207      final NamedAttributeMap<?> her = (NamedAttributeMap<?>)other;
208      // Note: no info or metadata on purpose
209      return
210        this.name().equals(her.name()) &&
211        this.attributes().equals(her.attributes());
212    } else {
213      return false;
214    }
215  }
216
217  @Override
218  public final int hashCode() {
219    int hashCode = 17;
220    hashCode = 31 * hashCode + this.name().hashCode();
221    return 31 * hashCode + this.attributes().hashCode();
222  }
223
224  @Override // Constable
225  public final Optional<DynamicConstantDesc<NamedAttributeMap<V>>> describeConstable() {
226    return Constables.describeConstable(this.attributes())
227      .flatMap(attributesDesc -> Constables.describeConstable(this.info())
228               .flatMap(infoDesc -> Constables.describeConstable(this.metadata())
229                        .map(mdDesc -> DynamicConstantDesc.of(BSM_INVOKE,
230                                                              MethodHandleDesc.ofMethod(STATIC,
231                                                                                        CD_NamedAttributeMap,
232                                                                                        "of",
233                                                                                        MethodTypeDesc.of(CD_NamedAttributeMap,
234                                                                                                          CD_String,
235                                                                                                          CD_Map,
236                                                                                                          CD_Map,
237                                                                                                          CD_Collection)),
238                                                              this.name(),
239                                                              attributesDesc,
240                                                              infoDesc,
241                                                              mdDesc))));
242  }
243
244  // Called by describeConstable().
245  public static final <V> NamedAttributeMap<V> of(final String name,
246                                                  final Map<String, V> attributes,
247                                                  final Map<String, V> info,
248                                                  final Collection<NamedAttributeMap<V>> metadata) {
249    return new NamedAttributeMap<>(name, attributes, info, metadata);
250  }
251
252}