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&lt;String, String&gt; <jv>map1</jv> = Map.of(<js>"key1"</js>, <js>"value1"</js>, <js>"key2"</js>, <js>"value2"</js>);
047 *    Map&lt;String, String&gt; <jv>map2</jv> = Map.of(<js>"key3"</js>, <js>"value3"</js>, <js>"key2"</js>, <js>"value2b"</js>);
048 *    Map&lt;String, String&gt; <jv>map3</jv> = Map.of(<js>"key4"</js>, <js>"value4"</js>);
049 *
050 *    MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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&lt;String, String&gt; <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&lt;String, String&gt; <jv>systemProps</jv> = getSystemProperties();
086 *    Map&lt;String, String&gt; <jv>envVars</jv> = getEnvironmentVariables();
087 *    Map&lt;String, String&gt; <jv>defaults</jv> = getDefaultConfig();
088 *
089 *    MultiMap&lt;String, String&gt; <jv>config</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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 &gt; 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&lt;String, String&gt; <jv>map1</jv> = <jk>new</jk> LinkedHashMap&lt;&gt;(Map.of(<js>"a"</js>, <js>"1"</js>));
126    *    Map&lt;String, String&gt; <jv>map2</jv> = <jk>new</jk> LinkedHashMap&lt;&gt;(Map.of(<js>"b"</js>, <js>"2"</js>));
127    *
128    *    MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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&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>
300    *    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>
301    *    MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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&lt;String, String&gt; <jv>map1</jv> = Map.of(<js>"a"</js>, <js>"1"</js>);
404    *    Map&lt;String, String&gt; <jv>map2</jv> = Map.of(<js>"b"</js>, <js>"2"</js>);
405    *    MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> MultiMap&lt;&gt;(<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