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 7dc7510edb Marshall module improvements
7dc7510edb is described below
commit 7dc7510edbe996ba69bd68d10e65b24705a97ac2
Author: James Bognar <[email protected]>
AuthorDate: Sat Dec 13 08:20:33 2025 -0500
Marshall module improvements
---
.../juneau/commons/collections/FilteredMap.java | 590 ++++++++++++
.../commons/collections/FilteredMap_Test.java | 1007 ++++++++++++++++++++
2 files changed, 1597 insertions(+)
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/FilteredMap.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/FilteredMap.java
new file mode 100644
index 0000000000..2e2abbeefa
--- /dev/null
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/FilteredMap.java
@@ -0,0 +1,590 @@
+/*
+ * 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 static org.apache.juneau.commons.utils.Utils.*;
+
+import java.util.*;
+import java.util.function.*;
+
+/**
+ * A map wrapper that filters entries based on a {@link BiPredicate} when they
are added.
+ *
+ * <p>
+ * This class wraps an underlying map and applies a filter to determine
whether entries should be added.
+ * Only entries that pass the filter (i.e., the predicate returns
<jk>true</jk>) are actually stored in
+ * the underlying map.
+ *
+ * <h5 class='section'>Features:</h5>
+ * <ul class='spaced-list'>
+ * <li><b>Flexible Filtering:</b> Use any {@link BiPredicate} to filter
entries based on key, value, or both
+ * <li><b>Custom Map Types:</b> Works with any map implementation via the
builder's <c>creator</c> function
+ * <li><b>Transparent Interface:</b> Implements the full {@link Map}
interface, so it can be used anywhere a map is expected
+ * <li><b>Filter on Add:</b> Filtering happens when entries are added via
{@link #put(Object, Object)}, {@link #putAll(Map)}, etc.
+ * </ul>
+ *
+ * <h5 class='section'>Usage:</h5>
+ * <p class='bjava'>
+ * <jc>// Create a filtered map that only accepts non-null values</jc>
+ * FilteredMap<String, String> <jv>map</jv> = FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>, String.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk>)
+ * .build();
+ *
+ * <jv>map</jv>.put(<js>"key1"</js>, <js>"value1"</js>); <jc>// Added</jc>
+ * <jv>map</jv>.put(<js>"key2"</js>, <jk>null</jk>); <jc>// Filtered
out</jc>
+ * <jv>map</jv>.put(<js>"key3"</js>, <js>"value3"</js>); <jc>// Added</jc>
+ *
+ * <jc>// map now contains: {"key1"="value1", "key3"="value3"}</jc>
+ * </p>
+ *
+ * <h5 class='section'>Example - Custom Map Type:</h5>
+ * <p class='bjava'>
+ * <jc>// Create a filtered TreeMap that only accepts positive values</jc>
+ * FilteredMap<String, Integer> <jv>map</jv> = FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk> && v > 0)
+ * .creator(() -> <jk>new</jk> TreeMap<>())
+ * .build();
+ *
+ * <jv>map</jv>.put(<js>"a"</js>, 5); <jc>// Added</jc>
+ * <jv>map</jv>.put(<js>"b"</js>, -1); <jc>// Filtered out</jc>
+ * <jv>map</jv>.put(<js>"c"</js>, 10); <jc>// Added</jc>
+ * </p>
+ *
+ * <h5 class='section'>Behavior Notes:</h5>
+ * <ul class='spaced-list'>
+ * <li>Filtering is applied to all entry addition methods: {@link
#put(Object, Object)}, {@link #putAll(Map)}, etc.
+ * <li>If the filter returns <jk>false</jk>, the entry is silently ignored
(not added)
+ * <li>When {@link #put(Object, Object)} filters out an entry, it returns
the previous value associated with the key
+ * (if any), or <jk>null</jk> if the key did not exist. This
allows callers to distinguish between a new entry
+ * being filtered out versus an existing entry being filtered out.
+ * <li>The filter is not applied when reading from the map (e.g., {@link
#get(Object)}, {@link #containsKey(Object)})
+ * <li>All other map operations behave as expected on the underlying map
+ * <li>The underlying map type is determined by the <c>creator</c>
function (defaults to {@link LinkedHashMap})
+ * </ul>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * This class is not thread-safe unless the underlying map is thread-safe. If
thread safety is required,
+ * use a thread-safe map type (e.g., {@link
java.util.concurrent.ConcurrentHashMap}) via the <c>creator</c> function.
+ *
+ * <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.
+ * @param <V> The value type.
+ */
+public class FilteredMap<K,V> extends AbstractMap<K,V> {
+
+ /**
+ * Builder for creating {@link FilteredMap} instances.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * FilteredMap<String, Integer> <jv>map</jv> = FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk> && v
> 0)
+ * .creator(() -> <jk>new</jk> TreeMap<>())
+ * .build();
+ * </p>
+ *
+ * @param <K> The key type.
+ * @param <V> The value type.
+ */
+ public static class Builder<K,V> {
+ private BiPredicate<K,V> filter;
+ private Supplier<Map<K,V>> creator = LinkedHashMap::new;
+ private Class<K> keyType;
+ private Class<V> valueType;
+ private Function<Object,K> keyFunction;
+ private Function<Object,V> valueFunction;
+
+ /**
+ * Sets the filter predicate that determines whether entries
should be added.
+ *
+ * <p>
+ * The predicate receives both the key and value of each entry.
If it returns <jk>true</jk>,
+ * the entry is added to the map. If it returns <jk>false</jk>,
the entry is silently ignored.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Filter out null values</jc>
+ * Builder<String, String> <jv>b</jv> =
FilteredMap.<jsm>create</jsm>(String.<jk>class</jk>, String.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk>);
+ *
+ * <jc>// Filter based on both key and value</jc>
+ * Builder<String, Integer> <jv>b2</jv> =
FilteredMap.<jsm>create</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .filter((k, v) -> !
k.startsWith(<js>"_"</js>) && v != <jk>null</jk> && v > 0);
+ * </p>
+ *
+ * @param value The filter predicate. Must not be <jk>null</jk>.
+ * @return This object for method chaining.
+ */
+ public Builder<K,V> filter(BiPredicate<K,V> value) {
+ filter = assertArgNotNull("value", value);
+ return this;
+ }
+
+ /**
+ * Sets the supplier that creates the underlying map instance.
+ *
+ * <p>
+ * This supplier is called once during {@link #build()} to
create the underlying map that will
+ * store the filtered entries. The default implementation
creates a {@link LinkedHashMap}.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Use a TreeMap for sorted keys</jc>
+ * Builder<String, Integer> <jv>b</jv> =
FilteredMap.<jsm>create</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .creator(() -> <jk>new</jk>
TreeMap<>());
+ *
+ * <jc>// Use a ConcurrentHashMap for thread safety</jc>
+ * Builder<String, Integer> <jv>b2</jv> =
FilteredMap.<jsm>create</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .creator(() -> <jk>new</jk>
ConcurrentHashMap<>());
+ * </p>
+ *
+ * @param value The creator supplier. Must not be <jk>null</jk>.
+ * @return This object for method chaining.
+ */
+ public Builder<K,V> creator(Supplier<Map<K,V>> value) {
+ creator = assertArgNotNull("value", value);
+ return this;
+ }
+
+ /**
+ * Sets the function to use for converting keys when using
{@link FilteredMap#add(Object, Object)}.
+ *
+ * <p>
+ * If specified, keys passed to {@link FilteredMap#add(Object,
Object)} will be converted using
+ * this function before being added to the map. If not
specified, keys must already be of the
+ * correct type (or <c>Object.class</c> is used if types were
not specified, which accepts any type).
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * FilteredMap<String, Integer> <jv>map</jv> =
FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk>)
+ * .keyFunction(o -> o.toString())
+ * .build();
+ *
+ * <jv>map</jv>.add(123, 5); <jc>// Key will be converted
from Integer to String</jc>
+ * </p>
+ *
+ * @param value The key conversion function. Can be
<jk>null</jk>.
+ * @return This object for method chaining.
+ */
+ public Builder<K,V> keyFunction(Function<Object,K> value) {
+ keyFunction = value;
+ return this;
+ }
+
+ /**
+ * Sets the function to use for converting values when using
{@link FilteredMap#add(Object, Object)}.
+ *
+ * <p>
+ * If specified, values passed to {@link
FilteredMap#add(Object, Object)} will be converted using
+ * this function before being added to the map. If not
specified, values must already be of the
+ * correct type (or <c>Object.class</c> is used if types were
not specified, which accepts any type).
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * FilteredMap<String, Integer> <jv>map</jv> =
FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk>
&& v > 0)
+ * .valueFunction(o ->
Integer.parseInt(o.toString()))
+ * .build();
+ *
+ * <jv>map</jv>.add(<js>"key"</js>, <js>"123"</js>);
<jc>// Value will be converted from String to Integer</jc>
+ * </p>
+ *
+ * @param value The value conversion function. Can be
<jk>null</jk>.
+ * @return This object for method chaining.
+ */
+ public Builder<K,V> valueFunction(Function<Object,V> value) {
+ valueFunction = value;
+ return this;
+ }
+
+ /**
+ * Sets both the key and value conversion functions at once.
+ *
+ * <p>
+ * This is a convenience method for setting both conversion
functions in a single call.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * FilteredMap<String, Integer> <jv>map</jv> =
FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk>
&& v > 0)
+ * .functions(
+ * o -> o.toString(),
<jc>// Key function</jc>
+ * o -> Integer.parseInt(o.toString())
<jc>// Value function</jc>
+ * )
+ * .build();
+ * </p>
+ *
+ * @param keyFunction The key conversion function. Can be
<jk>null</jk>.
+ * @param valueFunction The value conversion function. Can be
<jk>null</jk>.
+ * @return This object for method chaining.
+ */
+ public Builder<K,V> functions(Function<Object,K> keyFunction,
Function<Object,V> valueFunction) {
+ this.keyFunction = keyFunction;
+ this.valueFunction = valueFunction;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link FilteredMap} instance.
+ *
+ * @return A new filtered map instance.
+ * @throws IllegalArgumentException If the filter has not been
set.
+ */
+ public FilteredMap<K,V> build() {
+ assertArgNotNull("filter", filter);
+ return new FilteredMap<>(filter, creator.get(),
keyType, valueType, keyFunction, valueFunction);
+ }
+ }
+
+ /**
+ * Creates a new builder for constructing a filtered map.
+ *
+ * @param <K> The key type.
+ * @param <V> The value type.
+ * @param keyType The key type class. Must not be <jk>null</jk>.
+ * @param valueType The value type class. Must not be <jk>null</jk>.
+ * @return A new builder.
+ */
+ public static <K,V> Builder<K,V> create(Class<K> keyType, Class<V>
valueType) {
+ assertArgNotNull("keyType", keyType);
+ assertArgNotNull("valueType", valueType);
+ var builder = new Builder<K,V>();
+ builder.keyType = keyType;
+ builder.valueType = valueType;
+ return builder;
+ }
+
+ /**
+ * Creates a new builder for constructing a filtered map with generic
types.
+ *
+ * <p>
+ * This is a convenience method that creates a builder without
requiring explicit type class parameters.
+ * The generic types must be explicitly specified using the diamond
operator syntax.
+ *
+ * <p>
+ * When using this method, type checking is effectively disabled (using
<c>Object.class</c> as the type),
+ * allowing any key and value types to be added. However, the generic
type parameters should still be
+ * specified for type safety.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Explicitly specify generic types using diamond
operator</jc>
+ * <jk>var</jk> <jv>map</jv> = FilteredMap
+ * .<String, String><jsm>create</jsm>()
+ * .filter((<jv>k</jv>, <jv>v</jv>) -> <jv>v</jv> !=
<jk>null</jk>)
+ * .build();
+ * </p>
+ *
+ * @param <K> The key type.
+ * @param <V> The value type.
+ * @return A new builder.
+ */
+ @SuppressWarnings("unchecked")
+ public static <K,V> Builder<K,V> create() {
+ var builder = new Builder<K,V>();
+ builder.keyType = (Class<K>)Object.class;
+ builder.valueType = (Class<V>)Object.class;
+ return builder;
+ }
+ private final BiPredicate<K,V> filter;
+ private final Map<K,V> map;
+ private final Class<K> keyType;
+ private final Class<V> valueType;
+ private final Function<Object,K> keyFunction;
+ private final Function<Object,V> valueFunction;
+
+ /**
+ * Constructor.
+ *
+ * @param filter The filter predicate. Must not be <jk>null</jk>.
+ * @param map The underlying map. Must not be <jk>null</jk>.
+ * @param keyType The key type. Must not be <jk>null</jk> (use
<c>Object.class</c> to disable type checking).
+ * @param valueType The value type. Must not be <jk>null</jk> (use
<c>Object.class</c> to disable type checking).
+ * @param keyFunction The key conversion function, or <jk>null</jk> if
not specified.
+ * @param valueFunction The value conversion function, or <jk>null</jk>
if not specified.
+ */
+ protected FilteredMap(BiPredicate<K,V> filter, Map<K,V> map, Class<K>
keyType, Class<V> valueType, Function<Object,K> keyFunction, Function<Object,V>
valueFunction) {
+ this.filter = assertArgNotNull("filter", filter);
+ this.map = assertArgNotNull("map", map);
+ this.keyType = assertArgNotNull("keyType", keyType);
+ this.valueType = assertArgNotNull("valueType", valueType);
+ this.keyFunction = keyFunction;
+ this.valueFunction = valueFunction;
+ }
+
+ /**
+ * Associates the specified value with the specified key in this map,
if the entry passes the filter.
+ *
+ * <p>
+ * If the entry passes the filter, it is added to the map and this
method returns the previous value
+ * associated with the key (or <jk>null</jk> if there was no mapping).
+ *
+ * <p>
+ * If the entry is filtered out, it is not added to the map. This
method returns the previous value
+ * associated with the key (or <jk>null</jk> if there was no mapping).
This allows callers to distinguish
+ * between a new entry being filtered out versus an existing entry
being filtered out.
+ *
+ * @param key The key with which the specified value is to be
associated.
+ * @param value The value to be associated with the specified key.
+ * @return The previous value associated with <c>key</c>, or
<jk>null</jk> if there was no mapping for <c>key</c>.
+ * This applies whether the entry was added or filtered out.
+ */
+ @Override
+ public V put(K key, V value) {
+ if (filter.test(key, value))
+ return map.put(key, value);
+ return map.get(key); // Return previous value if exists, null
otherwise
+ }
+
+ @Override
+ public void putAll(Map<? extends K, ? extends V> m) {
+ for (var entry : m.entrySet()) {
+ if (filter.test(entry.getKey(), entry.getValue()))
+ map.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Tests whether the specified key-value pair would pass the filter and
be added to this map.
+ *
+ * <p>
+ * This method can be used to check if an entry would be accepted by
the filter without actually
+ * adding it to the map. This is useful for validation, debugging, or
pre-checking entries before
+ * attempting to add them.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * FilteredMap<String, Integer> <jv>map</jv> = FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk> && v
> 0)
+ * .build();
+ *
+ * <jk>if</jk> (<jv>map</jv>.wouldAccept(<js>"key"</js>, 5)) {
+ * <jv>map</jv>.put(<js>"key"</js>, 5); <jc>// Will be
added</jc>
+ * }
+ * </p>
+ *
+ * @param key The key to test.
+ * @param value The value to test.
+ * @return <jk>true</jk> if the entry would be added to the map,
<jk>false</jk> if it would be filtered out.
+ */
+ public boolean wouldAccept(K key, V value) {
+ return filter.test(key, value);
+ }
+
+ /**
+ * Returns the filter predicate used by this map.
+ *
+ * <p>
+ * This method provides access to the filter for debugging, inspection,
or advanced use cases.
+ * The returned predicate is the same instance used internally by this
map.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * FilteredMap<String, Integer> <jv>map</jv> = FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk> && v
> 0)
+ * .build();
+ *
+ * BiPredicate<String, Integer> <jv>filter</jv> =
<jv>map</jv>.getFilter();
+ * <jc>// Use filter for other purposes</jc>
+ * </p>
+ *
+ * @return The filter predicate. Never <jk>null</jk>.
+ */
+ public BiPredicate<K,V> getFilter() {
+ return filter;
+ }
+
+ @Override
+ public Set<Entry<K,V>> entrySet() {
+ return map.entrySet();
+ }
+
+ @Override
+ public V get(Object key) {
+ return map.get(key);
+ }
+
+ @Override
+ public V remove(Object key) {
+ return map.remove(key);
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return map.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return map.containsValue(value);
+ }
+
+ @Override
+ public int size() {
+ return map.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ @Override
+ public void clear() {
+ map.clear();
+ }
+
+ @Override
+ public Set<K> keySet() {
+ return map.keySet();
+ }
+
+ @Override
+ public Collection<V> values() {
+ return map.values();
+ }
+
+ /**
+ * Adds an entry to this map with automatic type conversion.
+ *
+ * <p>
+ * This method converts the key and value using the configured
converters (if any) and validates
+ * the types (if key/value types were specified when creating the map).
After conversion and validation,
+ * the entry is added using the standard {@link #put(Object, Object)}
method, which applies the filter.
+ *
+ * <h5 class='section'>Type Conversion:</h5>
+ * <ul>
+ * <li>If a key function is configured, the key is converted by
applying the function
+ * <li>If a value function is configured, the value is converted
by applying the function
+ * <li>If no function is configured, the object is used as-is (but
may be validated if types were specified)
+ * </ul>
+ *
+ * <h5 class='section'>Type Validation:</h5>
+ * <ul>
+ * <li>If key type was specified (via {@link #create(Class,
Class)}), the converted key must be an instance of that type
+ * <li>If value type was specified (via {@link #create(Class,
Class)}), the converted value must be an instance of that type
+ * <li>If {@link #create()} was used (no types specified),
<c>Object.class</c> is used, which accepts any type
+ * </ul>
+ *
+ * <h5 class='section'>Return Value:</h5>
+ * <ul>
+ * <li>If the entry is added successfully, returns the previous
value associated with the key (or <jk>null</jk> if there was no mapping)
+ * <li>If the entry is filtered out, returns <jk>null</jk> (even
if there was a previous value)
+ * </ul>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * FilteredMap<String, Integer> <jv>map</jv> = FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk> && v
> 0)
+ * .valueFunction(o -> Integer.parseInt(o.toString()))
+ * .build();
+ *
+ * <jv>map</jv>.add(<js>"key"</js>, <js>"123"</js>); <jc>// Value
converted from String to Integer</jc>
+ * </p>
+ *
+ * @param key The key to add. Will be converted if a key function is
configured.
+ * @param value The value to add. Will be converted if a value function
is configured.
+ * @return The previous value associated with the key if the entry was
added, or <jk>null</jk> if there was no mapping or the entry was filtered out.
+ * @throws RuntimeException If conversion fails or type validation
fails.
+ */
+ public V add(Object key, Object value) {
+ K convertedKey = convertKey(key);
+ V convertedValue = convertValue(value);
+ if (filter.test(convertedKey, convertedValue))
+ return put(convertedKey, convertedValue);
+ return null; // Filtered out, return null
+ }
+
+ /**
+ * Adds all entries from the specified map with automatic type
conversion.
+ *
+ * <p>
+ * This method iterates over all entries in the source map and adds
each one using {@link #add(Object, Object)},
+ * which applies conversion and validation before adding.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * FilteredMap<String, Integer> <jv>map</jv> = FilteredMap
+ * .<jsm>create</jsm>(String.<jk>class</jk>,
Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk> && v
> 0)
+ * .valueFunction(o -> Integer.parseInt(o.toString()))
+ * .build();
+ *
+ * Map<String, String> <jv>source</jv> =
Map.of(<js>"a"</js>, <js>"5"</js>, <js>"b"</js>, <js>"-1"</js>);
+ * <jv>map</jv>.addAll(<jv>source</jv>); <jc>// Values converted,
negative value filtered out</jc>
+ * </p>
+ *
+ * @param source The map containing entries to add. Can be
<jk>null</jk> (no-op).
+ */
+ public void addAll(Map<?,?> source) {
+ if (source != null) {
+ for (var entry : source.entrySet()) {
+ add(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private K convertKey(Object key) {
+ if (keyFunction != null) {
+ key = keyFunction.apply(key);
+ }
+ if (key == null) {
+ // Allow null for non-primitive types
+ if (keyType.isPrimitive())
+ throw rex("Cannot set null key for primitive
type {0}", keyType.getName());
+ return null;
+ }
+ if (keyType.isInstance(key))
+ return keyType.cast(key);
+ throw rex("Object of type {0} could not be converted to key
type {1}", cn(key), cn(keyType));
+ }
+
+ @SuppressWarnings("unchecked")
+ private V convertValue(Object value) {
+ if (valueFunction != null) {
+ value = valueFunction.apply(value);
+ }
+ if (value == null) {
+ // Allow null for non-primitive types
+ if (valueType.isPrimitive())
+ throw rex("Cannot set null value for primitive
type {0}", valueType.getName());
+ return null;
+ }
+ if (valueType.isInstance(value))
+ return valueType.cast(value);
+ throw rex("Object of type {0} could not be converted to value
type {1}", cn(value), cn(valueType));
+ }
+}
+
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/FilteredMap_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/FilteredMap_Test.java
new file mode 100644
index 0000000000..c430b3023e
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/FilteredMap_Test.java
@@ -0,0 +1,1007 @@
+/*
+ * 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.TestUtils.assertThrowsWithMessage;
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class FilteredMap_Test extends TestBase {
+
+
//====================================================================================================
+ // Basic filtering - filter out null values
+
//====================================================================================================
+
+ @Test
+ void a01_filterNullValues_put() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ assertNull(map.put("key1", "value1")); // Added
+ assertNull(map.put("key2", null)); // Filtered out
+ assertNull(map.put("key3", "value3")); // Added
+
+ assertSize(2, map);
+ assertEquals("value1", map.get("key1"));
+ assertNull(map.get("key2"));
+ assertFalse(map.containsKey("key2"));
+ assertEquals("value3", map.get("key3"));
+ }
+
+ @Test
+ void a02_filterNullValues_putAll() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ var source = map("key1", "value1", "key2", null, "key3",
"value3", "key4", null);
+
+ map.putAll(source);
+
+ assertSize(2, map);
+ assertEquals("value1", map.get("key1"));
+ assertEquals("value3", map.get("key3"));
+ assertFalse(map.containsKey("key2"));
+ assertFalse(map.containsKey("key4"));
+ }
+
+ @Test
+ void a03_filterNullValues_updateExisting() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ assertEquals("value1", map.put("key1", "value2")); // Update
existing
+ assertEquals("value2", map.put("key1", null)); // Filtered
out, returns previous value
+
+ assertSize(1, map);
+ assertEquals("value2", map.get("key1")); // Still has old value
+ }
+
+
//====================================================================================================
+ // Filter based on value - positive numbers only
+
//====================================================================================================
+
+ @Test
+ void b01_filterPositiveNumbers() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .build();
+
+ map.put("a", 5); // Added
+ map.put("b", -1); // Filtered out
+ map.put("c", 0); // Filtered out
+ map.put("d", 10); // Added
+ map.put("e", null); // Filtered out
+
+ assertSize(2, map);
+ assertEquals(5, map.get("a"));
+ assertEquals(10, map.get("d"));
+ assertFalse(map.containsKey("b"));
+ assertFalse(map.containsKey("c"));
+ assertFalse(map.containsKey("e"));
+ }
+
+
//====================================================================================================
+ // Filter based on key - exclude keys starting with underscore
+
//====================================================================================================
+
+ @Test
+ void c01_filterKeysStartingWithUnderscore() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> k != null && !k.startsWith("_"))
+ .build();
+
+ map.put("key1", "value1"); // Added
+ map.put("_key2", "value2"); // Filtered out
+ map.put("key3", "value3"); // Added
+ map.put("_key4", "value4"); // Filtered out
+
+ assertSize(2, map);
+ assertEquals("value1", map.get("key1"));
+ assertEquals("value3", map.get("key3"));
+ assertFalse(map.containsKey("_key2"));
+ assertFalse(map.containsKey("_key4"));
+ }
+
+
//====================================================================================================
+ // Filter based on both key and value
+
//====================================================================================================
+
+ @Test
+ void d01_filterKeyAndValue() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> k != null && !k.startsWith("_") && v
!= null && v > 0)
+ .build();
+
+ map.put("key1", 5); // Added
+ map.put("_key2", 10); // Filtered out (key starts with _)
+ map.put("key3", -1); // Filtered out (value <= 0)
+ map.put("key4", 20); // Added
+ map.put(null, 5); // Filtered out (null key)
+
+ assertSize(2, map);
+ assertEquals(5, map.get("key1"));
+ assertEquals(20, map.get("key4"));
+ }
+
+
//====================================================================================================
+ // Custom map types - TreeMap
+
//====================================================================================================
+
+ @Test
+ void e01_customMapType_TreeMap() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .creator(() -> new TreeMap<>())
+ .build();
+
+ map.put("zebra", 3);
+ map.put("apple", 1);
+ map.put("banana", 2);
+
+ // TreeMap maintains sorted order
+ var keys = new ArrayList<>(map.keySet());
+ assertEquals(List.of("apple", "banana", "zebra"), keys);
+ }
+
+
//====================================================================================================
+ // Custom map types - ConcurrentHashMap
+
//====================================================================================================
+
+ @Test
+ void e02_customMapType_ConcurrentHashMap() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .creator(() -> new ConcurrentHashMap<>())
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", null); // Filtered out
+ map.put("key3", "value3");
+
+ assertSize(2, map);
+ }
+
+
//====================================================================================================
+ // Map interface methods - get, containsKey, containsValue
+
//====================================================================================================
+
+ @Test
+ void f01_mapInterface_get() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", null); // Filtered out
+
+ assertEquals("value1", map.get("key1"));
+ assertNull(map.get("key2")); // Not in map
+ assertNull(map.get("nonexistent"));
+ }
+
+ @Test
+ void f02_mapInterface_containsKey() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", null); // Filtered out
+
+ assertTrue(map.containsKey("key1"));
+ assertFalse(map.containsKey("key2"));
+ assertFalse(map.containsKey("nonexistent"));
+ }
+
+ @Test
+ void f03_mapInterface_containsValue() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", null); // Filtered out
+
+ assertTrue(map.containsValue("value1"));
+ assertFalse(map.containsValue(null)); // null was filtered out
+ }
+
+ @Test
+ void f04_mapInterface_size() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ assertEquals(0, map.size());
+ map.put("key1", "value1");
+ assertEquals(1, map.size());
+ map.put("key2", null); // Filtered out
+ assertEquals(1, map.size());
+ map.put("key3", "value3");
+ assertEquals(2, map.size());
+ }
+
+ @Test
+ void f05_mapInterface_isEmpty() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ assertTrue(map.isEmpty());
+ map.put("key1", "value1");
+ assertFalse(map.isEmpty());
+ }
+
+ @Test
+ void f06_mapInterface_clear() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", "value2");
+ assertSize(2, map);
+
+ map.clear();
+ assertTrue(map.isEmpty());
+ assertSize(0, map);
+ }
+
+ @Test
+ void f07_mapInterface_remove() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", "value2");
+
+ assertEquals("value1", map.remove("key1"));
+ assertNull(map.remove("key1")); // Already removed
+ assertNull(map.remove("nonexistent"));
+
+ assertSize(1, map);
+ assertEquals("value2", map.get("key2"));
+ }
+
+ @Test
+ void f08_mapInterface_keySet() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", null); // Filtered out
+ map.put("key3", "value3");
+
+ var keySet = map.keySet();
+ assertSize(2, keySet);
+ assertTrue(keySet.contains("key1"));
+ assertTrue(keySet.contains("key3"));
+ assertFalse(keySet.contains("key2"));
+ }
+
+ @Test
+ void f09_mapInterface_values() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", null); // Filtered out
+ map.put("key3", "value3");
+
+ var values = map.values();
+ assertSize(2, values);
+ assertTrue(values.contains("value1"));
+ assertTrue(values.contains("value3"));
+ assertFalse(values.contains(null));
+ }
+
+ @Test
+ void f10_mapInterface_entrySet() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", null); // Filtered out
+ map.put("key3", "value3");
+
+ var entrySet = map.entrySet();
+ assertSize(2, entrySet);
+
+ var found = new HashSet<String>();
+ for (var entry : entrySet) {
+ found.add(entry.getKey() + "=" + entry.getValue());
+ }
+ assertTrue(found.contains("key1=value1"));
+ assertTrue(found.contains("key3=value3"));
+ assertFalse(found.contains("key2=null"));
+ }
+
+
//====================================================================================================
+ // Edge cases - null keys
+
//====================================================================================================
+
+ @Test
+ void g01_nullKey_allowed() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put(null, "value1"); // null key is allowed if filter
passes
+ map.put(null, null); // Filtered out (null value)
+
+ assertSize(1, map);
+ assertEquals("value1", map.get(null));
+ assertTrue(map.containsKey(null));
+ }
+
+ @Test
+ void g02_nullKey_filtered() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> k != null && v != null)
+ .build();
+
+ map.put(null, "value1"); // Filtered out (null key)
+ map.put("key1", "value1"); // Added
+
+ assertSize(1, map);
+ assertFalse(map.containsKey(null));
+ assertEquals("value1", map.get("key1"));
+ }
+
+
//====================================================================================================
+ // Edge cases - empty map
+
//====================================================================================================
+
+ @Test
+ void h01_emptyMap() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ assertTrue(map.isEmpty());
+ assertSize(0, map);
+ assertNull(map.get("any"));
+ assertFalse(map.containsKey("any"));
+ }
+
+
//====================================================================================================
+ // Builder validation
+
//====================================================================================================
+
+ @Test
+ void i01_builder_noFilter_throwsException() {
+ assertThrowsWithMessage(IllegalArgumentException.class,
"filter", () -> {
+ FilteredMap.create(String.class, String.class).build();
+ });
+ }
+
+ @Test
+ void i02_builder_nullFilter_throwsException() {
+ assertThrowsWithMessage(IllegalArgumentException.class,
"value", () -> {
+ FilteredMap.create(String.class,
String.class).filter(null);
+ });
+ }
+
+ @Test
+ void i03_builder_nullCreator_throwsException() {
+ assertThrowsWithMessage(IllegalArgumentException.class,
"value", () -> {
+ FilteredMap.create(String.class, String.class)
+ .filter((k, v) -> true)
+ .creator(null);
+ });
+ }
+
+
//====================================================================================================
+ // Builder - create() without parameters
+
//====================================================================================================
+
+ @Test
+ void j01_builder_createWithoutParameters() {
+ var map = FilteredMap
+ .<String,String>create()
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.put("key1", "value1");
+ map.put("key2", null); // Filtered out
+
+ assertSize(1, map);
+ assertEquals("value1", map.get("key1"));
+ }
+
+ @Test
+ void j02_builder_createWithoutParameters_usesObjectClass() {
+ var map = FilteredMap
+ .create()
+ .filter((k, v) -> v != null)
+ .build();
+
+ // Object.class accepts any type
+ map.add("string", 123); // String key, Integer value
+ map.add(456, "value"); // Integer key, String value
+ map.add(List.of(1, 2), Map.of()); // List key, Map value
+
+ assertSize(3, map);
+ assertEquals(123, map.get("string"));
+ assertEquals("value", map.get(456));
+ assertEquals(Map.of(), map.get(List.of(1, 2)));
+ }
+
+
//====================================================================================================
+ // Complex filter - string length
+
//====================================================================================================
+
+ @Test
+ void k01_filterStringLength() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null && v.length() > 3)
+ .build();
+
+ map.put("key1", "short"); // Added
+ map.put("key2", "ab"); // Filtered out (length <= 3)
+ map.put("key3", "longer"); // Added
+ map.put("key4", null); // Filtered out
+
+ assertSize(2, map);
+ assertEquals("short", map.get("key1"));
+ assertEquals("longer", map.get("key3"));
+ }
+
+
//====================================================================================================
+ // add() method - type validation with types specified
+
//====================================================================================================
+
+ @Test
+ void l01_add_withTypeValidation_validTypes() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .build();
+
+ map.add("key1", 5); // Valid types, added
+ map.add("key2", 10); // Valid types, added
+ map.add("key3", -1); // Valid types, but filtered out
+
+ assertSize(2, map);
+ assertEquals(5, map.get("key1"));
+ assertEquals(10, map.get("key2"));
+ }
+
+ @Test
+ void l02_add_withTypeValidation_invalidKeyType() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ assertThrowsWithMessage(RuntimeException.class, "could not be
converted to key type", () -> {
+ map.add(123, 5); // Invalid key type (Integer instead
of String)
+ });
+ }
+
+ @Test
+ void l03_add_withTypeValidation_invalidValueType() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ assertThrowsWithMessage(RuntimeException.class, "could not be
converted to value type", () -> {
+ map.add("key", "value"); // Invalid value type (String
instead of Integer)
+ });
+ }
+
+ @Test
+ void l04_add_withTypeValidation_noTypesSpecified() {
+ var map = FilteredMap
+ .<Object,Object>create()
+ .filter((k, v) -> v != null)
+ .build();
+
+ // Object.class is used when types not specified, which accepts
any type
+ map.add("key1", "value1");
+ map.add(123, 456); // Different types, but Object.class
accepts any type
+
+ assertSize(2, map);
+ assertEquals("value1", map.get("key1"));
+ assertEquals(456, map.get(123));
+ }
+
+
//====================================================================================================
+ // add() method - with keyFunction
+
//====================================================================================================
+
+ @Test
+ void m01_add_withKeyFunction() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .keyFunction(o -> o.toString())
+ .build();
+
+ map.add(123, 5); // Key converted from Integer to String
+ map.add(456, 10); // Key converted from Integer to String
+ map.add(789, -1); // Key converted, but value filtered out
+
+ assertSize(2, map);
+ assertEquals(5, map.get("123"));
+ assertEquals(10, map.get("456"));
+ assertFalse(map.containsKey("789"));
+ }
+
+ @Test
+ void m02_add_withKeyFunction_noTypeSpecified() {
+ var map = FilteredMap
+ .<String,String>create()
+ .filter((k, v) -> v != null)
+ .keyFunction(o -> o.toString())
+ .build();
+
+ map.add(123, "value1"); // Key converted using function
+
+ assertSize(1, map);
+ assertEquals("value1", map.get("123"));
+ }
+
+
//====================================================================================================
+ // add() method - with valueFunction
+
//====================================================================================================
+
+ @Test
+ void n01_add_withValueFunction() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .valueFunction(o -> Integer.parseInt(o.toString()))
+ .build();
+
+ map.add("key1", "5"); // Value converted from String to
Integer
+ map.add("key2", "10"); // Value converted from String to
Integer
+ map.add("key3", "-1"); // Value converted, but filtered out
+
+ assertSize(2, map);
+ assertEquals(5, map.get("key1"));
+ assertEquals(10, map.get("key2"));
+ assertFalse(map.containsKey("key3"));
+ }
+
+ @Test
+ void n02_add_withValueFunction_noTypeSpecified() {
+ var map = FilteredMap
+ .<String,Integer>create()
+ .filter((k, v) -> v != null && v > 0)
+ .valueFunction(o -> Integer.parseInt(o.toString()))
+ .build();
+
+ map.add("key1", "5"); // Value converted using function
+
+ assertSize(1, map);
+ assertEquals(5, map.get("key1"));
+ }
+
+
//====================================================================================================
+ // add() method - with both keyFunction and valueFunction
+
//====================================================================================================
+
+ @Test
+ void o01_add_withBothFunctions() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .keyFunction(o -> o.toString())
+ .valueFunction(o -> Integer.parseInt(o.toString()))
+ .build();
+
+ map.add(123, "5"); // Both key and value converted
+ map.add(456, "10"); // Both key and value converted
+ map.add(789, "-1"); // Both converted, but value filtered out
+
+ assertSize(2, map);
+ assertEquals(5, map.get("123"));
+ assertEquals(10, map.get("456"));
+ assertFalse(map.containsKey("789"));
+ }
+
+
//====================================================================================================
+ // addAll() method
+
//====================================================================================================
+
+ @Test
+ void p01_addAll_withTypeValidation() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .build();
+
+ var source = Map.of(
+ "key1", 5,
+ "key2", -1, // Filtered out
+ "key3", 10
+ );
+
+ map.addAll(source);
+
+ assertSize(2, map);
+ assertEquals(5, map.get("key1"));
+ assertEquals(10, map.get("key3"));
+ assertFalse(map.containsKey("key2"));
+ }
+
+ @Test
+ void p02_addAll_withValueFunction() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .valueFunction(o -> Integer.parseInt(o.toString()))
+ .build();
+
+ var source = Map.of(
+ "key1", "5",
+ "key2", "-1", // Converted then filtered out
+ "key3", "10"
+ );
+
+ map.addAll(source);
+
+ assertSize(2, map);
+ assertEquals(5, map.get("key1"));
+ assertEquals(10, map.get("key3"));
+ assertFalse(map.containsKey("key2"));
+ }
+
+ @Test
+ void p03_addAll_withKeyFunction() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .keyFunction(o -> o.toString())
+ .build();
+
+ var source = Map.of(
+ 123, 5,
+ 456, -1, // Filtered out
+ 789, 10
+ );
+
+ map.addAll(source);
+
+ assertSize(2, map);
+ assertEquals(5, map.get("123"));
+ assertEquals(10, map.get("789"));
+ assertFalse(map.containsKey("456"));
+ }
+
+ @Test
+ void p04_addAll_nullSource() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.addAll(null); // Should be no-op
+
+ assertTrue(map.isEmpty());
+ assertSize(0, map);
+ }
+
+ @Test
+ void p05_addAll_emptySource() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.addAll(Map.of()); // Empty map
+
+ assertTrue(map.isEmpty());
+ assertSize(0, map);
+ }
+
+
//====================================================================================================
+ // add() method - return value
+
//====================================================================================================
+
+ @Test
+ void q01_add_returnValue_newEntry() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ assertNull(map.add("key1", "value1")); // New entry, returns
null
+ }
+
+ @Test
+ void q02_add_returnValue_updateExisting() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.add("key1", "value1");
+ assertEquals("value1", map.add("key1", "value2")); // Update,
returns old value
+ }
+
+ @Test
+ void q03_add_returnValue_filteredOut() {
+ var map = FilteredMap
+ .create(String.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ map.add("key1", "value1");
+ assertNull(map.add("key1", null)); // Filtered out, returns
null (not old value)
+ assertEquals("value1", map.get("key1")); // Old value still
there
+ }
+
+
//====================================================================================================
+ // add() method - edge cases with functions
+
//====================================================================================================
+
+ @Test
+ void r01_add_keyFunctionThrowsException() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null)
+ .keyFunction(o -> {
+ if (o == null)
+ throw new IllegalArgumentException("Key
cannot be null");
+ return o.toString();
+ })
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ map.add(null, 5);
+ });
+ }
+
+ @Test
+ void r02_add_valueFunctionThrowsException() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null)
+ .valueFunction(o -> {
+ if (o == null)
+ throw new
IllegalArgumentException("Value cannot be null");
+ return Integer.parseInt(o.toString());
+ })
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ map.add("key", null);
+ });
+ }
+
+ @Test
+ void r03_add_valueFunctionReturnsNull() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .valueFunction(o -> {
+ try {
+ return Integer.parseInt(o.toString());
+ } catch (NumberFormatException e) {
+ return null; // Return null for
invalid numbers
+ }
+ })
+ .build();
+
+ map.add("key1", "5"); // Valid, added
+ map.add("key2", "abc"); // Function returns null, filtered out
+
+ assertSize(1, map);
+ assertEquals(5, map.get("key1"));
+ assertFalse(map.containsKey("key2"));
+ }
+
+
//====================================================================================================
+ // wouldAccept() method
+
//====================================================================================================
+
+ @Test
+ void s01_wouldAccept_returnsTrue() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .build();
+
+ assertTrue(map.wouldAccept("key1", 5));
+ assertTrue(map.wouldAccept("key2", 10));
+ }
+
+ @Test
+ void s02_wouldAccept_returnsFalse() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .build();
+
+ assertFalse(map.wouldAccept("key1", null));
+ assertFalse(map.wouldAccept("key2", -1));
+ assertFalse(map.wouldAccept("key3", 0));
+ }
+
+ @Test
+ void s03_wouldAccept_usedForPreValidation() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .build();
+
+ // Pre-validate before adding
+ if (map.wouldAccept("key1", 5)) {
+ map.put("key1", 5);
+ }
+ if (map.wouldAccept("key2", -1)) {
+ map.put("key2", -1);
+ }
+
+ assertSize(1, map);
+ assertTrue(map.containsKey("key1"));
+ assertFalse(map.containsKey("key2"));
+ }
+
+
//====================================================================================================
+ // getFilter() method
+
//====================================================================================================
+
+ @Test
+ void t01_getFilter_returnsFilter() {
+ var originalFilter = (BiPredicate<String, Integer>)((k, v) -> v
!= null && v > 0);
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter(originalFilter)
+ .build();
+
+ var retrievedFilter = map.getFilter();
+ assertNotNull(retrievedFilter);
+ assertSame(originalFilter, retrievedFilter); // Should be the
same instance
+ }
+
+ @Test
+ void t02_getFilter_canBeUsedForOtherPurposes() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .build();
+
+ var filter = map.getFilter();
+
+ // Use the filter independently
+ assertTrue(filter.test("key1", 5));
+ assertFalse(filter.test("key2", -1));
+ }
+
+
//====================================================================================================
+ // Builder.functions() convenience method
+
//====================================================================================================
+
+ @Test
+ void u01_builder_functions_setsBothAtOnce() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null && v > 0)
+ .functions(
+ o -> o.toString(), // Key
function
+ o -> Integer.parseInt(o.toString()) // Value
function
+ )
+ .build();
+
+ map.add(123, "5"); // Both key and value converted
+ map.add(456, "10"); // Both key and value converted
+
+ assertSize(2, map);
+ assertEquals(5, map.get("123"));
+ assertEquals(10, map.get("456"));
+ }
+
+ @Test
+ void u02_builder_functions_withNullFunctions() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> v != null)
+ .functions(null, null) // Both functions null
+ .build();
+
+ map.add("key1", 5); // No conversion, direct add
+
+ assertSize(1, map);
+ assertEquals(5, map.get("key1"));
+ }
+
+
//====================================================================================================
+ // Null handling for primitive types
+
//====================================================================================================
+
+ @Test
+ void v01_add_nullKeyForPrimitiveType_throwsException() {
+ var map = FilteredMap
+ .create(int.class, String.class)
+ .filter((k, v) -> v != null)
+ .build();
+
+ assertThrowsWithMessage(RuntimeException.class, "Cannot set
null key for primitive type", () -> {
+ map.add(null, "value");
+ });
+ }
+
+ @Test
+ void v02_add_nullValueForPrimitiveType_throwsException() {
+ var map = FilteredMap
+ .create(String.class, int.class)
+ .filter((k, v) -> true)
+ .build();
+
+ assertThrowsWithMessage(RuntimeException.class, "Cannot set
null value for primitive type", () -> {
+ map.add("key", null);
+ });
+ }
+
+ @Test
+ void v03_add_nullForWrapperType_allowed() {
+ var map = FilteredMap
+ .create(String.class, Integer.class)
+ .filter((k, v) -> true) // Accept all, including null
+ .build();
+
+ map.add("key1", null); // Should work for wrapper types
+ map.add(null, 5); // Should work for wrapper types
+
+ assertSize(2, map);
+ assertNull(map.get("key1"));
+ assertEquals(5, map.get(null));
+ }
+}
+