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&lt;String, String&gt; <jv>map1</jv> = Map.of(<js>"key1"</js>, <js>"value1"</js>, <js>"key2"</js>, <js>"value2"</js>);
048 *    Map&lt;String, String&gt; <jv>map2</jv> = Map.of(<js>"key3"</js>, <js>"value3"</js>, <js>"key2"</js>, <js>"value2b"</js>);
049 *    Map&lt;String, String&gt; <jv>map3</jv> = Map.of(<js>"key4"</js>, <js>"value4"</js>);
050 *
051 *    MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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&lt;String, String&gt; <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&lt;String, String&gt; <jv>systemProps</jv> = getSystemProperties();
087 *    Map&lt;String, String&gt; <jv>envVars</jv> = getEnvironmentVariables();
088 *    Map&lt;String, String&gt; <jv>defaults</jv> = getDefaultConfig();
089 *
090 *    MultiMap&lt;String, String&gt; <jv>config</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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 &gt; 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&lt;String, String&gt; <jv>map1</jv> = <jk>new</jk> LinkedHashMap&lt;&gt;(Map.of(<js>"a"</js>, <js>"1"</js>));
123    *    Map&lt;String, String&gt; <jv>map2</jv> = <jk>new</jk> LinkedHashMap&lt;&gt;(Map.of(<js>"b"</js>, <js>"2"</js>));
124    *
125    *    MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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&lt;String, String&gt; <jv>map1</jv> = Map.of(<js>"a"</js>, <js>"1"</js>, <js>"b"</js>, <js>"2"</js>);  <jc>// size = 2</jc>
289    *    Map&lt;String, String&gt; <jv>map2</jv> = Map.of(<js>"b"</js>, <js>"3"</js>, <js>"c"</js>, <js>"4"</js>);  <jc>// size = 2</jc>
290    *    MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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&lt;String, String&gt; <jv>map1</jv> = Map.of(<js>"a"</js>, <js>"1"</js>);
393    *    Map&lt;String, String&gt; <jv>map2</jv> = Map.of(<js>"b"</js>, <js>"2"</js>);
394    *    MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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