001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.collections; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.util.*; 022import java.util.stream.*; 023 024import org.apache.juneau.commons.utils.*; 025 026/** 027 * A composite map that presents multiple maps as a single unified map. 028 * 029 * <p> 030 * This class allows multiple maps to be viewed and accessed as if they were merged into 031 * a single map, without actually copying the entries. When the same key exists in multiple maps, 032 * the value from the first map (in constructor order) is returned. 033 * 034 * <h5 class='section'>Features:</h5> 035 * <ul class='spaced-list'> 036 * <li><b>Zero-Copy Composition:</b> No data is copied when creating a MultiMap; it simply wraps the provided maps 037 * <li><b>Transparent Access:</b> Accessing entries by key seamlessly searches all underlying maps in order 038 * <li><b>First-Wins Semantics:</b> When a key exists in multiple maps, the value from the first map is returned 039 * <li><b>Modification Support:</b> Entries can be removed via the iterator's {@link Iterator#remove()} method 040 * <li><b>Efficient Size Calculation:</b> The size is computed by counting unique keys across all maps 041 * </ul> 042 * 043 * <h5 class='section'>Usage:</h5> 044 * <p class='bjava'> 045 * <jc>// Create a MultiMap from three separate maps</jc> 046 * Map<String, String> <jv>map1</jv> = Map.of(<js>"key1"</js>, <js>"value1"</js>, <js>"key2"</js>, <js>"value2"</js>); 047 * Map<String, String> <jv>map2</jv> = Map.of(<js>"key3"</js>, <js>"value3"</js>, <js>"key2"</js>, <js>"value2b"</js>); 048 * Map<String, String> <jv>map3</jv> = Map.of(<js>"key4"</js>, <js>"value4"</js>); 049 * 050 * MultiMap<String, String> <jv>multiMap</jv> = <jk>new</jk> MultiMap<>(<jv>map1</jv>, <jv>map2</jv>, <jv>map3</jv>); 051 * 052 * <jc>// Access entries by key</jc> 053 * <jv>multiMap</jv>.get(<js>"key1"</js>); <jc>// Returns "value1" from map1</jc> 054 * <jv>multiMap</jv>.get(<js>"key2"</js>); <jc>// Returns "value2" from map1 (first match wins)</jc> 055 * <jv>multiMap</jv>.get(<js>"key3"</js>); <jc>// Returns "value3" from map2</jc> 056 * 057 * <jc>// Iterate over all entries from all maps</jc> 058 * <jk>for</jk> (Map.Entry<String, String> <jv>entry</jv> : <jv>multiMap</jv>.entrySet()) { 059 * System.<jsf>out</jsf>.println(<jv>entry</jv>); <jc>// Prints entries from all maps</jc> 060 * } 061 * 062 * <jc>// Get total size (unique keys across all maps)</jc> 063 * <jk>int</jk> <jv>totalSize</jv> = <jv>multiMap</jv>.size(); <jc>// Returns: 4 (key1, key2, key3, key4)</jc> 064 * </p> 065 * 066 * <h5 class='section'>Behavior Notes:</h5> 067 * <ul class='spaced-list'> 068 * <li>The order of key lookup follows the order of maps as provided in the constructor 069 * <li>When a key exists in multiple maps, {@link #get(Object)} returns the value from the first map containing that key 070 * <li>The underlying maps must not be <jk>null</jk>, but can be empty 071 * <li>Modifications via {@link Iterator#remove()} are delegated to the underlying map's entry set iterator 072 * <li>This class does not support {@link #put(Object, Object)}, {@link #remove(Object)}, or {@link #clear()} operations 073 * <li>The {@link #size()} method recomputes the count of unique keys each time it's called (not cached) 074 * <li>The {@link #entrySet()} iterator only returns each key once (first occurrence), even if it exists in multiple maps 075 * </ul> 076 * 077 * <h5 class='section'>Thread Safety:</h5> 078 * <p> 079 * This class is not inherently thread-safe. If the underlying maps are modified concurrently 080 * during iteration or access, the behavior is undefined. Synchronization must be handled externally if needed. 081 * 082 * <h5 class='section'>Example - Processing Multiple Configuration Sources:</h5> 083 * <p class='bjava'> 084 * <jc>// Combine configuration from system properties, environment, and defaults</jc> 085 * Map<String, String> <jv>systemProps</jv> = getSystemProperties(); 086 * Map<String, String> <jv>envVars</jv> = getEnvironmentVariables(); 087 * Map<String, String> <jv>defaults</jv> = getDefaultConfig(); 088 * 089 * MultiMap<String, String> <jv>config</jv> = <jk>new</jk> MultiMap<>(<jv>systemProps</jv>, <jv>envVars</jv>, <jv>defaults</jv>); 090 * 091 * <jc>// Access configuration (system props take precedence, then env vars, then defaults)</jc> 092 * String <jv>host</jv> = <jv>config</jv>.get(<js>"host"</js>); 093 * </p> 094 * 095 * <h5 class='section'>See Also:</h5> 096 * <ul> 097 * <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview > juneau-commons</a> 098 * </ul> 099 * 100 * @param <K> The key type of this map. 101 * @param <V> The value type of this map. 102 */ 103@SuppressWarnings({ 104 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 105 "java:S3740" // Raw Map/Iterator types used in multi-map structural operations where value collection type is erased 106}) 107public class MultiMap<K,V> extends AbstractMap<K,V> { 108 109 /** 110 * The underlying maps being wrapped by this MultiMap. 111 * <p> 112 * These maps are accessed directly during key lookup and iteration without copying. 113 */ 114 final Map<K,V>[] m; 115 116 /** 117 * Creates a new MultiMap that presents the specified maps as a single unified map. 118 * 119 * <p> 120 * The maps are stored by reference (not copied), so modifications made through the 121 * MultiMap's iterator will affect the original maps. 122 * 123 * <h5 class='section'>Example:</h5> 124 * <p class='bjava'> 125 * Map<String, String> <jv>map1</jv> = <jk>new</jk> LinkedHashMap<>(Map.of(<js>"a"</js>, <js>"1"</js>)); 126 * Map<String, String> <jv>map2</jv> = <jk>new</jk> LinkedHashMap<>(Map.of(<js>"b"</js>, <js>"2"</js>)); 127 * 128 * MultiMap<String, String> <jv>multiMap</jv> = <jk>new</jk> MultiMap<>(<jv>map1</jv>, <jv>map2</jv>); 129 * <jc>// multiMap now represents all entries from both maps</jc> 130 * </p> 131 * 132 * @param maps Zero or more maps to combine into this map. Must not be <jk>null</jk>, 133 * and no individual map can be <jk>null</jk> (but maps can be empty). 134 * @throws IllegalArgumentException if the maps array or any map within it is <jk>null</jk>. 135 */ 136 @SafeVarargs 137 public MultiMap(Map<K,V>...maps) { 138 reqnn("maps", maps); 139 for (var map : maps) 140 reqnn("maps", map); 141 m = maps; 142 } 143 144 /** 145 * Returns the value to which the specified key is mapped, or <jk>null</jk> if this map contains no mapping for the key. 146 * 147 * <p> 148 * This method searches the underlying maps in the order they were provided to the constructor. 149 * The first map containing the key determines the returned value. 150 * 151 * @param key The key whose associated value is to be returned. 152 * @return The value to which the specified key is mapped, or <jk>null</jk> if this map contains no mapping for the key. 153 */ 154 @Override 155 public V get(Object key) { 156 for (var map : m) { 157 if (map.containsKey(key)) 158 return map.get(key); 159 } 160 return null; 161 } 162 163 /** 164 * Returns <jk>true</jk> if this map contains a mapping for the specified key. 165 * 166 * @param key The key whose presence in this map is to be tested. 167 * @return <jk>true</jk> if this map contains a mapping for the specified key. 168 */ 169 @Override 170 public boolean containsKey(Object key) { 171 for (var map : m) { 172 if (map.containsKey(key)) 173 return true; 174 } 175 return false; 176 } 177 178 /** 179 * Returns <jk>true</jk> if this map maps one or more keys to the specified value. 180 * 181 * @param value The value whose presence in this map is to be tested. 182 * @return <jk>true</jk> if this map maps one or more keys to the specified value. 183 */ 184 @Override 185 public boolean containsValue(Object value) { 186 for (var map : m) { 187 if (map.containsValue(value)) 188 return true; 189 } 190 return false; 191 } 192 193 /** 194 * Returns a {@link Set} view of the mappings contained in this map. 195 * 196 * <p> 197 * The returned set is a composite view of all underlying maps. When a key exists in multiple maps, 198 * only the entry from the first map (in constructor order) is included. 199 * 200 * <p> 201 * The iterator supports the {@link Iterator#remove()} operation, which removes the entry 202 * from its underlying map. 203 * 204 * @return A set view of the mappings contained in this map. 205 */ 206 @Override 207 @SuppressWarnings({ 208 "java:S3776" // Cognitive complexity acceptable for entry set iteration over multiple maps 209 }) 210 public Set<Entry<K,V>> entrySet() { 211 return new AbstractSet<>() { 212 @Override 213 public Iterator<Entry<K,V>> iterator() { 214 return new EntryIterator(); 215 } 216 217 @Override 218 public int size() { 219 return MultiMap.this.size(); 220 } 221 }; 222 } 223 224 private final class EntryIterator implements Iterator<Entry<K,V>> { 225 int mapIndex; 226 Iterator<Entry<K,V>> currentIterator; 227 final Set<K> seenKeys = new HashSet<>(); 228 Entry<K,V> nextEntry; 229 Iterator<Entry<K,V>> lastIterator; 230 boolean canRemove; 231 232 EntryIterator() { 233 mapIndex = 0; 234 currentIterator = null; 235 nextEntry = null; 236 lastIterator = null; 237 canRemove = false; 238 if (m.length > 0) { 239 currentIterator = m[0].entrySet().iterator(); 240 advance(); 241 } 242 } 243 244 private void advance() { 245 nextEntry = null; 246 while (currentIterator != null) { 247 while (currentIterator.hasNext()) { 248 var entry = currentIterator.next(); 249 if (!seenKeys.contains(entry.getKey())) { 250 seenKeys.add(entry.getKey()); 251 nextEntry = entry; 252 return; 253 } 254 } 255 mapIndex++; 256 if (mapIndex < m.length) { 257 currentIterator = m[mapIndex].entrySet().iterator(); 258 } else { 259 currentIterator = null; 260 } 261 } 262 } 263 264 @Override 265 public boolean hasNext() { 266 return nextEntry != null; 267 } 268 269 @Override 270 public Entry<K,V> next() { 271 if (nextEntry == null) 272 throw new NoSuchElementException(); 273 var result = nextEntry; 274 lastIterator = currentIterator; 275 canRemove = true; 276 advance(); 277 return result; 278 } 279 280 @Override 281 public void remove() { 282 if (!canRemove || lastIterator == null) 283 throw new IllegalStateException(); 284 lastIterator.remove(); 285 canRemove = false; 286 } 287 } 288 289 /** 290 * Returns the number of unique key-value mappings in this map. 291 * 292 * <p> 293 * This method computes the size by counting unique keys across all underlying maps. 294 * If a key exists in multiple maps, it is counted only once. The size is recalculated 295 * each time this method is called (it is not cached). 296 * 297 * <h5 class='section'>Example:</h5> 298 * <p class='bjava'> 299 * Map<String, String> <jv>map1</jv> = Map.of(<js>"a"</js>, <js>"1"</js>, <js>"b"</js>, <js>"2"</js>); <jc>// size = 2</jc> 300 * Map<String, String> <jv>map2</jv> = Map.of(<js>"b"</js>, <js>"3"</js>, <js>"c"</js>, <js>"4"</js>); <jc>// size = 2</jc> 301 * MultiMap<String, String> <jv>multiMap</jv> = <jk>new</jk> MultiMap<>(<jv>map1</jv>, <jv>map2</jv>); 302 * 303 * <jk>int</jk> <jv>totalSize</jv> = <jv>multiMap</jv>.size(); <jc>// Returns: 3 (a, b, c - b is counted only once)</jc> 304 * </p> 305 * 306 * @return The number of unique key-value mappings in this map. 307 */ 308 @Override 309 public int size() { 310 var seenKeys = new HashSet<>(); 311 for (var map : m) { 312 seenKeys.addAll(map.keySet()); 313 } 314 return seenKeys.size(); 315 } 316 317 /** 318 * Returns <jk>true</jk> if this map contains no key-value mappings. 319 * 320 * @return <jk>true</jk> if this map contains no key-value mappings. 321 */ 322 @Override 323 public boolean isEmpty() { 324 for (var map : m) { 325 if (!map.isEmpty()) 326 return false; 327 } 328 return true; 329 } 330 331 /** 332 * Returns a {@link Collection} view of the values contained in this map. 333 * 334 * <p> 335 * The returned collection is a view of the values from the entries in {@link #entrySet()}. 336 * When a key exists in multiple maps, only the value from the first map (in constructor order) 337 * is included. 338 * 339 * @return A collection view of the values contained in this map. 340 */ 341 @Override 342 public Collection<V> values() { 343 return new AbstractCollection<>() { 344 @Override 345 public Iterator<V> iterator() { 346 return new Iterator<>() { 347 private final Iterator<Entry<K,V>> entryIterator = entrySet().iterator(); 348 349 @Override 350 public boolean hasNext() { 351 return entryIterator.hasNext(); 352 } 353 354 @Override 355 public V next() { 356 return entryIterator.next().getValue(); 357 } 358 359 @Override 360 public void remove() { 361 entryIterator.remove(); 362 } 363 }; 364 } 365 366 @Override 367 public int size() { 368 return MultiMap.this.size(); 369 } 370 }; 371 } 372 373 // Unsupported operations 374 @Override 375 public V put(K key, V value) { 376 throw uoex("put() not supported on MultiMap. Use underlying maps directly."); 377 } 378 379 @Override 380 public V remove(Object key) { 381 throw uoex("remove() not supported on MultiMap. Use underlying maps directly."); 382 } 383 384 @Override 385 public void putAll(Map<? extends K,? extends V> m) { 386 throw uoex("putAll() not supported on MultiMap. Use underlying maps directly."); 387 } 388 389 @Override 390 public void clear() { 391 throw uoex("clear() not supported on MultiMap. Use underlying maps directly."); 392 } 393 394 /** 395 * Returns a string representation of this MultiMap. 396 * 397 * <p> 398 * The format is <c>"[{...},{...},...]"</c> where each <c>{...}</c> is the standard 399 * standard string representation of each underlying map (as returned by {@link Object#toString()}). 400 * 401 * <h5 class='section'>Example:</h5> 402 * <p class='bjava'> 403 * Map<String, String> <jv>map1</jv> = Map.of(<js>"a"</js>, <js>"1"</js>); 404 * Map<String, String> <jv>map2</jv> = Map.of(<js>"b"</js>, <js>"2"</js>); 405 * MultiMap<String, String> <jv>multiMap</jv> = <jk>new</jk> MultiMap<>(<jv>map1</jv>, <jv>map2</jv>); 406 * <jv>multiMap</jv>.toString(); <jc>// Returns: "[{a=1}, {b=2}]"</jc> 407 * </p> 408 * 409 * @return A string representation of this MultiMap. 410 */ 411 @Override 412 public String toString() { 413 return Arrays.stream(m).map(Object::toString).collect(Collectors.joining(", ", "[", "]")); 414 } 415 416 /** 417 * Compares the specified object with this map for equality. 418 * 419 * <p> 420 * Returns <jk>true</jk> if the given object is also a map and the two maps represent the same 421 * mappings. More formally, two maps <c>m1</c> and <c>m2</c> represent the same mappings if 422 * <c>m1.entrySet().equals(m2.entrySet())</c>. 423 * 424 * <p> 425 * This implementation compares the entry sets of the two maps. 426 * 427 * @param o Object to be compared for equality with this map. 428 * @return <jk>true</jk> if the specified object is equal to this map. 429 */ 430 @Override 431 public boolean equals(Object o) { 432 return (o instanceof Map o2) && Shorts.eq(this, o2, (x, y) -> x.entrySet().equals(y.entrySet())); 433 } 434 435 /** 436 * Returns the hash code value for this map. 437 * 438 * <p> 439 * The hash code of a map is defined to be the sum of the hash codes of each entry in the map's 440 * <c>entrySet()</c> view. This ensures that <c>m1.equals(m2)</c> implies that 441 * <c>m1.hashCode()==m2.hashCode()</c> for any two maps <c>m1</c> and <c>m2</c>, as required 442 * by the general contract of {@link Object#hashCode()}. 443 * 444 * <p> 445 * This implementation computes the hash code from the entry set. 446 * 447 * @return The hash code value for this map. 448 */ 449 @Override 450 public int hashCode() { 451 return entrySet().hashCode(); 452 } 453} 454