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