You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/04/02 13:52:01 UTC

(juneau) branch master updated: New BasicConverter class

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 <ja...@jamesbog-ltmjqf2.internal.salesforce.com>
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())