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 dbe43ee1d7 org.apache.juneau.common.reflect API improvements
dbe43ee1d7 is described below

commit dbe43ee1d792a2d1b025fa9323656b8d2da8d249
Author: James Bognar <[email protected]>
AuthorDate: Tue Nov 25 07:55:03 2025 -0500

    org.apache.juneau.common.reflect API improvements
---
 TODO-reflectionMigrationPlan.md                    | 424 ---------------
 .../collections/ControlledArrayList_Test.java      | 187 -------
 .../collections/ControlledArrayList_Test.java      | 602 +++++++++++++++++++++
 .../juneau/common/collections/MapBuilder_Test.java | 397 ++++++++++++++
 4 files changed, 999 insertions(+), 611 deletions(-)

diff --git a/TODO-reflectionMigrationPlan.md b/TODO-reflectionMigrationPlan.md
deleted file mode 100644
index eeeaba5a07..0000000000
--- a/TODO-reflectionMigrationPlan.md
+++ /dev/null
@@ -1,424 +0,0 @@
-# Reflection Classes Migration Plan
-
-## Goal
-Move the following classes from `org.apache.juneau.reflect` (juneau-marshall) 
to `org.apache.juneau.common.reflect` (juneau-common):
-- `ClassInfo`
-- `ConstructorInfo`
-- `ExecutableInfo`
-- `FieldInfo`
-- `MethodInfo`
-- `ParamInfo`
-- `AnnotationInfo` *(added - must move with ClassInfo/MethodInfo due to 
circular references)*
-- `AnnotationList` *(added - must move with AnnotationInfo)*
-
-## Current Location
-All eight classes are currently in:
-- **Package**: `org.apache.juneau.reflect`
-- **Module**: `juneau-core/juneau-marshall`
-- **Path**: 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/`
-
-## Target Location
-- **Package**: `org.apache.juneau.common.reflect`
-- **Module**: `juneau-core/juneau-common`
-- **Path**: 
`juneau-core/juneau-common/src/main/java/org/apache/juneau/common/reflect/`
-
-## Note on Circular Dependencies
-`AnnotationInfo` and `AnnotationList` were added to the migration because they 
have circular references with `ClassInfo` and `MethodInfo`. Phase 1 
successfully removed all juneau-marshall dependencies from these classes so 
they can now move to juneau-common, but they must move together with the other 
reflection classes.
-
-## Dependency Analysis
-
-### 1. ClassInfo.java
-**Status**: ✅ **READY TO MOVE**
-
-**Current Imports from juneau-marshall**: None
-
-**Dependencies**:
-- ✅ `org.apache.juneau.common.collections.*` - Already in juneau-common
-- ✅ `org.apache.juneau.common.reflect.*` - Already in juneau-common
-- ✅ `org.apache.juneau.common.utils.*` - Already in juneau-common
-
-**Issues**: None
-
----
-
-### 2. FieldInfo.java
-**Status**: ⚠️ **HAS UNUSED IMPORT**
-
-**Current Imports from juneau-marshall**:
-- `import org.apache.juneau.*;` (line 28)
-
-**Dependencies**:
-- ✅ `org.apache.juneau.common.collections.*` - Already in juneau-common
-- ✅ `org.apache.juneau.common.reflect.*` - Already in juneau-common
-
-**Issues**: 
-- The wildcard import `org.apache.juneau.*` appears to be unused (no 
references found to BeanContext, Context, etc.)
-
-**Resolution**:
-- Remove the unused `import org.apache.juneau.*;` statement
-
----
-
-### 3. ParamInfo.java
-**Status**: ✅ **READY TO MOVE**
-
-**Current Imports from juneau-marshall**: None
-
-**Dependencies**:
-- ✅ `org.apache.juneau.common.collections.*` - Already in juneau-common
-- ✅ `org.apache.juneau.common.reflect.*` - Already in juneau-common
-
-**Issues**: None
-
-**Resolution Applied**: 
-- **Used reflection to work with any @Name annotation**
-- Removed compile-time dependency on `org.apache.juneau.annotation.Name`
-- `ParamInfo` now searches for any annotation with simple name "Name" using 
reflection
-- Calls the annotation's `value()` method dynamically to extract the parameter 
name
-- Works with `@Name` from any package without requiring a compile-time 
dependency
-- Maintains backward compatibility with existing `@Name` annotations
-- Implementation in `getNameFromAnnotation()` helper method
-
----
-
-### 4. MethodInfo.java
-**Status**: ❌ **BLOCKED - Requires ClassUtils2.getMatchingArgs()**
-
-**Current Imports from juneau-marshall**:
-- `import org.apache.juneau.internal.*;` (line 31)
-
-**Dependencies**:
-- ✅ `org.apache.juneau.common.collections.*` - Already in juneau-common
-- ✅ `org.apache.juneau.common.reflect.*` - Already in juneau-common
-- ❌ **`ClassUtils2.getMatchingArgs()`** - Currently in 
`org.apache.juneau.internal.ClassUtils2` (juneau-marshall)
-
-**Usage of ClassUtils2**:
-- Line 667: `return m.invoke(pojo, 
ClassUtils2.getMatchingArgs(m.getParameterTypes(), args));`
-
-**Issues**:
-- `MethodInfo.invoke()` uses `ClassUtils2.getMatchingArgs()` to match varargs 
parameters
-- `ClassUtils2` is in juneau-marshall's internal package
-
-**Resolution Options**:
-1. **Move getMatchingArgs() to ClassUtils in juneau-common** (RECOMMENDED)
-   - Extract `ClassUtils2.getMatchingArgs()` method
-   - Move to `org.apache.juneau.common.utils.ClassUtils`
-   - Make it public API (it's a useful utility)
-   - Update call in `MethodInfo`
-
-2. **Inline the logic**
-   - Copy the parameter matching logic directly into `MethodInfo.invoke()`
-   - Removes dependency but duplicates code
-
-3. **Remove the feature**
-   - Make `MethodInfo.invoke()` not support parameter reordering
-   - Would be a breaking change
-
----
-
-### 5. ConstructorInfo.java
-**Status**: ❌ **BLOCKED - Requires ClassUtils2.getMatchingArgs()**
-
-**Current Imports from juneau-marshall**:
-- `import org.apache.juneau.internal.*;` (line 28)
-
-**Dependencies**:
-- ✅ `org.apache.juneau.common.collections.*` - Already in juneau-common
-- ✅ `org.apache.juneau.common.reflect.*` - Already in juneau-common
-- ❌ **`ClassUtils2.getMatchingArgs()`** - Currently in 
`org.apache.juneau.internal.ClassUtils2` (juneau-marshall)
-
-**Usage of ClassUtils2**:
-- Line 249: `return invoke(ClassUtils2.getMatchingArgs(c.getParameterTypes(), 
args));`
-
-**Issues**: 
-- Same as MethodInfo - uses `ClassUtils2.getMatchingArgs()`
-
-**Resolution**:
-- Same as MethodInfo resolution
-
----
-
-### 6. ExecutableInfo.java
-**Status**: ✅ **READY TO MOVE**
-
-**Current Imports from juneau-marshall**: None
-
-**Dependencies**:
-- ✅ `org.apache.juneau.common.reflect.*` - Already in juneau-common
-
-**Issues**: None
-
----
-
-### 7. AnnotationInfo.java
-**Status**: ✅ **READY TO MOVE** (must move with ClassInfo/MethodInfo)
-
-**Current Imports from juneau-marshall**: None (all removed in Phase 1c)
-
-**Dependencies**:
-- ✅ `org.apache.juneau.common.annotation.*` - Already in juneau-common 
(AnnotationGroup)
-- ✅ `org.apache.juneau.common.reflect.*` - Already in juneau-common 
(ExecutableException)
-- ✅ `org.apache.juneau.common.utils.*` - Already in juneau-common
-
-**Circular References**:
-- Has fields of type `ClassInfo` and `MethodInfo`
-- Has static methods that take `ClassInfo` and `MethodInfo` as parameters
-- **Must move together with ClassInfo/MethodInfo**
-
-**Issues**: None (all juneau-marshall dependencies removed in Phase 1c)
-
-**Phase 1c Changes Applied**:
-- ✅ Refactored `toJsonMap()` → `toMap()` using `LinkedHashMap` instead of 
`JsonMap`
-- ✅ Simplified `toString()` to use standard Java instead of `Json5`
-- ✅ Removed `getApplies()` method (logic moved to `AnnotationWorkList`)
-- ✅ Removed `applyConstructors` field
-
----
-
-### 8. AnnotationList.java
-**Status**: ✅ **READY TO MOVE** (must move with AnnotationInfo)
-
-**Current Imports from juneau-marshall**: None
-
-**Dependencies**:
-- ✅ `org.apache.juneau.common.utils.*` - Already in juneau-common 
(PredicateUtils)
-
-**Issues**: None
-
----
-
-## Migration Plan
-
-### Phase 1: Prepare AnnotationInfo/AnnotationList for migration ✅ COMPLETED
-**Goal**: Remove juneau-marshall dependencies from 
AnnotationInfo/AnnotationList so they can move to juneau-common together with 
ClassInfo/MethodInfo.
-
-**Sub-phase 1a: Add static methods to AnnotationInfo** ✅
-- Added static methods that take ClassInfo/MethodInfo as parameters
-- Made `ClassInfo.splitRepeated()` and `MethodInfo.findMatchingOnClass()` 
package-private
-
-**Sub-phase 1b: Remove annotation methods from ClassInfo/MethodInfo** ✅
-- Removed methods returning AnnotationList from ClassInfo/MethodInfo
-- Updated all callers (~108 call sites) to use static methods on AnnotationInfo
-
-**Sub-phase 1c: Remove juneau-marshall dependencies from AnnotationInfo** ✅
-1. ✅ **Refactored `toJsonMap()` and `toString()`**
-   - Changed `toJsonMap()` → `toMap()` returning `LinkedHashMap<String, 
Object>`
-   - Simplified `toString()` to use standard Java `toString()`
-   - Removed dependencies: `JsonMap`, `Json5`
-
-2. ✅ **Moved `AnnotationGroup` to juneau-common** (user action)
-   - Moved from `org.apache.juneau.annotation.AnnotationGroup`
-   - To `org.apache.juneau.common.annotation.AnnotationGroup`
-   - Result: `isInGroup()` method now clean
-
-3. ✅ **Refactored `getApplies()` method**
-   - Moved applier instantiation logic from `AnnotationInfo.getApplies()` to 
`AnnotationWorkList.applyAnnotation()`
-   - Removed `getApplies()` method from AnnotationInfo
-   - Removed `applyConstructors` field from AnnotationInfo
-   - Removed dependencies: `VarResolverSession`, `ContextApply`, 
`AnnotationApplier`
-
-**Final AnnotationInfo dependencies (all juneau-common or JDK):**
-- ✅ Standard Java (JDK) classes only
-- ✅ `org.apache.juneau.common.annotation.*` (AnnotationGroup)
-- ✅ `org.apache.juneau.common.reflect.*` (ExecutableException)
-- ✅ `org.apache.juneau.common.utils.*` (utility methods)
-
-**Final AnnotationList dependencies (all juneau-common or JDK):**
-- ✅ Standard Java (JDK) classes only
-- ✅ `org.apache.juneau.common.utils.*` (PredicateUtils)
-
-**Result:** ✅ Both AnnotationInfo and AnnotationList are now ready to move to 
juneau-common (must move together with ClassInfo/MethodInfo due to circular 
references)
-
-### Phase 2: Prepare juneau-common ✅ COMPLETED (pending Phase 3)
-**Goal**: Add `getMatchingArgs()` method to ClassUtils in juneau-common so 
that MethodInfo and ConstructorInfo can use it after migration.
-
-**Changes completed:**
-
-1. ✅ **Added `getMatchingArgs()` to 
`org.apache.juneau.common.utils.ClassUtils`**
-   - Extracted logic from `ClassUtils2.getMatchingArgs()` in juneau-marshall
-   - Added comprehensive Javadoc with detailed examples
-   - Covers all use cases: argument reordering, missing parameters, extra 
parameters, primitive/wrapper handling, type hierarchy
-   - Method signature: `public static Object[] getMatchingArgs(Class<?>[] 
paramTypes, Object...args)`
-
-2. ✅ **Updated MethodInfo to use new location**
-   - Changed import from `org.apache.juneau.internal.*` to 
`org.apache.juneau.common.utils.*`
-   - Changed call from `ClassUtils2.getMatchingArgs()` to 
`ClassUtils.getMatchingArgs()`
-   - File: 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/MethodInfo.java`
 (line 654)
