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}