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