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 3e96bb1493 FiltereMap/MapBuilder improvements
3e96bb1493 is described below
commit 3e96bb14931ca99f1efa02e0d3bdc76cee4b2872
Author: James Bognar <[email protected]>
AuthorDate: Sun Dec 14 13:31:50 2025 -0500
FiltereMap/MapBuilder improvements
---
.../juneau/commons/collections/FilteredMap.java | 148 ++++++++--
.../juneau/commons/collections/MapBuilder.java | 318 +++++++++++++++------
.../juneau/commons/utils/CollectionUtils.java | 4 +-
.../org/apache/juneau/internal/ConverterUtils.java | 5 +-
.../juneau/commons/collections/BidiMap_Test.java | 1 -
.../commons/collections/FilteredMap_Test.java | 29 +-
.../juneau/commons/collections/Flag_Test.java | 1 -
.../commons/collections/KeywordSet_Test.java | 1 -
.../commons/collections/MapBuilder_Test.java | 305 ++++++++++++--------
.../juneau/commons/collections/MultiList_Test.java | 2 -
.../juneau/commons/collections/MultiMap_Test.java | 1 -
.../juneau/commons/collections/MultiSet_Test.java | 1 -
.../commons/collections/ReversedList_Test.java | 1 -
.../juneau/commons/collections/SimpleMap_Test.java | 1 -
14 files changed, 570 insertions(+), 248 deletions(-)
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
index 026747f1e6..f221e608b9 100644
---
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
@@ -34,7 +34,11 @@ import java.util.function.*;
* <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>Optional Filter:</b> Filter is optional when using the builder -
defaults to accepting all entries
+ * <li><b>Multiple Filters:</b> Can combine multiple filters using AND
logic
+ * <li><b>Custom Map Types:</b> Works with any map implementation via the
builder's <c>inner</c> method
+ * <li><b>Type Conversion:</b> Supports automatic type conversion via
key/value functions
+ * <li><b>Convenience Methods:</b> Provides {@link #add(Object, Object)},
{@link #addAll(Map)}, {@link #addAny(Object...)}, and {@link
#addPairs(Object...)} for easy entry addition
* <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>
@@ -60,7 +64,7 @@ import java.util.function.*;
* 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<>())
+ * .inner(<jk>new</jk> TreeMap<>())
* .build();
*
* <jv>map</jv>.put(<js>"a"</js>, 5); <jc>// Added</jc>
@@ -77,13 +81,13 @@ import java.util.function.*;
* 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})
+ * <li>The underlying map type is determined by the <c>inner</c> method
(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.
+ * use a thread-safe map type (e.g., {@link
java.util.concurrent.ConcurrentHashMap}) via the <c>inner</c> method.
*
* <h5 class='section'>See Also:</h5>
* <ul>
@@ -103,7 +107,7 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
* 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<>())
+ * .inner(<jk>new</jk> TreeMap<>())
* .build();
* </p>
*
@@ -111,8 +115,8 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
* @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 BiPredicate<K,V> filter = (k,v) -> true;
+ private Map<K,V> inner;
private Class<K> keyType;
private Class<V> valueType;
private Function<Object,K> keyFunction;
@@ -125,6 +129,13 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
* 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.
*
+ * <p>
+ * This method is optional. If not called, the map will accept
all entries (defaults to <c>(k,v) -> true</c>).
+ *
+ * <p>
+ * This method can be called multiple times. When called
multiple times, all filters are combined
+ * using AND logic - an entry must pass all filters to be added
to the map.
+ *
* <h5 class='section'>Example:</h5>
* <p class='bjava'>
* <jc>// Filter out null values</jc>
@@ -134,39 +145,53 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
* <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);
+ *
+ * <jc>// Multiple filters combined with AND</jc>
+ * Builder<String, Integer> <jv>b3</jv> =
FilteredMap.<jsm>create</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .filter((k, v) -> v != <jk>null</jk>)
<jc>// First filter</jc>
+ * .filter((k, v) -> v > 0)
<jc>// Second filter (ANDed with first)</jc>
+ * .filter((k, v) -> !
k.startsWith(<js>"_"</js>)); <jc>// Third filter (ANDed with previous)</jc>
* </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);
+ BiPredicate<K,V> newFilter = assertArgNotNull("value",
value);
+ if (filter == null)
+ filter = newFilter;
+ else
+ filter = filter.and(newFilter);
return this;
}
/**
- * Sets the supplier that creates the underlying map instance.
+ * Sets the underlying map instance that will store the
filtered entries.
*
* <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}.
+ * If not specified, a new {@link LinkedHashMap} will be
created during {@link #build()}.
*
* <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<>());
+ * .inner(<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<>());
+ * .inner(<jk>new</jk>
ConcurrentHashMap<>());
+ *
+ * <jc>// Use an existing map</jc>
+ * Map<String, Integer> <jv>existing</jv> =
<jk>new</jk> LinkedHashMap<>();
+ * Builder<String, Integer> <jv>b3</jv> =
FilteredMap.<jsm>create</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .inner(<jv>existing</jv>);
* </p>
*
- * @param value The creator supplier. Must not be <jk>null</jk>.
+ * @param value The underlying map instance. 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);
+ public Builder<K,V> inner(Map<K,V> value) {
+ inner = assertArgNotNull("value", value);
return this;
}
@@ -255,12 +280,15 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
/**
* Builds a new {@link FilteredMap} instance.
*
+ * <p>
+ * If {@link #filter(BiPredicate)} was not called, the map will
accept all entries
+ * (defaults to <c>(k,v) -> true</c>).
+ *
* @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);
+ Map<K,V> map = inner != null ? inner : new
LinkedHashMap<>();
+ return new FilteredMap<>(filter, map, keyType,
valueType, keyFunction, valueFunction);
}
}
@@ -324,7 +352,7 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
/**
* Constructor.
*
- * @param filter The filter predicate. Must not be <jk>null</jk>.
+ * @param filter The filter predicate. Can be <jk>null</jk> (if null,
all entries are accepted).
* @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).
@@ -405,7 +433,8 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
*
* <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.
+ * The returned predicate is the combined filter used internally by
this map. If multiple filters
+ * were set via the builder, they are combined using AND logic.
*
* <h5 class='section'>Example:</h5>
* <p class='bjava'>
@@ -478,7 +507,7 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
* 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
+ * This method converts the key and value using the configured
key/value functions (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.
*
@@ -546,13 +575,86 @@ public class FilteredMap<K,V> extends AbstractMap<K,V> {
* </p>
*
* @param source The map containing entries to add. Can be
<jk>null</jk> (no-op).
+ * @return This object for method chaining.
*/
- public void addAll(Map<?,?> source) {
+ public FilteredMap<K,V> addAll(Map<?,?> source) {
if (source != null) {
for (var entry : source.entrySet()) {
add(entry.getKey(), entry.getValue());
}
}
+ return this;
+ }
+
+ /**
+ * Adds arbitrary values to this map.
+ *
+ * <p>
+ * Objects can be any of the following:
+ * <ul>
+ * <li>Maps of key/value types convertible to the key/value types
of this map.
+ * </ul>
+ *
+ * <p>
+ * Each entry from the maps will be added using {@link #add(Object,
Object)}, which applies
+ * conversion and filtering.
+ *
+ * <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();
+ *
+ * Map<String, Integer> <jv>map1</jv> = Map.of(<js>"a"</js>,
5, <js>"b"</js>, -1);
+ * Map<String, Integer> <jv>map2</jv> = Map.of(<js>"c"</js>,
10);
+ * <jv>map</jv>.addAny(<jv>map1</jv>, <jv>map2</jv>); <jc>// Adds
a=5, c=10 (b=-1 filtered out)</jc>
+ * </p>
+ *
+ * @param values The values to add. Can be <jk>null</jk> or contain
<jk>null</jk> values (ignored).
+ * @return This object for method chaining.
+ */
+ public FilteredMap<K,V> addAny(Object...values) {
+ if (values != null) {
+ for (var o : values) {
+ if (o != null && o instanceof Map<?,?> m) {
+ addAll(m);
+ }
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of key/value pairs to this map.
+ *
+ * <p>
+ * The pairs are processed in order, with each pair consisting of two
consecutive elements
+ * in the array. Each pair is added using {@link #add(Object, Object)},
which applies
+ * conversion and filtering.
+ *
+ * <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();
+ *
+ * <jv>map</jv>.addPairs(<js>"a"</js>, 5, <js>"b"</js>, -1,
<js>"c"</js>, 10);
+ * <jc>// Adds a=5, c=10 (b=-1 filtered out)</jc>
+ * </p>
+ *
+ * @param pairs The pairs to add. Must have an even number of elements.
+ * @return This object for method chaining.
+ * @throws IllegalArgumentException If an odd number of parameters is
provided.
+ */
+ public FilteredMap<K,V> addPairs(Object...pairs) {
+ assertArgNotNull("pairs", pairs);
+ if (pairs.length % 2 != 0)
+ throw illegalArg("Odd number of parameters passed into
addPairs()");
+ for (var i = 0; i < pairs.length; i += 2)
+ add(pairs[i], pairs[i + 1]);
+ return this;
}
private K convertKey(Object key) {
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MapBuilder.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MapBuilder.java
index 0a63eca7b2..f619869d11 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MapBuilder.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/collections/MapBuilder.java
@@ -17,14 +17,12 @@
package org.apache.juneau.commons.collections;
import static org.apache.juneau.commons.utils.AssertionUtils.*;
-import static org.apache.juneau.commons.utils.CollectionUtils.*;
import static org.apache.juneau.commons.utils.ThrowableUtils.*;
import static org.apache.juneau.commons.utils.Utils.*;
import java.lang.reflect.*;
import java.util.*;
-
-import org.apache.juneau.commons.utils.*;
+import java.util.function.*;
/**
* A fluent builder for constructing {@link Map} instances with various
configuration options.
@@ -36,7 +34,7 @@ import org.apache.juneau.commons.utils.*;
*
* <p>
* Instances of this builder can be created using {@link #create(Class,
Class)} or the convenience method
- * {@link org.apache.juneau.commons.utils.CollectionUtils#mapb(Class, Class,
org.apache.juneau.commons.utils.Converter...)}.
+ * {@link org.apache.juneau.commons.utils.CollectionUtils#mapb(Class, Class)}.
*
* <h5 class='section'>Features:</h5>
* <ul class='spaced-list'>
@@ -44,30 +42,30 @@ import org.apache.juneau.commons.utils.*;
* <li>Multiple add methods - single entries, pairs, other maps
* <li>Arbitrary input support - automatic type conversion with {@link
#addAny(Object...)}
* <li>Pair adding - {@link #addPairs(Object...)} for varargs key-value
pairs
- * <li>Filtering support - exclude unwanted values via {@link #filtered()}
or {@link #filtered(java.util.function.Predicate)}
+ * <li>Filtering support - exclude unwanted entries via {@link
#filtered()} or {@link #filtered(BiPredicate)}
* <li>Sorting support - natural key order or custom {@link Comparator}
* <li>Sparse mode - return <jk>null</jk> for empty maps
* <li>Unmodifiable mode - create immutable maps
- * <li>Custom converters - type conversion via {@link Converter}
+ * <li>Custom conversion functions - type conversion via {@link
#keyFunction(Function)} and {@link #valueFunction(Function)}
* </ul>
*
* <h5 class='section'>Examples:</h5>
* <p class='bjava'>
* <jk>import static</jk>
org.apache.juneau.commons.utils.CollectionUtils.*;
*
- * <jc>// Basic usage</jc>
+ * <jc>// Basic usage - returns Map</jc>
* Map<String,Integer> <jv>map</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
* .add(<js>"one"</js>, 1)
* .add(<js>"two"</js>, 2)
* .add(<js>"three"</js>, 3)
* .build();
*
- * <jc>// Using pairs</jc>
+ * <jc>// Using pairs - returns Map</jc>
* Map<String,String> <jv>props</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, String.<jk>class</jk>)
* .addPairs(<js>"host"</js>, <js>"localhost"</js>,
<js>"port"</js>, <js>"8080"</js>)
* .build();
*
- * <jc>// With sorting by key</jc>
+ * <jc>// With sorting by key - returns Map</jc>
* Map<String,Integer> <jv>sorted</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
* .add(<js>"zebra"</js>, 3)
* .add(<js>"apple"</js>, 1)
@@ -75,14 +73,14 @@ import org.apache.juneau.commons.utils.*;
* .sorted()
* .build(); <jc>// Returns TreeMap with natural key order</jc>
*
- * <jc>// Immutable map</jc>
+ * <jc>// Immutable map - returns Map</jc>
* Map<String,String> <jv>config</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, String.<jk>class</jk>)
* .add(<js>"env"</js>, <js>"prod"</js>)
* .add(<js>"region"</js>, <js>"us-west"</js>)
* .unmodifiable()
* .build();
*
- * <jc>// From multiple sources</jc>
+ * <jc>// From multiple sources - returns Map</jc>
* Map<String,Integer> <jv>existing</jv> = Map.of(<js>"a"</js>, 1,
<js>"b"</js>, 2);
* Map<String,Integer> <jv>combined</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
* .addAll(<jv>existing</jv>)
@@ -93,6 +91,18 @@ import org.apache.juneau.commons.utils.*;
* Map<String,String> <jv>maybeNull</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, String.<jk>class</jk>)
* .sparse()
* .build(); <jc>// Returns null, not empty map</jc>
+ *
+ * <jc>// FluentMap wrapper - use buildFluent()</jc>
+ * FluentMap<String,Integer> <jv>fluent</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .add(<js>"one"</js>, 1)
+ * .buildFluent();
+ *
+ * <jc>// FilteredMap - use buildFiltered()</jc>
+ * FilteredMap<String,Integer> <jv>filtered</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .filtered((k, v) -> v > 0)
+ * .add(<js>"a"</js>, 5)
+ * .add(<js>"b"</js>, -1) <jc>// Filtered out</jc>
+ * .buildFiltered();
* </p>
*
* <h5 class='section'>Thread Safety:</h5>
@@ -153,11 +163,12 @@ public class MapBuilder<K,V> {
private boolean unmodifiable = false, sparse = false;
private Comparator<K> comparator;
- private java.util.function.Predicate<Object> filter;
+ private BiPredicate<K,V> filter;
private Class<K> keyType;
private Class<V> valueType;
- private List<Converter> converters;
+ private Function<Object,K> keyFunction;
+ private Function<Object,V> valueFunction;
/**
* Constructor.
@@ -174,16 +185,13 @@ public class MapBuilder<K,V> {
* Adds a single entry to this map.
*
* <p>
- * If a filter has been set via {@link
#filtered(java.util.function.Predicate)} or {@link #filtered()},
- * the value will only be added if it passes the filter (i.e., the
filter returns {@code true}).
+ * Note: Filtering is applied at build time, not when adding entries.
*
* @param key The map key.
* @param value The map value.
* @return This object.
*/
public MapBuilder<K,V> add(K key, V value) {
- if (filter != null && ! filter.test(value))
- return this;
if (map == null)
map = new LinkedHashMap<>();
map.put(key, value);
@@ -197,44 +205,49 @@ public class MapBuilder<K,V> {
* This is a no-op if the value is <jk>null</jk>.
*
* <p>
- * If a filter has been set, each value will be filtered before being
added.
+ * Note: Filtering is applied at build time, not when adding entries.
*
* @param value The map to add to this map.
* @return This object.
*/
public MapBuilder<K,V> addAll(Map<K,V> value) {
if (nn(value)) {
- for (Map.Entry<K,V> entry : value.entrySet())
- add(entry.getKey(), entry.getValue());
+ if (map == null)
+ map = new LinkedHashMap<>();
+ map.putAll(value);
}
return this;
}
/**
- * Adds arbitrary values to this list.
+ * Adds arbitrary values to this map.
*
* <p>
* Objects can be any of the following:
* <ul>
* <li>Maps of key/value types convertible to the key/value types
of this map.
- * <li>JSON object strings parsed and convertible to the key/value
types of this map.
* </ul>
*
- * @param values The values to add.
+ * <p>
+ * Each entry from the maps will be added using {@link #add(Object,
Object)}, which applies
+ * key/value function conversion if configured. Non-Map objects will
cause a {@link RuntimeException} to be thrown.
+ *
+ * @param values The values to add. Can contain <jk>null</jk> values
(ignored).
* @return This object.
+ * @throws RuntimeException If a non-Map object is provided.
*/
@SuppressWarnings("unchecked")
public MapBuilder<K,V> addAny(Object...values) {
for (var o : values) {
if (nn(o)) {
if (o instanceof Map o2) {
- o2.forEach((k, v) ->
add(toType(keyType, k), toType(valueType, v)));
+ o2.forEach((k, v) -> {
+ K key = convertKey(k);
+ V value = convertValue(v);
+ add(key, value);
+ });
} else {
- var m = converters.stream().map(x ->
x.convertTo(Map.class, o)).filter(Utils::nn).findFirst().orElse(null);
- if (nn(m))
- addAny(m);
- else
- throw rex("Object of type {0}
could not be converted to type {1}", cn(o), "Map");
+ throw rex("Object of type {0} could not
be converted to type {1}", cn(o), "Map");
}
}
}
@@ -251,7 +264,7 @@ public class MapBuilder<K,V> {
public MapBuilder<K,V> addPairs(Object...pairs) {
assertArgNotNull("pairs", pairs);
if (pairs.length % 2 != 0)
- throw illegalArg("Odd number of parameters passed into
AMap.ofPairs()");
+ throw illegalArg("Odd number of parameters passed into
MapBuilder.addPairs(...)");
for (var i = 0; i < pairs.length; i += 2)
add((K)pairs[i], (V)pairs[i + 1]);
return this;
@@ -260,48 +273,155 @@ public class MapBuilder<K,V> {
/**
* Builds the map.
*
- * @return A map conforming to the settings on this builder.
- */
- /**
- * Builds the map.
+ * <p>
+ * Applies filtering, sorting, unmodifiable, and sparse options.
*
* <p>
- * Applies sorting/unmodifiable/sparse options.
+ * If filtering is applied, the result is wrapped in a {@link
FilteredMap}. If sorting is applied,
+ * a {@link TreeMap} is used as the underlying map; otherwise a {@link
LinkedHashMap} is used.
*
- * @return The built map or {@code null} if {@link #sparse()} is set
and the map is empty.
+ * @return The built map, or {@code null} if {@link #sparse()} is set
and the map is empty.
*/
public Map<K,V> build() {
if (sparse) {
if (nn(map) && map.isEmpty())
- map = null;
+ return null;
} else {
if (map == null)
map = new LinkedHashMap<>();
}
- if (nn(map)) {
- if (nn(comparator)) {
+
+ Map<K,V> result = map;
+
+ if (nn(result)) {
+ // Apply filtering if specified
+ if (nn(filter)) {
+ Map<K,V> innerMap = nn(comparator)
+ ? new TreeMap<>(comparator)
+ : new LinkedHashMap<>();
+
+ var filteredMap =
FilteredMap.<K,V>create(keyType, valueType)
+ .filter(filter)
+ .inner(innerMap)
+ .build();
+
+ // Add all entries to the filtered map
+ result.forEach(filteredMap::put);
+ result = filteredMap;
+ } else if (nn(comparator)) {
+ // Apply sorting if no filter
var m2 = new TreeMap<K,V>(comparator);
- m2.putAll(map);
- map = m2;
+ m2.putAll(result);
+ result = m2;
}
+
+ // Apply unmodifiable if specified
if (unmodifiable)
- map = Collections.unmodifiableMap(map);
+ result = Collections.unmodifiableMap(result);
}
- return map;
+
+ return result;
+ }
+
+ /**
+ * Builds the map and wraps it in a {@link FluentMap}.
+ *
+ * <p>
+ * This is a convenience method that calls {@link #build()} and wraps
the result in a {@link FluentMap}.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jk>import static</jk>
org.apache.juneau.commons.utils.CollectionUtils.*;
+ *
+ * FluentMap<String,Integer> <jv>map</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .add(<js>"one"</js>, 1)
+ * .add(<js>"two"</js>, 2)
+ * .buildFluent();
+ * </p>
+ *
+ * @return The built map wrapped in a {@link FluentMap}, or {@code
null} if {@link #sparse()} is set and the map is empty.
+ */
+ public FluentMap<K,V> buildFluent() {
+ Map<K,V> result = build();
+ return result == null ? null : new FluentMap<>(result);
}
/**
- * Registers value converters that can adapt incoming values in {@link
#addAny(Object...)}.
+ * Builds the map as a {@link FilteredMap}.
+ *
+ * <p>
+ * If sorting is applied, a {@link TreeMap} is used as the underlying
map; otherwise a {@link LinkedHashMap} is used.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jk>import static</jk>
org.apache.juneau.commons.utils.CollectionUtils.*;
*
- * @param values Converters to register. Ignored if {@code null}.
+ * FilteredMap<String,Integer> <jv>map</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .filtered((k, v) -> v != <jk>null</jk> && v
> 0)
+ * .add(<js>"a"</js>, 5)
+ * .add(<js>"b"</js>, -1) <jc>// Will be filtered out</jc>
+ * .buildFiltered();
+ * </p>
+ *
+ * <p>
+ * Note: If {@link #unmodifiable()} is set, the returned map will be
wrapped in an unmodifiable view,
+ * which may cause issues if the FilteredMap tries to modify it
internally. It's recommended to avoid
+ * using {@link #unmodifiable()} when calling this method.
+ *
+ * @return The built map as a {@link FilteredMap}, or {@code null} if
{@link #sparse()} is set and the map is empty.
+ */
+ public FilteredMap<K,V> buildFiltered() {
+ var m = build();
+ if (m == null) // sparse mode and empty
+ return null;
+ if (m instanceof FilteredMap<K,V> m2)
+ return m2;
+ // Note that if unmodifiable is true, 'm' will be unmodifiable
and will cause an error if you try
+ // to insert a value from within FilteredMap.
+ return FilteredMap.create(keyType, valueType).inner(m).build();
+ }
+
+ /**
+ * Sets the key conversion function for converting keys in {@link
#addAny(Object...)}.
+ *
+ * <p>
+ * The function is applied to each key when adding entries from maps in
{@link #addAny(Object...)}.
+ *
+ * @param keyFunction The function to convert keys. Must not be
<jk>null</jk>.
* @return This object.
*/
- public MapBuilder<K,V> converters(Converter...values) {
- if (values.length == 0)
- return this;
- if (converters == null)
- converters = list();
- converters.addAll(l(values));
+ public MapBuilder<K,V> keyFunction(Function<Object,K> keyFunction) {
+ this.keyFunction = assertArgNotNull("keyFunction", keyFunction);
+ return this;
+ }
+
+ /**
+ * Sets the value conversion function for converting values in {@link
#addAny(Object...)}.
+ *
+ * <p>
+ * The function is applied to each value when adding entries from maps
in {@link #addAny(Object...)}.
+ *
+ * @param valueFunction The function to convert values. Must not be
<jk>null</jk>.
+ * @return This object.
+ */
+ public MapBuilder<K,V> valueFunction(Function<Object,V> valueFunction) {
+ this.valueFunction = assertArgNotNull("valueFunction",
valueFunction);
+ return this;
+ }
+
+ /**
+ * Sets both key and value conversion functions.
+ *
+ * <p>
+ * Convenience method for setting both functions at once.
+ *
+ * @param keyFunction The function to convert keys. Must not be
<jk>null</jk>.
+ * @param valueFunction The function to convert values. Must not be
<jk>null</jk>.
+ * @return This object.
+ */
+ public MapBuilder<K,V> functions(Function<Object,K> keyFunction,
Function<Object,V> valueFunction) {
+ this.keyFunction = assertArgNotNull("keyFunction", keyFunction);
+ this.valueFunction = assertArgNotNull("valueFunction",
valueFunction);
return this;
}
@@ -334,12 +454,12 @@ public class MapBuilder<K,V> {
* <p class='bjava'>
* <jk>import static</jk>
org.apache.juneau.commons.utils.CollectionUtils.*;
*
- * Map<String,Object> <jv>map</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Object.<jk>class</jk>)
+ * FluentMap<String,Object> <jv>map</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Object.<jk>class</jk>)
* .filtered()
* .add(<js>"name"</js>, <js>"John"</js>)
- * .add(<js>"age"</js>, -1) <jc>// Not
added</jc>
- * .add(<js>"enabled"</js>, <jk>false</jk>) <jc>// Not
added</jc>
- * .add(<js>"tags"</js>, <jk>new</jk> String[0]) <jc>//
Not added</jc>
+ * .add(<js>"age"</js>, -1) <jc>// Filtered
out at build time</jc>
+ * .add(<js>"enabled"</js>, <jk>false</jk>) <jc>//
Filtered out at build time</jc>
+ * .add(<js>"tags"</js>, <jk>new</jk> String[0]) <jc>//
Filtered out at build time</jc>
* .build();
* </p>
*
@@ -347,22 +467,30 @@ public class MapBuilder<K,V> {
*/
public MapBuilder<K,V> filtered() {
// @formatter:off
- return filtered(x -> ! (
- x == null
- || (x instanceof Boolean x2 && x2.equals(false))
- || (x instanceof Number x3 && x3.intValue() == -1)
- || (isArray(x) && Array.getLength(x) == 0)
- || (x instanceof Map x2 && x2.isEmpty())
- || (x instanceof Collection x3 && x3.isEmpty())
+ return filtered((k, v) -> ! (
+ v == null
+ || (v instanceof Boolean v2 && v2.equals(false))
+ || (v instanceof Number v3 && v3.intValue() == -1)
+ || (isArray(v) && Array.getLength(v) == 0)
+ || (v instanceof Map v2 && v2.isEmpty())
+ || (v instanceof Collection v3 && v3.isEmpty())
));
// @formatter:on
}
/**
- * Applies a filter predicate to values added via {@link #add(Object,
Object)}.
+ * Applies a filter predicate to entries when building the map.
*
* <p>
- * Values where the predicate returns {@code true} will be kept; values
where it returns {@code false} will not be added.
+ * The filter receives both the key and value of each entry. Entries
where the predicate returns
+ * {@code true} will be kept; entries where it returns {@code false}
will be filtered out.
+ *
+ * <p>
+ * This method can be called multiple times. When called multiple
times, all filters are combined
+ * using AND logic - an entry must pass all filters to be kept in the
map.
+ *
+ * <p>
+ * Note: Filtering is applied at build time, not when adding entries.
*
* <h5 class='section'>Example:</h5>
* <p class='bjava'>
@@ -370,18 +498,32 @@ public class MapBuilder<K,V> {
*
* <jc>// Keep only non-null, non-empty string values</jc>
* Map<String,String> <jv>map</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, String.<jk>class</jk>)
- * .filtered(<jv>x</jv> -> <jv>x</jv> != <jk>null</jk>
&& !<jv>x</jv>.equals(<js>""</js>))
+ * .filtered((k, v) -> v != <jk>null</jk> &&
!v.equals(<js>""</js>))
* .add(<js>"a"</js>, <js>"foo"</js>)
- * .add(<js>"b"</js>, <jk>null</jk>) <jc>// Not
added</jc>
- * .add(<js>"c"</js>, <js>""</js>) <jc>// Not
added</jc>
+ * .add(<js>"b"</js>, <jk>null</jk>) <jc>// Filtered
out at build time</jc>
+ * .add(<js>"c"</js>, <js>""</js>) <jc>// Filtered
out at build time</jc>
+ * .build();
+ *
+ * <jc>// Multiple filters combined with AND</jc>
+ * Map<String,Integer> <jv>map2</jv> =
<jsm>mapb</jsm>(String.<jk>class</jk>, Integer.<jk>class</jk>)
+ * .filtered((k, v) -> v != <jk>null</jk>)
<jc>// First filter</jc>
+ * .filtered((k, v) -> v > 0)
<jc>// Second filter (ANDed with first)</jc>
+ * .filtered((k, v) -> ! k.startsWith(<js>"_"</js>))
<jc>// Third filter (ANDed with previous)</jc>
+ * .add(<js>"a"</js>, 5)
+ * .add(<js>"_b"</js>, 10) <jc>// Filtered out (starts
with "_")</jc>
+ * .add(<js>"c"</js>, -1) <jc>// Filtered out (not >
0)</jc>
* .build();
* </p>
*
* @param filter The filter predicate. Must not be <jk>null</jk>.
* @return This object.
*/
- public MapBuilder<K,V> filtered(java.util.function.Predicate<Object>
filter) {
- this.filter = assertArgNotNull("filter", filter);
+ public MapBuilder<K,V> filtered(BiPredicate<K,V> filter) {
+ BiPredicate<K,V> newFilter = assertArgNotNull("filter", filter);
+ if (this.filter == null)
+ this.filter = newFilter;
+ else
+ this.filter = this.filter.and(newFilter);
return this;
}
@@ -444,22 +586,32 @@ public class MapBuilder<K,V> {
}
/**
- * Converts an object to the specified type.
+ * Converts a key object to the key type.
*
- * @param <T> The type to convert to.
- * @param c The type to convert to.
* @param o The object to convert.
- * @return The converted object.
- * @throws RuntimeException If the object cannot be converted to the
specified type.
+ * @return The converted key.
*/
- private <T> T toType(Class<T> c, Object o) {
- if (c.isInstance(o))
- return c.cast(o);
- if (nn(converters)) {
- var e = converters.stream().map(x -> x.convertTo(c,
o)).filter(Utils::nn).findFirst().orElse(null);
- if (nn(e))
- return e;
- }
- throw rex("Object of type {0} could not be converted to type
{1}", cn(o), cn(c));
+ @SuppressWarnings("unchecked")
+ private K convertKey(Object o) {
+ if (keyType.isInstance(o))
+ return (K)o;
+ if (nn(keyFunction))
+ return keyFunction.apply(o);
+ throw rex("Object of type {0} could not be converted to key
type {1}", cn(o), cn(keyType));
+ }
+
+ /**
+ * Converts a value object to the value type.
+ *
+ * @param o The object to convert.
+ * @return The converted value.
+ */
+ @SuppressWarnings("unchecked")
+ private V convertValue(Object o) {
+ if (valueType.isInstance(o))
+ return (V)o;
+ if (nn(valueFunction))
+ return valueFunction.apply(o);
+ throw rex("Object of type {0} could not be converted to value
type {1}", cn(o), cn(valueType));
}
}
\ No newline at end of file
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
index 6bd5a35391..5e94677692 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
@@ -1567,8 +1567,8 @@ public class CollectionUtils {
* @param converters Optional converters to use for converting values.
* @return A new map builder.
*/
- public static <K,V> MapBuilder<K,V> mapb(Class<K> keyType, Class<V>
valueType, Converter...converters) {
- return MapBuilder.create(keyType,
valueType).converters(converters);
+ public static <K,V> MapBuilder<K,V> mapb(Class<K> keyType, Class<V>
valueType) {
+ return MapBuilder.create(keyType, valueType);
}
/**
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ConverterUtils.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ConverterUtils.java
index 11f018c1d7..88c1d04631 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ConverterUtils.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ConverterUtils.java
@@ -255,7 +255,10 @@ public class ConverterUtils {
* @return A new {@link MapBuilder} containing the converted entries.
*/
public static <K,V> MapBuilder<K,V> toMapBuilder(Object value, Class<K>
keyType, Class<V> valueType) {
- return mapb(keyType, valueType,
GenericConverter.INSTANCE).addAny(value);
+ return mapb(keyType, valueType)
+ .keyFunction(o ->
GenericConverter.INSTANCE.convertTo(keyType, o))
+ .valueFunction(o ->
GenericConverter.INSTANCE.convertTo(valueType, o))
+ .addAny(value);
}
/**
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/BidiMap_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/BidiMap_Test.java
index dade9ee646..825ff9d9aa 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/BidiMap_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/BidiMap_Test.java
@@ -650,7 +650,6 @@ class BidiMap_Test extends TestBase {
.add("one", 1)
.build();
- assertFalse(map.equals("not a map"));
assertFalse(map.equals(null));
}
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
index 34a2e3df41..5b700bff7a 100644
---
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
@@ -164,7 +164,7 @@ class FilteredMap_Test extends TestBase {
var map = FilteredMap
.create(String.class, Integer.class)
.filter((k, v) -> v != null && v > 0)
- .creator(() -> new TreeMap<>())
+ .inner(new TreeMap<>())
.build();
map.put("zebra", 3);
@@ -185,7 +185,7 @@ class FilteredMap_Test extends TestBase {
var map = FilteredMap
.create(String.class, String.class)
.filter((k, v) -> v != null)
- .creator(() -> new ConcurrentHashMap<>())
+ .inner(new ConcurrentHashMap<>())
.build();
map.put("key1", "value1");
@@ -420,10 +420,18 @@ class FilteredMap_Test extends TestBase {
//====================================================================================================
@Test
- void i01_builder_noFilter_throwsException() {
- assertThrowsWithMessage(IllegalArgumentException.class,
"filter", () -> {
- FilteredMap.create(String.class, String.class).build();
- });
+ void i01_builder_noFilter_acceptsAllEntries() {
+ // Filter is optional - defaults to (k,v) -> true (accepts all
entries)
+ var map = FilteredMap.create(String.class, String.class)
+ .build();
+
+ assertNotNull(map);
+ map.put("key1", "value1");
+ map.put("key2", null); // Should be accepted (no filter)
+ map.put("key3", "value3");
+
+ assertSize(3, map);
+ assertMap(map, "key1=value1", "key2=<null>", "key3=value3");
}
@Test
@@ -434,11 +442,11 @@ class FilteredMap_Test extends TestBase {
}
@Test
- void i03_builder_nullCreator_throwsException() {
+ void i03_builder_nullInner_throwsException() {
assertThrowsWithMessage(IllegalArgumentException.class,
"value", () -> {
FilteredMap.create(String.class, String.class)
.filter((k, v) -> true)
- .creator(null);
+ .inner(null);
});
}
@@ -907,7 +915,10 @@ class FilteredMap_Test extends TestBase {
var retrievedFilter = map.getFilter();
assertNotNull(retrievedFilter);
- assertSame(originalFilter, retrievedFilter); // Should be the
same instance
+ // The filter may be combined with the default filter, so test
behavior instead of instance equality
+ assertTrue(retrievedFilter.test("key1", 5)); // Should accept
positive values
+ assertFalse(retrievedFilter.test("key2", -1)); // Should reject
negative values
+ assertFalse(retrievedFilter.test("key3", null)); // Should
reject null values
}
@Test
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Flag_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Flag_Test.java
index 82a6fdf144..0061eb2a8d 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Flag_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Flag_Test.java
@@ -281,7 +281,6 @@ class Flag_Test extends TestBase {
@Test
void d06_equals_notAFlag() {
var flag = Flag.of(true);
- assertFalse(flag.equals("not a flag"));
assertFalse(flag.equals(null));
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/KeywordSet_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/KeywordSet_Test.java
index a23f342655..938daf601e 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/KeywordSet_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/KeywordSet_Test.java
@@ -112,7 +112,6 @@ class KeywordSet_Test extends TestBase {
void w08_equals_notAKeywordSet() {
var ks = new KeywordSet("apple", "banana");
- assertFalse(ks.equals("not a keyword set"));
assertFalse(ks.equals(null));
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MapBuilder_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MapBuilder_Test.java
index 01c7d32bf2..f78cb25392 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MapBuilder_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/MapBuilder_Test.java
@@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import org.apache.juneau.*;
-import org.apache.juneau.commons.utils.*;
import org.junit.jupiter.api.*;
class MapBuilder_Test extends TestBase {
@@ -353,7 +352,7 @@ class MapBuilder_Test extends TestBase {
@Test
void i01_filtered_customPredicate() {
var map = MapBuilder.create(String.class, String.class)
- .filtered(x -> x != null && !x.equals(""))
+ .filtered((k, v) -> v != null && !v.equals(""))
.add("a", "foo")
.add("b", null) // Not added
.add("c", "") // Not added
@@ -382,7 +381,7 @@ class MapBuilder_Test extends TestBase {
@Test
void i03_filtered_rejectsValue() {
var map = MapBuilder.create(String.class, Integer.class)
- .filtered(x -> x != null && (Integer)x > 0)
+ .filtered((k, v) -> v != null && v > 0)
.add("a", 1)
.add("b", -1) // Not added
.add("c", 0) // Not added
@@ -395,7 +394,7 @@ class MapBuilder_Test extends TestBase {
@Test
void i04_add_withFilter() {
var map = MapBuilder.create(String.class, String.class)
- .filtered(x -> x != null && x instanceof String &&
((String)x).length() > 2)
+ .filtered((k, v) -> v != null && v.length() > 2)
.add("a", "foo") // Added
.add("b", "ab") // Not added (length <= 2)
.add("c", "bar") // Added
@@ -412,7 +411,7 @@ class MapBuilder_Test extends TestBase {
existing.put("z", "another");
var map = MapBuilder.create(String.class, String.class)
- .filtered(x -> x != null && x instanceof String &&
((String)x).length() > 2)
+ .filtered((k, v) -> v != null && v.length() > 2)
.addAll(existing)
.build();
@@ -420,13 +419,12 @@ class MapBuilder_Test extends TestBase {
}
//-----------------------------------------------------------------------------------------------------------------
- // Converters
+ // Key/Value Functions
//-----------------------------------------------------------------------------------------------------------------
@Test
- void j01_converters_emptyArray() {
+ void j01_keyValueFunctions_notSet() {
var map = MapBuilder.create(String.class, Integer.class)
- .converters() // Empty array
.add("a", 1)
.build();
@@ -434,23 +432,13 @@ class MapBuilder_Test extends TestBase {
}
@Test
- void j02_converters_withConverter() {
- Converter converter = new Converter() {
- @Override
- public <T> T convertTo(Class<T> type, Object o) {
- if (type == Integer.class && o instanceof
String) {
- return
type.cast(Integer.parseInt((String)o));
- }
- return null;
- }
- };
-
+ void j02_keyValueFunctions_withValueFunction() {
var inputMap = new LinkedHashMap<String,String>();
inputMap.put("a", "1");
inputMap.put("b", "2");
var map = MapBuilder.create(String.class, Integer.class)
- .converters(converter)
+ .valueFunction(o -> o instanceof String ?
Integer.parseInt((String)o) : (Integer)o)
.addAny(inputMap)
.build();
@@ -458,33 +446,35 @@ class MapBuilder_Test extends TestBase {
}
@Test
- void j03_converters_multipleConverters() {
- Converter converter1 = new Converter() {
- @Override
- public <T> T convertTo(Class<T> type, Object o) {
- return null; // Doesn't handle this
- }
- };
-
- Converter converter2 = new Converter() {
- @Override
- public <T> T convertTo(Class<T> type, Object o) {
- if (type == Integer.class && o instanceof
String) {
- return
type.cast(Integer.parseInt((String)o));
- }
- return null;
- }
- };
+ void j03_keyValueFunctions_withBothFunctions() {
+ var inputMap = new LinkedHashMap<Integer,String>();
+ inputMap.put(1, "10");
+ inputMap.put(2, "20");
- var inputMap = new LinkedHashMap<String,String>();
- inputMap.put("a", "1");
+ var map = MapBuilder.create(String.class, Integer.class)
+ .keyFunction(o -> String.valueOf(o))
+ .valueFunction(o -> o instanceof String ?
Integer.parseInt((String)o) : (Integer)o)
+ .addAny(inputMap)
+ .build();
+
+ assertMap(map, "1=10", "2=20");
+ }
+
+ @Test
+ void j04_keyValueFunctions_functionsConvenienceMethod() {
+ var inputMap = new LinkedHashMap<Integer,String>();
+ inputMap.put(1, "10");
+ inputMap.put(2, "20");
var map = MapBuilder.create(String.class, Integer.class)
- .converters(converter1, converter2)
+ .functions(
+ o -> String.valueOf(o), // key function
+ o -> o instanceof String ?
Integer.parseInt((String)o) : (Integer)o // value function
+ )
.addAny(inputMap)
.build();
- assertMap(map, "a=1");
+ assertMap(map, "1=10", "2=20");
}
//-----------------------------------------------------------------------------------------------------------------
@@ -550,25 +540,13 @@ class MapBuilder_Test extends TestBase {
@Test
void k04_addAny_withTypeConversion() {
- var converter = new Converter() {
- @Override
- public <T> T convertTo(Class<T> type, Object o) {
- if (type == String.class && o instanceof
Integer) {
- return type.cast(String.valueOf(o));
- }
- if (type == Integer.class && o instanceof
String) {
- return
type.cast(Integer.parseInt((String)o));
- }
- return null;
- }
- };
-
var inputMap = new LinkedHashMap<Integer,String>();
inputMap.put(1, "10");
inputMap.put(2, "20");
var map = MapBuilder.create(String.class, Integer.class)
- .converters(converter)
+ .keyFunction(o -> String.valueOf(o))
+ .valueFunction(o -> o instanceof String ?
Integer.parseInt((String)o) : (Integer)o)
.addAny(inputMap)
.build();
@@ -576,57 +554,33 @@ class MapBuilder_Test extends TestBase {
}
@Test
- void k05_addAny_withConverterToMap() {
- var converter = new Converter() {
- @Override
- public <T> T convertTo(Class<T> type, Object o) {
- if (type == Map.class && o instanceof String) {
- var m = new
LinkedHashMap<String,String>();
- // Simple parsing:
"key1=value1,key2=value2"
- var s = (String)o;
- for (var pair : s.split(",")) {
- var kv = pair.split("=");
- if (kv.length == 2) {
- m.put(kv[0], kv[1]);
- }
- }
- return type.cast(m);
- }
- return null;
+ void k05_addAny_withStringToMapConversion() {
+ // This test is no longer applicable since addAny only accepts
Map instances
+ // If we want to parse strings, we'd need to do it before
calling addAny
+ var parsedMap = new LinkedHashMap<String,String>();
+ for (var pair : "a=1,b=2".split(",")) {
+ var kv = pair.split("=");
+ if (kv.length == 2) {
+ parsedMap.put(kv[0], kv[1]);
}
- };
+ }
var map = MapBuilder.create(String.class, String.class)
- .converters(converter)
- .addAny("a=1,b=2")
+ .addAny(parsedMap)
.build();
assertMap(map, "a=1", "b=2");
}
@Test
- void k06_addAny_withConverterToMap_recursive() {
- var converter = new Converter() {
- @Override
- public <T> T convertTo(Class<T> type, Object o) {
- if (type == Map.class && o instanceof String) {
- var m = new
LinkedHashMap<String,String>();
- var s = (String)o;
- for (var pair : s.split(",")) {
- var kv = pair.split("=");
- if (kv.length == 2) {
- m.put(kv[0], kv[1]);
- }
- }
- return type.cast(m);
- }
- return null;
- }
- };
+ void k06_addAny_withMultipleMaps_parsedFromStrings() {
+ var map1 = new LinkedHashMap<String,String>();
+ map1.put("x", "foo");
+ var map2 = new LinkedHashMap<String,String>();
+ map2.put("y", "bar");
var map = MapBuilder.create(String.class, String.class)
- .converters(converter)
- .addAny("x=foo", "y=bar")
+ .addAny(map1, map2)
.build();
assertMap(map, "x=foo", "y=bar");
@@ -640,9 +594,9 @@ class MapBuilder_Test extends TestBase {
}
@Test
- void k08_addAny_conversionFailure() {
- // When converters is null and we try to add a non-Map, it will
throw NPE
- assertThrows(NullPointerException.class, () -> {
+ void k08_addAny_nonMapObject() {
+ // addAny only accepts Map instances, non-Map objects throw
exception
+ assertThrows(RuntimeException.class, () -> {
MapBuilder.create(String.class, Integer.class)
.addAny("not-a-map")
.build();
@@ -650,39 +604,40 @@ class MapBuilder_Test extends TestBase {
}
@Test
- void k09_addAny_converterReturnsNull() {
- // Converter exists but returns null (can't convert)
- var converter = new Converter() {
- @Override
- public <T> T convertTo(Class<T> type, Object o) {
- return null; // Can't convert
- }
- };
+ void k09_addAny_valueFunctionReturnsNull() {
+ // Value function that can't convert should throw exception
+ var inputMap = new LinkedHashMap<String,String>();
+ inputMap.put("a", "not-an-integer");
- // Should throw RuntimeException when converter can't convert
non-Map object
assertThrows(RuntimeException.class, () -> {
MapBuilder.create(String.class, Integer.class)
- .converters(converter)
- .addAny("not-a-map")
+ .valueFunction(o -> {
+ if (o instanceof String) {
+ try {
+ return
Integer.parseInt((String)o);
+ } catch (NumberFormatException
e) {
+ throw new
RuntimeException("Cannot convert", e);
+ }
+ }
+ return (Integer)o;
+ })
+ .addAny(inputMap)
.build();
});
}
@Test
- void k10_addAny_toType_conversionFailure() {
- var converter = new Converter() {
- @Override
- public <T> T convertTo(Class<T> type, Object o) {
- return null; // Can't convert
- }
- };
-
- var inputMap = new LinkedHashMap<String,String>();
- inputMap.put("a", "not-an-integer");
+ void k10_addAny_keyFunctionConversionFailure() {
+ var inputMap = new LinkedHashMap<Object,String>();
+ inputMap.put(new Object(), "value"); // Object can't be
converted to String
assertThrows(RuntimeException.class, () -> {
- MapBuilder.create(String.class, Integer.class)
- .converters(converter)
+ MapBuilder.create(String.class, String.class)
+ .keyFunction(o -> {
+ if (o instanceof String)
+ return (String)o;
+ throw new RuntimeException("Cannot
convert key");
+ })
.addAny(inputMap)
.build();
});
@@ -742,4 +697,112 @@ class MapBuilder_Test extends TestBase {
assertNotNull(map);
assertThrows(UnsupportedOperationException.class, () ->
map.put("a", 1));
}
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // BuildFluent
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test
+ void m01_buildFluent_returnsFluentMap() {
+ var map = MapBuilder.create(String.class, Integer.class)
+ .add("a", 1)
+ .add("b", 2)
+ .buildFluent();
+
+ assertNotNull(map);
+ assertSize(2, map);
+ assertMap(map, "a=1", "b=2");
+ }
+
+ @Test
+ void m02_buildFluent_sparseEmpty() {
+ var map = MapBuilder.create(String.class, Integer.class)
+ .sparse()
+ .buildFluent();
+
+ assertNull(map);
+ }
+
+ @Test
+ void m03_buildFluent_withFiltering() {
+ var map = MapBuilder.create(String.class, Integer.class)
+ .filtered((k, v) -> v != null && v > 0)
+ .add("a", 1)
+ .add("b", -1) // Filtered out
+ .buildFluent();
+
+ assertNotNull(map);
+ assertSize(1, map);
+ assertMap(map, "a=1");
+ }
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // BuildFiltered
+
//-----------------------------------------------------------------------------------------------------------------
+
+ @Test
+ void n01_buildFiltered_withFilter() {
+ var map = MapBuilder.create(String.class, Integer.class)
+ .filtered((k, v) -> v != null && v > 0)
+ .add("a", 1)
+ .add("b", -1) // Filtered out
+ .add("c", 2)
+ .buildFiltered();
+
+ assertNotNull(map);
+ assertSize(2, map);
+ assertMap(map, "a=1", "c=2");
+ }
+
+ @Test
+ void n02_buildFiltered_withoutFilter() {
+ // buildFiltered() should work even without explicit filter
(uses default filter)
+ var map = MapBuilder.create(String.class, Integer.class)
+ .add("a", 1)
+ .add("b", 2)
+ .buildFiltered();
+
+ assertNotNull(map);
+ assertSize(2, map);
+ }
+
+ @Test
+ void n03_buildFiltered_sparseEmpty() {
+ var map = MapBuilder.create(String.class, Integer.class)
+ .filtered((k, v) -> v != null && v > 0)
+ .sparse()
+ .buildFiltered();
+
+ assertNull(map);
+ }
+
+ @Test
+ void n04_buildFiltered_withSorted() {
+ var map = MapBuilder.create(String.class, Integer.class)
+ .filtered((k, v) -> v != null && v > 0)
+ .add("c", 3)
+ .add("a", 1)
+ .add("b", 2)
+ .sorted()
+ .buildFiltered();
+
+ assertNotNull(map);
+ assertList(map.keySet(), "a", "b", "c");
+ }
+
+ @Test
+ void n05_buildFiltered_multipleFilters() {
+ var map = MapBuilder.create(String.class, Integer.class)
+ .filtered((k, v) -> v != null)
+ .filtered((k, v) -> v > 0)
+ .filtered((k, v) -> !k.startsWith("_"))
+ .add("a", 1)
+ .add("_b", 2) // Filtered out (starts with _)
+ .add("c", -1) // Filtered out (not > 0)
+ .add("d", 3)
+ .buildFiltered();
+
+ assertNotNull(map);
+ assertMap(map, "a=1", "d=3");
+ }
}
\ 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 4704db7f84..94fee985f3 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
@@ -593,8 +593,6 @@ class MultiList_Test extends TestBase {
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));
}
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
index 9e42df341e..bf379f2263 100644
---
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
@@ -488,7 +488,6 @@ class MultiMap_Test extends TestBase {
var map1 = map("key1", "value1");
var multiMap = new MultiMap<>(map1);
- assertFalse(multiMap.equals("not a map"));
assertFalse(multiMap.equals(null));
}
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 4a2f2255a2..9dae3e02b6 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
@@ -283,7 +283,6 @@ class MultiSet_Test extends TestBase {
var l1 = l(a("1", "2"));
var multiSet = new MultiSet<>(l1);
- assertFalse(multiSet.equals("not a set"));
assertFalse(multiSet.equals(null));
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/ReversedList_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/ReversedList_Test.java
index 7b83ea7cde..c0a277ff7f 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/ReversedList_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/ReversedList_Test.java
@@ -567,7 +567,6 @@ class ReversedList_Test extends TestBase {
var original = new ArrayList<>(List.of("a", "b", "c"));
var reversed = new ReversedList<>(original);
- assertFalse(reversed.equals("not a list"));
assertFalse(reversed.equals(null));
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/SimpleMap_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/SimpleMap_Test.java
index c0918ca342..fcfd4013f4 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/SimpleMap_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/SimpleMap_Test.java
@@ -386,7 +386,6 @@ class SimpleMap_Test extends TestBase {
Object[] values = { "value1" };
SimpleMap<String,Object> map = new SimpleMap<>(keys, values);
- assertFalse(map.equals("not a map"));
assertFalse(map.equals(null));
}