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