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}