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