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 3a28337922 New BasicConverter class
3a28337922 is described below

commit 3a28337922f00d628abdbdff2b2a58d3bd1711f7
Author: James Bognar <[email protected]>
AuthorDate: Thu Apr 2 06:51:55 2026 -0700

    New BasicConverter class
---
 .gitignore                                         |   3 +-
 AGENTS.md                                          |  41 +-
 .../juneau/commons/concurrent/SimpleLock.java      |   1 +
 .../commons/concurrent/SimpleReadWriteLock.java    |   6 +
 .../juneau/commons/conversion/BasicConverter.java  | 513 ++++++++++++++
 .../commons/conversion/CachingConverter.java       | 165 +++++
 .../juneau/commons/conversion/Conversion.java      |  47 ++
 .../juneau/commons/conversion/Converter.java       |  60 +-
 .../conversion/InvalidConversionException.java     |  53 ++
 .../juneau/commons/io/FileReaderBuilder.java       |   3 +
 .../juneau/commons/io/FileWriterBuilder.java       |   3 +
 .../juneau/commons/io/NoCloseOutputStream.java     |   1 +
 .../apache/juneau/commons/io/NoCloseWriter.java    |   1 +
 .../juneau/commons/io/PathReaderBuilder.java       |   3 +
 .../juneau/commons/io/ReaderInputStream.java       |   4 +-
 .../org/apache/juneau/commons/logging/Logger.java  |   3 +
 .../apache/juneau/commons/reflect/ClassInfo.java   |   4 +-
 .../org/apache/juneau/commons/utils/IoUtils.java   |   5 +
 .../apache/juneau/config/internal/ConfigMap.java   |   5 +-
 .../src/main/java/org/apache/juneau/ClassMeta.java |   1 +
 .../juneau/httppart/SimplePartParserSession.java   |   4 +-
 .../juneau/httppart/SimplePartSerializer.java      |   2 +-
 .../httppart/SimplePartSerializerSession.java      |   4 +-
 .../org/apache/juneau/internal/ConverterUtils.java |   8 +-
 .../java/org/apache/juneau/reflect/Mutaters.java   |   2 +
 .../apache/juneau/uon/UonSerializerSession.java    |   6 +-
 ...ricConverter.java => BeanContextConverter.java} |  33 +-
 .../src/main/resources/META-INF/persistence.xml    |  36 -
 .../org/apache/juneau/rest/remote/RrpcServlet.java |   4 +-
 .../apache/juneau/bson/BsonOutputStream_Test.java  |   3 +
 .../juneau/commons/collections/Lists_Test.java     |  32 +-
 .../juneau/commons/collections/Sets_Test.java      |  32 +-
 .../commons/conversion/BasicConverter_Test.java    | 765 +++++++++++++++++++++
 .../commons/conversion/CachingConverter_Test.java  | 108 +++
 .../juneau/commons/conversion/Converter_Test.java  |  34 +-
 .../juneau/commons/io/CharSequenceReader_Test.java |   3 +
 .../commons/io/NoCloseOutputStream_Test.java       |   3 +
 .../juneau/commons/io/NoCloseWriter_Test.java      |   3 +
 .../juneau/commons/io/ReaderInputStream_Test.java  |   3 +
 .../commons/io/StringBuilderWriter_Test.java       |   3 +
 .../apache/juneau/hjson/HjsonTokenizer_Test.java   |   3 +
 .../apache/juneau/html/SimpleHtmlWriter_Test.java  |   3 +
 .../apache/juneau/uon/UonParserReader_Test.java    |   3 +-
 .../java/org/apache/juneau/utils/MutatersTest.java |   2 +-
 scripts/coverage.py                                | 284 ++++++++
 45 files changed, 2198 insertions(+), 107 deletions(-)

diff --git a/.gitignore b/.gitignore
index d2fb04dec0..4210a16677 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 **/.classpath
 **/.project
 **/.settings/
+/.metadata/
 /bin/
 derby.log
 
@@ -13,7 +14,7 @@ cursor-*
 **/dependency-reduced-pom.xml
 
 # Note: Maven site generation creates target/site/ (already ignored by 
**/target/ above)
-/juneau.code-workspace
+**/*.code-workspace
 /create-mvn-site.log
 /coverage/
 /coverage.csv
diff --git a/AGENTS.md b/AGENTS.md
index fbdb6192f5..373a11d0eb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -402,7 +402,46 @@ A reusable Python script is available at 
`scripts/build-and-test.py` for common
 - When you need to verify both build and tests pass
 - During iterative development to quickly test changes
 
-### 5.1. Command Execution Best Practices
+### 5.1. Coverage Script
+
+A reusable Python script is available at `scripts/coverage.py` for checking 
JaCoCo test coverage on any source file, package, or module.
+
+**When asked for current test coverage** on a class, package, or module, 
always use this script rather than manually parsing JaCoCo output.
+
+**Usage:**
+```bash
+# Coverage for an entire package
+./scripts/coverage.py 
juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/
+
+# Coverage for a single file
+./scripts/coverage.py 
juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
+
+# Show only lines with missed branches (hide instruction-only misses)
+./scripts/coverage.py path/to/file.java --branches
+
+# Re-run tests first to refresh coverage data, then report
+./scripts/coverage.py path/to/folder/ --run
+```
+
+**What it does:**
+- Detects the Maven module automatically from the path
+- Generates a JaCoCo report using the existing 
`juneau-utest/target/jacoco.exec`
+- Displays branch and instruction coverage percentages with a progress bar
+- Lists every uncovered line with the number of missed branches/instructions
+
+**How coverage data works:**
+- The `.exec` file is produced by `juneau-utest` during a test run and covers 
all loaded classes, including those from other modules (e.g. `juneau-commons`)
+- Use `--run` when tests have changed since the last run; omit it to reuse 
existing data for speed
+- Report generation takes ~2 seconds; test execution adds ~10-15 seconds
+
+**Trigger phrases:** When the user says any of the following, run this script:
+- "what's the coverage on ..."
+- "show coverage for ..."
+- "current test coverage"
+- "check coverage"
+- "coverage report"
+
+### 5.3. Command Execution Best Practices
 
 **Adding Timeouts to Commands:**
 
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/concurrent/SimpleLock.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/concurrent/SimpleLock.java
index be2a88f435..d5e784667d 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/concurrent/SimpleLock.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/concurrent/SimpleLock.java
@@ -100,6 +100,7 @@ public class SimpleLock implements AutoCloseable {
         *      <li class='note'>This instance wraps a <jk>null</jk> lock, so 
no actual locking occurs.
         * </ul>
         */