-
-3. ✅ **Updated ConstructorInfo to use new location**
-   - Changed import from `org.apache.juneau.internal.*` to 
`org.apache.juneau.common.utils.*`
-   - Changed call from `ClassUtils2.getMatchingArgs()` to 
`ClassUtils.getMatchingArgs()`
-   - File: 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/ConstructorInfo.java`
 (line 249)
-
-**Note:** juneau-common won't compile yet because 
`ClassUtils.getMatchingArgs()` references `ClassInfo.of()`, and `ClassInfo` is 
still in juneau-marshall. This will be resolved in Phase 3 when all 8 
reflection classes move together to juneau-common.
-
-### Phase 3: Move reflection classes ✅ COMPLETED
-**Goal**: Move all 8 reflection classes to juneau-common and update all 
imports across the codebase.
-
-**Changes completed:**
-
-1. ✅ **Created target directory** in juneau-common
-   - 
`juneau-core/juneau-common/src/main/java/org/apache/juneau/common/reflect/`
-
-2. ✅ **Moved all 8 classes using git mv**
-   - ClassInfo.java
-   - ConstructorInfo.java
-   - ExecutableInfo.java
-   - FieldInfo.java
-   - MethodInfo.java
-   - ParamInfo.java
-   - AnnotationInfo.java
-   - AnnotationList.java
-
-3. ✅ **Updated package declarations** in all 8 classes
-   - Changed from `package org.apache.juneau.reflect;`
-   - To `package org.apache.juneau.common.reflect;`
-
-4. ✅ **Updated all imports across codebase** (196+ files)
-   - Changed `import org.apache.juneau.reflect.*` to `import 
org.apache.juneau.common.reflect.*`
-   - Changed all static imports as well
-
-5. ✅ **Fixed remaining juneau-marshall files** that still needed access to 
Mutater/Mutaters
-   - Added `import org.apache.juneau.reflect.*;` for Mutater/Mutaters (which 
remain in juneau-marshall)
-   - Fixed: ClassMeta.java, SimplePartParserSession.java, 
SimplePartSerializerSession.java, UonSerializerSession.java, RrpcServlet.java
-
-6. ✅ **Moved test files** to match new package structure
-   - Moved all test files from 
`juneau-utest/src/test/java/org/apache/juneau/reflect/`
-   - To `juneau-utest/src/test/java/org/apache/juneau/common/reflect/`
-   - Updated package declarations in all test files
-   - This was necessary to maintain package-private access to internal methods
-   - Files moved: AnnotationInfoTest.java, ClassInfo_Test.java, 
ConstructorInfoTest.java, ExecutableInfo_Test.java, FieldInfo_Test.java, 
MethodInfo_Test.java, ParamInfoTest.java, AClass.java, AInterface.java, 
PA.java, package-info.java
-
-7. ✅ **Fixed test imports**
-   - Updated ClassMeta_Test.java to import from new location
-   - Fixed MutatersTest.java to import Mutaters from old location (still in 
juneau-marshall)
-   - Removed duplicate imports
-
-**Result:** ✅ **Full project compilation successful!**
-- juneau-common: 101 source files (up from 93)
-- juneau-marshall: Compiles successfully
-- All modules: Compile and test-compile successfully
-- Total time: ~17 seconds
-
-### Phase 4: Create backward compatibility
-1. **Add deprecated aliases in juneau-marshall**
-   - Create `@Deprecated` classes in old package `org.apache.juneau.reflect`
-   - Each class extends the new location
-   - Point users to new location in deprecation message
-
-### Phase 5: Testing & Documentation
-1. **Run all tests** to ensure nothing broke
-2. **Update documentation** to reference new package
-3. **Update MIGRATION.md** with notes about deprecated classes
-
-## Summary of Blockers
-
-### ✅ Resolved:
-
-1. **AnnotationInfo/AnnotationList juneau-marshall dependencies** - ✅ 
**RESOLVED**
-   - Status: Phase 1 completed successfully
-   - Solution implemented:
-     - Sub-phase 1a: Added static methods to AnnotationInfo that take 
ClassInfo/MethodInfo as parameters
-     - Sub-phase 1b: Removed AnnotationList-returning methods from 
ClassInfo/MethodInfo
-     - Sub-phase 1c: Removed all juneau-marshall dependencies from 
AnnotationInfo (toJsonMap/toString refactoring, getApplies refactoring, 
AnnotationGroup moved)
-   - Result: AnnotationInfo and AnnotationList can now move to juneau-common 
(must move together with ClassInfo/MethodInfo due to circular references)
-
-2. **ClassUtils2.getMatchingArgs()** (affects MethodInfo, ConstructorInfo) - ✅ 
**RESOLVED**
-   - Status: Phase 2 completed successfully
-   - Solution implemented:
-     - Added `getMatchingArgs()` to `org.apache.juneau.common.utils.ClassUtils`
-     - Updated MethodInfo and ConstructorInfo to use new location
-   - Result: MethodInfo and ConstructorInfo are ready to move (pending Phase 3 
when ClassInfo moves)
-
-### Can be fixed during migration:
-
-3. **Unused import** (affects FieldInfo)
-   - `import org.apache.juneau.*;` appears to be unused
-   - Simply remove it
-
-### ✅ Already Resolved:
-
-4. **@Name annotation dependency** (was affecting ParamInfo) - **FIXED**
-   - Used reflection to work with any annotation named "Name"
-   - No longer requires compile-time dependency on specific annotation
-   - Works with `@Name` from any package
-
-## Recommended Approach
-
-**Complete Migration with Refactoring** (IN PROGRESS)
-1. ✅ **Phase 1**: Prepare AnnotationInfo/AnnotationList for migration 
(COMPLETED)
-   - ✅ Sub-phase 1a: Added static methods to AnnotationInfo
-   - ✅ Sub-phase 1b: Removed AnnotationList-returning methods from 
ClassInfo/MethodInfo
-   - ✅ Sub-phase 1c: Removed all juneau-marshall dependencies from 
AnnotationInfo
-2. ⏳ **Phase 2**: Add getMatchingArgs() to ClassUtils in juneau-common (IN 
PROGRESS)
-3. ⏭️ **Phase 3**: Move all 8 reflection classes to juneau-common
-4. ⏭️ **Phase 4**: Create deprecated aliases for backward compatibility
-5. ⏭️ **Phase 5**: Test and document
-
-**Benefits:**
-- Provides full reflection capability in juneau-common
-- Removes circular dependencies between modules
-- All 8 reflection classes can move together to juneau-common
-- Better separation of concerns (annotation processing vs reflection utilities)
-
-## Phase 1c Summary (Completed)
-
-This phase was critical additional work discovered after starting Phase 1. The 
goal was to remove all juneau-marshall dependencies from `AnnotationInfo` and 
`AnnotationList` so they could move to juneau-common.
-
-**Work Completed:**
-
-1. **Refactored `toJsonMap()` and `toString()` methods**
-   - Problem: Used `JsonMap` (juneau-marshall) and `Json5` (juneau-marshall) 
-   - Solution: Changed to use standard Java `LinkedHashMap` and `.toString()`
-   - Files modified: `AnnotationInfo.java`
-
-2. **Moved `AnnotationGroup` annotation to juneau-common**
-   - Problem: `isInGroup()` method depended on `AnnotationGroup` annotation in 
juneau-marshall
-   - Solution: User moved `AnnotationGroup` from 
`org.apache.juneau.annotation` to `org.apache.juneau.common.annotation`
-   - Result: `isInGroup()` method now has no juneau-marshall dependencies
-
-3. **Refactored `getApplies()` method**
-   - Problem: Method used `VarResolverSession`, `ContextApply`, and 
`AnnotationApplier` from juneau-marshall
-   - Solution: Moved applier instantiation logic to 
`AnnotationWorkList.applyAnnotation()` helper method
-   - Files modified: `AnnotationInfo.java`, `AnnotationWorkList.java`
-   - Removed: `getApplies()` method, `applyConstructors` field
-   - Call sites updated: 1 (in `AnnotationWorkList`)
-
-**Final Result:**
-- ✅ `AnnotationInfo` now depends ONLY on juneau-common and JDK classes
-- ✅ `AnnotationList` now depends ONLY on juneau-common and JDK classes
-- ✅ Both classes ready to move (must move with ClassInfo/MethodInfo due to 
circular references)
-- ✅ Full project compilation successful
-
-## Next Steps
-
-1. ✅ **Phase 1 Complete**: Prepare AnnotationInfo/AnnotationList for migration
-   - ✅ Sub-phase 1a: Added static methods to AnnotationInfo
-   - ✅ Sub-phase 1b: Removed annotation methods from ClassInfo/MethodInfo
-   - ✅ Sub-phase 1c: Removed all juneau-marshall dependencies from 
AnnotationInfo
-     - ✅ Refactored toJsonMap()/toString()
-     - ✅ AnnotationGroup moved to juneau-common
-     - ✅ Refactored getApplies() method
-   - ✅ Full project compilation successful
-
-2. ✅ **Phase 2 Complete**: Add getMatchingArgs() to ClassUtils
-   - ✅ Extracted `ClassUtils2.getMatchingArgs()` logic from juneau-marshall
-   - ✅ Added as public method to `org.apache.juneau.common.utils.ClassUtils` 
with comprehensive javadoc
-   - ✅ Updated MethodInfo and ConstructorInfo to use new ClassUtils location
-   - Note: juneau-common won't compile until Phase 3 (ClassInfo still in 
juneau-marshall)
-
-3. ✅ **Phase 3 Complete**: Move all 8 reflection classes to juneau-common
-   - ✅ Moved all 8 classes using git mv
-   - ✅ Updated package declarations in all classes
-   - ✅ Updated 196+ import statements across entire codebase
-   - ✅ Fixed juneau-marshall files needing Mutater/Mutaters
-   - ✅ Moved test files to new package structure
-   - ✅ Full project compilation successful
-
-4. ⏭️ **Phase 4**: SKIPPED - Backward compatibility not needed for internal 
APIs
-
-5. ⏭️ **Phase 5**: Test and document
-   - Run all tests
-   - Update documentation (if needed)
-   - Update MIGRATION.md (if needed)
-
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/collections/ControlledArrayList_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/collections/ControlledArrayList_Test.java
deleted file mode 100644
index 54479a5119..0000000000
--- 
a/juneau-utest/src/test/java/org/apache/juneau/collections/ControlledArrayList_Test.java
+++ /dev/null
@@ -1,187 +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.
- */
-package org.apache.juneau.collections;
-
-import static org.apache.juneau.common.utils.CollectionUtils.*;
-import static org.junit.jupiter.api.Assertions.*;
-
-import org.apache.juneau.*;
-import org.apache.juneau.common.collections.*;
-import org.junit.jupiter.api.*;
-
-class ControlledArrayList_Test extends TestBase {
-
-       
//-----------------------------------------------------------------------------------------------------------------
-       // test - Basic tests
-       
//-----------------------------------------------------------------------------------------------------------------
-
-       @Test void a01_constructors() {
-               var x = new ControlledArrayList<>(false);
-               assertTrue(x.isModifiable());
-
-               x = new ControlledArrayList<>(true);
-               assertFalse(x.isModifiable());
-
-               x = new ControlledArrayList<>(false, l(1));
-               assertTrue(x.isModifiable());
-
-               x = new ControlledArrayList<>(true, l(1));
-               assertFalse(x.isModifiable());
-       }
-
-       @Test void a02_basicMethods() {
-               var x1 = new ControlledArrayList<>(false, l(1));
-               var x2 = new ControlledArrayList<>(true, l(1));
-
-               x1.set(0, 2);
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.set(0, 2));
-               x2.overrideSet(0, 2);
-               assertEquals(x2, x1);
-
-               x1.add(0, 2);
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.add(0, 2));
-               x2.overrideAdd(0, 2);
-               assertEquals(x2, x1);
-
-               x1.remove(0);
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.remove(0));
-               x2.overrideRemove(0);
-               assertEquals(x2, x1);
-
-               x1.addAll(0, l(3));
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.addAll(0, l(3)));
-               x2.overrideAddAll(0, l(3));
-               assertEquals(x2, x1);
-
-               x1.replaceAll(x -> x);
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.replaceAll(x -> x));
-               x2.overrideReplaceAll(x -> x);
-               assertEquals(x2, x1);
-
-               x1.sort(null);
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.sort(null));
-               x2.overrideSort(null);
-               assertEquals(x2, x1);
-
-               x1.add(1);
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.add(1));
-               x2.overrideAdd(1);
-               assertEquals(x2, x1);
-
-               x1.remove((Integer)1);
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.remove((Integer)1));
-               x2.overrideRemove((Integer)1);
-               assertEquals(x2, x1);
-
-               x1.addAll(l(3));
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.addAll(l(3)));
-               x2.overrideAddAll(l(3));
-               assertEquals(x2, x1);
-
-               x1.removeAll(l(3));
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.removeAll(l(3)));
-               x2.overrideRemoveAll(l(3));
-               assertEquals(x2, x1);
-
-               x1.retainAll(l(2));
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.retainAll(l(2)));
-               x2.overrideRetainAll(l(2));
-               assertEquals(x2, x1);
-
-               x1.clear();
-               assertThrows(UnsupportedOperationException.class, x2::clear);
-               x2.overrideClear();
-               assertEquals(x2, x1);
-
-               x1.add(1);
-               x2.overrideAdd(1);
-
-               x1.removeIf(x -> x == 1);
-               assertThrows(UnsupportedOperationException.class, () -> 
x2.removeIf(x -> x == 1));
-               x2.overrideRemoveIf(x -> x == 1);
-               assertEquals(x2, x1);
-
-               x1.add(1);
-               x2.overrideAdd(1);
-
-               var x1a = (ControlledArrayList<Integer>) x1.subList(0, 0);
-               var x2a = (ControlledArrayList<Integer>) x2.subList(0, 0);
-               assertTrue(x1a.isModifiable());
-               assertFalse(x2a.isModifiable());
-       }
-
-       @Test void a03_iterator() {
-               var x1 = new ControlledArrayList<>(false, l(1));
-               var x2 = new ControlledArrayList<>(true, l(1));
-
-               var i1 = x1.iterator();
-               var i2 = x2.iterator();
-
-               assertTrue(i1.hasNext());
-               assertTrue(i2.hasNext());
-
-               assertEquals(1, i1.next().intValue());
-               assertEquals(1, i2.next().intValue());
-
-               i1.remove();
-               assertThrows(UnsupportedOperationException.class, i2::remove);
-
-               i1.forEachRemaining(x -> {});
-               i2.forEachRemaining(x -> {});
-       }
-
-       @Test void a04_listIterator() {
-               var x1 = new ControlledArrayList<>(false, l(1));
-               var x2 = new ControlledArrayList<>(true, l(1));
-
-               var i1a = x1.listIterator();
-               var i2a = x2.listIterator();
-
-               assertTrue(i1a.hasNext());
-               assertTrue(i2a.hasNext());
-
-               assertEquals(1, i1a.next().intValue());
-               assertEquals(1, i2a.next().intValue());
-
-               assertTrue(i1a.hasPrevious());
-               assertTrue(i2a.hasPrevious());
-
-               assertEquals(1, i1a.nextIndex());
-               assertEquals(1, i2a.nextIndex());
-
-               assertEquals(0, i1a.previousIndex());
-               assertEquals(0, i2a.previousIndex());
-
-               i1a.previous();
-               i2a.previous();
-
-               i1a.set(1);
-               assertThrows(UnsupportedOperationException.class, () -> 
i2a.set(1));
-
-               i1a.add(1);
-               assertThrows(UnsupportedOperationException.class, () -> 
i2a.add(1));
-
-               i1a.next();
-               i2a.next();
-
-               i1a.remove();
-               assertThrows(UnsupportedOperationException.class, i2a::remove);
-
-               i1a.forEachRemaining(x -> {});
-               i2a.forEachRemaining(x -> {});
-       }
-}
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/common/collections/ControlledArrayList_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/common/collections/ControlledArrayList_Test.java
new file mode 100644
index 0000000000..ea49756827
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/common/collections/ControlledArrayList_Test.java
@@ -0,0 +1,602 @@
+/*
+ * 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.common.collections;
+
+import static org.apache.juneau.common.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class ControlledArrayList_Test extends TestBase {
+
+       @Nested
+       class A_ConstructorTests extends TestBase {
+
+               @Test
+               void a01_emptyModifiable() {
+                       var x = new ControlledArrayList<>(false);
+                       assertTrue(x.isModifiable());
+                       assertEmpty(x);
+               }
+
+               @Test
+               void a02_emptyUnmodifiable() {
+                       var x = new ControlledArrayList<>(true);
+                       assertFalse(x.isModifiable());
+                       assertEmpty(x);
+               }
+
+               @Test
+               void a03_withInitialListModifiable() {
+                       var x = new ControlledArrayList<>(false, l(1, 2, 3));
+                       assertTrue(x.isModifiable());
+                       assertList(x, 1, 2, 3);
+               }
+
+               @Test
+               void a04_withInitialListUnmodifiable() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3));
+                       assertFalse(x.isModifiable());
+                       assertList(x, 1, 2, 3);
+               }
+
+               @Test
+               void a05_withEmptyList() {
+                       var x1 = new ControlledArrayList<>(false, l());
+                       var x2 = new ControlledArrayList<>(true, l());
+                       assertTrue(x1.isModifiable());
+                       assertFalse(x2.isModifiable());
+                       assertEmpty(x1);
+                       assertEmpty(x2);
+               }
+       }
+
+       @Nested
+       class B_ModificationTests extends TestBase {
+
+               @Test
+               void b01_set() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       assertEquals(2, x1.set(1, 99));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.set(1, 99));
+                       x2.overrideSet(1, 99);
+                       assertList(x1, 1, 99, 3);
+                       assertList(x2, 1, 99, 3);
+               }
+
+               @Test
+               void b02_add() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2));
+
+                       assertTrue(x1.add(3));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.add(3));
+                       x2.overrideAdd(3);
+                       assertList(x1, 1, 2, 3);
+                       assertList(x2, 1, 2, 3);
+               }
+
+               @Test
+               void b03_addAtIndex() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 3));
+
+                       x1.add(1, 2);
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.add(1, 2));
+                       x2.overrideAdd(1, 2);
+                       assertList(x1, 1, 2, 3);
+                       assertList(x2, 1, 2, 3);
+               }
+
+               @Test
+               void b04_addAll() {
+                       var x1 = new ControlledArrayList<>(false, l(1));
+                       var x2 = new ControlledArrayList<>(true, l(1));
+
+                       assertTrue(x1.addAll(l(2, 3)));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.addAll(l(2, 3)));
+                       x2.overrideAddAll(l(2, 3));
+                       assertList(x1, 1, 2, 3);
+                       assertList(x2, 1, 2, 3);
+               }
+
+               @Test
+               void b05_addAllAtIndex() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 4));
+                       var x2 = new ControlledArrayList<>(true, l(1, 4));
+
+                       assertTrue(x1.addAll(1, l(2, 3)));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.addAll(1, l(2, 3)));
+                       x2.overrideAddAll(1, l(2, 3));
+                       assertList(x1, 1, 2, 3, 4);
+                       assertList(x2, 1, 2, 3, 4);
+               }
+
+               @Test
+               void b06_removeByIndex() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       assertEquals(2, x1.remove(1));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.remove(1));
+                       x2.overrideRemove(1);
+                       assertList(x1, 1, 3);
+                       assertList(x2, 1, 3);
+               }
+
+               @Test
+               void b07_removeByObject() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       assertTrue(x1.remove((Integer)2));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.remove((Integer)2));
+                       x2.overrideRemove((Integer)2);
+                       assertList(x1, 1, 3);
+                       assertList(x2, 1, 3);
+               }
+
+               @Test
+               void b08_removeAll() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3, 
4));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3, 4));
+
+                       assertTrue(x1.removeAll(l(2, 4)));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.removeAll(l(2, 4)));
+                       x2.overrideRemoveAll(l(2, 4));
+                       assertList(x1, 1, 3);
+                       assertList(x2, 1, 3);
+               }
+
+               @Test
+               void b09_retainAll() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3, 
4));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3, 4));
+
+                       assertTrue(x1.retainAll(l(2, 4)));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.retainAll(l(2, 4)));
+                       x2.overrideRetainAll(l(2, 4));
+                       assertList(x1, 2, 4);
+                       assertList(x2, 2, 4);
+               }
+
+               @Test
+               void b10_clear() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       x1.clear();
+                       assertThrows(UnsupportedOperationException.class, 
x2::clear);
+                       x2.overrideClear();
+                       assertEmpty(x1);
+                       assertEmpty(x2);
+               }
+
+               @Test
+               void b11_replaceAll() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       x1.replaceAll(x -> x * 2);
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.replaceAll(x -> x * 2));
+                       x2.overrideReplaceAll(x -> x * 2);
+                       assertList(x1, 2, 4, 6);
+                       assertList(x2, 2, 4, 6);
+               }
+
+               @Test
+               void b12_removeIf() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3, 
4));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3, 4));
+
+                       assertTrue(x1.removeIf(x -> x % 2 == 0));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.removeIf(x -> x % 2 == 0));
+                       x2.overrideRemoveIf(x -> x % 2 == 0);
+                       assertList(x1, 1, 3);
+                       assertList(x2, 1, 3);
+               }
+
+               @Test
+               void b13_sort() {
+                       var x1 = new ControlledArrayList<>(false, l(3, 1, 4, 
2));
+                       var x2 = new ControlledArrayList<>(true, l(3, 1, 4, 2));
+
+                       x1.sort(null);
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.sort(null));
+                       x2.overrideSort(null);
+                       assertList(x1, 1, 2, 3, 4);
+                       assertList(x2, 1, 2, 3, 4);
+               }
+
+               @Test
+               void b14_sortWithComparator() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3, 
4));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3, 4));
+
+                       x1.sort((a, b) -> b.compareTo(a));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.sort((a, b) -> b.compareTo(a)));
+                       x2.overrideSort((a, b) -> b.compareTo(a));
+                       assertList(x1, 4, 3, 2, 1);
+                       assertList(x2, 4, 3, 2, 1);
+               }
+       }
+
+       @Nested
+       class C_IteratorTests extends TestBase {
+
+               @Test
+               void c01_modifiableIterator() {
+                       var x = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var it = x.iterator();
+
+                       assertTrue(it.hasNext());
+                       assertEquals(1, it.next());
+                       it.remove();
+                       assertList(x, 2, 3);
+
+                       assertTrue(it.hasNext());
+                       assertEquals(2, it.next());
+                       assertTrue(it.hasNext());
+                       assertEquals(3, it.next());
+                       assertFalse(it.hasNext());
+               }
+
+               @Test
+               void c02_unmodifiableIterator() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3));
+                       var it = x.iterator();
+
+                       assertTrue(it.hasNext());
+                       assertEquals(1, it.next());
+                       assertThrows(UnsupportedOperationException.class, 
it::remove);
+
+                       assertTrue(it.hasNext());
+                       assertEquals(2, it.next());
+                       assertTrue(it.hasNext());
+                       assertEquals(3, it.next());
+                       assertFalse(it.hasNext());
+               }
+
+               @Test
+               void c03_iteratorForEachRemaining() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+                       var list1 = new java.util.ArrayList<Integer>();
+                       var list2 = new java.util.ArrayList<Integer>();
+
+                       x1.iterator().forEachRemaining(list1::add);
+                       x2.iterator().forEachRemaining(list2::add);
+
+                       assertList(list1, 1, 2, 3);
+                       assertList(list2, 1, 2, 3);
+               }
+
+               @Test
+               void c04_overrideIterator() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3));
+                       var it = x.overrideIterator();
+
+                       assertTrue(it.hasNext());
+                       assertEquals(1, it.next());
+                       // Note: overrideIterator() returns the underlying 
iterator, but iterator.remove()
+                       // still goes through the list's remove() method which 
checks modifiability.
+                       // So we can only test that it returns a readable 
iterator.
+                       var list = new java.util.ArrayList<Integer>();
+                       it.forEachRemaining(list::add);
+                       assertList(list, 2, 3);
+               }
+
+               @Test
+               void c05_emptyIterator() {
+                       var x1 = new ControlledArrayList<>(false);
+                       var x2 = new ControlledArrayList<>(true);
+
+                       assertFalse(x1.iterator().hasNext());
+                       assertFalse(x2.iterator().hasNext());
+               }
+       }
+
+       @Nested
+       class D_ListIteratorTests extends TestBase {
+
+               @Test
+               void d01_modifiableListIterator() {
+                       var x = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var it = x.listIterator();
+
+                       assertTrue(it.hasNext());
+                       assertFalse(it.hasPrevious());
+                       assertEquals(0, it.nextIndex());
+                       assertEquals(-1, it.previousIndex());
+
+                       assertEquals(1, it.next());
+                       assertTrue(it.hasPrevious());
+                       assertEquals(1, it.nextIndex());
+                       assertEquals(0, it.previousIndex());
+
+                       it.set(99);
+                       assertList(x, 99, 2, 3);
+
+                       it.add(100);
+                       assertList(x, 99, 100, 2, 3);
+
+                       assertEquals(2, it.nextIndex());
+                       assertEquals(1, it.previousIndex());
+               }
+
+               @Test
+               void d02_unmodifiableListIterator() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3));
+                       var it = x.listIterator();
+
+                       assertTrue(it.hasNext());
+                       assertFalse(it.hasPrevious());
+                       assertEquals(0, it.nextIndex());
+                       assertEquals(-1, it.previousIndex());
+
+                       assertEquals(1, it.next());
+                       assertTrue(it.hasPrevious());
+                       assertEquals(1, it.nextIndex());
+                       assertEquals(0, it.previousIndex());
+
+                       assertThrows(UnsupportedOperationException.class, () -> 
it.set(99));
+                       assertThrows(UnsupportedOperationException.class, () -> 
it.add(100));
+                       assertThrows(UnsupportedOperationException.class, 
it::remove);
+               }
+
+               @Test
+               void d03_listIteratorWithIndex() {
+                       var x = new ControlledArrayList<>(false, l(1, 2, 3, 4));
+                       var it = x.listIterator(2);
+
+                       assertTrue(it.hasNext());
+                       assertTrue(it.hasPrevious());
+                       assertEquals(2, it.nextIndex());
+                       assertEquals(1, it.previousIndex());
+
+                       assertEquals(3, it.next());
+                       assertEquals(2, it.previousIndex());
+                       assertEquals(3, it.previous());
+                       assertEquals(1, it.previousIndex());
+               }
+
+               @Test
+               void d04_listIteratorWithIndexUnmodifiable() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3, 4));
+                       var it = x.listIterator(2);
+
+                       assertTrue(it.hasNext());
+                       assertTrue(it.hasPrevious());
+                       assertEquals(2, it.nextIndex());
+                       assertEquals(1, it.previousIndex());
+
+                       assertEquals(3, it.next());
+                       assertThrows(UnsupportedOperationException.class, () -> 
it.set(99));
+                       assertThrows(UnsupportedOperationException.class, () -> 
it.add(100));
+                       assertThrows(UnsupportedOperationException.class, 
it::remove);
+               }
+
+               @Test
+               void d05_listIteratorForEachRemaining() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+                       var list1 = new java.util.ArrayList<Integer>();
+                       var list2 = new java.util.ArrayList<Integer>();
+
+                       x1.listIterator().forEachRemaining(list1::add);
+                       x2.listIterator().forEachRemaining(list2::add);
+
+                       assertList(list1, 1, 2, 3);
+                       assertList(list2, 1, 2, 3);
+               }
+
+               @Test
+               void d06_overrideListIterator() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3));
+                       var it = x.overrideListIterator(1);
+
+                       assertTrue(it.hasNext());
+                       assertEquals(2, it.next());
+                       // Note: overrideListIterator() returns the underlying 
iterator, but iterator.set()
+                       // still goes through the list's set() method which 
checks modifiability.
+                       // So we can only test that it returns a readable 
iterator at the correct position.
+                       assertTrue(it.hasPrevious());
+                       assertEquals(1, it.previousIndex());
+                       assertEquals(2, it.nextIndex());
+               }
+
+               @Test
+               void d07_listIteratorBidirectional() {
+                       var x = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var it = x.listIterator();
+
+                       assertEquals(1, it.next());
+                       assertEquals(2, it.next());
+                       assertEquals(2, it.previous());
+                       assertEquals(1, it.previous());
+                       assertFalse(it.hasPrevious());
+               }
+       }
+
+       @Nested
+       class E_SubListTests extends TestBase {
+
+               @Test
+               void e01_subListModifiable() {
+                       var x = new ControlledArrayList<>(false, l(1, 2, 3, 4, 
5));
+                       var sub = (ControlledArrayList<Integer>) x.subList(1, 
4);
+
+                       assertTrue(sub.isModifiable());
+                       assertList(sub, 2, 3, 4);
+
+                       // Note: subList creates a copy, not a view (because 
the constructor copies elements)
+                       // So modifications to subList don't affect the 
original list
+                       sub.set(0, 99);
+                       assertEquals(99, sub.get(0));
+                       assertList(sub, 99, 3, 4);
+                       // Original list is unchanged
+                       assertList(x, 1, 2, 3, 4, 5);
+               }
+
+               @Test
+               void e02_subListUnmodifiable() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3, 4, 
5));
+                       var sub = (ControlledArrayList<Integer>) x.subList(1, 
4);
+
+                       assertFalse(sub.isModifiable());
+                       assertList(sub, 2, 3, 4);
+
+                       assertThrows(UnsupportedOperationException.class, () -> 
sub.set(0, 99));
+               }
+
+               @Test
+               void e03_subListEmpty() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       var sub1 = (ControlledArrayList<Integer>) x1.subList(1, 
1);
+                       var sub2 = (ControlledArrayList<Integer>) x2.subList(1, 
1);
+
+                       assertTrue(sub1.isModifiable());
+                       assertFalse(sub2.isModifiable());
+                       assertEmpty(sub1);
+                       assertEmpty(sub2);
+               }
+
+               @Test
+               void e04_subListFullRange() {
+                       var x1 = new ControlledArrayList<>(false, l(1, 2, 3));
+                       var x2 = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       var sub1 = (ControlledArrayList<Integer>) x1.subList(0, 
3);
+                       var sub2 = (ControlledArrayList<Integer>) x2.subList(0, 
3);
+
+                       assertTrue(sub1.isModifiable());
+                       assertFalse(sub2.isModifiable());
+                       assertList(sub1, 1, 2, 3);
+                       assertList(sub2, 1, 2, 3);
+               }
+       }
+
+       @Nested
+       class F_SetUnmodifiableTests extends TestBase {
+
+               @Test
+               void f01_setUnmodifiable() {
+                       var x = new ControlledArrayList<>(false, l(1, 2, 3));
+
+                       assertTrue(x.isModifiable());
+                       x.set(0, 99); // Should work
+
+                       var result = x.setUnmodifiable();
+                       assertSame(x, result); // Should return this
+                       assertFalse(x.isModifiable());
+
+                       assertThrows(UnsupportedOperationException.class, () -> 
x.set(0, 100));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x.add(4));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x.remove(0));
+               }
+
+               @Test
+               void f02_setUnmodifiableAlreadyUnmodifiable() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       assertFalse(x.isModifiable());
+                       x.setUnmodifiable();
+                       assertFalse(x.isModifiable());
+               }
+       }
+
+       @Nested
+       class G_EdgeCaseTests extends TestBase {
+
+               @Test
+               void g01_emptyListOperations() {
+                       var x1 = new ControlledArrayList<>(false);
+                       var x2 = new ControlledArrayList<>(true);
+
+                       assertFalse(x1.addAll(l()));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.addAll(l()));
+
+                       assertFalse(x1.removeAll(l()));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.removeAll(l()));
+
+                       assertFalse(x1.retainAll(l()));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.retainAll(l()));
+
+                       assertFalse(x1.remove((Integer)1));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.remove((Integer)1));
+               }
+
+               @Test
+               void g02_singleElementList() {
+                       var x1 = new ControlledArrayList<>(false, l(42));
+                       var x2 = new ControlledArrayList<>(true, l(42));
+
+                       assertEquals(42, x1.get(0));
+                       assertEquals(42, x2.get(0));
+
+                       x1.set(0, 99);
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.set(0, 99));
+                       x2.overrideSet(0, 99);
+
+                       assertEquals(99, x1.remove(0));
+                       assertThrows(UnsupportedOperationException.class, () -> 
x2.remove(0));
+                       x2.overrideRemove(0);
+
+                       assertEmpty(x1);
+                       assertEmpty(x2);
+               }
+
+               @Test
+               void g03_overrideMethodsWorkWhenUnmodifiable() {
+                       var x = new ControlledArrayList<>(true, l(1, 2, 3));
+
+                       x.overrideAdd(4);
+                       x.overrideAdd(0, 0);
+                       x.overrideSet(2, 99);
+                       x.overrideRemove(3);
+                       x.overrideRemove((Integer)1);
+                       x.overrideAddAll(l(5, 6));
+                       x.overrideAddAll(0, l(-1));
+                       x.overrideRemoveAll(l(0));
+                       x.overrideRetainAll(l(2, 99, 5, 6));
+                       x.overrideReplaceAll(a -> a * 2);
+                       x.overrideRemoveIf(a -> a == 4);
+                       x.overrideSort(null);
+                       x.overrideClear();
+
+                       assertEmpty(x);
+               }
+
+               @Test
+               void g04_isModifiable() {
+                       var x1 = new ControlledArrayList<>(false);
+                       var x2 = new ControlledArrayList<>(true);
+
+                       assertTrue(x1.isModifiable());
+                       assertFalse(x2.isModifiable());
+
+                       x1.setUnmodifiable();
+                       assertFalse(x1.isModifiable());
+               }
+       }
+}
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/common/collections/MapBuilder_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/common/collections/MapBuilder_Test.java
index c2712c1223..3fa1f472b0 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/common/collections/MapBuilder_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/common/collections/MapBuilder_Test.java
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.common.utils.Converter;
 import org.junit.jupiter.api.*;
 
 class MapBuilder_Test extends TestBase {
@@ -344,4 +345,400 @@ class MapBuilder_Test extends TestBase {
                assertMap(map, "x=10", "y=20");
                assertSame(existing, map);
        }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Filtering
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test
+       void i01_filtered_customPredicate() {
+               var map = MapBuilder.create(String.class, String.class)
+                       .filtered(x -> x != null && !x.equals(""))
+                       .add("a", "foo")
+                       .add("b", null)     // Not added
+                       .add("c", "")       // Not added
+                       .add("d", "bar")
+                       .build();
+
+               assertMap(map, "a=foo", "d=bar");
+       }
+
+       @Test
+       void i02_filtered_defaultFilter() {
+               var map = MapBuilder.create(String.class, Object.class)
+                       .filtered()
+                       .add("name", "John")
+                       .add("age", -1)              // Not added
+                       .add("enabled", false)       // Not added
+                       .add("tags", new String[0])  // Not added
+                       .add("emptyMap", new LinkedHashMap<>())  // Not added
+                       .add("emptyList", new ArrayList<>())     // Not added
+                       .add("value", 42)
+                       .build();
+
+               assertMap(map, "name=John", "value=42");
+       }
+
+       @Test
+       void i03_filtered_rejectsValue() {
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .filtered(x -> x != null && (Integer)x > 0)
+                       .add("a", 1)
+                       .add("b", -1)  // Not added
+                       .add("c", 0)   // Not added
+                       .add("d", 2)
+                       .build();
+
+               assertMap(map, "a=1", "d=2");
+       }
+
+       @Test
+       void i04_add_withFilter() {
+               var map = MapBuilder.create(String.class, String.class)
+                       .filtered(x -> x != null && x instanceof String && 
((String)x).length() > 2)
+                       .add("a", "foo")   // Added
+                       .add("b", "ab")    // Not added (length <= 2)
+                       .add("c", "bar")   // Added
+                       .build();
+
+               assertMap(map, "a=foo", "c=bar");
+       }
+
+       @Test
+       void i05_addAll_withFilter() {
+               var existing = new LinkedHashMap<String,String>();
+               existing.put("x", "longvalue");
+               existing.put("y", "ab");  // Will be filtered out
+               existing.put("z", "another");
+
+               var map = MapBuilder.create(String.class, String.class)
+                       .filtered(x -> x != null && x instanceof String && 
((String)x).length() > 2)
+                       .addAll(existing)
+                       .build();
+
+               assertMap(map, "x=longvalue", "z=another");
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Converters
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test
+       void j01_converters_emptyArray() {
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .converters()  // Empty array
+                       .add("a", 1)
+                       .build();
+
+               assertMap(map, "a=1");
+       }
+
+       @Test
+       void j02_converters_withConverter() {
+               Converter converter = new Converter() {
+                       @Override
+                       public <T> T convertTo(Class<T> type, Object o) {
+                               if (type == Integer.class && o instanceof 
String) {
+                                       return 
type.cast(Integer.parseInt((String)o));
+                               }
+                               return null;
+                       }
+               };
+
+               var inputMap = new LinkedHashMap<String,String>();
+               inputMap.put("a", "1");
+               inputMap.put("b", "2");
+
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .converters(converter)
+                       .addAny(inputMap)
+                       .build();
+
+               assertMap(map, "a=1", "b=2");
+       }
+
+       @Test
+       void j03_converters_multipleConverters() {
+               Converter converter1 = new Converter() {
+                       @Override
+                       public <T> T convertTo(Class<T> type, Object o) {
+                               return null;  // Doesn't handle this
+                       }
+               };
+
+               Converter converter2 = new Converter() {
+                       @Override
+                       public <T> T convertTo(Class<T> type, Object o) {
+                               if (type == Integer.class && o instanceof 
String) {
+                                       return 
type.cast(Integer.parseInt((String)o));
+                               }
+                               return null;
+                       }
+               };
+
+               var inputMap = new LinkedHashMap<String,String>();
+               inputMap.put("a", "1");
+
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .converters(converter1, converter2)
+                       .addAny(inputMap)
+                       .build();
+
+               assertMap(map, "a=1");
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // AddAny
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test
+       void k01_addAny_withMap() {
+               var inputMap = new LinkedHashMap<String,Integer>();
+               inputMap.put("a", 1);
+               inputMap.put("b", 2);
+
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .addAny(inputMap)
+                       .build();
+
+               assertMap(map, "a=1", "b=2");
+       }
+
+       @Test
+       void k02_addAny_withMultipleMaps() {
+               var map1 = new LinkedHashMap<String,Integer>();
+               map1.put("a", 1);
+               map1.put("b", 2);
+
+               var map2 = new LinkedHashMap<String,Integer>();
+               map2.put("c", 3);
+               map2.put("d", 4);
+
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .addAny(map1, map2)
+                       .build();
+
+               assertMap(map, "a=1", "b=2", "c=3", "d=4");
+       }
+
+       @Test
+       void k03_addAny_withNullMap() {
+               var inputMap = new LinkedHashMap<String,Integer>();
+               inputMap.put("a", 1);
+               inputMap.put("b", 2);
+
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .addAny(inputMap, null)  // null map should be ignored
+                       .build();
+
+               assertMap(map, "a=1", "b=2");
+       }
+
+       @Test
+       void k03b_addAny_withNullValueInMap() {
+               // addAny uses toType which doesn't handle null values
+               var inputMap = new LinkedHashMap<String,Integer>();
+               inputMap.put("a", 1);
+               inputMap.put("b", null);
+
+               assertThrows(RuntimeException.class, () -> {
+                       MapBuilder.create(String.class, Integer.class)
+                               .addAny(inputMap)
+                               .build();
+               });
+       }
+
+       @Test
+       void k04_addAny_withTypeConversion() {
+               var converter = new Converter() {
+                       @Override
+                       public <T> T convertTo(Class<T> type, Object o) {
+                               if (type == String.class && o instanceof 
Integer) {
+                                       return type.cast(String.valueOf(o));
+                               }
+                               if (type == Integer.class && o instanceof 
String) {
+                                       return 
type.cast(Integer.parseInt((String)o));
+                               }
+                               return null;
+                       }
+               };
+
+               var inputMap = new LinkedHashMap<Integer,String>();
+               inputMap.put(1, "10");
+               inputMap.put(2, "20");
+
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .converters(converter)
+                       .addAny(inputMap)
+                       .build();
+
+               assertMap(map, "1=10", "2=20");
+       }
+
+       @Test
+       void k05_addAny_withConverterToMap() {
+               var converter = new Converter() {
+                       @Override
+                       public <T> T convertTo(Class<T> type, Object o) {
+                               if (type == Map.class && o instanceof String) {
+                                       var m = new 
LinkedHashMap<String,String>();
+                                       // Simple parsing: 
"key1=value1,key2=value2"
+                                       var s = (String)o;
+                                       for (var pair : s.split(",")) {
+                                               var kv = pair.split("=");
+                                               if (kv.length == 2) {
+                                                       m.put(kv[0], kv[1]);
+                                               }
+                                       }
+                                       return type.cast(m);
+                               }
+                               return null;
+                       }
+               };
+
+               var map = MapBuilder.create(String.class, String.class)
+                       .converters(converter)
+                       .addAny("a=1,b=2")
+                       .build();
+
+               assertMap(map, "a=1", "b=2");
+       }
+
+       @Test
+       void k06_addAny_withConverterToMap_recursive() {
+               var converter = new Converter() {
+                       @Override
+                       public <T> T convertTo(Class<T> type, Object o) {
+                               if (type == Map.class && o instanceof String) {
+                                       var m = new 
LinkedHashMap<String,String>();
+                                       var s = (String)o;
+                                       for (var pair : s.split(",")) {
+                                               var kv = pair.split("=");
+                                               if (kv.length == 2) {
+                                                       m.put(kv[0], kv[1]);
+                                               }
+                                       }
+                                       return type.cast(m);
+                               }
+                               return null;
+                       }
+               };
+
+               var map = MapBuilder.create(String.class, String.class)
+                       .converters(converter)
+                       .addAny("x=foo", "y=bar")
+                       .build();
+
+               assertMap(map, "x=foo", "y=bar");
+       }
+
+       @Test
+       void k07_addAny_noKeyOrValueType() {
+               var builder = new MapBuilder<String,Integer>(null, null);
+               assertThrows(IllegalStateException.class, () -> 
builder.addAny(new LinkedHashMap<>()));
+       }
+
+       @Test
+       void k08_addAny_conversionFailure() {
+               // When converters is null and we try to add a non-Map, it will 
throw NPE
+               assertThrows(NullPointerException.class, () -> {
+                       MapBuilder.create(String.class, Integer.class)
+                               .addAny("not-a-map")
+                               .build();
+               });
+       }
+
+       @Test
+       void k09_addAny_converterReturnsNull() {
+               // Converter exists but returns null (can't convert)
+               var converter = new Converter() {
+                       @Override
+                       public <T> T convertTo(Class<T> type, Object o) {
+                               return null;  // Can't convert
+                       }
+               };
+
+               // Should throw RuntimeException when converter can't convert 
non-Map object
+               assertThrows(RuntimeException.class, () -> {
+                       MapBuilder.create(String.class, Integer.class)
+                               .converters(converter)
+                               .addAny("not-a-map")
+                               .build();
+               });
+       }
+
+       @Test
+       void k10_addAny_toType_conversionFailure() {
+               var converter = new Converter() {
+                       @Override
+                       public <T> T convertTo(Class<T> type, Object o) {
+                               return null;  // Can't convert
+                       }
+               };
+
+               var inputMap = new LinkedHashMap<String,String>();
+               inputMap.put("a", "not-an-integer");
+
+               assertThrows(RuntimeException.class, () -> {
+                       MapBuilder.create(String.class, Integer.class)
+                               .converters(converter)
+                               .addAny(inputMap)
+                               .build();
+               });
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Build edge cases
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Test
+       void l01_build_sparseWithNullMap() {
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .sparse()
+                       .build();
+
+               assertNull(map);
+       }
+
+       @Test
+       void l02_build_sparseWithEmptyMap() {
+               var existing = new LinkedHashMap<String,Integer>();
+
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .to(existing)
+                       .sparse()
+                       .build();
+
+               assertNull(map);
+       }
+
+       @Test
+       void l03_build_notSparseWithNullMap() {
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .build();
+
+               assertNotNull(map);
+               assertEmpty(map);
+       }
+
+       @Test
+       void l04_build_sortedWithNullMap() {
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .sorted()
+                       .build();
+
+               assertNotNull(map);
+               assertTrue(map instanceof TreeMap);
+               assertEmpty(map);
+       }
+
+       @Test
+       void l05_build_unmodifiableWithNullMap() {
+               var map = MapBuilder.create(String.class, Integer.class)
+                       .unmodifiable()
+                       .build();
+
+               assertNotNull(map);
+               assertThrows(UnsupportedOperationException.class, () -> 
map.put("a", 1));
+       }
 }
\ No newline at end of file

Reply via email to