This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 6a9f1177d0 Marshall module improvements
6a9f1177d0 is described below

commit 6a9f1177d03648b740791168df2ff129fcc7f300
Author: James Bognar <[email protected]>
AuthorDate: Sat Dec 13 09:22:35 2025 -0500

    Marshall module improvements
---
 .../juneau/commons/collections/MultiList.java      |  82 ++++
 .../juneau/commons/collections/MultiMap.java       | 445 ++++++++++++++++++
 .../juneau/commons/collections/MultiSet.java       |  72 +++
 .../juneau/commons/collections/MultiList_Test.java | 129 +++++
 .../juneau/commons/collections/MultiMap_Test.java  | 518 +++++++++++++++++++++
 .../juneau/commons/collections/MultiSet_Test.java  | 129 +++++
 6 files changed, 1375 insertions(+)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiList.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiList.java
index 855775ed26..75145cee25 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiList.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiList.java
@@ -19,6 +19,7 @@ package org.apache.juneau.commons.collections;
 import static org.apache.juneau.commons.utils.AssertionUtils.*;
 
 import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * A composite list that presents multiple lists as a single unified list.
@@ -458,5 +459,86 @@ public class MultiList<E> extends AbstractList<E> {
                        i += list.size();
                return i;
        }
+
+       /**
+        * Returns a string representation of this MultiList.
+        *
+        * <p>
+        * The format is <c>"[[...],[...],...]"</c> where each <c>[...]</c> is 
the standard
+        * {@link List#toString()} representation of each underlying list.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      List&lt;String&gt; <jv>list1</jv> = List.of(<js>"a"</js>, 
<js>"b"</js>);
+        *      List&lt;String&gt; <jv>list2</jv> = List.of(<js>"c"</js>, 
<js>"d"</js>);
+        *      MultiList&lt;String&gt; <jv>multiList</jv> = <jk>new</jk> 
MultiList&lt;&gt;(<jv>list1</jv>, <jv>list2</jv>);
+        *      <jv>multiList</jv>.toString(); <jc>// Returns: "[[a, b], [c, 
d]]"</jc>
+        * </p>
+        *
+        * @return A string representation of this MultiList.
+        */
+       @Override
+       public String toString() {
+               return 
Arrays.stream(l).map(Object::toString).collect(Collectors.joining(", ", "[", 
"]"));
+       }
+
+       /**
+        * Compares the specified object with this list for equality.
+        *
+        * <p>
+        * Returns <jk>true</jk> if and only if the specified object is also a 
list, both lists have the
+        * same size, and all corresponding pairs of elements in the two lists 
are <i>equal</i>. In other
+        * words, two lists are defined to be equal if they contain the same 
elements in the same order.
+        *
+        * <p>
+        * This implementation first checks if the specified object is this 
list. If so, it returns
+        * <jk>true</jk>; if not, it checks if the specified object is a list. 
If not, it returns
+        * <jk>false</jk>; if so, it iterates over both lists, comparing 
corresponding pairs of elements.
+        *
+        * @param o The object to be compared for equality with this list.
+        * @return <jk>true</jk> if the specified object is equal to this list.
+        */
+       @Override
+       public boolean equals(Object o) {
+               if (o == this)
+                       return true;
+               if (!(o instanceof List))
+                       return false;
+               var e1 = listIterator();
+               var e2 = ((List<?>)o).listIterator();
+               while (e1.hasNext() && e2.hasNext()) {
+                       var o1 = e1.next();
+                       var o2 = e2.next();
+                       if (!(o1 == null ? o2 == null : o1.equals(o2)))
+                               return false;
+               }
+               return !(e1.hasNext() || e2.hasNext());
+       }
+
+       /**
+        * Returns the hash code value for this list.
+        *
+        * <p>
+        * The hash code of a list is defined to be the result of the following 
calculation:
+        * <p class='bcode w800'>
+        *      <jk>int</jk> hashCode = 1;
+        *      <jk>for</jk> (E e : list)
+        *              hashCode = 31 * hashCode + (e == <jk>null</jk> ? 0 : 
e.hashCode());
+        * </p>
+        *
+        * <p>
+        * This ensures that <c>list1.equals(list2)</c> implies that
+        * <c>list1.hashCode()==list2.hashCode()</c> for any two lists 
<c>list1</c> and <c>list2</c>,
+        * as required by the general contract of {@link Object#hashCode()}.
+        *
+        * @return The hash code value for this list.
+        */
+       @Override
+       public int hashCode() {
+               int hashCode = 1;
+               for (E e : this)
+                       hashCode = 31 * hashCode + (e == null ? 0 : 
e.hashCode());
+               return hashCode;
+       }
 }
 
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiMap.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiMap.java
new file mode 100644
index 0000000000..5dc821d5b5
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiMap.java
@@ -0,0 +1,445 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.collections;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * A composite map that presents multiple maps as a single unified map.
+ *
+ * <p>
+ * This class allows multiple maps to be viewed and accessed as if they were 
merged into
+ * a single map, without actually copying the entries. When the same key 
exists in multiple maps,
+ * the value from the first map (in constructor order) is returned.
+ *
+ * <h5 class='section'>Features:</h5>
+ * <ul class='spaced-list'>
+ *     <li><b>Zero-Copy Composition:</b> No data is copied when creating a 
MultiMap; it simply wraps the provided maps
+ *     <li><b>Transparent Access:</b> Accessing entries by key seamlessly 
searches all underlying maps in order
+ *     <li><b>First-Wins Semantics:</b> When a key exists in multiple maps, 
the value from the first map is returned
+ *     <li><b>Modification Support:</b> Entries can be removed via the 
iterator's {@link Iterator#remove()} method
+ *     <li><b>Efficient Size Calculation:</b> The size is computed by counting 
unique keys across all maps
+ * </ul>
+ *
+ * <h5 class='section'>Usage:</h5>
+ * <p class='bjava'>
+ *     <jc>// Create a MultiMap from three separate maps</jc>
+ *     Map&lt;String, String&gt; <jv>map1</jv> = Map.of(<js>"key1"</js>, 
<js>"value1"</js>, <js>"key2"</js>, <js>"value2"</js>);
+ *     Map&lt;String, String&gt; <jv>map2</jv> = Map.of(<js>"key3"</js>, 
<js>"value3"</js>, <js>"key2"</js>, <js>"value2b"</js>);
+ *     Map&lt;String, String&gt; <jv>map3</jv> = Map.of(<js>"key4"</js>, 
<js>"value4"</js>);
+ *
+ *     MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> 
MultiMap&lt;&gt;(<jv>map1</jv>, <jv>map2</jv>, <jv>map3</jv>);
+ *
+ *     <jc>// Access entries by key</jc>
+ *     <jv>multiMap</jv>.get(<js>"key1"</js>);  <jc>// Returns "value1" from 
map1</jc>
+ *     <jv>multiMap</jv>.get(<js>"key2"</js>);  <jc>// Returns "value2" from 
map1 (first match wins)</jc>
+ *     <jv>multiMap</jv>.get(<js>"key3"</js>);  <jc>// Returns "value3" from 
map2</jc>
+ *
+ *     <jc>// Iterate over all entries from all maps</jc>
+ *     <jk>for</jk> (Map.Entry&lt;String, String&gt; <jv>entry</jv> : 
<jv>multiMap</jv>.entrySet()) {
+ *             System.<jsf>out</jsf>.println(<jv>entry</jv>); <jc>// Prints 
entries from all maps</jc>
+ *     }
+ *
+ *     <jc>// Get total size (unique keys across all maps)</jc>
+ *     <jk>int</jk> <jv>totalSize</jv> = <jv>multiMap</jv>.size(); <jc>// 
Returns: 4 (key1, key2, key3, key4)</jc>
+ * </p>
+ *
+ * <h5 class='section'>Behavior Notes:</h5>
+ * <ul class='spaced-list'>
+ *     <li>The order of key lookup follows the order of maps as provided in 
the constructor
+ *     <li>When a key exists in multiple maps, {@link #get(Object)} returns 
the value from the first map containing that key
+ *     <li>The underlying maps must not be <jk>null</jk>, but can be empty
+ *     <li>Modifications via {@link Iterator#remove()} are delegated to the 
underlying map's entry set iterator
+ *     <li>This class does not support {@link #put(Object, Object)}, {@link 
#remove(Object)}, or {@link #clear()} operations
+ *     <li>The {@link #size()} method recomputes the count of unique keys each 
time it's called (not cached)
+ *     <li>The {@link #entrySet()} iterator only returns each key once (first 
occurrence), even if it exists in multiple maps
+ * </ul>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * This class is not inherently thread-safe. If the underlying maps are 
modified concurrently
+ * during iteration or access, the behavior is undefined. Synchronization must 
be handled externally if needed.
+ *
+ * <h5 class='section'>Example - Processing Multiple Configuration 
Sources:</h5>
+ * <p class='bjava'>
+ *     <jc>// Combine configuration from system properties, environment, and 
defaults</jc>
+ *     Map&lt;String, String&gt; <jv>systemProps</jv> = getSystemProperties();
+ *     Map&lt;String, String&gt; <jv>envVars</jv> = getEnvironmentVariables();
+ *     Map&lt;String, String&gt; <jv>defaults</jv> = getDefaultConfig();
+ *
+ *     MultiMap&lt;String, String&gt; <jv>config</jv> = <jk>new</jk> 
MultiMap&lt;&gt;(<jv>systemProps</jv>, <jv>envVars</jv>, <jv>defaults</jv>);
+ *
+ *     <jc>// Access configuration (system props take precedence, then env 
vars, then defaults)</jc>
+ *     String <jv>host</jv> = <jv>config</jv>.get(<js>"host"</js>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ *     <li class='link'><a class="doclink" 
href="../../../../../index.html#juneau-commons">Overview &gt; juneau-commons</a>
+ * </ul>
+ *
+ * @param <K> The key type of this map.
+ * @param <V> The value type of this map.
+ */
+public class MultiMap<K,V> extends AbstractMap<K,V> {
+
+       /**
+        * The underlying maps being wrapped by this MultiMap.
+        * <p>
+        * These maps are accessed directly during key lookup and iteration 
without copying.
+        */
+       final Map<K,V>[] m;
+
+       /**
+        * Creates a new MultiMap that presents the specified maps as a single 
unified map.
+        *
+        * <p>
+        * The maps are stored by reference (not copied), so modifications made 
through the
+        * MultiMap's iterator will affect the original maps.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      Map&lt;String, String&gt; <jv>map1</jv> = <jk>new</jk> 
LinkedHashMap&lt;&gt;(Map.of(<js>"a"</js>, <js>"1"</js>));
+        *      Map&lt;String, String&gt; <jv>map2</jv> = <jk>new</jk> 
LinkedHashMap&lt;&gt;(Map.of(<js>"b"</js>, <js>"2"</js>));
+        *
+        *      MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> 
MultiMap&lt;&gt;(<jv>map1</jv>, <jv>map2</jv>);
+        *      <jc>// multiMap now represents all entries from both maps</jc>
+        * </p>
+        *
+        * @param maps Zero or more maps to combine into this map. Must not be 
<jk>null</jk>,
+        *           and no individual map can be <jk>null</jk> (but maps can 
be empty).
+        * @throws IllegalArgumentException if the maps array or any map within 
it is <jk>null</jk>.
+        */
+       @SafeVarargs
+       public MultiMap(Map<K,V>...maps) {
+               assertArgNotNull("maps", maps);
+               for (var map : maps)
+                       assertArgNotNull("maps", map);
+               m = maps;
+       }
+
+       /**
+        * Returns the value to which the specified key is mapped, or 
<jk>null</jk> if this map contains no mapping for the key.
+        *
+        * <p>
+        * This method searches the underlying maps in the order they were 
provided to the constructor.
+        * The first map containing the key determines the returned value.
+        *
+        * @param key The key whose associated value is to be returned.
+        * @return The value to which the specified key is mapped, or 
<jk>null</jk> if this map contains no mapping for the key.
+        */
+       @Override
+       public V get(Object key) {
+               for (var map : m) {
+                       if (map.containsKey(key))
+                               return map.get(key);
+               }
+               return null;
+       }
+
+       /**
+        * Returns <jk>true</jk> if this map contains a mapping for the 
specified key.
+        *
+        * @param key The key whose presence in this map is to be tested.
+        * @return <jk>true</jk> if this map contains a mapping for the 
specified key.
+        */
+       @Override
+       public boolean containsKey(Object key) {
+               for (var map : m) {
+                       if (map.containsKey(key))
+                               return true;
+               }
+               return false;
+       }
+
+       /**
+        * Returns <jk>true</jk> if this map maps one or more keys to the 
specified value.
+        *
+        * @param value The value whose presence in this map is to be tested.
+        * @return <jk>true</jk> if this map maps one or more keys to the 
specified value.
+        */
+       @Override
+       public boolean containsValue(Object value) {
+               for (var map : m) {
+                       if (map.containsValue(value))
+                               return true;
+               }
+               return false;
+       }
+
+       /**
+        * Returns a {@link Set} view of the mappings contained in this map.
+        *
+        * <p>
+        * The returned set is a composite view of all underlying maps. When a 
key exists in multiple maps,
+        * only the entry from the first map (in constructor order) is included.
+        *
+        * <p>
+        * The iterator supports the {@link Iterator#remove()} operation, which 
removes the entry
+        * from its underlying map.
+        *
+        * @return A set view of the mappings contained in this map.
+        */
+       @Override
+       public Set<Entry<K,V>> entrySet() {
+               return new AbstractSet<>() {
+                       @Override
+                       public Iterator<Entry<K,V>> iterator() {
+                               return new Iterator<>() {
+                                       int mapIndex = 0;
+                                       Iterator<Entry<K,V>> currentIterator;
+                                       Set<K> seenKeys = new HashSet<>();
+                                       Entry<K,V> nextEntry;
+                                       Iterator<Entry<K,V>> lastIterator; // 
Iterator that produced the last entry
+                                       boolean canRemove = false; // Whether 
remove() can be called
+
+                                       {
+                                               // Initialize to first map's 
iterator
+                                               if (m.length > 0) {
+                                                       currentIterator = 
m[0].entrySet().iterator();
+                                                       advance();
+                                               }
+                                       }
+
+                                       private void advance() {
+                                               nextEntry = null;
+                                               while (currentIterator != null) 
{
+                                                       while 
(currentIterator.hasNext()) {
+                                                               var entry = 
currentIterator.next();
+                                                               if 
(!seenKeys.contains(entry.getKey())) {
+                                                                       
seenKeys.add(entry.getKey());
+                                                                       
nextEntry = entry;
+                                                                       return;
+                                                               }
+                                                       }
+                                                       // Move to next map
+                                                       mapIndex++;
+                                                       if (mapIndex < 
m.length) {
+                                                               currentIterator 
= m[mapIndex].entrySet().iterator();
+                                                       } else {
+                                                               currentIterator 
= null;
+                                                       }
+                                               }
+                                       }
+
+                                       @Override
+                                       public boolean hasNext() {
+                                               return nextEntry != null;
+                                       }
+
+                                       @Override
+                                       public Entry<K,V> next() {
+                                               if (nextEntry == null)
+                                                       throw new 
NoSuchElementException();
+                                               var result = nextEntry;
+                                               lastIterator = currentIterator; 
// Store the iterator that produced this entry
+                                               canRemove = true;
+                                               advance();
+                                               return result;
+                                       }
+
+                                       @Override
+                                       public void remove() {
+                                               if (!canRemove || lastIterator 
== null)
+                                                       throw new 
IllegalStateException();
+                                               lastIterator.remove();
+                                               canRemove = false;
+                                       }
+                               };
+                       }
+
+                       @Override
+                       public int size() {
+                               return MultiMap.this.size();
+                       }
+               };
+       }
+
+       /**
+        * Returns the number of unique key-value mappings in this map.
+        *
+        * <p>
+        * This method computes the size by counting unique keys across all 
underlying maps.
+        * If a key exists in multiple maps, it is counted only once. The size 
is recalculated
+        * each time this method is called (it is not cached).
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      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>
+        *      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>
+        *      MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> 
MultiMap&lt;&gt;(<jv>map1</jv>, <jv>map2</jv>);
+        *
+        *      <jk>int</jk> <jv>totalSize</jv> = <jv>multiMap</jv>.size(); 
<jc>// Returns: 3 (a, b, c - b is counted only once)</jc>
+        * </p>
+        *
+        * @return The number of unique key-value mappings in this map.
+        */
+       @Override
+       public int size() {
+               var seenKeys = new HashSet<>();
+               for (var map : m) {
+                       seenKeys.addAll(map.keySet());
+               }
+               return seenKeys.size();
+       }
+
+       /**
+        * Returns <jk>true</jk> if this map contains no key-value mappings.
+        *
+        * @return <jk>true</jk> if this map contains no key-value mappings.
+        */
+       @Override
+       public boolean isEmpty() {
+               for (var map : m) {
+                       if (!map.isEmpty())
+                               return false;
+               }
+               return true;
+       }
+
+       /**
+        * Returns a {@link Collection} view of the values contained in this 
map.
+        *
+        * <p>
+        * The returned collection is a view of the values from the entries in 
{@link #entrySet()}.
+        * When a key exists in multiple maps, only the value from the first 
map (in constructor order)
+        * is included.
+        *
+        * @return A collection view of the values contained in this map.
+        */
+       @Override
+       public Collection<V> values() {
+               return new AbstractCollection<>() {
+                       @Override
+                       public Iterator<V> iterator() {
+                               return new Iterator<>() {
+                                       private final Iterator<Entry<K,V>> 
entryIterator = entrySet().iterator();
+
+                                       @Override
+                                       public boolean hasNext() {
+                                               return entryIterator.hasNext();
+                                       }
+
+                                       @Override
+                                       public V next() {
+                                               return 
entryIterator.next().getValue();
+                                       }
+
+                                       @Override
+                                       public void remove() {
+                                               entryIterator.remove();
+                                       }
+                               };
+                       }
+
+                       @Override
+                       public int size() {
+                               return MultiMap.this.size();
+                       }
+               };
+       }
+
+       // Unsupported operations
+       @Override
+       public V put(K key, V value) {
+               throw unsupportedOp("put() not supported on MultiMap. Use 
underlying maps directly.");
+       }
+
+       @Override
+       public V remove(Object key) {
+               throw unsupportedOp("remove() not supported on MultiMap. Use 
underlying maps directly.");
+       }
+
+       @Override
+       public void putAll(Map<? extends K, ? extends V> m) {
+               throw unsupportedOp("putAll() not supported on MultiMap. Use 
underlying maps directly.");
+       }
+
+       @Override
+       public void clear() {
+               throw unsupportedOp("clear() not supported on MultiMap. Use 
underlying maps directly.");
+       }
+
+       /**
+        * Returns a string representation of this MultiMap.
+        *
+        * <p>
+        * The format is <c>"[{...},{...},...]"</c> where each <c>{...}</c> is 
the standard
+        * {@link Map#toString()} representation of each underlying map.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      Map&lt;String, String&gt; <jv>map1</jv> = Map.of(<js>"a"</js>, 
<js>"1"</js>);
+        *      Map&lt;String, String&gt; <jv>map2</jv> = Map.of(<js>"b"</js>, 
<js>"2"</js>);
+        *      MultiMap&lt;String, String&gt; <jv>multiMap</jv> = <jk>new</jk> 
MultiMap&lt;&gt;(<jv>map1</jv>, <jv>map2</jv>);
+        *      <jv>multiMap</jv>.toString(); <jc>// Returns: "[{a=1}, 
{b=2}]"</jc>
+        * </p>
+        *
+        * @return A string representation of this MultiMap.
+        */
+       @Override
+       public String toString() {
+               return 
Arrays.stream(m).map(Object::toString).collect(Collectors.joining(", ", "[", 
"]"));
+       }
+
+       /**
+        * Compares the specified object with this map for equality.
+        *
+        * <p>
+        * Returns <jk>true</jk> if the given object is also a map and the two 
maps represent the same
+        * mappings. More formally, two maps <c>m1</c> and <c>m2</c> represent 
the same mappings if
+        * <c>m1.entrySet().equals(m2.entrySet())</c>.
+        *
+        * <p>
+        * This implementation compares the entry sets of the two maps.
+        *
+        * @param o Object to be compared for equality with this map.
+        * @return <jk>true</jk> if the specified object is equal to this map.
+        */
+       @Override
+       public boolean equals(Object o) {
+               if (o == this)
+                       return true;
+               if (!(o instanceof Map))
+                       return false;
+               return entrySet().equals(((Map<?,?>)o).entrySet());
+       }
+
+       /**
+        * Returns the hash code value for this map.
+        *
+        * <p>
+        * The hash code of a map is defined to be the sum of the hash codes of 
each entry in the map's
+        * <c>entrySet()</c> view. This ensures that <c>m1.equals(m2)</c> 
implies that
+        * <c>m1.hashCode()==m2.hashCode()</c> for any two maps <c>m1</c> and 
<c>m2</c>, as required
+        * by the general contract of {@link Object#hashCode()}.
+        *
+        * <p>
+        * This implementation computes the hash code from the entry set.
+        *
+        * @return The hash code value for this map.
+        */
+       @Override
+       public int hashCode() {
+               return entrySet().hashCode();
+       }
+}
+
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiSet.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiSet.java
index 45a7edccbf..7d7faf2867 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiSet.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MultiSet.java
@@ -19,6 +19,7 @@ package org.apache.juneau.commons.collections;
 import static org.apache.juneau.commons.utils.AssertionUtils.*;
 
 import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * A composite set that presents multiple collections as a single unified set.
@@ -264,4 +265,75 @@ public class MultiSet<E> extends AbstractSet<E> {
                        i += c.size();
                return i;
        }
+
+       /**
+        * Returns a string representation of this MultiSet.
+        *
+        * <p>
+        * The format is <c>"[[...],[...],...]"</c> where each <c>[...]</c> is 
the standard
+        * {@link Collection#toString()} representation of each underlying 
collection.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      List&lt;String&gt; <jv>list1</jv> = List.of(<js>"a"</js>, 
<js>"b"</js>);
+        *      List&lt;String&gt; <jv>list2</jv> = List.of(<js>"c"</js>, 
<js>"d"</js>);
+        *      MultiSet&lt;String&gt; <jv>multiSet</jv> = <jk>new</jk> 
MultiSet&lt;&gt;(<jv>list1</jv>, <jv>list2</jv>);
+        *      <jv>multiSet</jv>.toString(); <jc>// Returns: "[[a, b], [c, 
d]]"</jc>
+        * </p>
+        *
+        * @return A string representation of this MultiSet.
+        */
+       @Override
+       public String toString() {
+               return 
Arrays.stream(l).map(Object::toString).collect(Collectors.joining(", ", "[", 
"]"));
+       }
+
+       /**
+        * Compares the specified object with this set for equality.
+        *
+        * <p>
+        * Returns <jk>true</jk> if the given object is also a set, the two 
sets have the same size,
+        * and every member of the given set is contained in this set.
+        *
+        * <p>
+        * This implementation checks if the specified object is a set, and if 
so, compares the sizes
+        * and checks if all elements in the specified set are contained in 
this set.
+        *
+        * @param o Object to be compared for equality with this set.
+        * @return <jk>true</jk> if the specified object is equal to this set.
+        */
+       @Override
+       public boolean equals(Object o) {
+               if (o == this)
+                       return true;
+               if (!(o instanceof Set))
+                       return false;
+               var s = (Set<?>)o;
+               if (s.size() != size())
+                       return false;
+               return containsAll(s);
+       }
+
+       /**
+        * Returns the hash code value for this set.
+        *
+        * <p>
+        * The hash code of a set is defined to be the sum of the hash codes of 
the elements in the set,
+        * where the hash code of a <jk>null</jk> element is defined to be 
zero. This ensures that
+        * <c>s1.equals(s2)</c> implies that 
<c>s1.hashCode()==s2.hashCode()</c> for any two sets
+        * <c>s1</c> and <c>s2</c>, as required by the general contract of 
{@link Object#hashCode()}.
+        *
+        * <p>
+        * This implementation iterates over the set, calling the 
<c>hashCode</c> method on each element
+        * in the set, and adding up the results.
+        *
+        * @return The hash code value for this set.
+        */
+       @Override
+       public int hashCode() {
+               int h = 0;
+               for (E e : this)
+                       h += e == null ? 0 : e.hashCode();
+               return h;
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiList_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiList_Test.java
index 7fb0d93e64..4704db7f84 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiList_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiList_Test.java
@@ -489,5 +489,134 @@ class MultiList_Test extends TestBase {
                assertEquals("3", array[2]);
                assertEquals("4", array[3]);
        }
+
+       
//====================================================================================================
+       // toString()
+       
//====================================================================================================
+
+       @Test
+       void f01_toString_singleList() {
+               var l1 = l(a("1", "2"));
+               var ml = new MultiList<>(l1);
+
+               var expected = "[" + l1.toString() + "]";
+               assertEquals(expected, ml.toString());
+       }
+
+       @Test
+       void f02_toString_multipleLists() {
+               var l1 = l(a("1", "2"));
+               var l2 = l(a("3", "4"));
+               var l3 = l(a("5", "6"));
+               var ml = new MultiList<>(l1, l2, l3);
+
+               var expected = "[" + l1.toString() + ", " + l2.toString() + ", 
" + l3.toString() + "]";
+               assertEquals(expected, ml.toString());
+       }
+
+       @Test
+       void f03_toString_emptyLists() {
+               var l1 = l(a());
+               var l2 = l(a());
+               var ml = new MultiList<>(l1, l2);
+
+               var expected = "[" + l1.toString() + ", " + l2.toString() + "]";
+               assertEquals(expected, ml.toString());
+       }
+
+       @Test
+       void f04_toString_mixedEmptyAndNonEmpty() {
+               List<String> l1 = l(a());
+               var l2 = l(a("1", "2"));
+               List<String> l3 = l(a());
+               var ml = new MultiList<>(l1, l2, l3);
+
+               var expected = "[" + l1.toString() + ", " + l2.toString() + ", 
" + l3.toString() + "]";
+               assertEquals(expected, ml.toString());
+       }
+
+       
//====================================================================================================
+       // equals() and hashCode()
+       
//====================================================================================================
+
+       @Test
+       void g01_equals_sameContents() {
+               var l1 = l(a("1", "2"));
+               var l2 = l(a("3", "4"));
+               var multiList1 = new MultiList<>(l1, l2);
+
+               var l3 = l(a("1", "2"));
+               var l4 = l(a("3", "4"));
+               var multiList2 = new MultiList<>(l3, l4);
+
+               assertTrue(multiList1.equals(multiList2));
+               assertTrue(multiList2.equals(multiList1));
+       }
+
+       @Test
+       void g02_equals_differentContents() {
+               var l1 = l(a("1", "2"));
+               var multiList1 = new MultiList<>(l1);
+
+               var l2 = l(a("1", "3"));
+               var multiList2 = new MultiList<>(l2);
+
+               assertFalse(multiList1.equals(multiList2));
+               assertFalse(multiList2.equals(multiList1));
+       }
+
+       @Test
+       void g03_equals_differentOrder() {
+               var l1 = l(a("1", "2"));
+               var l2 = l(a("3", "4"));
+               var multiList1 = new MultiList<>(l1, l2);
+
+               var l3 = l(a("3", "4"));
+               var l4 = l(a("1", "2"));
+               var multiList2 = new MultiList<>(l3, l4);
+
+               assertFalse(multiList1.equals(multiList2)); // Order matters 
for lists
+       }
+
+       @Test
+       void g04_equals_regularList() {
+               var l1 = l(a("1", "2", "3"));
+               var multiList = new MultiList<>(l1);
+
+               var regularList = new ArrayList<>(l(a("1", "2", "3")));
+
+               assertTrue(multiList.equals(regularList));
+               assertTrue(regularList.equals(multiList));
+       }
+
+       @Test
+       void g05_equals_notAList() {
+               var l1 = l(a("1", "2"));
+               var multiList = new MultiList<>(l1);
+
+               assertFalse(multiList.equals("not a list"));
+               assertFalse(multiList.equals(null));
+       }
+
+       @Test
+       void g06_hashCode_sameContents() {
+               var l1 = l(a("1", "2", "3"));
+               var multiList1 = new MultiList<>(l1);
+
+               var l2 = l(a("1", "2", "3"));
+               var multiList2 = new MultiList<>(l2);
+
+               assertEquals(multiList1.hashCode(), multiList2.hashCode());
+       }
+
+       @Test
+       void g07_hashCode_regularList() {
+               var l1 = l(a("1", "2", "3"));
+               var multiList = new MultiList<>(l1);
+
+               var regularList = new ArrayList<>(l(a("1", "2", "3")));
+
+               assertEquals(multiList.hashCode(), regularList.hashCode());
+       }
 }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiMap_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiMap_Test.java
new file mode 100644
index 0000000000..9e42df341e
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiMap_Test.java
@@ -0,0 +1,518 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.collections;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class MultiMap_Test extends TestBase {
+
+       
//====================================================================================================
+       // Basic functionality - get(Object key)
+       
//====================================================================================================
+
+       @Test
+       void a01_get_fromFirstMap() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertEquals("value1", multiMap.get("key1"));
+               assertEquals("value2", multiMap.get("key2"));
+               assertEquals("value3", multiMap.get("key3"));
+       }
+
+       @Test
+       void a02_get_duplicateKey_returnsFirstMatch() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key1", "value2");
+               var map3 = map("key1", "value3");
+               var multiMap = new MultiMap<>(map1, map2, map3);
+
+               assertEquals("value1", multiMap.get("key1")); // First match 
wins
+       }
+
+       @Test
+       void a03_get_nonexistentKey_returnsNull() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key2", "value2");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertNull(multiMap.get("key3"));
+       }
+
+       @Test
+       void a04_get_emptyMaps() {
+               var map1 = map();
+               var map2 = map();
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertNull(multiMap.get("key1"));
+               assertTrue(multiMap.isEmpty());
+       }
+
+       
//====================================================================================================
+       // containsKey(Object key)
+       
//====================================================================================================
+
+       @Test
+       void b01_containsKey_existsInFirstMap() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key2", "value2");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertTrue(multiMap.containsKey("key1"));
+               assertTrue(multiMap.containsKey("key2"));
+               assertFalse(multiMap.containsKey("key3"));
+       }
+
+       @Test
+       void b02_containsKey_existsInSecondMap() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key2", "value2");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertTrue(multiMap.containsKey("key2"));
+       }
+
+       @Test
+       void b03_containsKey_duplicateKey_returnsTrue() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key1", "value2");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertTrue(multiMap.containsKey("key1"));
+       }
+
+       
//====================================================================================================
+       // containsValue(Object value)
+       
//====================================================================================================
+
+       @Test
+       void c01_containsValue_existsInAnyMap() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key2", "value2");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertTrue(multiMap.containsValue("value1"));
+               assertTrue(multiMap.containsValue("value2"));
+               assertFalse(multiMap.containsValue("value3"));
+       }
+
+       @Test
+       void c02_containsValue_duplicateValue_returnsTrue() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key2", "value1");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertTrue(multiMap.containsValue("value1"));
+       }
+
+       
//====================================================================================================
+       // size()
+       
//====================================================================================================
+
+       @Test
+       void d01_size_countsUniqueKeys() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertEquals(3, multiMap.size());
+       }
+
+       @Test
+       void d02_size_duplicateKeys_countedOnce() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key2", "value2b", "key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertEquals(3, multiMap.size()); // key1, key2, key3 (key2 
counted once)
+       }
+
+       @Test
+       void d03_size_emptyMaps() {
+               var map1 = map();
+               var map2 = map();
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertEquals(0, multiMap.size());
+       }
+
+       @Test
+       void d04_size_singleMap() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var multiMap = new MultiMap<>(map1);
+
+               assertEquals(2, multiMap.size());
+       }
+
+       
//====================================================================================================
+       // isEmpty()
+       
//====================================================================================================
+
+       @Test
+       void e01_isEmpty_allMapsEmpty() {
+               var map1 = map();
+               var map2 = map();
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertTrue(multiMap.isEmpty());
+       }
+
+       @Test
+       void e02_isEmpty_someMapsHaveEntries() {
+               Map<String, String> map1 = map();
+               var map2 = map("key1", "value1");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertFalse(multiMap.isEmpty());
+       }
+
+       
//====================================================================================================
+       // entrySet()
+       
//====================================================================================================
+
+       @Test
+       void f01_entrySet_iteratesAllEntries() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               var entries = new ArrayList<Map.Entry<String, String>>();
+               multiMap.entrySet().forEach(entries::add);
+
+               assertEquals(3, entries.size());
+               assertTrue(entries.stream().anyMatch(e -> 
e.getKey().equals("key1") && e.getValue().equals("value1")));
+               assertTrue(entries.stream().anyMatch(e -> 
e.getKey().equals("key2") && e.getValue().equals("value2")));
+               assertTrue(entries.stream().anyMatch(e -> 
e.getKey().equals("key3") && e.getValue().equals("value3")));
+       }
+
+       @Test
+       void f02_entrySet_duplicateKeys_onlyFirstIncluded() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key1", "value2");
+               var map3 = map("key2", "value3");
+               var multiMap = new MultiMap<>(map1, map2, map3);
+
+               var entries = new ArrayList<Map.Entry<String, String>>();
+               multiMap.entrySet().forEach(entries::add);
+
+               assertEquals(2, entries.size());
+               // key1 should have value1 (from first map)
+               var key1Entry = entries.stream().filter(e -> 
e.getKey().equals("key1")).findFirst().orElse(null);
+               assertNotNull(key1Entry);
+               assertEquals("value1", key1Entry.getValue());
+       }
+
+       @Test
+       void f03_entrySet_iterator_remove() {
+               var map1 = new LinkedHashMap<>(map("key1", "value1", "key2", 
"value2"));
+               var map2 = new LinkedHashMap<>(map("key3", "value3"));
+               var multiMap = new MultiMap<>(map1, map2);
+
+               var iterator = multiMap.entrySet().iterator();
+               while (iterator.hasNext()) {
+                       var entry = iterator.next();
+                       if (entry.getKey().equals("key2")) {
+                               iterator.remove();
+                       }
+               }
+
+               assertFalse(map1.containsKey("key2"));
+               assertEquals(2, multiMap.size());
+       }
+
+       @Test
+       void f04_entrySet_size() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key2", "value2b", "key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertEquals(3, multiMap.entrySet().size()); // key1, key2, key3
+       }
+
+       
//====================================================================================================
+       // keySet()
+       
//====================================================================================================
+
+       @Test
+       void g01_keySet_containsAllKeys() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               var keySet = multiMap.keySet();
+               assertTrue(keySet.contains("key1"));
+               assertTrue(keySet.contains("key2"));
+               assertTrue(keySet.contains("key3"));
+               assertFalse(keySet.contains("key4"));
+               assertEquals(3, keySet.size());
+       }
+
+       @Test
+       void g02_keySet_duplicateKeys_countedOnce() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key2", "value2b", "key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               var keySet = multiMap.keySet();
+               assertEquals(3, keySet.size()); // key1, key2, key3
+               assertTrue(keySet.contains("key1"));
+               assertTrue(keySet.contains("key2"));
+               assertTrue(keySet.contains("key3"));
+       }
+
+       
//====================================================================================================
+       // values()
+       
//====================================================================================================
+
+       @Test
+       void h01_values_containsAllValues() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               var values = multiMap.values();
+               assertTrue(values.contains("value1"));
+               assertTrue(values.contains("value2"));
+               assertTrue(values.contains("value3"));
+               assertEquals(3, values.size());
+       }
+
+       @Test
+       void h02_values_duplicateKeys_usesFirstValue() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key1", "value2");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               var values = multiMap.values();
+               assertTrue(values.contains("value1"));
+               assertFalse(values.contains("value2")); // value2 is not 
returned because key1 is in first map
+               assertEquals(1, values.size());
+       }
+
+       
//====================================================================================================
+       // Unsupported operations
+       
//====================================================================================================
+
+       @Test
+       void i01_put_throwsUnsupportedOperationException() {
+               var map1 = map("key1", "value1");
+               var multiMap = new MultiMap<>(map1);
+
+               assertThrows(UnsupportedOperationException.class, () -> 
multiMap.put("key2", "value2"));
+       }
+
+       @Test
+       void i02_remove_throwsUnsupportedOperationException() {
+               var map1 = map("key1", "value1");
+               var multiMap = new MultiMap<>(map1);
+
+               assertThrows(UnsupportedOperationException.class, () -> 
multiMap.remove("key1"));
+       }
+
+       @Test
+       void i03_putAll_throwsUnsupportedOperationException() {
+               var map1 = map("key1", "value1");
+               var multiMap = new MultiMap<>(map1);
+               var map2 = map("key2", "value2");
+
+               assertThrows(UnsupportedOperationException.class, () -> 
multiMap.putAll(map2));
+       }
+
+       @Test
+       void i04_clear_throwsUnsupportedOperationException() {
+               var map1 = map("key1", "value1");
+               var multiMap = new MultiMap<>(map1);
+
+               assertThrows(UnsupportedOperationException.class, 
multiMap::clear);
+       }
+
+       
//====================================================================================================
+       // Edge cases
+       
//====================================================================================================
+
+       @Test
+       void j01_singleMap() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var multiMap = new MultiMap<>(map1);
+
+               assertEquals(2, multiMap.size());
+               assertEquals("value1", multiMap.get("key1"));
+               assertEquals("value2", multiMap.get("key2"));
+       }
+
+       @Test
+       void j02_threeMaps() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key2", "value2");
+               var map3 = map("key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2, map3);
+
+               assertEquals(3, multiMap.size());
+               assertEquals("value1", multiMap.get("key1"));
+               assertEquals("value2", multiMap.get("key2"));
+               assertEquals("value3", multiMap.get("key3"));
+       }
+
+       @Test
+       void j03_nullValue() {
+               Map<String, String> map1 = map("key1", null);
+               var map2 = map("key2", "value2");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertNull(multiMap.get("key1"));
+               assertTrue(multiMap.containsKey("key1"));
+               assertTrue(multiMap.containsValue(null));
+       }
+
+       @Test
+       void j04_nullKey() {
+               var map1 = new LinkedHashMap<String, String>();
+               map1.put(null, "value1");
+               var map2 = map("key2", "value2");
+               var multiMap = new MultiMap<>(map1, map2);
+
+               assertEquals("value1", multiMap.get(null));
+               assertTrue(multiMap.containsKey(null));
+       }
+
+       
//====================================================================================================
+       // toString()
+       
//====================================================================================================
+
+       @Test
+       void k01_toString_singleMap() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var multiMap = new MultiMap<>(map1);
+
+               var expected = "[" + map1.toString() + "]";
+               assertEquals(expected, multiMap.toString());
+       }
+
+       @Test
+       void k02_toString_multipleMaps() {
+               var map1 = map("key1", "value1");
+               var map2 = map("key2", "value2");
+               var map3 = map("key3", "value3");
+               var multiMap = new MultiMap<>(map1, map2, map3);
+
+               var expected = "[" + map1.toString() + ", " + map2.toString() + 
", " + map3.toString() + "]";
+               assertEquals(expected, multiMap.toString());
+       }
+
+       @Test
+       void k03_toString_emptyMaps() {
+               Map<String, String> map1 = map();
+               Map<String, String> map2 = map();
+               var multiMap = new MultiMap<>(map1, map2);
+
+               var expected = "[" + map1.toString() + ", " + map2.toString() + 
"]";
+               assertEquals(expected, multiMap.toString());
+       }
+
+       @Test
+       void k04_toString_mixedEmptyAndNonEmpty() {
+               Map<String, String> map1 = map();
+               var map2 = map("key1", "value1");
+               Map<String, String> map3 = map();
+               var multiMap = new MultiMap<>(map1, map2, map3);
+
+               var expected = "[" + map1.toString() + ", " + map2.toString() + 
", " + map3.toString() + "]";
+               assertEquals(expected, multiMap.toString());
+       }
+
+       
//====================================================================================================
+       // equals() and hashCode()
+       
//====================================================================================================
+
+       @Test
+       void l01_equals_sameContents() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key3", "value3");
+               var multiMap1 = new MultiMap<>(map1, map2);
+
+               var map3 = map("key1", "value1", "key2", "value2");
+               var map4 = map("key3", "value3");
+               var multiMap2 = new MultiMap<>(map3, map4);
+
+               assertTrue(multiMap1.equals(multiMap2));
+               assertTrue(multiMap2.equals(multiMap1));
+       }
+
+       @Test
+       void l02_equals_differentContents() {
+               var map1 = map("key1", "value1");
+               var multiMap1 = new MultiMap<>(map1);
+
+               var map2 = map("key1", "value2");
+               var multiMap2 = new MultiMap<>(map2);
+
+               assertFalse(multiMap1.equals(multiMap2));
+               assertFalse(multiMap2.equals(multiMap1));
+       }
+
+       @Test
+       void l03_equals_regularMap() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var multiMap = new MultiMap<>(map1);
+
+               var regularMap = new LinkedHashMap<>(map("key1", "value1", 
"key2", "value2"));
+
+               assertTrue(multiMap.equals(regularMap));
+               assertTrue(regularMap.equals(multiMap));
+       }
+
+       @Test
+       void l04_equals_notAMap() {
+               var map1 = map("key1", "value1");
+               var multiMap = new MultiMap<>(map1);
+
+               assertFalse(multiMap.equals("not a map"));
+               assertFalse(multiMap.equals(null));
+       }
+
+       @Test
+       void l05_hashCode_sameContents() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var map2 = map("key3", "value3");
+               var multiMap1 = new MultiMap<>(map1, map2);
+
+               var map3 = map("key1", "value1", "key2", "value2");
+               var map4 = map("key3", "value3");
+               var multiMap2 = new MultiMap<>(map3, map4);
+
+               assertEquals(multiMap1.hashCode(), multiMap2.hashCode());
+       }
+
+       @Test
+       void l06_hashCode_regularMap() {
+               var map1 = map("key1", "value1", "key2", "value2");
+               var multiMap = new MultiMap<>(map1);
+
+               var regularMap = new LinkedHashMap<>(map("key1", "value1", 
"key2", "value2"));
+
+               assertEquals(multiMap.hashCode(), regularMap.hashCode());
+       }
+}
+
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiSet_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiSet_Test.java
index e2b9c777b4..4a2f2255a2 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiSet_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MultiSet_Test.java
@@ -178,5 +178,134 @@ class MultiSet_Test extends TestBase {
                assertEquals("3", it.next());
                assertFalse(it.hasNext());
        }