+       @SuppressWarnings("resource") // Intentional singleton; wraps a null 
lock so close() is a no-op
        public static final SimpleLock NO_OP = new SimpleLock(null);
 
        private final Lock lock;
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/concurrent/SimpleReadWriteLock.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/concurrent/SimpleReadWriteLock.java
index e5dbf42aaf..ac9d21e432 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/concurrent/SimpleReadWriteLock.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/concurrent/SimpleReadWriteLock.java
@@ -169,6 +169,9 @@ public class SimpleReadWriteLock extends 
ReentrantReadWriteLock {
         *
         * @return A new {@link SimpleLock} that holds the read lock. The lock 
is automatically acquired.
         */
+       @SuppressWarnings({
+               "resource" // Caller takes ownership of the returned lock via 
try-with-resources
+       })
        public SimpleLock read() {
                return new SimpleLock(readLock());
        }
@@ -202,6 +205,9 @@ public class SimpleReadWriteLock extends 
ReentrantReadWriteLock {
         *
         * @return A new {@link SimpleLock} that holds the write lock. The lock 
is automatically acquired.
         */
+       @SuppressWarnings({
+               "resource" // Caller takes ownership of the returned lock via 
try-with-resources
+       })
        public SimpleLock write() {
                return new SimpleLock(writeLock());
        }
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
new file mode 100644
index 0000000000..822b43ffe6
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
@@ -0,0 +1,513 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.conversion;
+
+import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.concurrent.atomic.*;
+
+import org.apache.juneau.commons.reflect.*;
+
+/**
+ * A concrete {@link CachingConverter} that supports common type conversions 
without
+ * requiring a {@code BeanContext} or {@code BeanSession}.
+ *
+ * <p>
+ * The following conversions are supported:
+ *
+ * <table class='styled'>
+ *     <tr><th>Input type</th><th>Output type</th><th>Notes</th></tr>
+ *     <tr>
+ *             <td>Any type</td>
+ *             <td>Same or supertype</td>
+ *             <td>Identity / widening (no-op cast)</td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link Number}</td>
+ *             <td>
+ *                     {@link Integer}, {@link Long}, {@link Short}, {@link 
Float}, {@link Double}, {@link Byte},
+ *                     {@link AtomicInteger}, {@link AtomicLong}, and 
primitive equivalents
+ *             </td>
+ *             <td>Narrowing/widening numeric conversion</td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link Boolean}</td>
+ *             <td>{@link Number} types</td>
+ *             <td><c>true=1, false=0</c></td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link CharSequence}</td>
+ *             <td>{@link Number} types</td>
+ *             <td>Parsed via {@link 
org.apache.juneau.commons.utils.StringUtils#parseNumber(String, Class)}</td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link Number}</td>
+ *             <td>{@link Boolean}</td>
+ *             <td><c>intValue() != 0</c></td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link CharSequence}</td>
+ *             <td>{@link Boolean}</td>
+ *             <td>{@link Boolean#valueOf(String)}</td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link CharSequence} (length 1)</td>
+ *             <td>{@link Character}</td>
+ *             <td><c>charAt(0)</c></td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link Number}</td>
+ *             <td>{@link Character}</td>
+ *             <td><c>(char) intValue()</c></td>
+ *     </tr>
+ *     <tr>
+ *             <td>Any</td>
+ *             <td>{@link String}</td>
+ *             <td>{@link Object#toString()}, with array support via {@link 
Arrays#toString(Object[])}</td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link CharSequence}</td>
+ *             <td>Any {@link Enum}</td>
+ *             <td>{@link Enum#valueOf(Class, String)}</td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link Collection} or array</td>
+ *             <td>{@link Collection} subtype</td>
+ *             <td>Copies elements; element type from <c>args[0]</c></td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link Map}</td>
+ *             <td>{@link Map} subtype</td>
+ *             <td>Copies entries; key type from <c>args[0]</c>, value type 
from <c>args[1]</c></td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link Collection} or array</td>
+ *             <td>Array type</td>
+ *             <td>Element type from <c>outType.getComponentType()</c></td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link String}</td>
+ *             <td>{@link TimeZone}</td>
+ *             <td>{@link TimeZone#getTimeZone(String)}</td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link TimeZone}</td>
+ *             <td>{@link String}</td>
+ *             <td>{@link TimeZone#getID()}</td>
+ *     </tr>
+ *     <tr>
+ *             <td>{@link String}</td>
+ *             <td>{@link Locale}</td>
+ *             <td>{@link Locale#forLanguageTag(String)} (underscores 
converted to hyphens)</td>
+ *     </tr>
+ *     <tr>
+ *             <td>Any</td>
+ *             <td>Any with <c>static T 
fromString/valueOf/of/from/parse/create/forName/fromValue/builder(X)</c>
+ *                     or dynamic <c>fromX/forX/parseX</c> where X is the 
input class name</td>
+ *             <td>Reflection-based static factory lookup</td>
+ *     </tr>
+ *     <tr>
+ *             <td>Any</td>
+ *             <td>Any with <c>public T(X)</c> constructor</td>
+ *             <td>Reflection-based constructor lookup</td>
+ *     </tr>
+ *     <tr>
+ *             <td>Any with <c>toX()</c> instance method</td>
+ *             <td>Any type X</td>
+ *             <td>Instance method where name matches <c>to</c> + output class 
name (e.g. <c>toInteger()</c>)</td>
+ *     </tr>
+ * </table>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * This class is thread-safe. The singleton instance can be safely shared 
across multiple threads.
+ */
+@SuppressWarnings({
+       "rawtypes", // Raw types necessary for generic conversion dispatch
+       "unchecked", // Type erasure requires unchecked casts throughout 
conversion logic
+       "java:S3776", // Cognitive complexity of conversion dispatch methods is 
inherent to the number of supported type pairs
+       "java:S1067" // Complex boolean expressions in conversion checks 
reflect the natural type hierarchy
+})
+public class BasicConverter extends CachingConverter {
+
+       /**
+        * Singleton instance.
+        */
+       public static final BasicConverter INSTANCE = new BasicConverter();
+
+       private static final Set<String> FACTORY_METHOD_NAMES = Set.of(
+               "fromString", "valueOf", "of", "from", "parse", "create", 
"forName", "fromValue", "builder"
+       );
+
+       private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER = 
Map.of(
+               boolean.class, Boolean.class,
+               byte.class, Byte.class,
+               char.class, Character.class,
+               double.class, Double.class,
+               float.class, Float.class,
+               int.class, Integer.class,
+               long.class, Long.class,
+               short.class, Short.class
+       );
+
+       /**
+        * Constructor.
+        */
+       protected BasicConverter() {}
+
+       @Override
+       protected <I, O> Conversion<I, O> findConversion(Class<I> inType, 
Class<O> outType) {
+               var out = outType.isPrimitive() ? (Class<O>) 
PRIMITIVE_TO_WRAPPER.get(outType) : outType;
+
+               Conversion<I, O> c;
+
+               if ((c = findSpecialConversion(inType, out)) != null) return c;
+
+               if (out.isAssignableFrom(inType) && 
!Collection.class.isAssignableFrom(out) && !Map.class.isAssignableFrom(out))
+                       return (in, args) -> (O) in;
+
+               if (Number.class.isAssignableFrom(out) && (c = 
findNumberConversion(inType, out)) != null) return c;
+
+               if (out == Boolean.class && (c = findBooleanConversion(inType)) 
!= null) return c;
+
+               if (out == Character.class && (c = 
findCharacterConversion(inType)) != null) return c;
+
+               if (out == String.class)
+                       return (Conversion<I, O>) 
findToStringConversion(inType);
+
+               if (out.isEnum() && (c = findEnumConversion(inType, out)) != 
null) return c;
+
+               if (Collection.class.isAssignableFrom(out) && (c = 
findCollectionConversion(inType, out)) != null) return c;
+
+               if (Map.class.isAssignableFrom(out) && (c = 
findMapConversion(inType, out)) != null) return c;
+
+               if (out.isArray() && (c = findArrayConversion(inType, out)) != 
null) return c;
+
+               if ((c = findToXMethod(inType, out)) != null) return c;
+
+               if ((c = findStaticFactory(inType, out)) != null) return c;
+
+               if ((c = findConstructorConversion(inType, out)) != null) 
return c;
+
+               return null;
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Number conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findNumberConversion(Class<I> inType, 
Class<O> outType) {
+               if (Number.class.isAssignableFrom(inType))
+                       return findNumberFromNumber(outType);
+               if (inType == Boolean.class)
+                       return (Conversion<I, O>) 
findNumberFromBoolean(outType);
+               if (CharSequence.class.isAssignableFrom(inType))
+                       return (Conversion<I, O>) findNumberFromString(outType);
+               return null;
+       }
+
+       private <I, O> Conversion<I, O> findNumberFromNumber(Class<O> outType) {
+               if (outType == Integer.class) return (in, args) -> (O) 
Integer.valueOf(((Number) in).intValue());
+               if (outType == Long.class) return (in, args) -> (O) 
Long.valueOf(((Number) in).longValue());
+               if (outType == Short.class) return (in, args) -> (O) 
Short.valueOf(((Number) in).shortValue());
+               if (outType == Float.class) return (in, args) -> (O) 
Float.valueOf(((Number) in).floatValue());
+               if (outType == Double.class) return (in, args) -> (O) 
Double.valueOf(((Number) in).doubleValue());
+               if (outType == Byte.class) return (in, args) -> (O) 
Byte.valueOf(((Number) in).byteValue());
+               if (outType == AtomicInteger.class) return (in, args) -> (O) 
new AtomicInteger(((Number) in).intValue());
+               if (outType == AtomicLong.class) return (in, args) -> (O) new 
AtomicLong(((Number) in).longValue());
+               return null;
+       }
+
+       private <O> Conversion<Boolean, O> findNumberFromBoolean(Class<O> 
outType) {
+               if (outType == Integer.class) return (in, args) -> (O) 
Integer.valueOf(in.booleanValue() ? 1 : 0);
+               if (outType == Long.class) return (in, args) -> (O) 
Long.valueOf(in.booleanValue() ? 1L : 0L);
+               if (outType == Short.class) return (in, args) -> (O) 
Short.valueOf(in.booleanValue() ? (short) 1 : (short) 0);
+               if (outType == Float.class) return (in, args) -> (O) 
Float.valueOf(in.booleanValue() ? 1f : 0f);
+               if (outType == Double.class) return (in, args) -> (O) 
Double.valueOf(in.booleanValue() ? 1d : 0d);
+               if (outType == Byte.class) return (in, args) -> (O) 
Byte.valueOf(in.booleanValue() ? (byte) 1 : (byte) 0);
+               if (outType == AtomicInteger.class) return (in, args) -> (O) 
new AtomicInteger(in.booleanValue() ? 1 : 0);
+               if (outType == AtomicLong.class) return (in, args) -> (O) new 
AtomicLong(in.booleanValue() ? 1L : 0L);
+               return null;
+       }
+
+       private <O> Conversion<CharSequence, O> findNumberFromString(Class<O> 
outType) {
+               return (in, args) -> (O) parseNumber(in.toString(), (Class<? 
extends Number>) outType);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Boolean conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findBooleanConversion(Class<I> inType) {
+               if (Number.class.isAssignableFrom(inType))
+                       return (in, args) -> (O) Boolean.valueOf(((Number) 
in).intValue() != 0);
+               return null;
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Character conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findCharacterConversion(Class<I> 
inType) {
+               if (CharSequence.class.isAssignableFrom(inType))
+                       return (in, args) -> {
+                               var s = in.toString();
+                               return s.length() == 1 ? (O) 
Character.valueOf(s.charAt(0)) : null;
+                       };
+               if (Number.class.isAssignableFrom(inType))
+                       return (in, args) -> (O) Character.valueOf((char) 
((Number) in).intValue());
+               return null;
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // String conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I> Conversion<I, String> findToStringConversion(Class<I> 
inType) {
+               if (inType.isArray()) {
+                       if (inType == int[].class) return (in, args) -> 
Arrays.toString((int[]) in);
+                       if (inType == long[].class) return (in, args) -> 
Arrays.toString((long[]) in);
+                       if (inType == double[].class) return (in, args) -> 
Arrays.toString((double[]) in);
+                       if (inType == float[].class) return (in, args) -> 
Arrays.toString((float[]) in);
+                       if (inType == boolean[].class) return (in, args) -> 
Arrays.toString((boolean[]) in);
+                       if (inType == byte[].class) return (in, args) -> 
Arrays.toString((byte[]) in);
+                       if (inType == short[].class) return (in, args) -> 
Arrays.toString((short[]) in);
+                       if (inType == char[].class) return (in, args) -> 
Arrays.toString((char[]) in);
+                       return (in, args) -> Arrays.deepToString((Object[]) in);
+               }
+               return (in, args) -> in.toString();
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Enum conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findEnumConversion(Class<I> inType, 
Class<O> outType) {
+               if (CharSequence.class.isAssignableFrom(inType))
+                       return (in, args) -> (O) Enum.valueOf((Class<Enum>) 
outType, in.toString());
+               return null;
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Collection conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findCollectionConversion(Class<I> 
inType, Class<O> outType) {
+               if (Collection.class.isAssignableFrom(inType) || 
inType.isArray()) {
+                       return (in, args) -> {
+                               var elemType = args.length > 0 ? args[0] : null;
+                               if (elemType == null && !inType.isArray() && 
outType.isAssignableFrom(inType))
+                                       return (O) in;
+                               var result = newCollection(outType);
+                               if (Collection.class.isAssignableFrom(inType)) {
+                                       for (var elem : (Collection<?>) in)
+                                               result.add(elemType != null ? 
to(elem, elemType) : elem);
+                               } else {
+                                       var len = Array.getLength(in);
+                                       for (var i = 0; i < len; i++) {
+                                               var elem = Array.get(in, i);
+                                               result.add(elemType != null ? 
to(elem, elemType) : elem);
+                                       }
+                               }
+                               return (O) result;
+                       };
+               }
+               return null;
+       }
+
+       private Collection<Object> newCollection(Class<?> outType) {
+               if (outType == List.class || outType == Collection.class || 
outType == Iterable.class /* HTT: Iterable is a supertype of Collection so 
findCollectionConversion never passes Iterable as outType */ || outType == 
AbstractList.class)
+                       return new ArrayList<>();
+               if (outType == Set.class || outType == LinkedHashSet.class || 
outType == AbstractSet.class)
+                       return new LinkedHashSet<>();
+               if (outType == SortedSet.class || outType == NavigableSet.class 
|| outType == TreeSet.class)
+                       return new TreeSet<>();
+               if (outType == Queue.class || outType == Deque.class || outType 
== LinkedList.class)
+                       return new LinkedList<>();
+               return newInstanceOrDefault(outType, ArrayList::new);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Map conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findMapConversion(Class<I> inType, 
Class<O> outType) {
+               if (Map.class.isAssignableFrom(inType)) {
+                       return (in, args) -> {
+                               var keyType = args.length > 0 ? args[0] : null;
+                               var valType = args.length > 1 ? args[1] : null;
+                               if (keyType == null && 
outType.isAssignableFrom(inType))
+                                       return (O) in;
+                               var result = newMap(outType);
+                               ((Map<?, ?>) in).forEach((k, v) -> result.put(
+                                       keyType != null ? to(k, keyType) : k,
+                                       valType != null ? to(v, valType) : v
+                               ));
+                               return (O) result;
+                       };
+               }
+               return null;
+       }
+
+       private Map<Object, Object> newMap(Class<?> outType) {
+               if (outType == Map.class || outType == LinkedHashMap.class || 
outType == AbstractMap.class)
+                       return new LinkedHashMap<>();
+               if (outType == SortedMap.class || outType == NavigableMap.class 
|| outType == TreeMap.class)
+                       return new TreeMap<>();
+               if (outType == HashMap.class)
+                       return new HashMap<>();
+               return newInstanceOrDefault(outType, LinkedHashMap::new);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Array conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findArrayConversion(Class<I> inType, 
Class<O> outType) {
+               if (Collection.class.isAssignableFrom(inType) || 
inType.isArray()) {
+                       var componentType = outType.getComponentType();
+                       return (in, args) -> {
+                               if (Collection.class.isAssignableFrom(inType)) {
+                                       var list = (Collection<?>) in;
+                                       var arr = 
Array.newInstance(componentType, list.size());
+                                       var i = 0;
+                                       for (var elem : list)
+                                               Array.set(arr, i++, to(elem, 
componentType));
+                                       return (O) arr;
+                               }
+                               var len = Array.getLength(in);
+                               var arr = Array.newInstance(componentType, len);
+                               for (var i = 0; i < len; i++)
+                                       Array.set(arr, i, to(Array.get(in, i), 
componentType));
+                               return (O) arr;
+                       };
+               }
+               return null;
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Special-case conversions
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findSpecialConversion(Class<I> inType, 
Class<O> outType) {
+               if (inType == String.class && outType == TimeZone.class)
+                       return (Conversion<I, O>) (Conversion<String, 
TimeZone>) (in, args) -> TimeZone.getTimeZone(in);
+               if (TimeZone.class.isAssignableFrom(inType) && outType == 
String.class)
+                       return (Conversion<I, O>) (Conversion<TimeZone, 
String>) (in, args) -> in.getID();
+               if (inType == String.class && outType == Locale.class)
+                       return (Conversion<I, O>) (Conversion<String, Locale>) 
(in, args) -> Locale.forLanguageTag(in.replace('_', '-'));
+               if (CharSequence.class.isAssignableFrom(inType) && outType == 
Boolean.class)
+                       return (in, args) -> {
+                               var s = in.toString();
+                               if (s.isEmpty() || "null".equals(s))
+                                       return null;
+                               return (O) Boolean.valueOf(s);
+                       };
+               return null;
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Reflection: static factory methods
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findStaticFactory(Class<I> inType, 
Class<O> outType) {
+               var ci = info(outType);
+
+               for (var name : FACTORY_METHOD_NAMES) {
+                       var opt = findStaticMethod(ci, name, inType, outType);
+                       if (opt.isPresent())
+                               return (in, args) -> opt.get().invoke(null, in);
+               }
+
+               var inName = inType.getSimpleName();
+               for (var prefix : new String[]{"from", "for", "parse"}) {
+                       var opt = findStaticMethod(ci, prefix + inName, inType, 
outType);
+                       if (opt.isPresent())
+                               return (in, args) -> opt.get().invoke(null, in);
+               }
+
+               return null;
+       }
+
+       private Optional<MethodInfo> findStaticMethod(ClassInfo ci, String 
name, Class<?> inType, Class<?> outType) {
+               return ci.getPublicMethod(m ->
+                       m.isStatic()
+                       && m.isNotDeprecated()
+                       && m.hasName(name)
+                       && m.getParameterCount() == 1
+                       && m.getParameterTypes().get(0).isAssignableFrom(inType)
+                       && m.hasReturnTypeParent(outType)
+               );
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Reflection: public constructors
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findConstructorConversion(Class<I> 
inType, Class<O> outType) {
+               var opt = info(outType).getPublicConstructor(c ->
+                       c.getParameterCount() == 1
+                       && c.isNotDeprecated()
+                       && c.getParameterTypes().get(0).isAssignableFrom(inType)
+               );
+               if (opt.isPresent()) {
+                       var ctor = opt.get();
+                       return (in, args) -> ctor.newInstance(in);
+               }
+               return null;
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Reflection: toX() instance methods
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <I, O> Conversion<I, O> findToXMethod(Class<I> inType, Class<O> 
outType) {
+               var methodName = "to" + outType.getSimpleName();
+               var opt = info(inType).getPublicMethod(m ->
+                       m.isNotStatic()
+                       && m.isNotDeprecated()
+                       && m.getParameterCount() == 0
+                       && m.hasName(methodName)
+                       && m.hasReturnTypeParent(outType)
+               );
+               if (opt.isPresent()) {
+                       var method = opt.get();
+                       return (in, args) -> method.invoke(in);
+               }
+               return null;
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Helpers
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       private <T> T newInstanceOrDefault(Class<?> type, 
java.util.function.Supplier<T> defaultSupplier) {
+               var opt = info(type).getPublicConstructor(c -> 
c.getParameterCount() == 0);
+               if (opt.isPresent()) {
+                       var ctor = opt.get();
+               try {
+                       return ctor.newInstance();
+               } catch (@SuppressWarnings("unused") Exception e) {
+                       return defaultSupplier.get(); // HTT
+               }
+               }
+               return defaultSupplier.get();
+       }
+}
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/CachingConverter.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/CachingConverter.java
new file mode 100644
index 0000000000..6f9a94ee38
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/CachingConverter.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.conversion;
+
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.*;
+
+/**
+ * An abstract {@link Converter} implementation that caches previously 
determined type conversions.
+ *
+ * <p>
+ * Conversion functions are discovered lazily on the first call for a given 
input/output type pair and
+ * stored in a two-level {@link ConcurrentHashMap} for subsequent fast lookup.
+ *
+ * <p>
+ * Subclasses implement {@link #findConversion(Class, Class)} to provide the 
actual conversion logic.
+ * The result is cached so that reflection or other expensive discovery 
happens only once per type pair.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     <jk>public class</jk> MyConverter <jk>extends</jk> CachingConverter {
+ *
+ *             <ja>@Override</ja>
+ *             <jk>protected</jk> &lt;I,O&gt; Conversion&lt;I,O&gt; 
findConversion(Class&lt;I&gt; inType, Class&lt;O&gt; outType) {
+ *                     <jk>if</jk> (inType == String.<jk>class</jk> &amp;&amp; 
outType == Integer.<jk>class</jk>)
+ *                             <jk>return</jk> (Conversion&lt;I,O&gt;) 
(Conversion&lt;String,Integer&gt;) (<jv>s</jv>, <jv>args</jv>) -&gt; 
Integer.<jsm>valueOf</jsm>(<jv>s</jv>);
+ *                     <jk>return null</jk>;
+ *             }
+ *     }
+ * </p>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * This class is thread-safe. The cache uses {@link ConcurrentHashMap} for 
safe concurrent access.
+ * </p>
+ */
+public abstract class CachingConverter implements Converter {
+
+       // Sentinel stored in the cache when findConversion() returns null.
+       // ConcurrentHashMap does not permit null values, so we cannot store 
null directly.
+       // Using this sentinel avoids re-invoking findConversion() for 
unconvertable type pairs.
+       private static final Conversion<?,?> NO_CONVERSION = (in, args) -> null;
+
+       // Two-level cache: input type -> output type -> conversion function 
(or NO_CONVERSION sentinel).
+       private final Map<Class<?>, Map<Class<?>, Conversion<?,?>>> conversions 
= new ConcurrentHashMap<>();
+
+       /**
+        * Finds a conversion function from the specified input type to the 
specified output type.
+        *
+        * <p>
+        * This method is called lazily the first time a conversion between a 
given type pair is requested.
+        * The result is cached so subsequent calls for the same type pair 
bypass this method entirely.
+        * When this method returns <jk>null</jk>, a sentinel is stored so that 
unconvertable type pairs
+        * are also only evaluated once.
+        *
+        * <p>
+        * Implementations should return <jk>null</jk> if no conversion is 
possible for the given types,
+        * which will cause {@link #to(Object, Class)} and {@link #to(Object, 
Type, Type...)} to throw
+        * {@link InvalidConversionException} for that type pair.
+        *
+        * @param <I> The input type.
+        * @param <O> The output type.
+        * @param inType The runtime class of the input object.
+        * @param outType The target output class.
+        * @return A {@link Conversion} function, or <jk>null</jk> if no 
conversion is available.
+        */
+       protected abstract <I, O> Conversion<I, O> findConversion(Class<I> 
inType, Class<O> outType);
+
+       @SuppressWarnings("unchecked")
+       private <I, O> Conversion<I, O> lookupConversion(Class<I> inType, 
Class<O> outType) {
+               var fn = conversions
+                       .computeIfAbsent(inType, k -> new ConcurrentHashMap<>())
+                       .computeIfAbsent(outType, k -> {
+                               var found = findConversion(inType, outType);
+                               return found != null ? found : NO_CONVERSION;
+                       });
+               return fn == NO_CONVERSION ? null : (Conversion<I, O>) fn;
+       }
+
+       @Override
+       public boolean canConvert(Class<?> inType, Class<?> outType) {
+               if (inType == outType)
+                       return true;
+               return lookupConversion(inType, outType) != null;
+       }
+
+       /**
+        * Converts the specified object to the specified type.
+        *
+        * <p>
+        * On the first call for a given input/output type pair, {@link 
#findConversion(Class, Class)} is
+        * invoked and the result is cached. Subsequent calls use the cached 
function directly.
+        *
+        * @param o The object to convert. Can be <jk>null</jk>.
+        * @param type The target type.
+        * @param <T> The target type.
+        * @return The converted object, or <jk>null</jk> if the input is 
<jk>null</jk>.
+        * @throws InvalidConversionException If no conversion path exists from 
the input type to the target type.
+        */
+       @Override
+       @SuppressWarnings("unchecked")
+       public <T> T to(Object o, Class<T> type) {
+               if (o == null)
+                       return null;
+               var inType = o.getClass();
+               if (inType == type)
+                       return (T) o;
+               var fn = (Conversion<Object, T>) lookupConversion(inType, type);
+               if (fn == null)
+                       throw new InvalidConversionException(inType, type);
+               return fn.to(o);
+       }
+
+       /**
+        * Converts the specified object to the specified parameterized type.
+        *
+        * <p>
+        * The raw class is extracted from {@code mainType} (supporting both 
{@link Class} and
+        * {@link ParameterizedType} values). Type arguments are extracted from 
{@code args} the same way
+        * and passed through to the cached {@link Conversion} function at call 
time.
+        *
+        * <p>
+        * The cache is keyed on the raw input and output classes only. The 
{@code args} are not part of
+        * the cache key — a single cached {@link Conversion} handles all 
parameterizations of a given
+        * output type.
+        *
+        * @param o The object to convert. Can be <jk>null</jk>.
+        * @param mainType The target type. May be a {@link Class} or {@link 
ParameterizedType}.
+        * @param args The type arguments of the target type (e.g. element type 
for collections).
+        * @param <T> The target type.
+        * @return The converted object, or <jk>null</jk> if the input is 
<jk>null</jk>.
+        * @throws InvalidConversionException If no conversion path exists from 
the input type to the target type.
+        */
+       @Override
+       @SuppressWarnings("unchecked")
+       public <T> T to(Object o, Type mainType, Type... args) {
+               if (o == null)
+                       return null;
+               var rawType = (Class<T>) (mainType instanceof ParameterizedType 
pt ? pt.getRawType() : (Class<?>) mainType);
+               var argClasses = Stream.of(args)
+                       .map(t -> (Class<?>) (t instanceof ParameterizedType 
pt2 ? pt2.getRawType() : t))
+                       .toArray(Class[]::new);
+               var inType = o.getClass();
+               var fn = (Conversion<Object, T>) lookupConversion(inType, 
rawType);
+               if (fn == null)
+                       throw new InvalidConversionException(inType, rawType);
+               return fn.to(o, argClasses);
+       }
+}
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Conversion.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Conversion.java
new file mode 100644
index 0000000000..f758fd8e0b
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Conversion.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.conversion;
+
+import java.util.*;
+
+/**
+ * Functional interface representing a single type conversion used by {@link 
CachingConverter}.
+ *
+ * <p>
+ * The {@code args} parameter carries any type arguments needed for 
parameterized output types.
+ * For example, when converting to a {@link List}, {@code args[0]} would be 
the element type.
+ * For a {@link Map}, {@code args[0]} is the key type and {@code args[1]} is 
the value type.
+ *
+ * <p>
+ * A single cached {@code Conversion} function can serve all parameterizations 
of the same raw output
+ * type by accepting different {@code args} values at runtime.
+ *
+ * @param <I> The input type.
+ * @param <O> The output type.
+ */
+@FunctionalInterface
+public interface Conversion<I, O> {
+
+       /**
+        * Converts the input object to the output type.
+        *
+        * @param in The input object.
+        * @param args Optional type arguments for parameterized output types 
(e.g. element type for collections).
+        * @return The converted object.
+        */
+       O to(I in, Class<?>... args);
+}
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Converter.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Converter.java
index 0bcf6da49c..f67b158512 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Converter.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Converter.java
@@ -16,18 +16,70 @@
  */
 package org.apache.juneau.commons.conversion;
 
+import java.lang.reflect.*;
+
 /**
- * Temporary interface.  To be replaced with Mutator once that's part of the 
common module.
+ * Interface for converting objects between types.
+ *
+ * <p>
+ * Use {@link BasicConverter#INSTANCE} for a default singleton implementation 
that supports
+ * a wide range of common type conversions without requiring a {@code 
BeanContext} or {@code BeanSession}.
  */
 public interface Converter {
 
+       /**
+        * Returns <jk>true</jk> if this converter can convert from the 
specified input type to the specified output type.
+        *
+        * @param inType The input type.
+        * @param outType The output type.
+        * @return <jk>true</jk> if a conversion path exists.
+        */
+       default boolean canConvert(Class<?> inType, Class<?> outType) {
+               return true;
+       }
+
        /**
         * Converts the specified object to the specified type.
         *
-        * @param <T> The type to convert to.
+        * <p>
+        * Returns <jk>null</jk> only when the input object is <jk>null</jk>.
+        * Throws {@link InvalidConversionException} when no conversion path 
exists.
+        * Use {@link #canConvert(Class, Class)} to pre-check if uncertain.
+        *
+        * @param o The object to convert.
         * @param type The type to convert to.
+        * @param <T> The type to convert to.
+        * @return The converted object, or <jk>null</jk> if the input is 
<jk>null</jk>.
+        * @throws InvalidConversionException If no conversion path exists from 
the input type to the target type.
+        */
+       <T> T to(Object o, Class<T> type);
+
+       /**
+        * Converts the specified object to the specified parameterized type.
+        *
+        * <p>
+        * This method allows conversion to complex parameterized types such as 
collections and maps.
+        * The <c>args</c> parameter specifies the type parameters of the main 
type.
+        *
+        * <p>
+        * Returns <jk>null</jk> only when the input object is <jk>null</jk>.
+        * Throws {@link InvalidConversionException} when no conversion path 
exists.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <jc>// Convert to List&lt;String&gt;</jc>
+        *      List&lt;String&gt; <jv>list</jv> = 
<jv>converter</jv>.to(<jv>o</jv>, List.<jk>class</jk>, String.<jk>class</jk>);
+        *
+        *      <jc>// Convert to Map&lt;String,Integer&gt;</jc>
+        *      Map&lt;String,Integer&gt; <jv>map</jv> = 
<jv>converter</jv>.to(<jv>o</jv>, Map.<jk>class</jk>, String.<jk>class</jk>, 
Integer.<jk>class</jk>);
+        * </p>
+        *
         * @param o The object to convert.
-        * @return The converted object, or <jk>null</jk> if the conversion is 
not possible.
+        * @param mainType The main type to convert to.
+        * @param args The type parameters of the main type.
+        * @param <T> The type to convert to.
+        * @return The converted object, or <jk>null</jk> if the input is 
<jk>null</jk>.
+        * @throws InvalidConversionException If no conversion path exists from 
the input type to the target type.
         */
-       <T> T convertTo(Class<T> type, Object o);
+       <T> T to(Object o, Type mainType, Type...args);
 }
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/InvalidConversionException.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/InvalidConversionException.java
new file mode 100644
index 0000000000..ef6ba8af60
--- /dev/null
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/InvalidConversionException.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.conversion;
+
+/**
+ * Thrown by {@link Converter#to(Object, Class)} when no conversion path 
exists between two types.
+ *
+ * <p>
+ * This is an unchecked exception. Callers that are uncertain whether a 
conversion is possible should
+ * call {@link Converter#canConvert(Class, Class)} first, or catch this 
exception and handle accordingly.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     <jk>try</jk> {
+ *             Integer <jv>x</jv> = 
BasicConverter.<jsf>INSTANCE</jsf>.to(<js>"hello"</js>, Integer.<jk>class</jk>);
+ *     } <jk>catch</jk> (InvalidConversionException <jv>e</jv>) {
+ *             <jc>// Handle unsupported conversion</jc>
+ *     }
+ *
+ *     <jc>// Or check first:</jc>
+ *     <jk>if</jk> 
(BasicConverter.<jsf>INSTANCE</jsf>.canConvert(String.<jk>class</jk>, 
Integer.<jk>class</jk>)) {
+ *             Integer <jv>x</jv> = 
BasicConverter.<jsf>INSTANCE</jsf>.to(<js>"42"</js>, Integer.<jk>class</jk>);
+ *     }
+ * </p>
+ */
+public class InvalidConversionException extends RuntimeException {
+
+       private static final long serialVersionUID = 1L;
+
+       /**
+        * Constructor.
+        *
+        * @param inType The runtime type of the input object.
+        * @param outType The target output type.
+        */
+       public InvalidConversionException(Class<?> inType, Class<?> outType) {
+               super("Cannot convert " + inType.getName() + " to " + 
outType.getName());
+       }
+}
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/FileReaderBuilder.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/FileReaderBuilder.java
index 6a92dad9e7..a8948d02ea 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/FileReaderBuilder.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/FileReaderBuilder.java
@@ -177,6 +177,9 @@ public class FileReaderBuilder {
         * @return A new {@link Reader} for reading from the file.
         * @throws FileNotFoundException If the file could not be found and 
{@link #allowNoFile()} was not called.
         */
+       @SuppressWarnings({
+               "resource" // Caller takes ownership of the returned Reader
+       })
        public Reader build() throws FileNotFoundException {
                if (allowNoFile && (file == null || ! file.exists()))
                        return new StringReader("");
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/FileWriterBuilder.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/FileWriterBuilder.java
index 8f719412a5..0c904a5935 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/FileWriterBuilder.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/FileWriterBuilder.java
@@ -217,6 +217,9 @@ public class FileWriterBuilder {
         * @return A new {@link Writer} for writing to the file.
         * @throws FileNotFoundException If the file could not be created or 
opened for writing.
         */
+       @SuppressWarnings({
+               "resource" // Caller takes ownership of the returned Writer
+       })
        public Writer build() throws FileNotFoundException {
                assertArgNotNull(ARG_file, file);
                var os = (OutputStream)new FileOutputStream(file, append);
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/NoCloseOutputStream.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/NoCloseOutputStream.java
index 073a2d727d..fe0d8251ca 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/NoCloseOutputStream.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/NoCloseOutputStream.java
@@ -79,6 +79,7 @@ public class NoCloseOutputStream extends OutputStream {
        private static final String ARG_b = "b";
        private static final String ARG_os = "os";
 
+       @SuppressWarnings("resource") // Intentionally not owned; this wrapper 
deliberately does not close the underlying stream
        private final OutputStream os;
 
        /**
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/NoCloseWriter.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/NoCloseWriter.java
index 290053541b..ce1bae5a0d 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/NoCloseWriter.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/NoCloseWriter.java
@@ -80,6 +80,7 @@ public class NoCloseWriter extends Writer {
        private static final String ARG_str = "str";
        private static final String ARG_w = "w";
 
+       @SuppressWarnings("resource") // Intentionally not owned; this wrapper 
deliberately does not close the underlying writer
        private final Writer w;
 
        /**
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/PathReaderBuilder.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/PathReaderBuilder.java
index f82fe30638..37d453bb08 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/PathReaderBuilder.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/PathReaderBuilder.java
@@ -190,6 +190,9 @@ public class PathReaderBuilder {
         * @throws NoSuchFileException If the path does not exist and {@link 
#allowNoFile()} was not called.
         * @throws IOException If an I/O error occurs opening the path.
         */
+       @SuppressWarnings({
+               "resource" // Caller takes ownership of the returned Reader
+       })
        public Reader build() throws IOException {
                if (! allowNoFile && path == null) {
                        throw new IllegalStateException("No path");
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/ReaderInputStream.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/ReaderInputStream.java
index 3df8624d14..bfa4aceec5 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/ReaderInputStream.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/io/ReaderInputStream.java
@@ -84,6 +84,7 @@ public class ReaderInputStream extends InputStream {
        private static final String ARG_encoder = "encoder";
        private static final String ARG_reader = "reader";
 
+       @SuppressWarnings("resource") // Intentionally not owned; caller 
retains responsibility for closing the underlying Reader
        private final Reader reader;
        private final CharsetEncoder encoder;
 
@@ -121,9 +122,6 @@ public class ReaderInputStream extends InputStream {
         * @param charset the charset encoding.  Must not be <jk>null</jk>.
         * @param bufferSize the size of the input buffer in number of 
characters.  Must be positive.
         */
-       @SuppressWarnings({
-               "resource" // Reader resource managed by caller
-       })
        public ReaderInputStream(Reader reader, Charset charset, int 
bufferSize) {
                // @formatter:off
                this(assertArgNotNull(ARG_reader, reader),
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/logging/Logger.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/logging/Logger.java
index 2d528a1d61..371b9da43c 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/logging/Logger.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/logging/Logger.java
@@ -145,6 +145,9 @@ public class Logger extends java.util.logging.Logger {
         *
         * @return A LogRecordCapture instance.
         */
+       @SuppressWarnings({
+               "resource" // Caller takes ownership of the returned 
LogRecordCapture
+       })
        public LogRecordCapture captureEvents() {
                return new LogRecordCapture(this);
        }
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ClassInfo.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ClassInfo.java
index 8c8fa2ef7a..799bea8512 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ClassInfo.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ClassInfo.java
@@ -306,7 +306,6 @@ public class ClassInfo extends ElementInfo implements 
Annotatable, Type, Compara
         *      The same StringBuilder for method chaining.
         */
        @SuppressWarnings({
-               "null",       // Null analysis handled by runtime checks
                "java:S3776", // Cognitive complexity acceptable for name 
formatting logic
                "java:S6541"  // Synchronization not needed for local 
StringBuilder operations
        })
@@ -1781,6 +1780,9 @@ public class ClassInfo extends ElementInfo implements 
Annotatable, Type, Compara
         * @param name The resource name.
         * @return An input stream for reading the resource, or <jk>null</jk> 
if the resource could not be found.
         */
+       @SuppressWarnings({
+               "resource" // Caller takes ownership of the returned InputStream
+       })
        public java.io.InputStream getResourceAsStream(String name) {
                return inner == null ? null : inner.getResourceAsStream(name);
        }
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/IoUtils.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/IoUtils.java
index 2a08d30c56..cbb45173dc 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/IoUtils.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/IoUtils.java
@@ -38,6 +38,7 @@ public class IoUtils {
        public static final Charset UTF8 = StandardCharsets.UTF_8;
 
        /** Reusable empty input stream. */
+       @SuppressWarnings("resource") // Intentional singleton; read() always 
returns -1 and close() is a no-op
        public static final InputStream EMPTY_INPUT_STREAM = new InputStream() {
                @Override
                public int read() {
@@ -82,6 +83,7 @@ public class IoUtils {
        }
 
        /** Reusable empty reader. */
+       @SuppressWarnings("resource") // Intentional singleton; read() always 
returns -1 and close() is a no-op
        public static final Reader EMPTY_READER = new Reader() {
                @Override
                public void close() throws IOException { /* no-op */ }
@@ -1042,6 +1044,9 @@ public class IoUtils {
         *      The reader wrapped in a {@link BufferedReader}, or the original 
{@link Reader} if it's already a buffered
         *      reader.
         */
+       @SuppressWarnings({
+               "resource" // Caller takes ownership of the returned Reader
+       })
        public static Reader toBufferedReader(Reader r) {
                if (r == null || r instanceof BufferedReader || r instanceof 
StringReader)
                        return r;
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
index c0daa4d589..cf768919eb 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/internal/ConfigMap.java
@@ -482,9 +482,6 @@ public class ConfigMap implements ConfigStoreListener {
                return this;
        }
 
-       @SuppressWarnings({
-               "null" // Null analysis not applicable to this config listener 
method
-       })
        @Override /* Overridden from ConfigStoreListener */
        public void onChange(String newContents) {
                ConfigEvents changes2 = null;
@@ -499,7 +496,7 @@ public class ConfigMap implements ConfigStoreListener {
                } catch (IOException e) {
                        throw toRex(e);
                }
-               if (nn(changes2) && ! changes2.isEmpty())
+               if (changes2 != null && ! changes2.isEmpty())
                        signal(changes2);
        }
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
index 0750b2a9be..41ddb2b486 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
@@ -68,6 +68,7 @@ import org.apache.juneau.swap.*;
  */
 @Bean(properties = 
"innerClass,elementType,keyType,valueType,notABeanReason,initException,beanMeta")
 @SuppressWarnings({
+       "deprecation", // Mutaters is deprecated but still used here pending 
full migration to Converter.INSTANCE
        "java:S1200",  // Class has 23 dependencies, acceptable for this core 
reflection metadata class
        "java:S1452"   // Wildcard required - ClassMeta<?>, ObjectSwap<T,?>, 
Mutater<T,?>, etc. for element/component types
 })
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartParserSession.java
index c4b2cdf204..b4c0e5e6d7 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartParserSession.java
@@ -18,7 +18,7 @@ package org.apache.juneau.httppart;
 
 import org.apache.juneau.*;
 import org.apache.juneau.parser.*;
-import org.apache.juneau.reflect.*;
+import org.apache.juneau.commons.conversion.BasicConverter;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
SimplePartParser}.
@@ -35,6 +35,6 @@ public class SimplePartParserSession extends 
BaseHttpPartParserSession {
 
        @Override /* Overridden from HttpPartParserSession */
        public <T> T parse(HttpPartType partType, HttpPartSchema schema, String 
in, ClassMeta<T> toType) throws ParseException, SchemaValidationException {
-               return Mutaters.fromString(toType.inner(), in);
+               return BasicConverter.INSTANCE.to(in, toType.inner());
        }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartSerializer.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartSerializer.java
index 101cb53f18..759acc0265 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartSerializer.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartSerializer.java
@@ -25,7 +25,7 @@ import org.apache.juneau.commons.collections.*;
  * An implementation of {@link HttpPartSerializer} that simply serializes 
everything using {@link Object#toString()}.
  *
  * <p>
- * More precisely, uses the {@link 
org.apache.juneau.reflect.Mutaters#toString(Object)} method to stringify 
objects.
+ * More precisely, uses {@link 
org.apache.juneau.commons.conversion.Converter#INSTANCE} to stringify objects.
  *
  * <h5 class='section'>Notes:</h5><ul>
  *     <li class='note'>This class is thread safe and reusable.
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartSerializerSession.java
index e50c4191a9..602a9aaa09 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/SimplePartSerializerSession.java
@@ -16,7 +16,7 @@
  */
 package org.apache.juneau.httppart;
 
-import org.apache.juneau.reflect.*;
+import org.apache.juneau.commons.conversion.BasicConverter;
 
 /**
  * Session object that lives for the duration of a single use of {@link 
SimplePartSerializer}.
@@ -33,6 +33,6 @@ import org.apache.juneau.reflect.*;
 public class SimplePartSerializerSession extends BaseHttpPartSerializerSession 
{
        @Override /* Overridden from PartSerializer */
        public String serialize(HttpPartType type, HttpPartSchema schema, 
Object value) {
-               return Mutaters.toString(value);
+               return BasicConverter.INSTANCE.to(value, String.class);
        }
 }
\ No newline at end of file
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 921633ff63..72f4189cb9 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
@@ -238,7 +238,7 @@ public class ConverterUtils {
         * @return A new {@link Lists} containing the converted elements.
         */
        public static <T> Lists<T> toListBuilder(Object value, Class<T> type) {
-               return listb(type).elementFunction(o -> 
GenericConverter.INSTANCE.convertTo(type, o)).addAny(value);
+               return listb(type).elementFunction(o -> 
BeanContextConverter.INSTANCE.to(o, type)).addAny(value);
        }
 
        /**
@@ -261,8 +261,8 @@ public class ConverterUtils {
         */
        public static <K,V> Maps<K,V> toMapBuilder(Object value, Class<K> 
keyType, Class<V> valueType) {
                return mapb(keyType, valueType)
-                       .keyFunction(o -> 
GenericConverter.INSTANCE.convertTo(keyType, o))
-                       .valueFunction(o -> 
GenericConverter.INSTANCE.convertTo(valueType, o))
+                       .keyFunction(o -> BeanContextConverter.INSTANCE.to(o, 
keyType))
+                       .valueFunction(o -> BeanContextConverter.INSTANCE.to(o, 
valueType))
                        .addAny(value);
        }
 
@@ -287,6 +287,6 @@ public class ConverterUtils {
         * @return A new {@link Sets} containing the converted elements.
         */
        public static <T> Sets<T> toSetBuilder(Object value, Class<T> type) {
-               return setb(type).elementFunction(o -> 
GenericConverter.INSTANCE.convertTo(type, o)).addAny(value);
+               return setb(type).elementFunction(o -> 
BeanContextConverter.INSTANCE.to(o, type)).addAny(value);
        }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutaters.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutaters.java
index defaaaa2cb..43a6460a29 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutaters.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/Mutaters.java
@@ -29,7 +29,9 @@ import org.apache.juneau.commons.reflect.*;
 /**
  * Cache of object that convert POJOs to and from common types such as 
strings, readers, and input streams.
  *
+ * @deprecated Use {@link 
org.apache.juneau.commons.conversion.Converter#INSTANCE} instead.
  */
+@Deprecated
 public class Mutaters {
 
        /**
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
index dac79b4d4f..d8d6b879e3 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/uon/UonSerializerSession.java
@@ -30,7 +30,7 @@ import java.util.function.*;
 import org.apache.juneau.*;
 import org.apache.juneau.commons.lang.*;
 import org.apache.juneau.httppart.*;
-import org.apache.juneau.reflect.*;
+import org.apache.juneau.commons.conversion.BasicConverter;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
 import org.apache.juneau.utils.*;
@@ -221,9 +221,9 @@ public class UonSerializerSession extends 
WriterSerializerSession implements Htt
                        var cm = getClassMetaForObject(value);
                        if (nn(cm) && (schema == null || schema.getType() == 
HttpPartDataType.NO_TYPE)) {
                                if (cm.isNumber() || cm.isBoolean())
-                                       return Mutaters.toString(value);
+                                       return 
BasicConverter.INSTANCE.to(value, String.class);
                                if (cm.isString()) {
-                                       var s = Mutaters.toString(value);
+                                       var s = 
BasicConverter.INSTANCE.to(value, String.class);
                                        if (s.isEmpty() || ! 
UonUtils.needsQuotes(s))
                                                return s;
                                }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/GenericConverter.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/BeanContextConverter.java
similarity index 76%
rename from 
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/GenericConverter.java
rename to 
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/BeanContextConverter.java
index 1861c14b8b..c6c5385dcb 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/GenericConverter.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/BeanContextConverter.java
@@ -16,6 +16,8 @@
  */
 package org.apache.juneau.utils;
 
+import java.lang.reflect.*;
+
 import org.apache.juneau.*;
 import org.apache.juneau.commons.conversion.*;
 
@@ -54,7 +56,7 @@ import org.apache.juneau.commons.conversion.*;
        "java:S6541", // Stateless converter, singleton for convenience
        "java:S6542"  // Singleton required for stateless Converter; 
thread-safe shared instance
 })
-public class GenericConverter implements Converter {
+public class BeanContextConverter implements Converter {
 
        /**
         * Singleton instance of the generic converter.
@@ -62,12 +64,12 @@ public class GenericConverter implements Converter {
         * <p>
         * This instance can be safely shared across multiple threads and 
reused for all conversion operations.
         */
-       public static final GenericConverter INSTANCE = new GenericConverter();
+       public static final BeanContextConverter INSTANCE = new 
BeanContextConverter();
 
        /**
         * Constructor.
         */
-       private GenericConverter() {}
+       private BeanContextConverter() {}
 
        /**
         * Converts the specified object to the specified type.
@@ -87,15 +89,34 @@ public class GenericConverter implements Converter {
         *      <li>Object swap conversions
         *      <li>And many more...
         * </ul>
+        * @param o The object to convert.
+        * @param type The target class type.
         *
         * @param <T> The target type to convert to.
-        * @param type The target class type.
-        * @param o The object to convert.
         * @return The converted object, or <jk>null</jk> if the input object 
is <jk>null</jk>.
         * @throws InvalidDataConversionException If the object cannot be 
converted to the specified type.
         */
        @Override
-       public <T> T convertTo(Class<T> type, Object o) {
+       public <T> T to(Object o, Class<T> type) {
                return BeanContext.DEFAULT_SESSION.convertToType(o, type);
        }
+
+       /**
+        * Converts the specified object to the specified parameterized type.
+        *
+        * <p>
+        * This method delegates to the default {@link BeanContext} session for 
the actual conversion logic,
+        * supporting complex parameterized types such as collections and maps.
+        *
+        * @param o The object to convert.
+        * @param mainType The main type to convert to.
+        * @param args The type parameters of the main type.
+        * @param <T> The target type to convert to.
+        * @return The converted object, or <jk>null</jk> if the input object 
is <jk>null</jk>.
+        * @throws InvalidDataConversionException If the object cannot be 
converted to the specified type.
+        */
+       @Override
+       public <T> T to(Object o, Type mainType, Type... args) {
+               return BeanContext.DEFAULT_SESSION.convertToType(o, mainType, 
args);
+       }
 }
diff --git 
a/juneau-examples/juneau-examples-rest/src/main/resources/META-INF/persistence.xml
 
b/juneau-examples/juneau-examples-rest/src/main/resources/META-INF/persistence.xml
deleted file mode 100644
index 939abb3f01..0000000000
--- 
a/juneau-examples/juneau-examples-rest/src/main/resources/META-INF/persistence.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<!--
-  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.
--->
-<persistence
-       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
-       xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd";
-       version="2.0" xmlns="http://java.sun.com/xml/ns/persistence";>
-       <persistence-unit name="test" transaction-type="RESOURCE_LOCAL">
-               <class>org.apache.juneau.examples.rest.petstore.dto.Pet</class>
-               
<class>org.apache.juneau.examples.rest.petstore.dto.Order</class>
-               <class>org.apache.juneau.examples.rest.petstore.dto.User</class>
-               <properties>
-                       <property name="javax.persistence.jdbc.driver" 
value="org.apache.derby.jdbc.EmbeddedDriver" />
-                       <property name="javax.persistence.jdbc.url" 
value="jdbc:derby:target/derby/testDB;create=true" />
-                       <property name="javax.persistence.jdbc.user" value="" />
-                       <property name="javax.persistence.jdbc.password" 
value="" />
-                       <property name="hibernate.dialect" 
value="org.hibernate.dialect.DerbyDialect" />
-                       <property name="hibernate.hbm2ddl.auto" 
value="create-drop" />
-                       <property name="show_sql" value="true" />
-                       <property 
name="hibernate.temp.use_jdbc_metadata_defaults" value="false" />
-               </properties>
-       </persistence-unit>
-</persistence>
\ No newline at end of file
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/remote/RrpcServlet.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/remote/RrpcServlet.java
index 4e1e72806a..03c2b822f7 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/remote/RrpcServlet.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/remote/RrpcServlet.java
@@ -37,7 +37,7 @@ import org.apache.juneau.http.header.*;
 import org.apache.juneau.http.remote.*;
 import org.apache.juneau.http.response.*;
 import org.apache.juneau.parser.*;
-import org.apache.juneau.reflect.*;
+import org.apache.juneau.commons.conversion.BasicConverter;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.servlet.*;
 
@@ -185,7 +185,7 @@ public abstract class RrpcServlet extends BasicRestServlet {
                } else {
                        t.child(tr(th("Index"), th("Type"), th("Value")));
                        for (var i = 0; i < types.length; i++) {
-                               String type = Mutaters.toString(types[i]);
+                               String type = 
BasicConverter.INSTANCE.to(types[i], String.class);
                                t.child(tr(td(i), td(type), 
td(input().name(String.valueOf(i)).type("text"))));
                        }
                }
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/bson/BsonOutputStream_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/bson/BsonOutputStream_Test.java
index 36f4ba37f0..a80eb51368 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/bson/BsonOutputStream_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/bson/BsonOutputStream_Test.java
@@ -28,6 +28,9 @@ import org.junit.jupiter.api.*;
 /**
  * Tests for {@link BsonOutputStream}.
  */
+@SuppressWarnings({
+       "resource" // BsonOutputStream is intentionally not closed in unit tests
+})
 class BsonOutputStream_Test extends TestBase {
 
        @Test
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Lists_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Lists_Test.java
index f0d67673f0..c3cdcc69fc 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Lists_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Lists_Test.java
@@ -346,16 +346,19 @@ class Lists_Test extends TestBase {
        void k02_elementFunction_withConverter() {
                var converter = new 
org.apache.juneau.commons.conversion.Converter() {
                        @Override
-                       public <T> T convertTo(Class<T> type, Object o) {
-                               if (type == Integer.class && o instanceof 
String) {
+                       public <T> T to(Object o, Class<T> type) {
+                               if (type == Integer.class && o instanceof 
String)
                                        return 
type.cast(Integer.parseInt((String)o));
-                               }
+                               return null;
+                       }
+                       @Override
+                       public <T> T to(Object o, java.lang.reflect.Type 
mainType, java.lang.reflect.Type...args) {
                                return null;
                        }
                };
 
                var list = Lists.create(Integer.class)
-                       .elementFunction(o -> 
converter.convertTo(Integer.class, o))
+                       .elementFunction(o -> converter.to(o, Integer.class))
                        .addAny("1", "2", "3")
                        .build();
 
@@ -366,26 +369,33 @@ class Lists_Test extends TestBase {
        void k03_elementFunction_multipleConverters() {
                var converter1 = new 
org.apache.juneau.commons.conversion.Converter() {
                        @Override
-                       public <T> T convertTo(Class<T> type, Object o) {
-                               return null;  // Doesn't handle this
+                       public <T> T to(Object o, Class<T> type) {
+                               return null;
+                       }
+                       @Override
+                       public <T> T to(Object o, java.lang.reflect.Type 
mainType, java.lang.reflect.Type...args) {
+                               return null;
                        }
                };
 
                var converter2 = new 
org.apache.juneau.commons.conversion.Converter() {
                        @Override
-                       public <T> T convertTo(Class<T> type, Object o) {
-                               if (type == Integer.class && o instanceof 
String) {
+                       public <T> T to(Object o, Class<T> type) {
+                               if (type == Integer.class && o instanceof 
String)
                                        return 
type.cast(Integer.parseInt((String)o));
-                               }
+                               return null;
+                       }
+                       @Override
+                       public <T> T to(Object o, java.lang.reflect.Type 
mainType, java.lang.reflect.Type...args) {
                                return null;
                        }
                };
 
                var list = Lists.create(Integer.class)
                        .elementFunction(o -> {
-                               Integer result = 
converter1.convertTo(Integer.class, o);
+                               Integer result = converter1.to(o, 
Integer.class);
                                if (result != null) return result;
-                               return converter2.convertTo(Integer.class, o);
+                               return converter2.to(o, Integer.class);
                        })
                        .addAny("1", "2")
                        .build();
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
index 957c138a0f..a33dff4c06 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
@@ -371,16 +371,19 @@ class Sets_Test extends TestBase {
        void l02_elementFunction_withConverter() {
                var converter = new 
org.apache.juneau.commons.conversion.Converter() {
                        @Override
-                       public <T> T convertTo(Class<T> type, Object o) {
-                               if (type == Integer.class && o instanceof 
String) {
+                       public <T> T to(Object o, Class<T> type) {
+                               if (type == Integer.class && o instanceof 
String)
                                        return 
type.cast(Integer.parseInt((String)o));
-                               }
+                               return null;
+                       }
+                       @Override
+                       public <T> T to(Object o, java.lang.reflect.Type 
mainType, java.lang.reflect.Type...args) {
                                return null;
                        }
                };
 
                var set = Sets.create(Integer.class)
-                       .elementFunction(o -> 
converter.convertTo(Integer.class, o))
+                       .elementFunction(o -> converter.to(o, Integer.class))
                        .addAny("1", "2", "3")
                        .build();
 
@@ -391,26 +394,33 @@ class Sets_Test extends TestBase {
        void l03_elementFunction_multipleConverters() {
                var converter1 = new 
org.apache.juneau.commons.conversion.Converter() {
                        @Override
-                       public <T> T convertTo(Class<T> type, Object o) {
-                               return null;  // Doesn't handle this
+                       public <T> T to(Object o, Class<T> type) {
+                               return null;
+                       }
+                       @Override
+                       public <T> T to(Object o, java.lang.reflect.Type 
mainType, java.lang.reflect.Type...args) {
+                               return null;
                        }
                };
 
                var converter2 = new 
org.apache.juneau.commons.conversion.Converter() {
                        @Override
-                       public <T> T convertTo(Class<T> type, Object o) {
-                               if (type == Integer.class && o instanceof 
String) {
+                       public <T> T to(Object o, Class<T> type) {
+                               if (type == Integer.class && o instanceof 
String)
                                        return 
type.cast(Integer.parseInt((String)o));
-                               }
+                               return null;
+                       }
+                       @Override
+                       public <T> T to(Object o, java.lang.reflect.Type 
mainType, java.lang.reflect.Type...args) {
                                return null;
                        }
                };
 
                var set = Sets.create(Integer.class)
                        .elementFunction(o -> {
-                               Integer result = 
converter1.convertTo(Integer.class, o);
+                               Integer result = converter1.to(o, 
Integer.class);
                                if (result != null) return result;
-                               return converter2.convertTo(Integer.class, o);
+                               return converter2.to(o, Integer.class);
                        })
                        .addAny("1", "2")
                        .build();
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/BasicConverter_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/BasicConverter_Test.java
new file mode 100644
index 0000000000..be46a4a37c
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/BasicConverter_Test.java
@@ -0,0 +1,765 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.conversion;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+@SuppressWarnings({
+       "unused" // Test helper classes have fields read only via assertions
+})
+
+class BasicConverter_Test extends TestBase {
+
+       private static final BasicConverter C = BasicConverter.INSTANCE;
+
+       
//====================================================================================================
+       // a - Number conversions
+       
//====================================================================================================
+
+       @Test void a01_numberToNumber() {
+               assertEquals(42, C.to(42L, Integer.class));
+               assertEquals(42L, C.to(42, Long.class));
+               assertEquals((short) 42, C.to(42, Short.class));
+               assertEquals(42f, C.to(42, Float.class));
+               assertEquals(42d, C.to(42, Double.class));
+               assertEquals((byte) 42, C.to(42, Byte.class));
+               assertEquals(42, C.to(42L, AtomicInteger.class).get());
+               assertEquals(42L, C.to(42, AtomicLong.class).get());
+       }
+
+       @Test void a02_booleanToNumber() {
+               assertEquals(1, C.to(true, Integer.class));
+               assertEquals(0, C.to(false, Integer.class));
+               assertEquals(1L, C.to(true, Long.class));
+               assertEquals(0L, C.to(false, Long.class));
+               assertEquals((short) 1, C.to(true, Short.class));
+               assertEquals((short) 0, C.to(false, Short.class));
+               assertEquals(1f, C.to(true, Float.class));
+               assertEquals(0f, C.to(false, Float.class));
+               assertEquals(1d, C.to(true, Double.class));
+               assertEquals(0d, C.to(false, Double.class));
+               assertEquals((byte) 1, C.to(true, Byte.class));
+               assertEquals((byte) 0, C.to(false, Byte.class));
+               assertEquals(1, C.to(true, AtomicInteger.class).get());
+               assertEquals(0, C.to(false, AtomicInteger.class).get());
+               assertEquals(1L, C.to(true, AtomicLong.class).get());
+               assertEquals(0L, C.to(false, AtomicLong.class).get());
+       }
+
+       @Test void a03_stringToNumber() {
+               assertEquals(42, C.to("42", Integer.class));
+               assertEquals(42L, C.to("42", Long.class));
+               assertEquals((short) 42, C.to("42", Short.class));
+               assertEquals(42f, C.to("42", Float.class));
+               assertEquals(42d, C.to("42", Double.class));
+               assertEquals((byte) 42, C.to("42", Byte.class));
+       }
+
+       @Test void a04_primitiveTargets() {
+               assertEquals(42, C.to("42", int.class));
+               assertEquals(42L, C.to("42", long.class));
+               assertEquals((short) 42, C.to("42", short.class));
+               assertEquals(42f, C.to("42", float.class));
+               assertEquals(42d, C.to("42", double.class));
+               assertEquals((byte) 42, C.to("42", byte.class));
+       }
+
+       @Test void a05_numberAndBooleanToUnsupportedNumericType() {
+               // Number → BigDecimal: hits findNumberFromNumber line 234 
false branch (not AtomicLong), returns null
+               assertThrows(InvalidConversionException.class, () -> C.to(42, 
java.math.BigDecimal.class));
+               // Boolean → BigDecimal: hits findNumberFromBoolean line 246 
false branch (not AtomicLong), returns null
+               assertThrows(InvalidConversionException.class, () -> C.to(true, 
java.math.BigDecimal.class));
+       }
+
+       
//====================================================================================================
+       // b - Boolean conversions
+       
//====================================================================================================
+
+       @Test void b01_numberToBoolean() {
+               assertEquals(true, C.to(1, Boolean.class));
+               assertEquals(false, C.to(0, Boolean.class));
+               assertEquals(true, C.to(-1, Boolean.class));
+               assertEquals(true, C.to(42, Boolean.class));
+       }
+
+       @Test void b02_stringToBoolean() {
+               assertEquals(true, C.to("true", Boolean.class));
+               assertEquals(false, C.to("false", Boolean.class));
+               assertEquals(false, C.to("xyz", Boolean.class));
+               assertNull(C.to("", Boolean.class));
+               assertNull(C.to("null", Boolean.class));
+       }
+
+       @Test void b03_primitiveBoolean() {
+               assertEquals(true, C.to("true", boolean.class));
+               assertEquals(false, C.to(0, boolean.class));
+       }
+
+       @Test void b04_unconvertibleToBoolean() {
+               assertThrows(InvalidConversionException.class, () -> C.to(new 
Object(), Boolean.class));
+       }
+
+       
//====================================================================================================
+       // c - Character conversions
+       
//====================================================================================================
+
+       @Test void c01_stringToChar() {
+               assertEquals('A', C.to("A", Character.class));
+               assertNull(C.to("AB", Character.class));
+       }
+
+       @Test void c02_numberToChar() {
+               assertEquals('A', C.to(65, Character.class));
+       }
+
+       @Test void c03_primitiveChar() {
+               assertEquals('X', C.to("X", char.class));
+       }
+
+       @Test void c04_unconvertibleToCharacter() {
+               assertThrows(InvalidConversionException.class, () -> C.to(true, 
Character.class));
+       }
+
+       
//====================================================================================================
+       // d - String conversions
+       
//====================================================================================================
+
+       @Test void d01_objectToString() {
+               assertEquals("42", C.to(42, String.class));
+               assertEquals("true", C.to(true, String.class));
+               assertEquals("3.14", C.to(3.14, String.class));
+       }
+
+       @Test void d02_arrayToString() {
+               assertEquals("[1, 2, 3]", C.to(new int[]{1, 2, 3}, 
String.class));
+               assertEquals("[1, 2, 3]", C.to(new long[]{1L, 2L, 3L}, 
String.class));
+               assertEquals("[1.0, 2.0]", C.to(new double[]{1.0, 2.0}, 
String.class));
+               assertEquals("[1.0, 2.0]", C.to(new float[]{1.0f, 2.0f}, 
String.class));
+               assertEquals("[true, false]", C.to(new boolean[]{true, false}, 
String.class));
+               assertEquals("[1, 2, 3]", C.to(new byte[]{1, 2, 3}, 
String.class));
+               assertEquals("[1, 2, 3]", C.to(new short[]{1, 2, 3}, 
String.class));
+               assertEquals("[a, b, c]", C.to(new char[]{'a', 'b', 'c'}, 
String.class));
+               assertEquals("[a, b, c]", C.to(new String[]{"a", "b", "c"}, 
String.class));
+       }
+
+       @Test void d03_collectionToString() {
+               assertEquals("[a, b]", C.to(List.of("a", "b"), String.class));
+       }
+
+       
//====================================================================================================
+       // e - Enum conversions
+       
//====================================================================================================
+
+       enum E01_TestEnum { FOO, BAR, BAZ }
+
+       @Test void e01_stringToEnum() {
+               assertEquals(E01_TestEnum.FOO, C.to("FOO", E01_TestEnum.class));
+               assertEquals(E01_TestEnum.BAR, C.to("BAR", E01_TestEnum.class));
+               assertEquals(E01_TestEnum.BAZ, C.to("BAZ", E01_TestEnum.class));
+       }
+
+       @Test void e02_unconvertibleToEnum() {
+               assertThrows(InvalidConversionException.class, () -> C.to(42, 
E01_TestEnum.class));
+       }
+
+       
//====================================================================================================
+       // f - Collection conversions
+       
//====================================================================================================
+
+       @Test void f01_listToList() {
+               var a = C.to(List.of("a", "b", "c"), List.class);
+               assertNotNull(a);
+               assertEquals(3, a.size());
+               assertEquals("a", a.get(0));
+       }
+
+       @Test void f02_listToListTyped() {
+               var a = C.to(List.of("1", "2", "3"), List.class, Integer.class);
+               assertNotNull(a);
+               assertEquals(3, ((List<?>) a).size());
+               assertEquals(1, ((List<?>) a).get(0));
+               assertInstanceOf(Integer.class, ((List<?>) a).get(0));
+       }
+
+       @Test void f03_arrayToList() {
+               var a = C.to(new String[]{"x", "y"}, List.class);
+               assertNotNull(a);
+               assertEquals(2, ((List<?>) a).size());
+               assertEquals("x", ((List<?>) a).get(0));
+       }
+
+       @Test void f03b_arrayToTypedList() {
+               // array branch with elemType != null — covers line 326 true 
branch of ternary
+               var a = C.to(new String[]{"1", "2", "3"}, List.class, 
Integer.class);
+               assertNotNull(a);
+               assertEquals(List.of(1, 2, 3), a);
+       }
+
+       @Test void f04_listToSet() {
+               var a = C.to(List.of("a", "b", "a"), Set.class);
+               assertNotNull(a);
+               assertInstanceOf(Set.class, a);
+               assertEquals(2, ((Set<?>) a).size());
+       }
+
+       @Test void f05_listToSortedSet() {
+               var a = C.to(List.of("c", "a", "b"), SortedSet.class);
+               assertNotNull(a);
+               assertInstanceOf(SortedSet.class, a);
+               assertEquals("a", ((SortedSet<?>) a).first());
+       }
+
+       @Test void f06_primitiveArrayToList() {
+               var a = C.to(new int[]{1, 2, 3}, List.class);
+               assertNotNull(a);
+               assertEquals(3, ((List<?>) a).size());
+               assertEquals(1, ((List<?>) a).get(0));
+       }
+
+       @Test void f07_collectionTargetVariants() {
+               var src = List.of("a", "b");
+               assertInstanceOf(Collection.class, C.to(src, Collection.class));
+               assertInstanceOf(List.class, C.to(src, Iterable.class));
+               assertInstanceOf(List.class, C.to(src, AbstractList.class));
+               assertInstanceOf(LinkedHashSet.class, C.to(src, 
LinkedHashSet.class));
+               assertInstanceOf(Set.class, C.to(src, AbstractSet.class));
+               assertInstanceOf(NavigableSet.class, C.to(src, 
NavigableSet.class));
+               assertInstanceOf(TreeSet.class, C.to(src, TreeSet.class));
+       }
+
+       @Test void f09_listToQueue() {
+               var a = C.to(List.of("a", "b"), Queue.class);
+               assertNotNull(a);
+               assertInstanceOf(Queue.class, a);
+               assertEquals(2, ((Queue<?>) a).size());
+       }
+
+       @Test void f10_listToConcreteCollection() {
+               var a = C.to(List.of("a", "b"), ArrayDeque.class);
+               assertNotNull(a);
+               assertInstanceOf(ArrayDeque.class, a);
+               assertEquals(2, ((ArrayDeque<?>) a).size());
+       }
+
+       public static class F11_NoDefaultCtorCollection extends 
ArrayList<Object> {
+               private static final long serialVersionUID = 1L;
+               public F11_NoDefaultCtorCollection(int initialCapacity) { 
super(initialCapacity); }
+       }
+
+       @Test void f11_listToCollectionWithNoDefaultCtor() {
+               var a = (Collection<?>) C.to(List.of("a", "b"), 
F11_NoDefaultCtorCollection.class);
+               assertNotNull(a);
+               assertInstanceOf(ArrayList.class, a);
+               assertEquals(2, a.size());
+       }
+
+       @Test void f12_arrayToCollectionInterface() {
+               // Array input forces newCollection(Collection.class) — hits 
line 336 B=true branch
+               var a = C.to(new String[]{"a", "b"}, Collection.class);
+               assertNotNull(a);
+               assertInstanceOf(Collection.class, a);
+               assertEquals(2, ((Collection<?>) a).size());
+       }
+
+       @Test void f13_arrayToAbstractList() {
+               // Array input forces newCollection(AbstractList.class) — hits 
line 336 D=true branch
+               var a = C.to(new String[]{"a", "b"}, AbstractList.class);
+               assertNotNull(a);
+               assertInstanceOf(AbstractList.class, a);
+               assertEquals(2, ((AbstractList<?>) a).size());
+       }
+
+       @Test void f14_listToDeque() {
+               // Hits line 342 B=true branch (Deque.class)
+               var a = C.to(List.of("a", "b"), Deque.class);
+               assertNotNull(a);
+               assertInstanceOf(Deque.class, a);
+               assertEquals(2, ((Deque<?>) a).size());
+       }
+
+       @Test void f15_listToLinkedList() {
+               // Hits line 342 C=true branch (LinkedList.class)
+               var a = C.to(List.of("a", "b"), LinkedList.class);
+               assertNotNull(a);
+               assertInstanceOf(LinkedList.class, a);
+               assertEquals(2, ((LinkedList<?>) a).size());
+       }
+
+       
//====================================================================================================
+       // g - Map conversions
+       
//====================================================================================================
+
+       @Test void g01_mapToMap() {
+               var a = C.to(Map.of("a", 1, "b", 2), Map.class);
+               assertNotNull(a);
+               assertInstanceOf(Map.class, a);
+               assertEquals(2, ((Map<?, ?>) a).size());
+       }
+
+       @Test void g02_mapToMapTyped() {
+               var a = C.to(Map.of(1, "100", 2, "200"), Map.class, 
String.class, Integer.class);
+               assertNotNull(a);
+               var map = (Map<?, ?>) a;
+               assertTrue(map.containsKey("1") || map.containsKey("2"));
+               for (var v : map.values())
+                       assertInstanceOf(Integer.class, v);
+       }
+
+       @Test void g03_mapToSortedMap() {
+               var a = C.to(Map.of("c", 3, "a", 1, "b", 2), SortedMap.class);
+               assertNotNull(a);
+               assertInstanceOf(SortedMap.class, a);
+               assertEquals("a", ((SortedMap<?, ?>) a).firstKey());
+       }
+
+       @Test void g04_mapToHashMap() {
+               var a = C.to(Map.of("a", 1), HashMap.class);
+               assertNotNull(a);
+               assertInstanceOf(HashMap.class, a);
+               assertEquals(1, ((HashMap<?, ?>) a).size());
+       }
+
+       @Test void g05_mapToConcreteMap() {
+               var a = C.to(Map.of("a", 1), ConcurrentHashMap.class);
+               assertNotNull(a);
+               assertInstanceOf(ConcurrentHashMap.class, a);
+               assertEquals(1, ((ConcurrentHashMap<?, ?>) a).size());
+       }
+
+       public static class G06_NoDefaultCtorMap extends HashMap<Object,Object> 
{
+               private static final long serialVersionUID = 1L;
+               public G06_NoDefaultCtorMap(int initialCapacity) { 
super(initialCapacity); }
+       }
+
+       @Test void g06_mapToMapWithNoDefaultCtor() {
+               var a = (Map<?, ?>) C.to(Map.of("a", 1), 
G06_NoDefaultCtorMap.class);
+               assertNotNull(a);
+               assertInstanceOf(LinkedHashMap.class, a);
+               assertEquals(1, a.size());
+       }
+
+       @Test void g07_mapToLinkedHashMap() {
+               // Hits line 370 B=true branch (LinkedHashMap.class)
+               var a = (Map<?, ?>) C.to(Map.of("a", 1), LinkedHashMap.class);
+               assertNotNull(a);
+               assertInstanceOf(LinkedHashMap.class, a);
+               assertEquals(1, a.size());
+       }
+
+       @Test void g08_mapToAbstractMap() {
+               // Hits line 370 C=true branch (AbstractMap.class); use typed 
args to bypass short-circuit
+               var a = (Map<?, ?>) C.to(Map.of("a", 1), AbstractMap.class, 
String.class, Integer.class);
+               assertNotNull(a);
+               assertInstanceOf(AbstractMap.class, a);
+               assertEquals(1, a.size());
+       }
+
+       @Test void g09_mapToNavigableMap() {
+               // Hits line 372 B=true branch (NavigableMap.class)
+               var a = (Map<?, ?>) C.to(Map.of("a", 1), NavigableMap.class);
+               assertNotNull(a);
+               assertInstanceOf(NavigableMap.class, a);
+               assertEquals(1, a.size());
+       }
+
+       @Test void g10_mapToTreeMap() {
+               // Hits line 372 C=true branch (TreeMap.class)
+               var a = (Map<?, ?>) C.to(Map.of("a", 1), TreeMap.class);
+               assertNotNull(a);
+               assertInstanceOf(TreeMap.class, a);
+               assertEquals(1, a.size());
+       }
+
+       @Test void g11_unconvertibleToMap() {
+               assertThrows(InvalidConversionException.class, () -> 
C.to("hello", Map.class));
+       }
+
+       
//====================================================================================================
+       // h - Array conversions
+       
//====================================================================================================
+
+       @Test void h01_listToArray() {
+               var a = C.to(List.of("a", "b", "c"), String[].class);
+               assertNotNull(a);
+               assertArrayEquals(new String[]{"a", "b", "c"}, a);
+       }
+
+       @Test void h02_arrayToTypedArray() {
+               var a = C.to(new Object[]{"1", "2", "3"}, Integer[].class);
+               assertNotNull(a);
+               assertArrayEquals(new Integer[]{1, 2, 3}, a);
+       }
+
+       @Test void h03_listToIntArray() {
+               var a = C.to(List.of(1, 2, 3), int[].class);
+               assertNotNull(a);
+               assertArrayEquals(new int[]{1, 2, 3}, (int[]) a);
+       }
+
+       @Test void h04_unconvertibleToArray() {
+               assertThrows(InvalidConversionException.class, () -> C.to(42, 
String[].class));
+       }
+
+       
//====================================================================================================
+       // i - Reflection: static factory methods
+       
//====================================================================================================
+
+       public static class I01_Target {
+               public final String value;
+               private I01_Target(String v) { value = v; }
+               public static I01_Target valueOf(String s) { return new 
I01_Target(s); }
+       }
+
+       @Test void i01_staticValueOf() {
+               var a = C.to("hello", I01_Target.class);
+               assertNotNull(a);
+               assertEquals("hello", a.value);
+       }
+
+       public static class I02_Target {
+               public final String value;
+               private I02_Target(String v) { value = v; }
+               public static I02_Target fromString(String s) { return new 
I02_Target(s); }
+       }
+
+       @Test void i02_staticFromString() {
+               var a = C.to("world", I02_Target.class);
+               assertNotNull(a);
+               assertEquals("world", a.value);
+       }
+
+       public static class I03_Target {
+               public final String value;
+               private I03_Target(String v) { value = v; }
+               public static I03_Target of(String s) { return new 
I03_Target(s); }
+       }
+
+       @Test void i03_staticOf() {
+               var a = C.to("test", I03_Target.class);
+               assertNotNull(a);
+               assertEquals("test", a.value);
+       }
+
+       public static class I04_Target {
+               public final String value;
+               private I04_Target(String v) { value = v; }
+               public static I04_Target create(String s) { return new 
I04_Target(s); }
+       }
+
+       @Test void i04_staticCreate() {
+               var a = C.to("create-test", I04_Target.class);
+               assertNotNull(a);
+               assertEquals("create-test", a.value);
+       }
+
+       public static class I05_Target {
+               public final String value;
+               private I05_Target(String v) { value = v; }
+               public static I05_Target parse(String s) { return new 
I05_Target(s); }
+       }
+
+       @Test void i05_staticParse() {
+               var a = C.to("parse-test", I05_Target.class);
+               assertNotNull(a);
+               assertEquals("parse-test", a.value);
+       }
+
+       public static class I06_Target {
+               public final String value;
+               private I06_Target(String v) { value = v; }
+               public static I06_Target from(String s) { return new 
I06_Target(s); }
+       }
+
+       @Test void i06_staticFrom() {
+               var a = C.to("from-test", I06_Target.class);
+               assertNotNull(a);
+               assertEquals("from-test", a.value);
+       }
+
+       public static class I07_Target {
+               public final String value;
+               private I07_Target(String v) { value = v; }
+               public static I07_Target forName(String s) { return new 
I07_Target(s); }
+       }
+
+       @Test void i07_staticForName() {
+               var a = C.to("forName-test", I07_Target.class);
+               assertNotNull(a);
+               assertEquals("forName-test", a.value);
+       }
+
+       public static class I08_Target {
+               public final String value;
+               private I08_Target(String v) { value = v; }
+               public static I08_Target fromValue(String s) { return new 
I08_Target(s); }
+       }
+
+       @Test void i08_staticFromValue() {
+               var a = C.to("fromValue-test", I08_Target.class);
+               assertNotNull(a);
+               assertEquals("fromValue-test", a.value);
+       }
+
+       public static class I09_Target {
+               public final String value;
+               private I09_Target(String v) { value = v; }
+               public static I09_Target builder(String s) { return new 
I09_Target(s); }
+       }
+
+       @Test void i09_staticBuilder() {
+               var a = C.to("builder-test", I09_Target.class);
+               assertNotNull(a);
+               assertEquals("builder-test", a.value);
+       }
+
+       public static class I10_Target {
+               public final String value;
+               private I10_Target(String v) { value = v; }
+               public static I10_Target fromString(String s) { return new 
I10_Target(s); }
+       }
+
+       @Test void i10_dynamicFromX() {
+               var a = C.to("fromString-test", I10_Target.class);
+               assertNotNull(a);
+               assertEquals("fromString-test", a.value);
+       }
+
+       public static class I11_Input {
+               public final String value;
+               public I11_Input(String v) { value = v; }
+       }
+
+       public static class I11_Target {
+               public final String value;
+               private I11_Target(String v) { value = v; }
+               public static I11_Target fromI11_Input(I11_Input s) { return 
new I11_Target(s.value); }
+       }
+
+       @Test void i11_dynamicFromInputClassName() {
+               var a = C.to(new I11_Input("dynamic-from"), I11_Target.class);
+               assertNotNull(a);
+               assertEquals("dynamic-from", a.value);
+       }
+
+       public static class I12_Input {
+               public final String value;
+               public I12_Input(String v) { value = v; }
+       }
+
+       public static class I12_Target {
+               public final String value;
+               private I12_Target(String v) { value = v; }
+               public static I12_Target forI12_Input(I12_Input s) { return new 
I12_Target(s.value); }
+       }
+
+       @Test void i12_dynamicForInputClassName() {
+               var a = C.to(new I12_Input("dynamic-for"), I12_Target.class);
+               assertNotNull(a);
+               assertEquals("dynamic-for", a.value);
+       }
+
+       public static class I13_Input {
+               public final String value;
+               public I13_Input(String v) { value = v; }
+       }
+
+       public static class I13_Target {
+               public final String value;
+               private I13_Target(String v) { value = v; }
+               public static I13_Target parseI13_Input(I13_Input s) { return 
new I13_Target(s.value); }
+       }
+
+       @Test void i13_dynamicParseInputClassName() {
+               var a = C.to(new I13_Input("dynamic-parse"), I13_Target.class);
+               assertNotNull(a);
+               assertEquals("dynamic-parse", a.value);
+       }
+
+       public static class I14_DecoyStaticMethod {
+               public static String fromString(String s) { return s; }  // 
returns String, not I14_DecoyStaticMethod
+       }
+
+       @Test void i14_staticMethodWrongReturnType() {
+               assertThrows(InvalidConversionException.class, () -> 
C.to("hello", I14_DecoyStaticMethod.class));
+       }
+
+       
//====================================================================================================
+       // j - Reflection: public constructors
+       
//====================================================================================================
+
+       public static class J01_Target {
+               public final String value;
+               public J01_Target(String v) { value = v; }
+       }
+
+       @Test void j01_constructorFromString() {
+               var a = C.to("ctor-test", J01_Target.class);
+               assertNotNull(a);
+               assertEquals("ctor-test", a.value);
+       }
+
+       public static class J02_Target {
+               public final int value;
+               public J02_Target(Integer v) { value = v; }
+       }
+
+       @Test void j02_constructorFromInteger() {
+               var a = C.to(99, J02_Target.class);
+               assertNotNull(a);
+               assertEquals(99, a.value);
+       }
+
+       
//====================================================================================================
+       // k - Reflection: toX() instance methods
+       
//====================================================================================================
+
+       public static class K01_Source {
+               private final int value;
+               public K01_Source(int v) { value = v; }
+               public Integer toInteger() { return value; }
+       }
+
+       @Test void k01_toXMethod() {
+               var a = C.to(new K01_Source(42), Integer.class);
+               assertNotNull(a);
+               assertEquals(42, a);
+       }
+
+       public static class K02_Source {
+               private final String value;
+               public K02_Source(String v) { value = v; }
+               public List<String> toList() { return 
List.of(value.split(",")); }
+       }
+
+       @Test void k02_toXMethodReturningCollection() {
+               var a = C.to(new K02_Source("a,b,c"), List.class);
+               assertNotNull(a);
+               assertEquals(List.of("a", "b", "c"), a);
+       }
+
+       public static class K03_WrongReturnType {
+               public String toInteger() { return "not-an-integer"; }  // 
returns String, not Integer
+       }
+
+       @Test void k03_toXMethodWrongReturnType() {
+               // toInteger() exists but returns String — hits line 488 false 
branch (hasReturnTypeParent fails)
+               assertThrows(InvalidConversionException.class, () -> C.to(new 
K03_WrongReturnType(), Integer.class));
+       }
+
+       
//====================================================================================================
+       // l - Special case conversions (TimeZone, Locale, String→Boolean)
+       
//====================================================================================================
+
+       @Test void l01_stringToTimeZone() {
+               var a = C.to("GMT", TimeZone.class);
+               assertNotNull(a);
+               assertEquals("GMT", a.getID());
+       }
+
+       @Test void l02_stringToTimeZoneWithOffset() {
+               var a = C.to("America/New_York", TimeZone.class);
+               assertNotNull(a);
+               assertEquals("America/New_York", a.getID());
+       }
+
+       @Test void l03_timeZoneToString() {
+               var a = C.to(TimeZone.getTimeZone("PST"), String.class);
+               assertEquals("PST", a);
+       }
+
+       @Test void l04_timeZoneToNonString() {
+               assertThrows(InvalidConversionException.class, () -> 
C.to(TimeZone.getTimeZone("PST"), Integer.class));
+       }
+
+       @Test void l05_stringToLocale() {
+               var a = C.to("en-US", Locale.class);
+               assertNotNull(a);
+               assertEquals("en", a.getLanguage());
+               assertEquals("US", a.getCountry());
+       }
+
+       @Test void l06_stringToLocaleWithUnderscore() {
+               var a = C.to("en_US", Locale.class);
+               assertNotNull(a);
+               assertEquals("en", a.getLanguage());
+               assertEquals("US", a.getCountry());
+       }
+
+       @Test void l07_stringToBooleanEmpty() {
+               assertNull(C.to("", Boolean.class));
+       }
+
+       @Test void l08_stringToBooleanNull() {
+               assertNull(C.to("null", Boolean.class));
+       }
+
+       @Test void l09_stringToBooleanTrue() {
+               assertEquals(true, C.to("true", Boolean.class));
+       }
+
+       @Test void l10_stringToBooleanFalse() {
+               assertEquals(false, C.to("false", Boolean.class));
+       }
+
+       
//====================================================================================================
+       // m - Identity / assignability
+       
//====================================================================================================
+
+       @Test void m01_identity() {
+               var a = "hello";
+               assertSame(a, C.to(a, String.class));
+       }
+
+       @Test void m02_widening() {
+               var a = new ArrayList<>(List.of("a", "b"));
+               var b = C.to(a, List.class);
+               assertSame(a, b);
+       }
+
+       @Test void m03_numberWidening() {
+               var a = Integer.valueOf(42);
+               var b = C.to(a, Number.class);
+               assertSame(a, b);
+       }
+
+       
//====================================================================================================
+       // n - Null handling
+       
//====================================================================================================
+
+       @Test void n01_nullInput() {
+               assertNull(C.to(null, String.class));
+               assertNull(C.to(null, Integer.class));
+               assertNull(C.to(null, List.class));
+               assertNull(C.to(null, Map.class));
+               assertNull(C.to(null, int.class));
+       }
+
+       
//====================================================================================================
+       // o - No conversion available
+       
//====================================================================================================
+
+       @Test void o01_noConversion() {
+               assertThrows(InvalidConversionException.class, () -> C.to(new 
Object(), BasicConverter_Test.class));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/CachingConverter_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/CachingConverter_Test.java
new file mode 100644
index 0000000000..91caefca46
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/CachingConverter_Test.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.conversion;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class CachingConverter_Test extends TestBase {
+
+       private static final BasicConverter C = BasicConverter.INSTANCE;
+
+       
//====================================================================================================
+       // a - canConvert
+       
//====================================================================================================
+
+       @Test void a01_canConvertSameType() {
+               assertTrue(C.canConvert(String.class, String.class));
+       }
+
+       @Test void a02_canConvertKnownConversion() {
+               assertTrue(C.canConvert(String.class, Integer.class));
+       }
+
+       @Test void a03_canConvertNoConversionAvailable() {
+               assertFalse(C.canConvert(StringBuilder.class, 
java.net.URI.class));
+       }
+
+       
//====================================================================================================
+       // b - to(Object, Type, Type...)
+       
//====================================================================================================
+
+       @Test void b01_toTypeNullInput() {
+               assertNull(C.to(null, (Type) List.class));
+       }
+
+       // Helper to get a ParameterizedType for List<String> via reflection.
+       @SuppressWarnings("unused")
+       private List<String> listOfStringField;
+       private static final Type LIST_OF_STRING;
+       static {
+               try {
+                       LIST_OF_STRING = 
CachingConverter_Test.class.getDeclaredField("listOfStringField").getGenericType();
+               } catch (NoSuchFieldException e) {
+                       throw new ExceptionInInitializerError(e);
+               }
+       }
+
+       @Test void b02_toParameterizedType() {
+               var result = C.to(List.of("a", "b"), LIST_OF_STRING);
+               assertNotNull(result);
+               assertInstanceOf(List.class, result);
+       }
+
+       @Test void b03_toTypeWithPlainClass() {
+               var result = C.to(42, (Type) String.class);
+               assertEquals("42", result);
+       }
+
+       @Test void b04_toTypeNoConversionAvailable() {
+               assertThrows(InvalidConversionException.class, () -> C.to(new 
StringBuilder("x"), (Type) java.net.URI.class));
+       }
+
+       @Test void b05_toTypeWithPlainClassArg() {
+               // args contains a plain Class (false branch of line 157 
ternary)
+               var result = C.to(List.of("1", "2"), (Type) List.class, (Type) 
Integer.class);
+               assertNotNull(result);
+               assertInstanceOf(List.class, result);
+               assertEquals(List.of(1, 2), result);
+       }
+
+       // Helper field used to obtain a ParameterizedType for use as an arg.
+       @SuppressWarnings("unused")
+       private List<String> listArgField;
+       private static final Type LIST_ARG_TYPE;
+       static {
+               try {
+                       LIST_ARG_TYPE = 
CachingConverter_Test.class.getDeclaredField("listArgField").getGenericType();
+               } catch (NoSuchFieldException e) {
+                       throw new ExceptionInInitializerError(e);
+               }
+       }
+
+       @Test void b06_toTypeWithParameterizedTypeArg() {
+               // args contains a ParameterizedType (true branch of line 157 
ternary); raw type List is extracted
+               var result = C.to(List.of(List.of("a", "b")), (Type) 
List.class, LIST_ARG_TYPE);
+               assertNotNull(result);
+               assertInstanceOf(List.class, result);
+       }
+}
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Converter.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/Converter_Test.java
similarity index 51%
copy from 
juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Converter.java
copy to 
juneau-utest/src/test/java/org/apache/juneau/commons/conversion/Converter_Test.java
index 0bcf6da49c..a592d83fc5 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/Converter.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/conversion/Converter_Test.java
@@ -16,18 +16,26 @@
  */
 package org.apache.juneau.commons.conversion;
 
-/**
- * Temporary interface.  To be replaced with Mutator once that's part of the 
common module.
- */
-public interface Converter {
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class Converter_Test extends TestBase {
+
+       
//====================================================================================================
+       // a - default canConvert
+       
//====================================================================================================
+
+       private static final Converter STUB = new Converter() {
+               @Override public <T> T to(Object o, Class<T> type) { return 
type.cast(o); }
+               @Override public <T> T to(Object o, Type mainType, Type... 
args) { return null; }
+       };
 
-       /**
-        * Converts the specified object to the specified type.
-        *
-        * @param <T> The type to convert to.
-        * @param type The type to convert to.
-        * @param o The object to convert.
-        * @return The converted object, or <jk>null</jk> if the conversion is 
not possible.
-        */
-       <T> T convertTo(Class<T> type, Object o);
+       @Test void a01_defaultCanConvertAlwaysTrue() {
+               assertTrue(STUB.canConvert(String.class, String.class));
+               assertTrue(STUB.canConvert(String.class, Integer.class));
+       }
 }
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/CharSequenceReader_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/CharSequenceReader_Test.java
index e29550ecbe..4cd6f026ec 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/CharSequenceReader_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/CharSequenceReader_Test.java
@@ -21,6 +21,9 @@ import static org.junit.jupiter.api.Assertions.*;
 import org.apache.juneau.*;
 import org.junit.jupiter.api.*;
 
+@SuppressWarnings({
+       "resource" // Readers intentionally not closed in unit tests
+})
 class CharSequenceReader_Test extends TestBase {
 
        
//====================================================================================================
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/NoCloseOutputStream_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/NoCloseOutputStream_Test.java
index c41d2238dd..540db7ded9 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/NoCloseOutputStream_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/NoCloseOutputStream_Test.java
@@ -23,6 +23,9 @@ import java.io.*;
 import org.apache.juneau.*;
 import org.junit.jupiter.api.*;
 
+@SuppressWarnings({
+       "resource" // NoCloseOutputStream wrappers intentionally not closed in 
unit tests
+})
 class NoCloseOutputStream_Test extends TestBase {
 
        
//====================================================================================================
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/NoCloseWriter_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/NoCloseWriter_Test.java
index 3c8842ae1c..8505017b4a 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/NoCloseWriter_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/NoCloseWriter_Test.java
@@ -23,6 +23,9 @@ import java.io.*;
 import org.apache.juneau.*;
 import org.junit.jupiter.api.*;
 
+@SuppressWarnings({
+       "resource" // NoCloseWriter wrappers intentionally not closed in unit 
tests
+})
 class NoCloseWriter_Test extends TestBase {
 
        
//====================================================================================================
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/ReaderInputStream_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/ReaderInputStream_Test.java
index 1d9efb4791..4fa3f9a06d 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/ReaderInputStream_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/ReaderInputStream_Test.java
@@ -24,6 +24,9 @@ import java.nio.charset.*;
 import org.apache.juneau.*;
 import org.junit.jupiter.api.*;
 
+@SuppressWarnings({
+       "resource" // Streams intentionally not closed in unit tests
+})
 class ReaderInputStream_Test extends TestBase {
 
        
//====================================================================================================
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/StringBuilderWriter_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/StringBuilderWriter_Test.java
index a60d30e9fb..eee414af67 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/io/StringBuilderWriter_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/io/StringBuilderWriter_Test.java
@@ -23,6 +23,9 @@ import java.io.*;
 import org.apache.juneau.*;
 import org.junit.jupiter.api.*;
 
+@SuppressWarnings({
+       "resource" // Writers intentionally not closed in unit tests
+})
 class StringBuilderWriter_Test extends TestBase {
 
        
//====================================================================================================
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/hjson/HjsonTokenizer_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/hjson/HjsonTokenizer_Test.java
index 3229e8fd96..73f68b7de9 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/hjson/HjsonTokenizer_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/hjson/HjsonTokenizer_Test.java
@@ -33,6 +33,9 @@ import org.junit.jupiter.params.provider.MethodSource;
 /**
  * Tests for {@link HjsonTokenizer}.
  */
+@SuppressWarnings({
+       "resource" // ParserPipe intentionally not closed in unit tests
+})
 class HjsonTokenizer_Test extends TestBase {
 
        private static HjsonTokenizer tokenizer(String input) {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/html/SimpleHtmlWriter_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/html/SimpleHtmlWriter_Test.java
index 4e32a8040b..6abefbff9b 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/html/SimpleHtmlWriter_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/html/SimpleHtmlWriter_Test.java
@@ -21,6 +21,9 @@ import org.junit.jupiter.api.*;
 /**
  * Tests for SimpleHtmlWriter fluent setter overrides.
  */
+@SuppressWarnings({
+       "resource" // Writers intentionally not closed in unit tests
+})
 class SimpleHtmlWriter_Test extends TestBase {
 
        @Test void a01_fluentChaining_tagMethods() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/uon/UonParserReader_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/uon/UonParserReader_Test.java
index dbaefb5e03..d2ef62cd85 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/uon/UonParserReader_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/uon/UonParserReader_Test.java
@@ -26,7 +26,8 @@ import org.apache.juneau.parser.*;
 import org.junit.jupiter.api.*;
 
 @SuppressWarnings({
-       "java:S5961" // High assertion count acceptable in comprehensive test
+       "java:S5961", // High assertion count acceptable in comprehensive test
+       "resource" // Readers intentionally not closed in unit tests
 })
 class UonParserReader_Test extends TestBase {
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/utils/MutatersTest.java 
b/juneau-utest/src/test/java/org/apache/juneau/utils/MutatersTest.java
index 093d7a883e..2e8eff62ef 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/utils/MutatersTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/utils/MutatersTest.java
@@ -23,7 +23,7 @@ import org.apache.juneau.*;
 import org.junit.jupiter.api.*;
 
 @SuppressWarnings({
-       "java:S1172" // Unused parameters in tests are intentional
+       "java:S1172"   // Unused parameters in tests are intentional
 })
 class MutatersTest extends TestBase {
 
diff --git a/scripts/coverage.py b/scripts/coverage.py
new file mode 100755
index 0000000000..cd7aeb7511
--- /dev/null
+++ b/scripts/coverage.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+# 
***************************************************************************************************************************
+# * 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.
+# 
***************************************************************************************************************************
+"""
+Coverage reporter for Apache Juneau.
+
+Shows JaCoCo branch and instruction coverage for a source file or folder.
+
+Usage:
+    ./scripts/coverage.py <path> [options]
+
+Arguments:
+    path    A source file (.java) or source folder to report on.
+            Paths can be absolute or relative to the repo root.
+
+Options:
+    --run, -r       Re-run tests before reporting (updates the .exec data).
+    --branches, -b  Show only lines with missed branches (default: show all 
uncovered).
+    --help, -h      Show this help message.
+
+Examples:
+    ./scripts/coverage.py 
juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/
+    ./scripts/coverage.py 
juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
+    ./scripts/coverage.py 
juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/ 
--run
+"""
+
+import subprocess
+import sys
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+UTEST_MODULE = REPO_ROOT / "juneau-utest"
+UTEST_EXEC = UTEST_MODULE / "target" / "jacoco.exec"
+
+SRC_MARKERS = ["src/main/java", "src/test/java"]
+
+
+def die(msg):
+    print(f"ERROR: {msg}", file=sys.stderr)
+    sys.exit(1)
+
+
+def find_maven_module(path: Path) -> Path:
+    """Walk up from path to find the nearest directory containing a pom.xml."""
+    p = path if path.is_dir() else path.parent
+    while p != REPO_ROOT.parent:
+        if (p / "pom.xml").exists() and p != REPO_ROOT:
+            return p
+        p = p.parent
+    return None
+
+
+def path_to_jacoco_package(path: Path) -> tuple[str | None, str | None]:
+    """
+    Given a source path, return (jacoco_package, filename).
+    jacoco_package uses slash separators (e.g. 
org/apache/juneau/commons/conversion).
+    filename is the .java filename, or None if path is a directory.
+    """
+    parts = path.parts
+    for marker in SRC_MARKERS:
+        marker_parts = tuple(marker.split("/"))
+        for i in range(len(parts) - len(marker_parts) + 1):
+            if parts[i:i + len(marker_parts)] == marker_parts:
+                remainder = parts[i + len(marker_parts):]
+                if path.is_file():
+                    pkg = "/".join(remainder[:-1])
+                    fname = remainder[-1]
+                else:
+                    pkg = "/".join(remainder).rstrip("/")
+                    fname = None
+                return pkg, fname
+    return None, None
+
+
+def run_tests():
+    """Re-run all tests in juneau-utest to refresh the .exec file."""
+    print("Running tests to refresh coverage data...")
+    result = subprocess.run(
+        ["mvn", "test", "-Drat.skip=true", "-q"],
+        cwd=UTEST_MODULE,
+        capture_output=True,
+        text=True
+    )
+    if result.returncode != 0:
+        print(result.stderr[-3000:], file=sys.stderr)
+        die("Tests failed. Fix failures before checking coverage.")
+    print("Tests passed.\n")
+
+
+def generate_report(module: Path) -> Path:
+    """Generate JaCoCo XML report for the given module using the utest exec 
file."""
+    xml_path = module / "target" / "site" / "jacoco" / "jacoco.xml"
+    result = subprocess.run(
+        ["mvn", "jacoco:report", f"-Djacoco.dataFile={UTEST_EXEC}", "-q"],
+        cwd=module,
+        capture_output=True,
+        text=True
+    )
+    if result.returncode != 0:
+        print(result.stderr[-2000:], file=sys.stderr)
+        die(f"Failed to generate JaCoCo report for {module}.")
+    return xml_path
+
+
+def bar(covered, total, width=20):
+    """Render a simple ASCII progress bar."""
+    if total == 0:
+        filled = width
+    else:
+        filled = round(covered / total * width)
+    return "[" + "#" * filled + "." * (width - filled) + "]"
+
+
+def pct(covered, total):
+    if total == 0:
+        return "100%"
+    return f"{covered / total * 100:.0f}%"
+
+
+def report(xml_path: Path, pkg_filter: str, file_filter: str | None, 
branches_only: bool):
+    """Parse jacoco.xml and print coverage for the matching package/file."""
+    if not xml_path.exists():
+        die(f"JaCoCo report not found at {xml_path}. Run with --run to 
generate it.")
+
+    tree = ET.parse(xml_path)
+    root = tree.getroot()
+
+    matched_packages = []
+    for pkg in root.findall("package"):
+        name = pkg.get("name", "")
+        if pkg_filter and not (name == pkg_filter or 
name.startswith(pkg_filter + "/")):
+            continue
+        matched_packages.append(pkg)
+
+    if not matched_packages:
+        die(f"No JaCoCo data found for package '{pkg_filter}'.\n"
+            f"Make sure the module is built and the exec file is up to date.")
+
+    # Collect per-file data
+    files_data = []  # list of (pkg_name, fname, lines_with_issues)
+    total_mb = total_cb = total_mi = total_ci = 0
+
+    for pkg in matched_packages:
+        pkg_name = pkg.get("name", "")
+        for sf in pkg.findall("sourcefile"):
+            fname = sf.get("name", "")
+            if file_filter and fname != file_filter:
+                continue
+
+            # Aggregate counters from <counter> elements
+            mb = cb = mi = ci = 0
+            for ctr in sf.findall("counter"):
+                t = ctr.get("type")
+                m = int(ctr.get("missed", 0))
+                c = int(ctr.get("covered", 0))
+                if t == "BRANCH":
+                    mb, cb = m, c
+                elif t == "INSTRUCTION":
+                    mi, ci = m, c
+
+            total_mb += mb
+            total_cb += cb
+            total_mi += mi
+            total_ci += ci
+
+            # Collect uncovered lines
+            uncovered = []
+            for line in sf.findall("line"):
+                ln = int(line.get("nr", 0))
+                lmb = int(line.get("mb", 0))
+                lmi = int(line.get("mi", 0))
+                if branches_only and lmb > 0:
+                    lcb = int(line.get("cb", 0))
+                    uncovered.append((ln, lmb, lmb + lcb, lmi))
+                elif not branches_only and (lmb > 0 or lmi > 0):
+                    lcb = int(line.get("cb", 0))
+                    uncovered.append((ln, lmb, lmb + lcb, lmi))
+
+            files_data.append((pkg_name, fname, mb, cb, mi, ci, uncovered))
+
+    if not files_data:
+        print(f"No data found for the specified path.")
+        return
+
+    # Print per-file results
+    for pkg_name, fname, mb, cb, mi, ci, uncovered in sorted(files_data):
+        branch_total = mb + cb
+        instr_total = mi + ci
+        branch_pct = pct(cb, branch_total)
+        instr_pct = pct(ci, instr_total)
+        print(f"\n{'='*70}")
+        print(f"  {pkg_name.replace('/', '.')}.{fname.removesuffix('.java')}")
+        print(f"{'='*70}")
+        print(f"  Branches:     {bar(cb, branch_total)}  {branch_pct:>4}  
({cb}/{branch_total} covered, {mb} missed)")
+        print(f"  Instructions: {bar(ci, instr_total)}  {instr_pct:>4}  
({ci}/{instr_total} covered, {mi} missed)")
+        if uncovered:
+            print(f"\n  Uncovered lines:")
+            for ln, lmb, ltotal, lmi in sorted(uncovered):
+                parts = []
+                if lmb > 0:
+                    parts.append(f"{lmb}/{ltotal} branches missed")
+                if lmi > 0:
+                    parts.append(f"{lmi} instructions missed")
+                print(f"    line {ln:4d}:  {', '.join(parts)}")
+        else:
+            print(f"\n  All lines covered!")
+
+    # Print summary if multiple files
+    if len(files_data) > 1:
+        branch_total = total_mb + total_cb
+        instr_total = total_mi + total_ci
+        print(f"\n{'='*70}")
+        print(f"  TOTAL SUMMARY")
+        print(f"{'='*70}")
+        print(f"  Branches:     {bar(total_cb, branch_total)}  {pct(total_cb, 
branch_total):>4}  ({total_cb}/{branch_total} covered, {total_mb} missed)")
+        print(f"  Instructions: {bar(total_ci, instr_total)}  {pct(total_ci, 
instr_total):>4}  ({total_ci}/{instr_total} covered, {total_mi} missed)")
+        print()
+
+
+def main():
+    args = sys.argv[1:]
+    if not args or "--help" in args or "-h" in args:
+        print(__doc__)
+        return 0
+
+    path_arg = None
+    do_run = False
+    branches_only = False
+
+    for arg in args:
+        if arg in ("--run", "-r"):
+            do_run = True
+        elif arg in ("--branches", "-b"):
+            branches_only = True
+        elif arg.startswith("-"):
+            die(f"Unknown option: {arg}")
+        else:
+            path_arg = arg
+
+    if not path_arg:
+        die("No path specified.")
+
+    path = Path(path_arg)
+    if not path.is_absolute():
+        path = REPO_ROOT / path
+    path = path.resolve()
+
+    if not path.exists():
+        die(f"Path does not exist: {path}")
+
+    module = find_maven_module(path)
+    if not module:
+        die(f"Could not determine Maven module for path: {path}")
+
+    pkg_filter, file_filter = path_to_jacoco_package(path)
+    if pkg_filter is None:
+        die(f"Path does not appear to be under src/main/java or src/test/java: 
{path}")
+
+    if do_run:
+        run_tests()
+
+    if not UTEST_EXEC.exists():
+        die(f"No exec file found at {UTEST_EXEC}. Run with --run first.")
+
+    print(f"Generating JaCoCo report for module: 
{module.relative_to(REPO_ROOT)}")
+    xml_path = generate_report(module)
+
+    report(xml_path, pkg_filter, file_filter, branches_only)
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

Reply via email to