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}