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