001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2025 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.attributes;
015
016import java.lang.constant.ClassDesc;
017import java.lang.constant.DynamicConstantDesc;
018import java.lang.constant.MethodHandleDesc;
019import java.lang.constant.MethodTypeDesc;
020
021import java.util.Optional;
022
023import java.util.function.Predicate;
024
025import java.util.Arrays;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Objects;
031import java.util.TreeMap;
032
033import org.microbean.constant.Constables;
034
035import static java.lang.constant.ConstantDescs.BSM_INVOKE;
036import static java.lang.constant.ConstantDescs.CD_Map;
037import static java.lang.constant.ConstantDescs.CD_String;
038import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
039
040import static java.util.Collections.unmodifiableSortedMap;
041
042/**
043 * An {@linkplain Attributed attributed} {@link Value} with a {@linkplain #name() name}, {@linkplain #values() named
044 * values}, and {@linkplain #notes() non-normative named values}.
045 *
046 * @param name a non-{@code null} name of this {@link Attributes}; names that begin with the {@code $} character are
047 * reserved for internal use
048 *
049 * @param values a non-{@code null} {@link Map} of named {@linkplain Value values} associated with this {@link
050 * Attributes}; keys in this {@link Map} that begin with the {@code $} character are reserved for internal use
051 *
052 * @param notes a non-{@code null} {@link Map} of non-normative named {@linkplain Value values} associated with this
053 * {@link Attributes}; keys in this {@link Map} that begin with the {@code $} character are reserved for internal use
054 *
055 * @param attributesMap a non-{@code null} {@link Map} of named metadata associated with this {@link Attributes}; any
056 * {@link List} stored under a key that is equal to the supplied {@code name} is considered to be a list of {@link
057 * Attributes} pertaining to this {@link Attributes} itself; other keys are normally (but are not required to be) the
058 * names of keys in the supplied {@code values} or {@code notes} {@link Map}s; keys that begin with the {@code $}
059 * character are reserved for internal use
060 *
061 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
062 */
063public final record Attributes(String name,
064                               Map<String, Value<?>> values,
065                               Map<String, Value<?>> notes,
066                               Map<String, List<Attributes>> attributesMap)
067  implements Attributed, Value<Attributes> {
068
069  /**
070   * Creates a new {@link Attributes}.
071   *
072   * @param name a non-{@code null} name of this {@link Attributes}
073   *
074   * @param values a non-{@code null} {@link Map} of named {@linkplain Value values} associated with this {@link
075   * Attributes}; keys in this {@link Map} that begin with the {@code $} character are reserved for internal use
076   *
077   * @param notes a non-{@code null} {@link Map} of non-normative named {@linkplain Value values} associated with this
078   * {@link Attributes}; keys in this {@link Map} that begin with the {@code $} character are reserved for internal use
079   *
080   * @param attributesMap a non-{@code null} {@link Map} of named metadata associated with this {@link Attributes}; any
081   * {@link List} stored under a key that is equal to the supplied {@code name} is considered to be a list of {@link
082   * Attributes} pertaining to this {@link Attributes} itself; other keys are normally (but are not required to be) the
083   * names of keys in the supplied {@code values} or {@code notes} {@link Map}s; keys in this {@link Map} that begin
084   * with the {@code $} character are reserved for internal use
085   *
086   * @exception NullPointerException if any argument is {@code null}
087   */
088  public Attributes {
089    Objects.requireNonNull(name, "name");
090    switch (values.size()) {
091    case 0:
092      values = Map.of();
093      break;
094    case 1:
095      values = Map.copyOf(values);
096      break;
097    default:
098      final TreeMap<String, Value<?>> sortedValues = new TreeMap<>();
099      sortedValues.putAll(values);
100      values = unmodifiableSortedMap(sortedValues);
101    }
102    if (values.containsKey("")) {
103      throw new IllegalArgumentException("values: " + values);
104    }
105    switch (notes.size()) {
106    case 0:
107      notes = Map.of();
108      break;
109    case 1:
110      notes = Map.copyOf(notes);
111      break;
112    default:
113      final TreeMap<String, Value<?>> sortedNotes = new TreeMap<>();
114      sortedNotes.putAll(notes);
115      notes = unmodifiableSortedMap(sortedNotes);
116    }
117    if (notes.containsKey("")) {
118      throw new IllegalArgumentException("notes: " + notes);
119    }
120    switch (attributesMap.size()) {
121    case 0:
122      attributesMap = Map.of();
123      break;
124    case 1:
125      attributesMap = Map.copyOf(attributesMap);
126      break;
127    default:
128      final TreeMap<String, List<Attributes>> sortedAttributesMap = new TreeMap<>();
129      for (final Entry<String, List<Attributes>> e : attributesMap.entrySet()) {
130        sortedAttributesMap.put(e.getKey(), List.copyOf(e.getValue()));
131      }
132      attributesMap = unmodifiableSortedMap(sortedAttributesMap);
133      break;
134    }
135    if (attributesMap.containsKey("")) {
136      throw new IllegalArgumentException("attributesMap: " + attributesMap);
137    }
138  }
139
140  /**
141   * Returns a {@link List} of {@link Attributes} associated with this {@link Attributes} directly, or an {@linkplain
142   * List#isEmpty() empty <code>List</code>} if there is no such {@link List}.
143   *
144   * @return a non-{@code null} {@link List} of {@link Attributes}
145   *
146   * @see #attributes(String)
147   *
148   * @see #name()
149   */
150  @Override // Attributed
151  public final List<Attributes> attributes() {
152    return this.attributes(this.name());
153  }
154
155  /**
156   * Returns a {@link List} of {@link Attributes} associated with the supplied {@code key}, or an {@linkplain
157   * List#isEmpty() empty <code>List</code>} if there is no such {@link List}.
158   *
159   * @param key a {@link String}; must not be {@code null}
160   *
161   * @return a non-{@code null} {@link List} of {@link Attributes}
162   *
163   * @exception NullPointerException if {@code key} is {@code null}
164   */
165  public final List<Attributes> attributes(final String key) {
166    return this.attributesMap().getOrDefault(key, List.of());
167  }
168
169  @Override // Comparable<Attributes>
170  public final int compareTo(final Attributes other) {
171    if (other == null) {
172      return -1;
173    } else if (this.equals(other)) {
174      return 0;
175    }
176    final int c = (this.name() + this.values()).compareTo(other.name() + other.values());
177    // (Possible? that c simply cannot be 0 here?)
178    return c != 0 ? c : (this.notes().toString() + this.attributesMap()).compareTo(other.notes().toString() + other.attributesMap());
179  }
180
181  @Override // Constable
182  public final Optional<DynamicConstantDesc<Attributes>> describeConstable() {
183    final ClassDesc me = ClassDesc.of(this.getClass().getName());
184    return Constables.describeConstable(this.values())
185      .flatMap(valuesDesc -> Constables.describeConstable(this.notes())
186               .flatMap(notesDesc -> Constables.describeConstable(this.attributesMap())
187                        .map(attributesDesc -> DynamicConstantDesc.of(BSM_INVOKE,
188                                                                      MethodHandleDesc.ofMethod(STATIC,
189                                                                                                me,
190                                                                                                "of",
191                                                                                                MethodTypeDesc.of(me,
192                                                                                                                  CD_String,
193                                                                                                                  CD_Map,
194                                                                                                                  CD_Map,
195                                                                                                                  CD_Map)),
196                                                                      this.name(),
197                                                                      valuesDesc,
198                                                                      notesDesc,
199                                                                      attributesDesc))));
200  }
201
202  /**
203   * Returns {@code true} if this {@link Attributes} equals the supplied {@link Object}.
204   *
205   * <p>If the supplied {@link Object} is also an {@link Attributes} and has a {@linkplain #name() name} equal to this
206   * {@link Attributes}' {@linkplain #name() name} and a {@linkplain #values() values} {@link Map} {@linkplain
207   * Map#equals(Object) equal to} this {@link Attributes}' {@linkplain #values() values} {@link Map}, this method
208   * returns {@code true}.</p>
209   *
210   * <p>This method returns {@code false} in all other cases.</p>
211   *
212   * @param other an {@link Object}; may be {@code null}
213   *
214   * @return {@code true} if this {@link Attributes} equals the supplied {@link Object}
215   *
216   * @see #hashCode()
217   *
218   * @see #name()
219   *
220   * @see #values()
221   */
222  @Override // Record
223  public final boolean equals(final Object other) {
224    return
225      other == this ||
226      // Follow java.lang.annotation.Annotation requirements.
227      other instanceof Attributes a && this.name().equals(a.name()) && this.values().equals(a.values());
228  }
229
230  /**
231   * Returns a hash code value for this {@link Attributes} derived solely from its {@linkplain #values() values}.
232   *
233   * @return a hash code value
234   *
235   * @see #values()
236   *
237   * @see #equals(Object)
238   */
239  @Override // Record
240  public final int hashCode() {
241    // Follow java.lang.annotation.Annotation requirements.
242    int hashCode = 0;
243    for (final Entry<String, Value<?>> e : this.values().entrySet()) {
244      hashCode += (127 * e.getKey().hashCode()) ^ e.getValue().hashCode();
245    }
246    return hashCode;
247  }
248
249  @Override // Record
250  public final String toString() {
251    final StringBuilder sb = new StringBuilder();
252    sb.append("@").append(this.name());
253    final Map<String, Value<?>> values = this.values();
254    final Map<String, Value<?>> notes = this.notes();
255    if (values.isEmpty() && notes.isEmpty()) {
256      return sb.toString();
257    }
258    sb.append("(");
259    if (!values.isEmpty()) {
260      final Iterator<Entry<String, Value<?>>> i = values.entrySet().iterator();
261      while (i.hasNext()) {
262        final Entry<String, Value<?>> e = i.next();
263        sb.append(e.getKey()).append(" = ").append(e.getValue());
264        if (i.hasNext()) {
265          sb.append(", ");
266        }
267      }
268    }
269    if (!notes.isEmpty()) {
270      sb.append(" (");
271      final Iterator<Entry<String, Value<?>>> i = notes.entrySet().iterator();
272      while (i.hasNext()) {
273        final Entry<String, Value<?>> e = i.next();
274        sb.append(e.getKey()).append(" = ").append(e.getValue());
275        if (i.hasNext()) {
276          sb.append(", ");
277        }
278      }
279      sb.append(")");
280    }
281    sb.append(")");
282    return sb.toString();
283  }
284
285  /**
286   * Returns a suitably-typed {@link Value} indexed under the supplied {@code name}, or {@code null} if no such {@link
287   * Value} exists.
288   *
289   * @param <T> the type of the {@link Value}
290   *
291   * @param name the name; must not be {@code null}
292   *
293   * @return a suitably-typed {@link Value} indexed under the supplied {@code name}, or {@code null} if no such {@link
294   * Value} exists
295   *
296   * @exception NullPointerException if {@code name} is {@code null}
297   *
298   * @exception ClassCastException if {@code <T>} does not match the actual type of the {@link Value} indexed under the
299   * supplied {@code name}
300   */
301  @SuppressWarnings("unchecked")
302  public final <T extends Value<T>> T value(final String name) {
303    return (T)this.values().get(name);
304  }
305
306  /**
307   * Returns an {@link Attributes} comprising the supplied arguments.
308   *
309   * @param name the name; must not be {@code null}
310   *
311   * @return a non-{@code null} {@link Attributes}
312   *
313   * @exception NullPointerException if {@code name} is {@code null}
314   *
315   * @see #of(String, Map, Map, Map)
316   */
317  public static final Attributes of(final String name) {
318    return of(name, Map.of(), Map.of(), Map.of());
319  }
320
321  /**
322   * Returns an {@link Attributes} comprising the supplied arguments.
323   *
324   * @param name the name; must not be {@code null}
325   *
326   * @param valueValue a {@link String} that will be indexed under the key "{@code value}"; must not be {@code null}
327   *
328   * @return a non-{@code null} {@link Attributes}
329   *
330   * @exception NullPointerException if {@code name} or {@code valueValue} is {@code null}
331   *
332   * @see #of(String, Map, Map, Map)
333   */
334  public static final Attributes of(final String name, final String valueValue) {
335    return of(name, Map.of("value", new StringValue(valueValue)), Map.of(), Map.of());
336  }
337
338  /**
339   * Returns an {@link Attributes} comprising the supplied arguments.
340   *
341   * @param name the name; must not be {@code null}
342   *
343   * @param attributes an array of {@link Attributes}; may be {@code null}
344   *
345   * @return a non-{@code null} {@link Attributes}
346   *
347   * @exception NullPointerException if {@code name} is {@code null}
348   *
349   * @see #of(String, List)
350   */
351  public static final Attributes of(final String name, final Attributes... attributes) {
352    return of(name, attributes == null || attributes.length == 0 ? List.of() : Arrays.asList(attributes));
353  }
354
355  /**
356   * Returns an {@link Attributes} comprising the supplied arguments.
357   *
358   * @param name the name; must not be {@code null}
359   *
360   * @param attributes a non-{@code null} {@link List} of {@link Attributes}
361   *
362   * @return a non-{@code null} {@link Attributes}
363   *
364   * @exception NullPointerException if any argument is {@code null}
365   *
366   * @see #of(String, Map, Map, Map)
367   */
368  public static final Attributes of(final String name, final List<Attributes> attributes) {
369    return of(name, Map.of(), Map.of(), Map.of(name, attributes));
370  }
371
372  /**
373   * Returns an {@link Attributes} comprising the supplied arguments.
374   *
375   * @param name the name; must not be {@code null}
376   *
377   * @param values a {@link Map} of {@link Value}s indexed by {@link String} keys; must not be {@code null}
378   *
379   * @param notes a {@link Map} of {@link Value}s indexed by {@link String} keys containing descriptive information
380   * only; must not be {@code null}; not incorporated into equality calculations
381   *
382   * @param attributes a {@link Map} of {@link List}s of {@link Attributes} instances denoting metadata for a given
383   * value in {@code values} (or for this {@link Attributes} as a whole if the key in question is equal to {@code
384   * name}); must not be {@code null}
385   *
386   * @return a non-{@code null} {@link Attributes}
387   *
388   * @exception NullPointerException if any argument is {@code null}
389   */
390  public static final Attributes of(final String name,
391                                    final Map<String, Value<?>> values,
392                                    final Map<String, Value<?>> notes,
393                                    final Map<String, List<Attributes>> attributes) {
394    return new Attributes(name, values, notes, attributes);
395  }
396
397}