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<String> <jv>list1</jv> = List.of(<js>"a"</js>,
<js>"b"</js>);
+ * List<String> <jv>list2</jv> = List.of(<js>"c"</js>,
<js>"d"</js>);
+ * MultiList<String> <jv>multiList</jv> = <jk>new</jk>
MultiList<>(<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<String, String> <jv>map1</jv> = Map.of(<js>"key1"</js>,
<js>"value1"</js>, <js>"key2"</js>, <js>"value2"</js>);
+ * Map<String, String> <jv>map2</jv> = Map.of(<js>"key3"</js>,
<js>"value3"</js>, <js>"key2"</js>, <js>"value2b"</js>);
+ * Map<String, String> <jv>map3</jv> = Map.of(<js>"key4"</js>,
<js>"value4"</js>);
+ *
+ * MultiMap<String, String> <jv>multiMap</jv> = <jk>new</jk>
MultiMap<>(<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<String, String> <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<String, String> <jv>systemProps</jv> = getSystemProperties();
+ * Map<String, String> <jv>envVars</jv> = getEnvironmentVariables();
+ * Map<String, String> <jv>defaults</jv> = getDefaultConfig();
+ *
+ * MultiMap<String, String> <jv>config</jv> = <jk>new</jk>
MultiMap<>(<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 > 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<String, String> <jv>map1</jv> = <jk>new</jk>
LinkedHashMap<>(Map.of(<js>"a"</js>, <js>"1"</js>));
+ * Map<String, String> <jv>map2</jv> = <jk>new</jk>
LinkedHashMap<>(Map.of(<js>"b"</js>, <js>"2"</js>));
+ *
+ * MultiMap<String, String> <jv>multiMap</jv> = <jk>new</jk>
MultiMap<>(<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<String, String> <jv>map1</jv> = Map.of(<js>"a"</js>,
<js>"1"</js>, <js>"b"</js>, <js>"2"</js>); <jc>// size = 2</jc>
+ * Map<String, String> <jv>map2</jv> = Map.of(<js>"b"</js>,
<js>"3"</js>, <js>"c"</js>, <js>"4"</js>); <jc>// size = 2</jc>
+ * MultiMap<String, String> <jv>multiMap</jv> = <jk>new</jk>
MultiMap<>(<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<String, String> <jv>map1</jv> = Map.of(<js>"a"</js>,
<js>"1"</js>);
+ * Map<String, String> <jv>map2</jv> = Map.of(<js>"b"</js>,
<js>"2"</js>);
+ * MultiMap<String, String> <jv>multiMap</jv> = <jk>new</jk>
MultiMap<>(<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<String> <jv>list1</jv> = List.of(<js>"a"</js>,
<js>"b"</js>);
+ * List<String> <jv>list2</jv> = List.of(<js>"c"</js>,
<js>"d"</js>);
+ * MultiSet<String> <jv>multiSet</jv> = <jk>new</jk>
MultiSet<>(<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());
+ }
}