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 65e0baba85 org.apache.juneau.common.reflect API improvements
65e0baba85 is described below
commit 65e0baba8521fc28729a75774d54687bd07fa1c7
Author: James Bognar <[email protected]>
AuthorDate: Thu Nov 20 16:11:52 2025 -0500
org.apache.juneau.common.reflect API improvements
---
.../juneau/junit/bct/BasicBeanConverter.java | 112 +++++++++++++++++++--
.../org/apache/juneau/junit/bct/BctAssertions.java | 48 +--------
.../org/apache/juneau/junit/bct/BeanConverter.java | 14 +++
.../java/org/apache/juneau/junit/bct/Sizer.java | 58 +++++++++++
.../juneau/junit/bct/AssertionArgs_Test.java | 34 +++++--
.../juneau/junit/bct/BctAssertions_Test.java | 4 +-
6 files changed, 207 insertions(+), 63 deletions(-)
diff --git
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BasicBeanConverter.java
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BasicBeanConverter.java
index 4ddc6c1fce..e018f8921c 100644
---
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BasicBeanConverter.java
+++
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BasicBeanConverter.java
@@ -30,6 +30,7 @@ import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
+import org.apache.juneau.common.reflect.*;
import org.apache.juneau.common.utils.*;
/**
@@ -273,6 +274,7 @@ public class BasicBeanConverter implements BeanConverter {
private Map<String,Object> settings = map();
private List<StringifierEntry<?>> stringifiers = list();
private List<ListifierEntry<?>> listifiers = list();
+ private List<SizerEntry<?>> sizers = list();
private List<SwapperEntry<?>> swappers = list();
private List<PropertyExtractor> propertyExtractors = list();
@@ -289,6 +291,20 @@ public class BasicBeanConverter implements BeanConverter {
*/
public <T> Builder addListifier(Class<T> c, Listifier<T> l) {
listifiers.add(new ListifierEntry<>(c, l)); return this; }
+ /**
+ * Registers a custom sizer for a specific type.
+ *
+ * <p>Sizers compute the size of collection-like objects for
test assertions.
+ * The function receives the object to size and the converter
instance for accessing
+ * additional utilities if needed.</p>
+ *
+ * @param <T> The type to handle
+ * @param c The class to register the sizer for
+ * @param s The sizing function
+ * @return This builder for method chaining
+ */
+ public <T> Builder addSizer(Class<T> c, Sizer<T> s) {
sizers.add(new SizerEntry<>(c, s)); return this; }
+
/**
* Registers a custom property extractor for specialized
property access logic.
*
@@ -494,6 +510,16 @@ public class BasicBeanConverter implements BeanConverter {
}
}
+ static class SizerEntry<T> {
+ private Class<T> forClass;
+ private Sizer<T> function;
+
+ private SizerEntry(Class<T> forClass, Sizer<T> function) {
+ this.forClass = forClass;
+ this.function = function;
+ }
+ }
+
/**
* Default converter instance with standard settings and handlers.
*
@@ -677,6 +703,7 @@ public class BasicBeanConverter implements BeanConverter {
private final List<StringifierEntry<?>> stringifiers;
private final List<ListifierEntry<?>> listifiers;
+ private final List<SizerEntry<?>> sizers;
private final List<SwapperEntry<?>> swappers;
private final List<PropertyExtractor> propertyExtractors;
@@ -687,11 +714,14 @@ public class BasicBeanConverter implements BeanConverter {
private final ConcurrentHashMap<Class,Optional<Listifier<?>>>
listifierMap = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<Class,Optional<Sizer<?>>> sizerMap =
new ConcurrentHashMap<>();
+
private final ConcurrentHashMap<Class,Optional<Swapper<?>>> swapperMap
= new ConcurrentHashMap<>();
protected BasicBeanConverter(Builder b) {
stringifiers = copyOf(b.stringifiers);
listifiers = copyOf(b.listifiers);
+ sizers = copyOf(b.sizers);
swappers = copyOf(b.swappers);
propertyExtractors = copyOf(b.propertyExtractors);
settings = copyOf(b.settings);
@@ -741,7 +771,10 @@ public class BasicBeanConverter implements BeanConverter {
@Override
public Object getProperty(Object object, String name) {
var o = swap(object);
- return propertyExtractors.stream().filter(x ->
x.canExtract(this, o, name)).findFirst()
+ return propertyExtractors
+ .stream()
+ .filter(x -> x.canExtract(this, o, name))
+ .findFirst()
.orElseThrow(() -> runtimeException("Could not find
extractor for object of type {0}", cn(o))).extract(this, o, name);
}
@@ -755,23 +788,77 @@ public class BasicBeanConverter implements BeanConverter {
@SuppressWarnings("unchecked")
public List<Object> listify(Object o) {
assertArgNotNull("o", o);
+
o = swap(o);
- if (o instanceof List)
- return (List<Object>)o;
+
+ if (o instanceof List) return (List<Object>)o;
+ if (o.getClass().isArray()) return arrayToList(o);
+
var c = o.getClass();
- if (c.isArray())
- return arrayToList(o);
var o2 = o;
- return listifierMap.computeIfAbsent(c,
this::findListifier).map(x -> (Listifier)x).map(x ->
(List<Object>)x.apply(this, o2))
+ return listifierMap
+ .computeIfAbsent(c, this::findListifier)
+ .map(x -> (Listifier)x)
+ .map(x -> (List<Object>)x.apply(this, o2))
.orElseThrow(() -> illegalArg("Object of type {0} could
not be converted to a list.", scn(o2)));
}
+ @Override
+ @SuppressWarnings("unchecked")
+ public int size(Object o) {
+ assertArgNotNull("o", o);
+
+ // Checks for Optional before unpacking.
+ if (o instanceof Optional) return ((Optional)o).isEmpty() ? 0 :
1;
+
+ o = swap(o);
+
+ // Check standard object types.
+ if (o == null) return 0;
+ if (o instanceof Collection) return ((Collection<?>)o).size();
+ if (o instanceof Map) return ((Map<?,?>)o).size();
+ if (o.getClass().isArray()) return Array.getLength(o);
+ if (o instanceof String) return ((String)o).length();
+
+ // Check for registered custom Sizer
+ var c = o.getClass();
+ var o2 = o;
+ var sizer = sizerMap.computeIfAbsent(c, this::findSizer);
+ if (sizer.isPresent()) return ((Sizer)sizer.get()).size(o2,
this);
+
+ // Try to find size() or length() method via reflection
+ var sizeResult = ClassInfo.of(c).getPublicMethods().stream()
+ .filter(m -> ! m.hasParameters())
+ .filter(m -> m.hasAnyName("size", "length"))
+ .filter(m -> m.getReturnType().isAny(int.class,
Integer.class))
+ .findFirst()
+ .map(m -> safe(() -> (int) m.invoke(o2)))
+ .filter(Objects::nonNull);
+ if (sizeResult.isPresent()) return sizeResult.get();
+
+ // Fall back to listify
+ if (canListify(o)) return listify(o).size();
+
+ // Try to find isEmpty() method via reflection
+ var isEmpty = ClassInfo.of(o).getPublicMethods().stream()
+ .filter(m -> ! m.hasParameters())
+ .filter(m -> m.hasName("isEmpty"))
+ .filter(m -> m.getReturnType().isAny(boolean.class,
Boolean.class))
+ .map(m -> safe(() -> (Boolean)m.invoke(o2)))
+ .findFirst();
+ if (isEmpty.isPresent()) return isEmpty.get() ? 0 : 1;
+
+ throw illegalArg("Object of type {0} does not have a
determinable size.", scn(o));
+ }
+
@Override
@SuppressWarnings("unchecked")
public String stringify(Object o) {
+
o = swap(o);
- if (o == null)
- return getSetting(SETTING_nullValue, null);
+
+ if (o == null) return getSetting(SETTING_nullValue, null);
+
var c = o.getClass();
var stringifier = stringifierMap.computeIfAbsent(c,
this::findStringifier);
if (stringifier.isEmpty()) {
@@ -856,6 +943,15 @@ public class BasicBeanConverter implements BeanConverter {
return findListifier(c.getSuperclass());
}
+ private Optional<Sizer<?>> findSizer(Class<?> c) {
+ if (c == null)
+ return empty();
+ var s = sizers.stream().filter(x ->
x.forClass.isAssignableFrom(c)).findFirst().orElse(null);
+ if (nn(s))
+ return of(s.function);
+ return findSizer(c.getSuperclass());
+ }
+
private Optional<Stringifier<?>> findStringifier(Class<?> c) {
if (c == null)
return empty();
diff --git
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
index cf8dbc1e33..d495be29bf 100644
---
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
+++
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
@@ -23,12 +23,10 @@ import static org.apache.juneau.common.utils.Utils.*;
import static org.apache.juneau.junit.bct.BctUtils.*;
import static org.junit.jupiter.api.Assertions.*;
-import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
-import org.apache.juneau.common.collections.*;
import org.apache.juneau.common.utils.*;
import org.opentest4j.*;
@@ -603,24 +601,8 @@ public class BctAssertions {
public static void assertEmpty(AssertionArgs args, Object value) {
assertArgNotNull("args", args);
assertNotNull(value, "Value was null.");
-
- if (value instanceof String s) {
- assertTrue(s.isEmpty(), args.getMessage("String was not
empty. value: <{0}>", s));
- } else if (value instanceof Optional<?> v2) {
- assertTrue(v2.isEmpty(), args.getMessage("Optional was
not empty"));
- } else if (value instanceof Value<?> v2) {
- assertTrue(v2.isEmpty(), args.getMessage("Value was not
empty"));
- } else if (value instanceof Map<?,?> v2) {
- assertTrue(v2.isEmpty(), args.getMessage("Map was not
empty"));
- } else if (value instanceof Collection<?> v2) {
- assertTrue(v2.isEmpty(), args.getMessage("Collection
was not empty"));
- } else if (value.getClass().isArray()) {
- assertEquals(0, Array.getLength(value),
args.getMessage("Array was not empty."));
- } else {
- var converter =
args.getBeanConverter().orElse(DEFAULT_CONVERTER);
- assertTrue(converter.canListify(value),
args.getMessage("Value cannot be converted to a list. Class=<{0}>",
scn(value)));
- assertTrue(converter.listify(value).isEmpty(),
args.getMessage("Value was not empty."));
- }
+ var size =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).size(value);
+ assertEquals(0, size, args.getMessage("Value was not empty.
Size=<{0}>", size));
}
/**
@@ -982,22 +964,8 @@ public class BctAssertions {
public static void assertNotEmpty(AssertionArgs args, Object value) {
assertArgNotNull("args", args);
assertNotNull(value, "Value was null.");
-
- if (value instanceof String s) {
- assertFalse(s.isEmpty(), args.getMessage("String was
empty."));
- } else if (value instanceof Optional<?> v2) {
- assertFalse(v2.isEmpty(), args.getMessage("Optional was
empty"));
- } else if (value instanceof Value<?> v2) {
- assertFalse(v2.isEmpty(), args.getMessage("Value was
empty"));
- } else if (value instanceof Map<?,?> v2) {
- assertFalse(v2.isEmpty(), args.getMessage("Map was
empty"));
- } else if (value instanceof Collection<?> v2) {
- assertFalse(v2.isEmpty(), args.getMessage("Collection
was empty"));
- } else if (value.getClass().isArray()) {
- assertTrue(Array.getLength(value) > 0,
args.getMessage("Array was empty."));
- } else {
-
assertFalse(args.getBeanConverter().orElse(DEFAULT_CONVERTER).listify(value).isEmpty(),
args.getMessage("Value was empty."));
- }
+ var size =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).size(value);
+ assertTrue(size > 0, args.getMessage("Value was empty."));
}
/**
@@ -1061,13 +1029,7 @@ public class BctAssertions {
public static void assertSize(AssertionArgs args, int expected, Object
actual) {
assertArgNotNull("args", args);
assertNotNull(actual, "Value was null.");
-
- if (actual instanceof String a) {
- assertEquals(expected, a.length(),
args.getMessage("Value not expected size. value: <{0}>", a));
- return;
- }
-
- var size =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).listify(actual).size();
+ var size =
args.getBeanConverter().orElse(DEFAULT_CONVERTER).size(actual);
assertEquals(expected, size, args.getMessage("Value not
expected size."));
}
diff --git
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BeanConverter.java
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BeanConverter.java
index 4aeae581b7..9d3b6bfb22 100644
---
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BeanConverter.java
+++
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BeanConverter.java
@@ -105,6 +105,20 @@ public interface BeanConverter {
*/
List<Object> listify(Object o);
+ /**
+ * Computes the size of an object.
+ *
+ * <p>
+ * This method determines the size of collection-like objects for test
assertions.
+ * The size is computed based on registered {@link Sizer}
implementations, with
+ * built-in support for collections, maps, arrays, and strings.
+ *
+ * @param o The object to compute the size of. Must not be
<jk>null</jk>.
+ * @return The size of the object.
+ * @throws IllegalArgumentException if the object's size cannot be
determined.
+ */
+ int size(Object o);
+
/**
* Converts an object to its string representation for testing purposes.
*
diff --git
a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Sizer.java
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Sizer.java
new file mode 100644
index 0000000000..7a030afdce
--- /dev/null
+++
b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Sizer.java
@@ -0,0 +1,58 @@
+/*
+ * 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.junit.bct;
+
+/**
+ * Functional interface for computing the size of objects in test assertions.
+ *
+ * <p>
+ * Sizers are used by {@link BeanConverter#size(Object)} to determine the size
of collection-like
+ * objects when validating test assertions. This provides a flexible way to
compute sizes for custom
+ * types without needing to convert them to lists first.
+ *
+ * <h5 class='section'>Usage Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Register a sizer for a custom collection type</jc>
+ * <jk>var</jk> <jv>converter</jv> =
BasicBeanConverter.<jsm>builder</jsm>()
+ * .defaultSettings()
+ * .addSizer(MyCustomCollection.<jk>class</jk>, (<jp>coll</jp>,
<jp>conv</jp>) -> <jp>coll</jp>.count())
+ * .build();
+ *
+ * <jc>// Use in assertions</jc>
+ * <jk>int</jk> <jv>size</jv> =
<jv>converter</jv>.size(<jv>myCustomCollection</jv>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul class='seealso'>
+ * <li class='jic'>{@link BeanConverter}
+ * </ul>
+ *
+ * @param <T> The type of object this sizer handles.
+ */
+@FunctionalInterface
+public interface Sizer<T> {
+
+ /**
+ * Computes the size of the given object.
+ *
+ * @param o The object to compute the size of. Will not be
<jk>null</jk>.
+ * @param bc The bean converter for accessing additional conversion
utilities if needed.
+ * @return The size of the object.
+ */
+ int size(T o, BeanConverter bc);
+}
+
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/AssertionArgs_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/AssertionArgs_Test.java
index d187215fb4..62e09e811a 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/AssertionArgs_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/AssertionArgs_Test.java
@@ -421,11 +421,18 @@ class AssertionArgs_Test extends TestBase {
return String.valueOf(o);
}
- @Override
- public List<Object> listify(Object o) {
- if (o instanceof List) return (List<Object>) o;
- return l(o);
- }
+ @Override
+ public List<Object> listify(Object o) {
+ if (o instanceof List) return (List<Object>) o;
+ return l(o);
+ }
+
+ @Override
+ public int size(Object o) {
+ if (o instanceof List) return ((List<?>)
o).size();
+ if (o instanceof String) return ((String)
o).length();
+ return 1;
+ }
@Override
public boolean canListify(Object o) {
@@ -473,11 +480,18 @@ class AssertionArgs_Test extends TestBase {
return String.valueOf(o);
}
- @Override
- public List<Object> listify(Object o) {
- if (o instanceof List) return (List<Object>) o;
- return l(o);
- }
+ @Override
+ public List<Object> listify(Object o) {
+ if (o instanceof List) return (List<Object>) o;
+ return l(o);
+ }
+
+ @Override
+ public int size(Object o) {
+ if (o instanceof List) return ((List<?>)
o).size();
+ if (o instanceof String) return ((String)
o).length();
+ return 1;
+ }
@Override
public boolean canListify(Object o) {
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
index 7c6ab972e8..b13e6901f4 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
@@ -310,7 +310,7 @@ class BctAssertions_Test extends TestBase {
@Test
void g03_notEmpty() {
var e = assertThrows(AssertionFailedError.class, () ->
assertEmpty(l("item")));
- assertContains("Collection was not empty",
e.getMessage());
+ assertContains("Value was not empty", e.getMessage());
}
@Test
@@ -579,7 +579,7 @@ class BctAssertions_Test extends TestBase {
@Test
void i03_actuallyEmpty() {
var e = assertThrows(AssertionFailedError.class, () ->
assertNotEmpty(l()));
- assertContains("Collection was empty", e.getMessage());
+ assertContains("Value was empty", e.getMessage());
}
@Test