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}