+
+       
//====================================================================================================
+       // toString()
+       
//====================================================================================================
+
+       @Test
+       void toString_singleCollection() {
+               var l1 = l(a("1", "2"));
+               var ms = new MultiSet<>(l1);
+
+               var expected = "[" + l1.toString() + "]";
+               assertEquals(expected, ms.toString());
+       }
+
+       @Test
+       void toString_multipleCollections() {
+               var l1 = l(a("1", "2"));
+               var l2 = l(a("3", "4"));
+               var l3 = l(a("5", "6"));
+               var ms = new MultiSet<>(l1, l2, l3);
+
+               var expected = "[" + l1.toString() + ", " + l2.toString() + ", 
" + l3.toString() + "]";
+               assertEquals(expected, ms.toString());
+       }
+
+       @Test
+       void toString_emptyCollections() {
+               var l1 = l(a());
+               var l2 = l(a());
+               var ms = new MultiSet<>(l1, l2);
+
+               var expected = "[" + l1.toString() + ", " + l2.toString() + "]";
+               assertEquals(expected, ms.toString());
+       }
+
+       @Test
+       void toString_mixedEmptyAndNonEmpty() {
+               List<String> l1 = l(a());
+               var l2 = l(a("1", "2"));
+               List<String> l3 = l(a());
+               var ms = new MultiSet<>(l1, l2, l3);
+
+               var expected = "[" + l1.toString() + ", " + l2.toString() + ", 
" + l3.toString() + "]";
+               assertEquals(expected, ms.toString());
+       }
+
+       
//====================================================================================================
+       // equals() and hashCode()
+       
//====================================================================================================
+
+       @Test
+       void equals_sameContents() {
+               var l1 = l(a("1", "2"));
+               var l2 = l(a("3", "4"));
+               var multiSet1 = new MultiSet<>(l1, l2);
+
+               var l3 = l(a("1", "2"));
+               var l4 = l(a("3", "4"));
+               var multiSet2 = new MultiSet<>(l3, l4);
+
+               assertTrue(multiSet1.equals(multiSet2));
+               assertTrue(multiSet2.equals(multiSet1));
+       }
+
+       @Test
+       void equals_differentContents() {
+               var l1 = l(a("1", "2"));
+               var multiSet1 = new MultiSet<>(l1);
+
+               var l2 = l(a("1", "3"));
+               var multiSet2 = new MultiSet<>(l2);
+
+               assertFalse(multiSet1.equals(multiSet2));
+               assertFalse(multiSet2.equals(multiSet1));
+       }
+
+       @Test
+       void equals_differentOrder() {
+               var l1 = l(a("1", "2"));
+               var l2 = l(a("3", "4"));
+               var multiSet1 = new MultiSet<>(l1, l2);
+
+               var l3 = l(a("3", "4"));
+               var l4 = l(a("1", "2"));
+               var multiSet2 = new MultiSet<>(l3, l4);
+
+               assertTrue(multiSet1.equals(multiSet2)); // Order doesn't 
matter for sets
+       }
+
+       @Test
+       void equals_regularSet() {
+               var l1 = l(a("1", "2", "3"));
+               var multiSet = new MultiSet<>(l1);
+
+               var regularSet = new LinkedHashSet<>(l(a("1", "2", "3")));
+
+               assertTrue(multiSet.equals(regularSet));
+               assertTrue(regularSet.equals(multiSet));
+       }
+
+       @Test
+       void equals_notASet() {
+               var l1 = l(a("1", "2"));
+               var multiSet = new MultiSet<>(l1);
+
+               assertFalse(multiSet.equals("not a set"));
+               assertFalse(multiSet.equals(null));
+       }
+
+       @Test
+       void hashCode_sameContents() {
+               var l1 = l(a("1", "2", "3"));
+               var multiSet1 = new MultiSet<>(l1);
+
+               var l2 = l(a("1", "2", "3"));
+               var multiSet2 = new MultiSet<>(l2);
+
+               assertEquals(multiSet1.hashCode(), multiSet2.hashCode());
+       }
+
+       @Test
+       void hashCode_regularSet() {
+               var l1 = l(a("1", "2", "3"));
+               var multiSet = new MultiSet<>(l1);
+
+               var regularSet = new LinkedHashSet<>(l(a("1", "2", "3")));
+
+               assertEquals(multiSet.hashCode(), regularSet.hashCode());
+       }
 }
 

Reply via email to