001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2023 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.qualifier; 015 016import java.lang.System.Logger; 017 018import java.lang.constant.Constable; 019import java.lang.constant.DynamicConstantDesc; 020import java.lang.constant.MethodHandleDesc; 021import java.lang.constant.MethodTypeDesc; 022 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.Objects; 031import java.util.Optional; 032import java.util.SortedMap; 033import java.util.TreeMap; 034 035import org.microbean.constant.Constables; 036 037import org.microbean.invoke.ContentHashable; 038 039import static java.lang.System.Logger.Level.DEBUG; 040import static java.lang.System.Logger.Level.WARNING; 041 042import static java.lang.constant.ConstantDescs.BSM_INVOKE; 043import static java.lang.constant.ConstantDescs.CD_Collection; 044import static java.lang.constant.ConstantDescs.CD_Map; 045import static java.lang.constant.ConstantDescs.CD_String; 046import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; 047 048import static org.microbean.qualifier.ConstantDescs.CD_NamedAttributeMap; 049 050public record NamedAttributeMap<V>(String name, 051 Map<String, V> attributes, 052 Map<String, V> info, 053 Collection<NamedAttributeMap<V>> metadata) 054 implements Comparable<NamedAttributeMap<V>>, Constable, ContentHashable { 055 056 private static final Logger LOGGER = System.getLogger(NamedAttributeMap.class.getName()); 057 058 public NamedAttributeMap(final String name) { 059 this(name, Map.of(), Map.of(), List.of()); 060 } 061 062 public NamedAttributeMap(final String name, final V value) { 063 this(name, Map.of("value", value), Map.of(), List.of()); 064 } 065 066 public NamedAttributeMap(final String name, final Map<String, V> attributes) { 067 this(name, attributes, Map.of(), List.of()); 068 } 069 070 @SuppressWarnings("unchecked") 071 public NamedAttributeMap { 072 Objects.requireNonNull(name, "name"); 073 if (attributes.isEmpty()) { 074 attributes = Collections.emptySortedMap(); 075 } else { 076 final SortedMap<String, V> m = 077 attributes instanceof TreeMap<String, V> cloneMe ? 078 (SortedMap<String, V>)cloneMe.clone() : 079 new TreeMap<>(attributes instanceof SortedMap<String, V> sm ? sm.comparator() : null); 080 m.putAll(attributes); 081 attributes = Collections.unmodifiableSortedMap(m); 082 } 083 if (info.isEmpty()) { 084 info = Collections.emptySortedMap(); 085 } else { 086 final SortedMap<String, V> m = 087 info instanceof TreeMap<String, V> cloneMe ? 088 (TreeMap<String, V>)cloneMe.clone() : 089 new TreeMap<>(info instanceof SortedMap<String, V> sm ? sm.comparator() : null); 090 m.putAll(info); 091 info = Collections.unmodifiableSortedMap(m); 092 } 093 if (metadata.isEmpty()) { 094 metadata = List.of(); 095 } else { 096 final List<NamedAttributeMap<V>> l = 097 metadata instanceof ArrayList<NamedAttributeMap<V>> cloneMe ? 098 (List<NamedAttributeMap<V>>)cloneMe.clone() : 099 new ArrayList<>(metadata); 100 Collections.sort(l); 101 metadata = Collections.unmodifiableList(l); 102 } 103 } 104 105 public final boolean isEmpty() { 106 return this.attributes().isEmpty(); 107 } 108 109 public final boolean containsKey(final String k) { 110 return this.attributes().containsKey(k); 111 } 112 113 public final V get(final String k) { 114 return this.attributes().get(k); 115 } 116 117 @Override // ContentHashable 118 public final Optional<String> contentHashInput() { 119 final StringBuilder sb = new StringBuilder(this.name()); 120 for (final Entry<String, ?> e : this.attributes().entrySet()) { 121 sb.append(e.getKey()); 122 final Object value = e.getValue(); 123 if (value instanceof ContentHashable ch) { 124 final CharSequence cs = ch.contentHashInput().orElse(null); 125 if (cs == null) { 126 return Optional.empty(); 127 } 128 sb.append(cs); 129 } else if (value != null) { 130 sb.append(value); 131 } 132 } 133 return Optional.of(sb.toString()); 134 } 135 136 // Consistent with equals(). 137 @Override // Comparable 138 @SuppressWarnings("unchecked") 139 public final int compareTo(final NamedAttributeMap<V> other) { 140 if (other == null) { 141 return -1; 142 } 143 int cmp = this.name().compareTo(other.name()); 144 if (cmp != 0) { 145 return cmp; 146 } 147 final Map<String, V> attributes = this.attributes(); 148 final Map<String, V> herAttributes = other.attributes(); 149 cmp = Integer.signum(attributes.size() - herAttributes.size()); 150 if (cmp != 0) { 151 return cmp; 152 } 153 final Iterator<Entry<String, V>> myIterator = attributes.entrySet().iterator(); 154 final Iterator<Entry<String, V>> herIterator = herAttributes.entrySet().iterator(); 155 while (myIterator.hasNext()) { 156 final Entry<String, V> myEntry = myIterator.next(); 157 final Entry<String, V> herEntry = herIterator.next(); 158 final String myKey = myEntry.getKey(); 159 final String herKey = herEntry.getKey(); 160 if (myKey == null) { 161 if (herKey != null) { 162 return 1; 163 } 164 } else if (herKey == null) { 165 return -1; 166 } 167 cmp = myKey.compareTo(herKey); 168 if (cmp != 0) { 169 return cmp; 170 } 171 final V myValue = myEntry.getValue(); 172 final V herValue = herEntry.getValue(); 173 if (myValue == null) { 174 if (herValue != null) { 175 return 1; 176 } 177 } else if (herValue == null) { 178 return -1; 179 } else if (myValue instanceof Comparable) { 180 try { 181 cmp = ((Comparable<V>)myValue).compareTo(herValue); 182 } catch (final ClassCastException ohWell) { 183 if (LOGGER.isLoggable(WARNING)) { 184 LOGGER.log(WARNING, ohWell); 185 } 186 } 187 } else if (!myValue.equals(herValue)) { 188 if (LOGGER.isLoggable(DEBUG)) { 189 LOGGER.log(DEBUG, "Using toString() for value comparison: {0} <=> {1}", myValue.toString(), herValue.toString()); 190 } 191 cmp = myValue.toString().compareTo(herValue.toString()); 192 } 193 if (cmp != 0) { 194 return cmp; 195 } 196 } 197 // Note: we do not consider info or metadata on purpose 198 assert cmp == 0; 199 return 0; 200 } 201 202 @Override 203 public final boolean equals(final Object other) { 204 if (other == this) { 205 return true; 206 } else if (other != null && other.getClass() == this.getClass()) { 207 final NamedAttributeMap<?> her = (NamedAttributeMap<?>)other; 208 // Note: no info or metadata on purpose 209 return 210 this.name().equals(her.name()) && 211 this.attributes().equals(her.attributes()); 212 } else { 213 return false; 214 } 215 } 216 217 @Override 218 public final int hashCode() { 219 int hashCode = 17; 220 hashCode = 31 * hashCode + this.name().hashCode(); 221 return 31 * hashCode + this.attributes().hashCode(); 222 } 223 224 @Override // Constable 225 public final Optional<DynamicConstantDesc<NamedAttributeMap<V>>> describeConstable() { 226 return Constables.describeConstable(this.attributes()) 227 .flatMap(attributesDesc -> Constables.describeConstable(this.info()) 228 .flatMap(infoDesc -> Constables.describeConstable(this.metadata()) 229 .map(mdDesc -> DynamicConstantDesc.of(BSM_INVOKE, 230 MethodHandleDesc.ofMethod(STATIC, 231 CD_NamedAttributeMap, 232 "of", 233 MethodTypeDesc.of(CD_NamedAttributeMap, 234 CD_String, 235 CD_Map, 236 CD_Map, 237 CD_Collection)), 238 this.name(), 239 attributesDesc, 240 infoDesc, 241 mdDesc)))); 242 } 243 244 // Called by describeConstable(). 245 public static final <V> NamedAttributeMap<V> of(final String name, 246 final Map<String, V> attributes, 247 final Map<String, V> info, 248 final Collection<NamedAttributeMap<V>> metadata) { 249 return new NamedAttributeMap<>(name, attributes, info, metadata); 250 } 251 252}