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

Reply via email to