diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..1bfce9588
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,326 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## π¨ CRITICAL RULES - READ FIRST π¨
+
+**BEFORE doing ANYTHING else, understand these NON-NEGOTIABLE requirements:**
+
+### MANDATORY FULL TEST SUITE VALIDATION
+
+**EVERY change, no matter how small, MUST be followed by running the full test suite:**
+
+```bash
+mvn clean test
+```
+
+**ALL 10,000+ tests MUST pass before:**
+- Moving to the next issue/file/task
+- Committing any changes
+- Asking for human approval
+- Starting any new work
+
+**If even ONE test fails:**
+- Stop immediately
+- Fix the failing test(s)
+- Run the full test suite again
+- Only proceed when ALL tests pass
+
+**This rule applies to ANY code modification and is MORE IMPORTANT than the actual change itself.**
+
+### MANDATORY HUMAN APPROVAL FOR COMMITS
+
+**NEVER commit without explicit "Y" or "Yes" approval from human.**
+
+### MANDATORY HUMAN APPROVAL FOR DEPLOYMENT
+
+**NEVER deploy without explicit human approval. Always ask for permission before starting any deployment process.**
+
+## π― WORK PHILOSOPHY - INCREMENTAL ATOMIC CHANGES π―
+
+**Mental Model: Work with a "List of Changes" approach**
+
+### The Change Hierarchy
+- **Top-level changes** (e.g., "Fix security issues in DateUtilities")
+ - **Sub-changes** (e.g., "Fix ReDoS vulnerability", "Fix thread safety")
+ - **Sub-sub-changes** (e.g., "Limit regex repetition", "Add validation tests")
+
+### Workflow for EACH Individual Change
+1. **Pick ONE change** from any level (top-level, sub-change, sub-sub-change)
+2. **Implement the change**
+ - During development: Use single test execution for speed (`mvn test -Dtest=SpecificTest`)
+ - Iterate until the specific functionality works
+3. **When you think the change is complete:**
+ - **MANDATORY**: Run full test suite: `mvn clean test`
+ - **ALL 10,000+ tests MUST pass**
+ - **If ANY test fails**: Fix immediately, run full tests again
+4. **Once ALL tests pass:**
+ - Ask for commit approval: "Should I commit this change? (Y/N)"
+ - Human approves, commit immediately
+ - Move to next change in the list
+
+### Core Principles
+- **Start work**: At the start of new work, create a "Todo" list.
+- **Chat First**: As a general work guideline, when starting a new Todo list, or a feature idea, always "chat first, get agreement from human, then code."
+- **Minimize Work-in-Process**: Keep delta between local files and committed git files as small as possible
+- **Always Healthy State**: Committed code is always in perfect health (all tests pass)
+- **Atomic Commits**: Each commit represents one complete, tested, working change
+- **Human Controls Push**: Human decides when to push commits to remote
+
+**π― GOAL: Each change is complete, tested, and committed before starting the next change**
+
+## ADDITIONAL TESTING REQUIREMENTS
+
+**CRITICAL BUILD REQUIREMENT**: The full maven test suite MUST run all 10,000+ tests. If you see only ~10,000 tests, there is an OSGi or JPMS bundle issue that MUST be fixed before continuing any work. Use `mvn -Dbundle.skip=true test` to bypass bundle issues during development, but the underlying bundle configuration must be resolved.
+
+**CRITICAL TESTING REQUIREMENT**: When adding ANY new code (security fixes, new methods, validation logic, etc.), you MUST add corresponding JUnit tests to prove the changes work correctly. This includes:
+- Testing the new functionality works as expected
+- Testing edge cases and error conditions
+- Testing security boundary conditions
+- Testing that the fix actually prevents the vulnerability
+- All new tests MUST pass along with the existing 10,000+ tests
+## Build Commands
+
+**Maven-based Java project with JDK 8 compatibility**
+
+- **Build**: `mvn compile`
+- **Test**: `mvn test`
+- **Package**: `mvn package`
+- **Install**: `mvn install`
+- **Run single test**: `mvn test -Dtest=ClassName`
+- **Run tests with pattern**: `mvn test -Dtest="*Pattern*"`
+- **Clean**: `mvn clean`
+- **Generate docs**: `mvn javadoc:javadoc`
+
+## Architecture Overview
+
+**java-util** is a high-performance Java utilities library focused on memory efficiency, thread-safety, and enhanced collections. The architecture follows these key patterns:
+
+### Core Structure
+- **Main package**: `com.cedarsoftware.util` - Core utilities and enhanced collections
+- **Convert package**: `com.cedarsoftware.util.convert` - Comprehensive type conversion system
+- **Cache package**: `com.cedarsoftware.util.cache` - Caching strategies and implementations
+
+### Key Architectural Patterns
+
+**Memory-Efficient Collections**: CompactMap/CompactSet dynamically adapt storage structure based on size, using arrays for small collections and switching to hash-based storage as they grow.
+
+**Null-Safe Concurrent Collections**: ConcurrentHashMapNullSafe, ConcurrentNavigableMapNullSafe, etc. extend JDK concurrent collections to safely handle null keys/values.
+
+**Dynamic Code Generation**: CompactMap/CompactSet use JDK compiler at runtime to generate optimized subclasses when builder API is used (requires full JDK).
+
+**Converter Architecture**: Modular conversion system with dedicated conversion classes for each target type, supporting thousands of built-in conversions between Java types.
+
+**ClassValue Optimization**: ClassValueMap/ClassValueSet leverage JVM's ClassValue for extremely fast Class-based lookups.
+
+## Development Conventions
+
+### Code Style (from agents.md)
+- Use **four spaces** for indentationβno tabs
+- Keep lines under **120 characters**
+- End files with newline, use Unix line endings
+- Follow standard Javadoc for public APIs
+- **JDK 1.8 source compatibility** - do not use newer language features
+
+### Library Usage Patterns
+- Use `ReflectionUtils` APIs instead of direct reflection
+- Use `DeepEquals.deepEquals()` for data structure verification in tests (pass options to see diff)
+- Use null-safe ConcurrentMaps from java-util for null support
+- Use `DateUtilities.parse()` or `Converter.convert()` for date parsing
+- Use `Converter.convert()` for type marshaling
+- Use `FastByteArrayInputStream/OutputStream` and `FastReader/FastWriter` for performance
+- Use `StringUtilities` APIs for null-safe string operations
+- Use `UniqueIdGenerator.getUniqueId19()` for unique IDs (up to 10,000/ms, strictly increasing)
+- Use `IOUtilities` for stream handling and transfers
+- Use `ClassValueMap/ClassValueSet` for fast Class-based lookups
+- Use `CaseInsensitiveMap` for case-insensitive string keys
+- Use `CompactMap/CompactSet` for memory-efficient large collections
+
+## Testing Framework
+
+- **JUnit 5** (Jupiter) with parameterized tests
+- **AssertJ** for fluent assertions
+- **Mockito** for mocking
+- Test resources in `src/test/resources/`
+- Comprehensive test coverage with pattern: `*Test.java`
+
+## Special Considerations
+
+### JDK vs JRE Environments
+- Builder APIs (`CompactMap.builder()`, `CompactSet.builder()`) require full JDK (compiler tools)
+- These APIs throw `IllegalStateException` in JRE-only environments
+- Use pre-built classes (`CompactLinkedMap`, `CompactCIHashMap`, etc.) or custom subclasses in JRE environments
+
+### OSGi and JPMS Support
+- Full OSGi bundle with proper manifest entries
+- JPMS module `com.cedarsoftware.util` with exports for main packages
+- No runtime dependencies on external libraries
+
+### Thread Safety
+- Many collections are thread-safe by design (Concurrent* classes)
+- LRUCache and TTLCache are thread-safe with configurable strategies
+- Use appropriate concurrent collections for multi-threaded scenarios
+
+## Enhanced Review Loop
+
+**This workflow follows the INCREMENTAL ATOMIC CHANGES philosophy for systematic code reviews and improvements:**
+
+### Step 1: Build Change List (Analysis Phase)
+- Review Java source files using appropriate analysis framework
+- For **Security**: Prioritize by risk (network utilities, reflection, file I/O, crypto, system calls)
+- For **Performance**: Focus on hot paths, collection usage, algorithm efficiency
+- For **Features**: Target specific functionality or API enhancements
+- **Create hierarchical todo list:**
+ - Top-level items (e.g., "Security review of DateUtilities")
+ - Sub-items (e.g., "Fix ReDoS vulnerability", "Fix thread safety")
+ - Sub-sub-items (e.g., "Limit regex repetition", "Add test coverage")
+
+### Step 2: Pick ONE Change from the List
+- Select the highest priority change from ANY level (top, sub, sub-sub)
+- Mark as "in_progress" in todo list
+- **Focus on this ONE change only**
+
+### Step 3: Implement the Single Change
+- Make targeted improvement to address the ONE selected issue
+- **During development iterations**: Use targeted test execution for speed (`mvn test -Dtest=SpecificTest`)
+ - This allows quick feedback loops while developing the specific feature/fix
+ - Continue iterating until the targeted tests pass and functionality works
+- **MANDATORY**: Add comprehensive JUnit tests for this specific change:
+ - Tests that verify the improvement works correctly
+ - Tests for edge cases and boundary conditions
+ - Tests for error handling and regression prevention
+- Follow coding best practices and maintain API compatibility
+- Update Javadoc and comments where appropriate
+
+### Step 4: Completion Gate - ABSOLUTELY MANDATORY
+**When you believe the issue/fix is complete and targeted tests are passing:**
+
+- **Run FULL test suite**: `mvn test` (ALL 10,000+ tests must pass)
+- **If any test fails**: Fix issues immediately, run full tests again
+- **NEVER proceed until ALL tests pass**
+- Mark improvement todos as "completed" only when ALL tests pass
+
+**Development Process:**
+1. **Development Phase**: Use targeted tests (`mvn test -Dtest=SpecificTest`) for fast iteration
+2. **Completion Gate**: Run full test suite (`mvn test`) when you think you're done
+3. **Quality Verification**: ALL 10,000+ tests must pass before proceeding
+
+### Step 5: Update Documentation (for this ONE change)
+- **changelog.md**: Add entry for this specific change under appropriate version
+- **userguide.md**: Update if this change affects public APIs or usage patterns
+- **Javadoc**: Ensure documentation reflects this change
+- **README.md**: Update if this change affects high-level functionality
+
+### Step 6: Request Atomic Commit Approval
+**MANDATORY HUMAN APPROVAL STEP for this ONE change:**
+Present a commit approval request to the human with:
+- Summary of this ONE improvement made (specific security fix, performance enhancement, etc.)
+- List of files modified for this change
+- Test results confirmation (ALL 10,000+ tests passing)
+- Documentation updates made for this change
+- Clear description of this change and its benefits
+- Ask: "Should I commit this change?"
+
+### Step 7: Atomic Commit (Only After Human Approval)
+- **Immediately commit this ONE change** after receiving "Y" approval
+- Use descriptive commit message format for this specific change:
+ ```
+ [Type]: [Brief description of this ONE change]
+
+ - [This specific change implemented]
+ - [Test coverage added for this change]
+ - [Any documentation updated]
+
+ π€ Generated with [Claude Code](https://claude.ai/code)
+
+ Co-Authored-By: Claude
+ ```
+ Where [Type] = Security, Performance, Feature, Refactor, etc.
+- Mark this specific todo as "completed"
+- **Repository is now in healthy state with this change committed**
+
+### Step 8: Return to Change List
+- **Pick the NEXT change** from the hierarchical list (top-level, sub, sub-sub)
+- **Repeat Steps 2-7 for this next change**
+- **Continue until all changes in the list are complete**
+- Maintain todo list to track progress across entire scope
+
+**Special Cases - Tinkering/Exploratory Work:**
+For non-systematic changes, individual experiments, or small targeted fixes, the process can be adapted:
+- Steps 1-2 can be simplified or skipped for well-defined changes
+- Steps 4-6 remain mandatory (testing, documentation, human approval)
+- Commit messages should still be descriptive and follow format
+
+**This loop ensures systematic code improvement with proper testing, documentation, and human oversight for all changes.**
+
+## π¦ DEPLOYMENT PROCESS π¦
+
+**Maven deployment to Maven Central via Sonatype OSSRH**
+
+### Prerequisites Check
+Before deployment, verify the following conditions are met:
+
+0. **Version Updates**: Ensure version numbers are updated in documentation files
+ - Update README.md version references (e.g., 3.5.0 β 3.6.0)
+ - Update changelog.md: move current "(Unreleased)" to release version, add new "(Unreleased)" section for next version
+ - Add recent git commit history to changelog for the release version, for each item you cannot already find in the changelog.md
+
+1. **Clean Working Directory**: No uncommitted local files
+```bash
+git status
+# Should show: "nothing to commit, working tree clean"
+```
+
+2. **Remote Sync**: All local commits are pushed to remote
+```bash
+git push origin master
+# Should be up to date with origin/master
+```
+
+3. **Dependency Verification**: json-io dependency must be correct version
+ - json-io is test-scope only (java-util has zero runtime dependencies)
+ - json-io version must be "1 behind" the current java-util version
+ - This prevents circular dependency (java-util β json-io β java-util)
+ - Current: json-io 4.55.0 in pom.xml (test scope)
+
+### Deployment Steps
+
+1. **Run Maven Deploy with Release Profile**
+```bash
+mvn clean deploy -DperformRelease=true
+```
+ - This will take significant time due to additional tests enabled with performRelease=true
+ - Includes GPG signing of artifacts (requires GPG key and passphrase configured)
+ - Uploads to Sonatype OSSRH staging repository
+ - Automatically releases to Maven Central (autoReleaseAfterClose=true)
+
+2. **Tag the Release**
+```bash
+git tag -a x.y.z -m "x.y.zYYYYMMDDHHMMSS"
+```
+ - Replace x.y.z with actual version (e.g., 3.6.0)
+ - Replace YYYYMMDDHHMMSS with current timestamp in 24-hour format
+ - Example: `git tag -a 3.6.0 -m "3.6.020250101120000"`
+
+3. **Push Tags to Remote**
+```bash
+git push --tags
+```
+
+### Configuration Details
+- **Sonatype OSSRH**: Configured in pom.xml distributionManagement
+- **GPG Signing**: Automated via maven-gpg-plugin when performRelease=true
+- **Nexus Staging**: Uses nexus-staging-maven-plugin with autoReleaseAfterClose
+- **Bundle Generation**: OSGi bundle via maven-bundle-plugin
+- **JPMS Module**: Module-info.java added via moditect-maven-plugin
+
+### Security Notes
+- GPG key and passphrase must be configured in Maven settings.xml
+- OSSRH credentials required for Sonatype deployment
+- Never commit GPG passphrases or credentials to repository
+
+### Post-Deployment Verification
+1. Check Maven Central: https://search.maven.org/artifact/com.cedarsoftware/java-util
+2. Verify OSGi bundle metadata in deployed JAR
+3. Confirm module-info.class present for JPMS support
+4. Test dependency resolution in downstream projects (json-io, n-cube)
\ No newline at end of file
diff --git a/.github/workflows/build-maven.yml b/.github/workflows/build-maven.yml
new file mode 100644
index 000000000..825103e87
--- /dev/null
+++ b/.github/workflows/build-maven.yml
@@ -0,0 +1,26 @@
+# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
+
+name: Java CI with Maven
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ cache: maven
+ - name: Build with Maven
+ run: mvn -B package --file pom.xml
diff --git a/.gitignore b/.gitignore
index 8c6655104..8d8ca07e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,14 @@ CVS/
.classpath
.project
.settings/
+.nondex
+
+# Compiled class files
+*.class
+
+# Claude-specific documentation files
+CLAUDE.md
+CODE_REVIEW.md
+CONVERTER_ROADMAP.md
+RELEASE_PROCESS.md
+CLAUDE.md
diff --git a/README.md b/README.md
index efb276a93..923d24721 100644
--- a/README.md
+++ b/README.md
@@ -1,155 +1,1139 @@
-java-util
-=========
-Rarely available and hard-to-write Java utilities, written correctly, and thoroughly tested (> 98% code coverage via JUnit tests).
+
A Map wrapper that provides case-insensitive, case-retentive keys and inherits the features of the wrapped map (e.g., thread-safety from ConcurrentMap types, multi-key support from MultiKeyMap, sorted, thread-safe, allow nulls from ConcurrentNavigableMapNullSafe).
A Map wrapper that tracks key access. Inherits features from wrapped Map, including thread-safety (ConcurrentMap types), sorted, thread-safe, with null support (ConcurrentNavigableMapNullSafe)
An extensive and extensible conversion utility with thousands of built-in transformations between common JDK types (Dates, Collections, Primitives, EnumSets, etc.).
+## Integration and Module Support
+
+### JPMS (Java Platform Module System)
+
+This library is fully compatible with JPMS, commonly known as Java Modules. It includes a `module-info.class` file that
+specifies module dependencies and exports.
+
+### OSGi
+
+This library also supports OSGi environments. It comes with pre-configured OSGi metadata in the `MANIFEST.MF` file, ensuring easy integration into any OSGi-based application.
+
+### Using in an OSGi Runtime
+
+The jar already ships with all necessary OSGi headers and a `module-info.class`. No `Import-Package` entries for `java.*` packages are required when consuming the bundle.
+
+To add the bundle to an Eclipse feature or any OSGi runtime simply reference it:
+
+```xml
+
+```
+
+Both of these features ensure that our library can be seamlessly integrated into modular Java applications, providing robust dependency management and encapsulation.
+
+### Maven and Gradle Integration
To include in your project:
+
+##### Gradle
+```groovy
+implementation 'com.cedarsoftware:java-util:4.1.0'
```
+
+##### Maven
+```xml
com.cedarsoftwarejava-util
- 1.19.3
+ 4.1.0
```
-Like **java-util** and find it useful? **Tip** bitcoin: 1MeozsfDpUALpnu3DntHWXxoPJXvSAXmQA
-
-Also, check out json-io at https://github.com/jdereg/json-io
-
-Including in java-util:
-* **ArrayUtilities** - Useful utilities for working with Java's arrays [ ]
-* **ByteUtilities** - Useful routines for converting byte[] to HEX character [] and visa-versa.
-* **CaseInsensitiveMap** - When Strings are used as keys, they are compared without case. Can be used as regular Map with any Java object as keys, just specially handles Strings.
-* **CaseInsensitiveSet** - Set implementation that ignores String case for contains() calls, yet can have any object added to it (does not limit you to adding only Strings to it).
-* **Converter** - Convert from once instance to another. For example, convert("45.3", BigDecimal.class) will convert the String to a BigDecimal. Works for all primitives, primitive wrappers, Date, java.sql.Date, String, BigDecimal, and BigInteger. The method is very generous on what it allows to be converted. For example, a Calendar instance can be input for a Date or Long. Examine source to see all possibilities.
-* **DateUtilities** - Robust date String parser that handles date/time, date, time, time/date, string name months or numeric months, skips comma, etc. English month names only (plus common month name abbreviations), time with/without seconds or milliseconds, y/m/d and m/d/y ordering as well.
-* **DeepEquals** - Compare two object graphs and return 'true' if they are equivalent, 'false' otherwise. This will handle cycles in the graph, and will call an equals() method on an object if it has one, otherwise it will do a field-by-field equivalency check for non-transient fields.
-* **EncryptionUtilities** - Makes it easy to compute MD5 checksums for Strings, byte[], as well as making it easy to AES-128 encrypt Strings and byte[]'s.
-* **IOUtilities** - Handy methods for simplifying I/O including such niceties as properly setting up the input stream for HttpUrlConnections based on their specified encoding. Single line .close() method that handles exceptions for you.
-* **MathUtilities** - Handy mathematical algorithms to make your code smaller. For example, minimum of array of values.
-* **ReflectionUtils** - Simple one-liners for many common reflection tasks.
-* **SafeSimpleDateFormat** - Instances of this class can be stored as member variables and reused without any worry about thread safety. Fixing the problems with the JDK's SimpleDateFormat and thread safety (no reentrancy support).
-* **StringUtilities** - Helpful methods that make simple work of common String related tasks.
-* **SystemUtilities** - A Helpful utility methods for working with external entities like the OS, environment variables, and system properties.
-* **Traverser** - Pass any Java object to this Utility class, it will call your passed in anonymous method for each object it encounters while traversing the complete graph. It handles cycles within the graph. Permits you to perform generalized actions on all objects within an object graph.
-* **UniqueIdGenerator** - Generates a Java long unique id, that is unique across server in a cluster, never hands out the same value, has massive entropy, and runs very quickly.
-* **UrlUtitilies** - Fetch cookies from headers, getUrlConnections(), HTTP Response error handler, and more.
-* **UrlInvocationHandler** - Use to easily communicate with RESTful JSON servers, especially ones that implement a Java interface that you have access to.
-
-### Sponsors
-[](https://www.yourkit.com/.net/profiler/index.jsp)
-
-YourKit supports open source projects with its full-featured Java Profiler.
-YourKit, LLC is the creator of YourKit Java Profiler
-and YourKit .NET Profiler,
-innovative and intelligent tools for profiling Java and .NET applications.
-
-[](https://www.jetbrains.com/idea/)
-
-Version History
-* 1.19.3
- * Bug fix: `CaseInsensitiveMap.entrySet()` - calling `entry.setValue(k, v)` while iterating the entry set, was not updating the underlying value. This has been fixed and test case added.
-* 1.19.2
- * The order in which system properties are read versus environment variables via the `SystemUtilities.getExternalVariable()` method has changed. System properties are checked first, then environment variables.
-* 1.19.1
- * Fixed issue in `DeepEquals.deepEquals()` where a Container type (`Map` or `Collection`) was being compared to a non-container - the result of this comparison was inconsistent. It is always false if a Container is compared to a non-container type (anywhere within the object graph), regardless of the comparison order A, B versus comparing B, A.
-* 1.19.0
- * `StringUtilities.createUtf8String(byte[])` API added which is used to easily create UTF-8 strings without exception handling code.
- * `StringUtilities.getUtf8Bytes(String s)` API added which returns a byte[] of UTF-8 bytes from the passed in Java String without any exception handling code required.
- * `ByteUtilities.isGzipped(bytes[])` API added which returns true if the `byte[]` represents gzipped data.
- * `IOUtilities.compressBytes(byte[])` API added which returns the gzipped version of the passed in `byte[]` as a `byte[]`
- * `IOUtilities.uncompressBytes(byte[])` API added which returns the original byte[] from the passed in gzipped `byte[]`.
- * JavaDoc issues correct to support Java 1.8 stricter JavaDoc compilation.
-* 1.18.1
- * `UrlUtilities` now allows for per-thread `userAgent` and `referrer` as well as maintains backward compatibility for setting these values globally.
- * `StringUtilities` `getBytes()` and `createString()` now allow null as input, and return null for output for null input.
- * Javadoc updated to remove errors flagged by more stringent Javadoc 1.8 generator.
-* 1.18.0
- * Support added for `Timestamp` in `Converter.convert()`
- * `null` can be passed into `Converter.convert()` for primitive types, and it will return their logical 0 value (0.0f, 0.0d, etc.). For primitive wrappers, atomics, etc, null will be returned.
- * "" can be passed into `Converter.convert()` and it will set primitives to 0, and the object types (primitive wrappers, dates, atomics) to null. `String` will be set to "".
-* 1.17.1
- * Added full support for `AtomicBoolean`, `AtomicInteger`, and `AtomicLong` to `Converter.convert(value, AtomicXXX)`. Any reasonable value can be converted to/from these, including Strings, Dates (`AtomicLong`), all `Number` types.
- * `IOUtilities.flush()` now supports `XMLStreamWriter`
-* 1.17.0
- * `UIUtilities.close()` now supports `XMLStreamReader` and `XMLStreamWriter` in addition to `Closeable`.
- * `Converter.convert(value, type)` - a value of null is supported, and returns null. A null type, however, throws an `IllegalArgumentException`.
-* 1.16.1
- * In `Converter.convert(value, type)`, the value is trimmed of leading / trailing white-space if it is a String and the type is a `Number`.
-* 1.16.0
- * Added `Converter.convert()` API. Allows converting instances of one type to another. Handles all primitives, primitive wrappers, `Date`, `java.sql.Date`, `String`, `BigDecimal`, and `BigInteger`. Additionally, input (from) argument accepts `Calendar`.
- * Added static `getDateFormat()` to `SafeSimpleDateFormat` for quick access to thread local formatter (per format `String`).
-* 1.15.0
- * Switched to use Log4J2 () for logging.
-* 1.14.1
- * bug fix: `CaseInsensitiveMa.keySet()` was only initializing the iterator once. If `keySet()` was called a 2nd time, it would no longer work.
-* 1.14.0
- * bug fix: `CaseInsensitiveSet()`, the return value for `addAll()`, `returnAll()`, and `retainAll()` was wrong in some cases.
-* 1.13.3
- * `EncryptionUtilities` - Added byte[] APIs. Makes it easy to encrypt/decrypt `byte[]` data.
- * `pom.xml` had extraneous characters inadvertently added to the file - these are removed.
- * 1.13.1 & 13.12 - issues with sonatype
-* 1.13.0
- * `DateUtilities` - Day of week allowed (properly ignored).
- * `DateUtilities` - First (st), second (nd), third (rd), and fourth (th) ... supported.
- * `DateUtilities` - The default toString() standard date / time displayed by the JVM is now supported as a parseable format.
- * `DateUtilities` - Extra whitespace can exist within the date string.
- * `DateUtilities` - Full time zone support added.
- * `DateUtilities` - The date (or date time) is expected to be in isolation. Whitespace on either end is fine, however, once the date time is parsed from the string, no other content can be left (prevents accidently parsing dates from dates embedded in text).
- * `UrlUtilities` - Removed proxy from calls to `URLUtilities`. These are now done through the JVM.
-* 1.12.0
- * `UniqueIdGenerator` uses 99 as the cluster id when the JAVA_UTIL_CLUSTERID environment variable or System property is not available. This speeds up execution on developer's environments when they do not specify `JAVA_UTIL_CLUSTERID`.
- * All the 1.11.x features rolled up.
-* 1.11.3
- * `UrlUtilities` - separated out call that resolves `res://` to a public API to allow for wider use.
-* 1.11.2
- * Updated so headers can be set individually by the strategy (`UrlInvocationHandler`)
- * `InvocationHandler` set to always uses `POST` method to allow additional `HTTP` headers.
-* 1.11.1
- * Better IPv6 support (`UniqueIdGenerator`)
- * Fixed `UrlUtilities.getContentFromUrl()` (`byte[]`) no longer setting up `SSLFactory` when `HTTP` protocol used.
-* 1.11.0
- * `UrlInvocationHandler`, `UrlInvocationStrategy` - Updated to allow more generalized usage. Pass in your implementation of `UrlInvocationStrategy` which allows you to set the number of retry attempts, fill out the URL pattern, set up the POST data, and optionally set/get cookies.
- * Removed dependency on json-io. Only remaining dependency is Apache commons-logging.
-* 1.10.0
- * Issue #3 fixed: `DeepEquals.deepEquals()` allows similar `Map` (or `Collection`) types to be compared without returning 'not equals' (false). Example, `HashMap` and `LinkedHashMap` are compared on contents only. However, compare a `SortedSet` (like `TreeMap`) to `HashMap` would fail unless the Map keys are in the same iterative order.
- * Tests added for `UrlUtilities`
- * Tests added for `Traverser`
-* 1.9.2
- * Added wildcard to regex pattern to `StringUtilities`. This API turns a DOS-like wildcard pattern (where * matches anything and ? matches a single character) into a regex pattern useful in `String.matches()` API.
-* 1.9.1
- * Floating-point allow difference by epsilon value (currently hard-coded on `DeepEquals`. Will likely be optional parameter in future version).
-* 1.9.0
- * `MathUtilities` added. Currently, variable length `minimum(arg0, arg1, ... argn)` and `maximum()` functions added. Available for `long`, `double`, `BigInteger`, and `BigDecimal`. These cover the smaller types.
- * `CaseInsensitiveMap` and `CaseInsensitiveSet` `keySet()` and `entrySet()` are faster as they do not make a copy of the entries. Internally, `CaseInsensitiveString` caches it's hash, speeding up repeated access.
- * `StringUtilities levenshtein()` and `damerauLevenshtein()` added to compute edit length. See Wikipedia for understand of the difference. Currently recommend using `levenshtein()` as it uses less memory.
- * The Set returned from the `CaseInsensitiveMap.entrySet()` now contains mutable entry's (value-side). It had been using an immutable entry, which disallowed modification of the value-side during entry walk.
-* 1.8.4
- * `UrlUtilities`, fixed issue where the default settings for the connection were changed, not the settings on the actual connection.
-* 1.8.3
- * `ReflectionUtilities` has new `getClassAnnotation(classToCheck, annotation)` API which will return the annotation if it exists within the classes super class hierarchy or interface hierarchy. Similarly, the `getMethodAnnotation()` API does the same thing for method annotations (allow inheritance - class or interface).
-* 1.8.2
- * `CaseInsensitiveMap` methods `keySet()` and `entrySet()` return Sets that are identical to how the JDK returns 'view' Sets on the underlying storage. This means that all operations, besides `add()` and `addAll()`, are supported.
- * `CaseInsensitiveMap.keySet()` returns a `Set` that is case insensitive (not a `CaseInsensitiveSet`, just a `Set` that ignores case). Iterating this `Set` properly returns each originally stored item.
-* 1.8.1
- * Fixed `CaseInsensitiveMap() removeAll()` was not removing when accessed via `keySet()`
-* 1.8.0
- * Added `DateUtilities`. See description above.
-* 1.7.4
- * Added "res" protocol (resource) to `UrlUtilities` to allow files from classpath to easily be loaded. Useful for testing.
-* 1.7.2
- * `UrlUtilities.getContentFromUrl() / getContentFromUrlAsString()` - removed hard-coded proxy server name
-* 1.7.1
- * `UrlUtilities.getContentFromUrl() / getContentFromUrlAsString()` - allow content to be fetched as `String` or binary (`byte[]`).
-* 1.7.0
- * `SystemUtilities` added. New API to fetch value from environment or System property
- * `UniqueIdGenerator` - checks for environment variable (or System property) JAVA_UTIL_CLUSTERID (0-99). Will use this if set, otherwise last IP octet mod 100.
-* 1.6.1
- * Added: `UrlUtilities.getContentFromUrl()`
-* 1.6.0
- * Added `CaseInsensitiveSet`.
-* 1.5.0
- * Fixed: `CaseInsensitiveMap's iterator.remove()` method, it did not remove items.
- * Fixed: `CaseInsensitiveMap's equals()` method, it required case to match on keys.
-* 1.4.0
- * Initial version
-
-By: John DeRegnaucourt and Ken Partlow
+
+### π Framework Integration Examples
+
+For comprehensive framework integration examples including Spring, Jakarta EE, Spring Boot Auto-Configuration, Microservices, Testing, and Performance Monitoring, see **[frameworks.md](frameworks.md)**.
+
+Key integrations include:
+- **Spring Framework** - Configuration beans and case-insensitive property handling
+- **Jakarta EE/JEE** - CDI producers and validation services
+- **Spring Boot** - Auto-configuration with corrected cache constructors
+- **Microservices** - Service discovery and cloud-native configuration
+- **Testing** - Enhanced test comparisons with DeepEquals
+- **Monitoring** - Micrometer metrics integration
+
+## Feature Options
+
+Modern enterprise applications demand libraries that adapt to diverse security requirements, performance constraints, and operational environments. Following the architectural principles embraced by industry leaders like Google (with their extensive use of feature flags), Netflix (with their chaos engineering configurations), Amazon (with their service-specific tuning), and Meta (with their A/B testing infrastructure), java-util embraces a **flexible feature options approach** that puts control directly in the hands of developers and operations teams.
+
+This approach aligns with current best practices in cloud-native development, including GitOps configurations, service mesh policies, and progressive delivery patterns that define the cutting edge of modern software architecture.
+
+Rather than forcing a one-size-fits-all configuration, java-util provides granular control over every aspect of its behavior through system properties. This approach enables:
+
+- **Zero-downtime security hardening** - Enable security features without code changes
+- **Environment-specific tuning** - Different limits for development vs. production
+- **Gradual rollout strategies** - Test new security features with feature flags
+- **Compliance flexibility** - Meet varying regulatory requirements across deployments
+- **Performance optimization** - Fine-tune resource limits based on actual usage patterns
+
+All security features are **disabled by default** to ensure seamless upgrades, with the flexibility to enable and configure them per environment. This design philosophy allows java-util to serve both lightweight applications and enterprise-grade systems from the same codebase.
+
+
+
+
Fully Qualified Property Name
+
Allowed Values
+
Default Value
+
Description
+
+
+
ArrayUtilities
+
+
+
arrayutilities.security.enabled
+
true, false
+
false
+
Master switch for all ArrayUtilities security features
+
+
+
arrayutilities.component.type.validation.enabled
+
true, false
+
false
+
Block dangerous system classes in array operations
Master switch for all UrlUtilities security features
+
+
+
urlutilities.max.download.size
+
Long
+
0 (disabled)
+
Max download size in bytes
+
+
+
urlutilities.max.content.length
+
Long
+
0 (disabled)
+
Max Content-Length header value
+
+
+
urlutilities.allow.internal.hosts
+
true, false
+
true
+
Allow access to internal/local hosts
+
+
+
urlutilities.allowed.protocols
+
Comma-separated
+
http,https,ftp
+
Allowed protocols
+
+
+
urlutilities.strict.cookie.domain
+
true, false
+
false
+
Enable strict cookie domain validation
+
+
+
Converter
+
+
+
converter.modern.time.long.precision
+
millis, nanos
+
millis
+
Precision for Instant, ZonedDateTime, OffsetDateTime conversions
+
+
+
converter.duration.long.precision
+
millis, nanos
+
millis
+
Precision for Duration conversions
+
+
+
converter.localtime.long.precision
+
millis, nanos
+
millis
+
Precision for LocalTime conversions
+
+
+
Other
+
+
+
java.util.force.jre
+
true, false
+
false
+
Force JRE simulation (testing only)
+
+
+
+> **Note:** All security features are disabled by default for backward compatibility. Most properties accepting `0` disable the feature entirely. Properties can be set via system properties (`-D` flags) or environment variables.
+
+### Logging
+
+Because `java-util` has no dependencies on other libraries, `java-util` uses the Java built-in `java.util.logging` for all output. See the
+[user guide](userguide.md#redirecting-javautillogging) for ways to route
+these logs to SLF4J or Log4j 2.
+
+### User Guide
+[View detailed documentation on all utilities.](userguide.md)
+
+See [changelog.md](/changelog.md) for revision history.
diff --git a/TestDeltaDebug.java b/TestDeltaDebug.java
new file mode 100644
index 000000000..f6880c57d
--- /dev/null
+++ b/TestDeltaDebug.java
@@ -0,0 +1,98 @@
+import com.cedarsoftware.util.GraphComparator;
+import com.cedarsoftware.util.GraphComparator.Delta;
+import com.cedarsoftware.util.UniqueIdGenerator;
+import com.cedarsoftware.util.Traverser;
+import java.util.List;
+
+public class TestDeltaDebug {
+
+ static class Pet {
+ long id;
+ String[] nickNames;
+
+ Pet(long id, String[] nickNames) {
+ this.id = id;
+ this.nickNames = nickNames;
+ }
+ }
+
+ static class Person {
+ long id;
+ Pet[] pets;
+
+ Person(long id) {
+ this.id = id;
+ }
+ }
+
+ public static void main(String[] args) {
+ // Create test data similar to the failing test
+ Person person1 = new Person(UniqueIdGenerator.getUniqueId());
+ Person person2 = new Person(person1.id);
+
+ long petId = UniqueIdGenerator.getUniqueId();
+ Pet pet1 = new Pet(petId, new String[]{"fido", "bruiser"});
+ Pet pet2 = new Pet(petId, new String[0]); // Empty array
+
+ person1.pets = new Pet[]{pet1};
+ person2.pets = new Pet[]{pet2};
+
+ // Create ID fetcher
+ GraphComparator.ID idFetcher = new GraphComparator.ID() {
+ public Object getId(Object objectToFetch) {
+ if (objectToFetch instanceof Person) {
+ return ((Person) objectToFetch).id;
+ } else if (objectToFetch instanceof Pet) {
+ return ((Pet) objectToFetch).id;
+ }
+ return null;
+ }
+ };
+
+ // Compare
+ List deltas = GraphComparator.compare(person1, person2, idFetcher);
+
+ System.out.println("Number of deltas: " + deltas.size());
+ for (Delta delta : deltas) {
+ System.out.println("Delta: cmd=" + delta.getCmd() +
+ ", fieldName=" + delta.getFieldName() +
+ ", id=" + delta.getId() +
+ ", optionalKey=" + delta.getOptionalKey() +
+ ", sourceValue=" + delta.getSourceValue() +
+ ", targetValue=" + delta.getTargetValue());
+ }
+
+ // Check if person1.pets[0] has an ID
+ System.out.println("\nBefore applying deltas:");
+ System.out.println("person1.pets[0].id = " + person1.pets[0].id);
+ System.out.println("petId = " + petId);
+
+ // Debug: Let's see what's being traversed and what has IDs
+ System.out.println("\nObjects being traversed:");
+ Traverser.traverse(person1, visit -> {
+ Object o = visit.getNode();
+ boolean hasId = idFetcher.getId(o) != null;
+ if (o instanceof Person) {
+ System.out.println(" Person: id=" + ((Person)o).id + ", hasId=" + hasId);
+ System.out.println(" Fields: " + visit.getFields());
+ } else if (o instanceof Pet) {
+ System.out.println(" Pet: id=" + ((Pet)o).id + ", hasId=" + hasId);
+ System.out.println(" Fields: " + visit.getFields());
+ } else if (o != null) {
+ System.out.println(" " + o.getClass().getSimpleName() + ": " + o + ", hasId=" + hasId);
+ }
+ }, null);
+
+ // Apply deltas
+ List errors = GraphComparator.applyDelta(person1, deltas, idFetcher, GraphComparator.getJavaDeltaProcessor());
+
+ System.out.println("\nErrors: " + errors.size());
+ for (GraphComparator.DeltaError error : errors) {
+ System.out.println("Error: " + error.getError());
+ }
+
+ System.out.println("\nAfter applying deltas:");
+ System.out.println("person1.pets[0].nickNames.length = " + person1.pets[0].nickNames.length);
+ System.out.println("Expected: 0");
+ }
+}
\ No newline at end of file
diff --git a/agents.md b/agents.md
new file mode 100644
index 000000000..1b448fe0f
--- /dev/null
+++ b/agents.md
@@ -0,0 +1,43 @@
+# AGENTS
+
+These instructions guide any automated agent (such as Codex) that modifies this
+repository.
+
+## Coding Conventions
+- Use **four spaces** for indentationβno tabs.
+- End every file with a newline and use Unix line endings.
+- Keep code lines under **120 characters** where possible.
+- Follow standard Javadoc style for any new public APIs.
+- This library maintains JDK 1.8 source compatibility, please make sure to not use source constructs or expected JDK libary calls beyond JDK 1.8.
+- Whenever you need to use reflection, make sure you use ReflectionUtils APIs from java-util.
+- For data structure verification in JUnit tests, use DeepEquals.deepEquals() [make sure to pass the option so you can see the "diff"]. This will make it clear where there is a difference in a complex data structure.
+- If you need null support in ConcurrentMap implementations, use java-utils ConcurrentMaps that are null safe.
+- Whenever parsing a String date, use either java-util DateUtilities.parse() (Date or ZonedDateTime), or use Converter.converter() which will use it inside.
+- Use Converter.convert() as needed to marshal data types to match.
+- For faster stream reading, use the FastByteArrayInputStream and FastByteArrayOutputStream.
+- For faster Readers, use FastReader and FastWriter.
+- USe StringUtilities APIs for common simplifications like comparing without worrying about null, for example. Many other APIs on there.
+- When a Unique ID is needed, use the UniqueIdGenerator.getUniqueId19() as it will give you a long, up to 10,000 per millisecond, and you can always get the time of when it was created, from it, and it is strictly increasing.
+- IOUtilities has some nice APIs to close streams without extra try/catch blocks, and also has a nice transfer APIs, and transfer APIs that show call back with transfer stats.
+- ClassValueMap and ClassValueSet make using JDK's ClassValue much easier yet retain the benefits of ClassValue in terms of speed.
+- Of course, for CaseInsensitiveMaps, there is no better one that CaseInsensitiveMap.
+- And if you need to create large amounts of Maps, CompactMap (and its variants) use significantly less space than regular JDK maps.
+
+## Commit Messages
+- Start with a short imperative summary (max ~50 characters).
+- Leave a blank line after the summary, then add further details if needed.
+- Donβt amend or rewrite existing commits.
+- Please list the Codex agent as the author so we can see that in the "Blame" view at the line number level.
+
+## Testing
+- Run `mvn -q test` before committing to ensure tests pass.
+- If tests canβt run due to environment limits, note this in the PR description.
+
+## Documentation
+- Update `changelog.md` with a bullet about your change.
+- Update `userguide.md` whenever you add or modify public-facing APIs.
+
+## Pull Request Notes
+- Summarize key changes and reference the main files touched.
+- Include a brief βTestingβ section summarizing test results or noting any limitations.
+
diff --git a/badge.svg b/badge.svg
new file mode 100644
index 000000000..bd7153751
--- /dev/null
+++ b/badge.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 000000000..40adafcd3
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,1060 @@
+### Revision History
+#### 4.2.0 (unreleased)
+
+#### 4.1.0
+> * **FIXED**: `ClassUtilities.setUseUnsafe()` is now thread-local instead of global, preventing race conditions in multi-threaded environments where concurrent threads need different unsafe mode settings
+>
+> * **IMPROVED**: `ClassUtilities` comprehensive improvements from GPT-5 review:
+>
+> **π SECURITY FIXES:**
+> * **Enhanced class loading security with additional blocked prefixes**: Added blocking for `jdk.nashorn.` package to prevent Nashorn JavaScript engine exploitation; added blocking for `java.lang.invoke.MethodHandles$Lookup` class which can open modules reflectively and bypass security boundaries
+> * **Added percent-encoded path traversal blocking**: Enhanced resource path validation to block percent-encoded traversal sequences (%2e%2e, %2E%2E, etc.) before normalization; prevents bypass attempts using URL encoding
+> * **Enhanced resource path security**: Added blocking of absolute Windows drive paths (e.g., "C:/...", "D:/...") in resource loading to prevent potential security issues
+> * **Enhanced security blocking**: Added package-level blocking for `javax.script.*` to prevent loading of any class in that package
+> * **Added belt-and-suspenders alias security**: addPermanentClassAlias() now validates classes through SecurityChecker.verifyClass() to prevent aliasing to blocked classes
+> * **Fixed security bypass in cache hits**: Alias and cache hits now properly go through SecurityChecker.verifyClass() to prevent bypassing security checks
+> * **Updated Unsafe permission check**: Replaced outdated "accessClassInPackage.sun.misc" permission with custom "com.cedarsoftware.util.enableUnsafe" permission appropriate for modern JDKs
+> * **Simplified resource path validation**: Removed over-eager validation that blocked legitimate resources, focusing on actual security risks (.., null bytes, backslashes)
+> * **Improved validateResourcePath() precision**: Made validation more precise - now only blocks null bytes, backslashes, and ".." path segments (not substrings), allowing legitimate filenames like "my..proto"
+>
+> **β‘ PERFORMANCE OPTIMIZATIONS:**
+> * **Optimized constructor matching performance**: Eliminated redundant toArray() calls per constructor attempt by converting collection to array once
+> * **Optimized resource path validation**: Replaced regex pattern matching with simple character checks, eliminating regex engine overhead
+> * **Optimized findClosest() performance**: Pull distance map once from ClassHierarchyInfo to avoid repeated computeInheritanceDistance() calls
+> * **Optimized findLowestCommonSupertypesExcluding performance**: Now iterates the smaller set when finding intersection
+> * **Optimized findInheritanceMatches hot path**: Pre-cache ClassHierarchyInfo lookups for unique value classes
+> * **Optimized loadClass() string operations**: Refactored JVM descriptor parsing to count brackets once upfront, reducing string churn
+> * **Optimized hot-path logging performance**: Added isLoggable() guards to all varargs logging calls to prevent unnecessary array allocations
+> * **Optimized getParameters() calls**: Cached constructor.getParameters() results to avoid repeated allocations
+> * **Optimized buffer creation**: Cached zero-length ByteBuffer and CharBuffer instances to avoid repeated allocations
+> * **Optimized trySetAccessible caching**: Fixed to actually use its accessibility cache, preventing repeated failed setAccessible() attempts
+> * **Added accessibility caching**: Implemented caching for trySetAccessible using synchronized WeakHashMap for memory-safe caching
+> * **Prevented zombie cache entries**: Implemented NamedWeakRef with ReferenceQueue to automatically clean up dead WeakReference entries
+>
+> **π BUG FIXES:**
+> * **Fixed interface depth calculation**: Changed ClassHierarchyInfo to use max BFS distance instead of superclass chain walking
+> * **Fixed tie-breaking for common supertypes**: Changed findLowestCommonSupertypesExcluding to sort by sum of distances from both classes
+> * **Fixed JPMS SecurityException handling**: Added proper exception handling for trySetAccessible calls under JPMS
+> * **Fixed nameToClass initialization inconsistency**: Added "void" type to static initializer and included common aliases in clearCaches()
+> * **Fixed tie-breaker logic**: Corrected shouldPreferNewCandidate() to properly prefer more specific types
+> * **Fixed areAllConstructorsPrivate() for implicit constructors**: Method now correctly returns false for classes with no declared constructors
+> * **Fixed mutable buffer sharing**: ByteBuffer, CharBuffer, and array default instances are now created fresh on each call
+> * **Fixed inner class construction**: Inner class constructors with additional parameters beyond enclosing instance are now properly matched
+> * **Fixed varargs ArrayStoreException vulnerability**: Added proper guards when packing values into varargs arrays
+> * **Fixed named-parameter gating**: Constructor parameter name detection now checks ALL parameters have real names
+> * **Fixed Currency default creation**: Currency.getInstance(Locale.getDefault()) now gracefully falls back to USD
+> * **Fixed generated-key Map ordering**: Fixed bug where Maps with generated keys could inject nulls when keys had gaps
+> * **Fixed loadResourceAsBytes() leading slash handling**: Added fallback to strip leading slash when ClassLoader.getResourceAsStream() fails
+> * **Fixed OSGi class loading consistency**: OSGi framework classes now loaded using consistent classloader
+> * **Fixed ClassLoader key mismatch**: Consistently resolve null ClassLoader to same instance
+> * **Fixed computeIfAbsent synchronization**: Replaced non-synchronized computeIfAbsent with properly synchronized getLoaderCache()
+> * **Fixed off-by-one in class load depth**: Now validates nextDepth instead of currentDepth
+> * **Fixed OSGi/JPMS classloader resolution**: Simplified loadClass() to consistently use getClassLoader() method
+> * **Fixed permanent alias preservation**: Split aliases into built-in and user maps so clearCaches() preserves user-added permanent aliases
+> * **Fixed removePermanentClassAlias loader cache invalidation**: Both add and remove methods now properly clear per-loader cache entries
+> * **Fixed findLowestCommonSupertypesExcluding NPE**: Added null-check for excluded parameter
+> * **Fixed ArrayStoreException in matchArgumentsWithVarargs**: Added final try-catch guard for exotic conversion edge cases
+> * **Fixed OSGi loader cache cleanup**: clearCaches() now properly clears the osgiClassLoaders cache
+> * **Fixed OSGi cache NPE**: Fixed potential NullPointerException in getOSGiClassLoader() when using computeIfAbsent()
+> * **Fixed incorrect comment**: Updated accessibilityCache comment to correctly state it uses Collections.synchronizedMap
+>
+> **π― API IMPROVEMENTS:**
+> * **Added boxing support in computeInheritanceDistance()**: Primitive types can now reach reference types through boxing
+> * **Added primitive widening support**: Implemented JLS 5.1.2 primitive widening conversions (byteβshortβintβlongβfloatβdouble)
+> * **Added Java-style array support**: loadClass() now supports Java-style array names like "int[][]" and "java.lang.String[]"
+> * **Added varargs constructor support**: Implemented proper handling for varargs constructors
+> * **Enhanced varargs support with named parameters**: newInstanceWithNamedParameters() now properly handles varargs parameters
+> * **Improved API clarity for wrapper types**: Changed getArgForType to only provide default values for actual primitives
+> * **Improved API clarity**: Renamed defaultClass parameter to defaultValue in findClosest() method
+> * **Fixed API/docs consistency for null handling**: All primitive/wrapper conversion methods now consistently throw IllegalArgumentException
+> * **Added null safety**: Made doesOneWrapTheOther() null-safe, returning false for null inputs
+> * **Added cache management**: Added clearCaches() method for testing and hot-reload scenarios
+> * **Added deterministic Map fallback ordering**: When constructor parameter matching falls back to Map.values() and Map is HashMap, values are sorted alphabetically
+> * **Implemented ClassLoader-scoped caching**: Added WeakHashMap-based caching with ClassLoader keys and WeakReference values
+>
+> **π DOCUMENTATION & CLEANUP:**
+> * **Updated documentation**: Enhanced class-level Javadoc and userguide.md to accurately reflect all public methods
+> * **Documented Map ordering requirement**: Added documentation to newInstance() methods clarifying LinkedHashMap usage
+> * **Improved documentation clarity**: Updated computeInheritanceDistance() documentation to clarify caching
+> * **Added comprehensive edge case test coverage**: Created ClassUtilitiesEdgeCaseTest with tests for deep interface hierarchies
+> * **Added tests for public utility methods**: Added tests for logMethodAccessIssue(), logConstructorAccessIssue(), and clearCaches()
+> * **Removed deprecated method**: Removed deprecated indexOfSmallestValue() method
+> * **Removed unused private method**: Removed getMaxReflectionOperations() and associated constant
+> * **Removed unnecessary flush() call**: Eliminated no-op ByteArrayOutputStream.flush() in readInputStreamFully()
+> * **Clarified Converter usage**: Added comment explaining why ClassUtilities uses legacy Converter.getInstance()
+>
+> **π§ CONFIGURATION & DEFAULTS:**
+> * **Fixed surprising default values**: Changed default instance creation to use predictable, stable values:
+> * Date/time types now default to epoch (1970-01-01) instead of current time
+> * UUID defaults to nil UUID (all zeros) instead of random UUID
+> * Pattern defaults to empty pattern instead of match-all ".*"
+> * URL/URI mappings commented out to return null instead of potentially connectable localhost URLs
+> * **Removed problematic defaults**:
+> * Removed EnumMap default mapping to TimeUnit.class
+> * Removed EnumSet.class null supplier from ASSIGNABLE_CLASS_MAPPING
+> * Removed Class.class β String.class mapping
+> * Removed Comparableβempty string mapping
+> * **Preserved mapping order**: Changed ASSIGNABLE_CLASS_MAPPING to LinkedHashMap for deterministic iteration
+> * **Improved immutability**: Made PRIMITIVE_WIDENING_DISTANCES and all inner maps unmodifiable
+> * **Reduced logging noise**: Changed various warnings from WARNING to FINE level for expected JPMS violations
+> * **Improved OSGi loader discovery order**: Changed getClassLoader() to try context loader first, then anchor, then OSGi
+> * **Improved resource path handling for Windows developers**: Backslashes in resource paths are now normalized to forward slashes
+> * **Simplified primitive checks**: Removed redundant isPrimitive() OR checks since methods handle both primitives and wrappers
+> * **Simplified SecurityManager checks**: Removed redundant ReflectPermission check in trySetAccessible()
+> * **Made record support fields volatile**: Proper thread-safe lazy initialization for JDK 14+ features
+>
+> * **IMPROVED**: `CaseInsensitiveSet` refactored to use `Collections.newSetFromMap()` for cleaner implementation:
+> * Simplified implementation using Collections.newSetFromMap(CaseInsensitiveMap) internally
+> * Added Java 8+ support: spliterator(), removeIf(Predicate), and enhanced forEach() methods
+> * Fixed removeAll behavior for proper case-insensitive removal with non-CaseInsensitive collections
+> * Maintained full API compatibility
+>
+> * **FIXED**: `DeepEquals` collection comparison was too strict when comparing different Collection implementations:
+> * Fixed UnmodifiableCollection comparison with Lists/ArrayLists based on content
+> * Relaxed plain Collection vs List comparison as unordered collections
+> * Preserved Set vs List distinction due to incompatible equality semantics
+>
+> * **FIXED**: `SafeSimpleDateFormat` thread-safety and lenient mode issues:
+> * Fixed NPE in setters by initializing parent DateFormat fields
+> * Fixed lenient propagation to both Calendar and SimpleDateFormat
+> * Keep parent fields in sync when setters are called
+>
+> * **IMPROVED**: `SafeSimpleDateFormat` completely redesigned with copy-on-write semantics:
+> * Copy-on-write mutations create new immutable state snapshots
+> * Thread-local LRU caching for SimpleDateFormat instances
+> * No locks on hot path - format/parse use thread-local cached instances
+> * Immutable state tracking for all configuration
+> * Smart cache invalidation on configuration changes
+> * Backward compatibility maintained
+>
+> * **FIXED**: `UniqueIdGenerator` Java 8 compatibility:
+> * Fixed Thread.onSpinWait() using reflection for Java 9+, no-op fallback for Java 8
+>
+> * **PERFORMANCE**: Optimized `DeepEquals` based on GPT-5 code review:
+> * **Algorithm & Data Structure Improvements:**
+> * Migrated from LinkedList to ArrayDeque for stack operations
+> * Pop-immediately optimization eliminating double iterations
+> * Depth tracking optimization avoiding costly parent chain traversal
+> * Early termination optimization using LIFO comparison order
+> * Primitive array optimization comparing directly without stack allocations
+> * Pre-size hash buckets to avoid rehashing on large inputs
+> * Fixed O(nΒ²) path building using forward build and single reverse
+> * Optimized probe comparisons to bypass diff generation completely
+> * Added Arrays.equals fast-path for primitive arrays
+> * Optimized decomposeMap to compute hash once per iteration
+> * Added fast path for integral number comparison avoiding BigDecimal
+>
+> * **Correctness Fixes:**
+> * Changed epsilon value from 1e-15 to 1e-12 for practical floating-point comparisons
+> * Adjusted hash scales to maintain hash-equals contract with new epsilon
+> * Fixed List comparison semantics - Lists only compare equal to other Lists
+> * Fixed floating-point comparison using absolute tolerance for near-zero
+> * Made NaN comparison consistent via bitwise equality
+> * Fixed hash-equals contract for floating-point with proper NaN/infinity handling
+> * Fixed infinity comparison preventing infinities from comparing equal to finite numbers
+> * Fixed ConcurrentModificationException using iterator.remove()
+> * Fixed formatDifference crash using detailNode approach
+> * Fixed deepHashCode bucket misalignment with slow-path fallback
+> * Fixed leftover detection for unmatched elements
+> * Fixed visited set leakage in candidate matching
+> * Fixed non-monotonic depth budget clamping
+> * Fixed deepHashCode Map collisions using XOR for key-value pairs
+>
+> * **Features & Improvements:**
+> * Added Java Record support using record components instead of fields
+> * Added Deque support with List compatibility
+> * Improved sensitive data detection with refined patterns
+> * Improved MAP_MISSING_KEY error messages with clearer formatting
+> * Added security check in formatComplexObject for sensitive fields
+> * Added string sanitization for secure errors
+> * Type-safe visited set using Set
+> * Skip static/transient fields in formatting
+> * Implemented global depth budget across recursive paths
+> * Added Locale.ROOT for consistent formatting
+> * Gated diff_item storage behind option to prevent retention
+> * Added DIFF_ITEM constant for type-safe usage
+>
+> * **Code Quality:**
+> * Removed static initializer mutating global system properties
+> * Removed unreachable AtomicInteger/AtomicLong branches
+> * Fixed Javadoc typos and added regex pattern commentary
+> * Fixed documentation to match default security settings
+> * Performance micro-optimizations hoisting repeated lookups
+>
+> * **SECURITY & CORRECTNESS**: `ReflectionUtils` comprehensive fixes based on GPT-5 security audit:
+> * Fixed over-eager setAccessible() only for non-public members
+> * Fixed getNonOverloadedMethod enforcement for ANY parameter count
+> * Added interface hierarchy search using breadth-first traversal
+> * Fixed method annotation search traversing super-interfaces
+> * Fixed trusted-caller bypass - ReflectionUtils no longer excludes itself
+> * Removed static System.setProperty calls during initialization
+> * **Fixed Javadoc typo**: Corrected "instants hashCode()" to "instance's hashCode()" in deepHashCode documentation
+> * **Added regex pattern commentary**: Clarified that HEX_32_PLUS and UUID_PATTERN use lowercase patterns since strings are lowercased before matching
+> * **Type-safe visited set**: Changed visited set type from Set
+ *
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ * Ken Partlow (kpartlow@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed 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
+ *
+ * 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 com.cedarsoftware.util;
+public final class ByteUtilities {
+ // Security Configuration - using dynamic property reading for testability
+ // Default limits used when security is enabled but no custom limits specified
+ private static final int DEFAULT_MAX_HEX_STRING_LENGTH = 1000000; // 1MB hex string
+ private static final int DEFAULT_MAX_ARRAY_SIZE = 10000000; // 10MB byte array
+
+ private static boolean isSecurityEnabled() {
+ return Boolean.parseBoolean(System.getProperty("byteutilities.security.enabled", "false"));
+ }
+
+ private static int getMaxHexStringLength() {
+ if (!isSecurityEnabled()) {
+ return 0; // Disabled
+ }
+ String value = System.getProperty("byteutilities.max.hex.string.length");
+ if (value == null) {
+ return DEFAULT_MAX_HEX_STRING_LENGTH;
+ }
+ try {
+ int limit = Integer.parseInt(value);
+ return limit <= 0 ? 0 : limit; // 0 or negative means disabled
+ } catch (NumberFormatException e) {
+ return DEFAULT_MAX_HEX_STRING_LENGTH;
+ }
+ }
+
+ private static int getMaxArraySize() {
+ if (!isSecurityEnabled()) {
+ return 0; // Disabled
+ }
+ String value = System.getProperty("byteutilities.max.array.size");
+ if (value == null) {
+ return DEFAULT_MAX_ARRAY_SIZE;
+ }
+ try {
+ int limit = Integer.parseInt(value);
+ return limit <= 0 ? 0 : limit; // 0 or negative means disabled
+ } catch (NumberFormatException e) {
+ return DEFAULT_MAX_ARRAY_SIZE;
+ }
+ }
-public final class ByteUtilities
-{
- private static final char[] _hex =
- {
- '0', '1', '2', '3', '4', '5', '6', '7',
- '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
- };
+ // For encode: Array of hex digits.
+ static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
+ // For decode: Precomputed lookup table for hex digits.
+ // Maps ASCII codes (0β127) to their hex value or -1 if invalid.
+ private static final int[] HEX_LOOKUP = new int[128];
+ static {
+ Arrays.fill(HEX_LOOKUP, -1);
+ for (char c = '0'; c <= '9'; c++) {
+ HEX_LOOKUP[c] = c - '0';
+ }
+ for (char c = 'A'; c <= 'F'; c++) {
+ HEX_LOOKUP[c] = 10 + (c - 'A');
+ }
+ for (char c = 'a'; c <= 'f'; c++) {
+ HEX_LOOKUP[c] = 10 + (c - 'a');
+ }
+ }
/**
- *
- * {@code StringUtilities} instances should NOT be constructed in standard
- * programming. Instead, the class should be used statically as
- * {@code StringUtilities.trim();}.
- *
+ * Magic number identifying a gzip byte stream.
*/
- private ByteUtilities() {
- super();
- }
-
- // Turn hex String into byte[]
- // If string is not even length, return null.
+ private static final byte[] GZIP_MAGIC = {(byte) 0x1f, (byte) 0x8b};
- public static byte[] decode(final String s)
- {
- int len = s.length();
- if (len % 2 != 0)
- {
- return null;
- }
+ private ByteUtilities() { }
- byte[] bytes = new byte[len / 2];
- int pos = 0;
+ /**
+ * Convert the specified value (0 .. 15) to the corresponding hex digit.
+ *
+ * @param value to be converted
+ * @return '0'...'F' in char format.
+ */
+ public static char toHexChar(final int value) {
+ return HEX_ARRAY[value & 0x0f];
+ }
- for (int i = 0; i < len; i += 2)
- {
- byte hi = (byte)Character.digit(s.charAt(i), 16);
- byte lo = (byte)Character.digit(s.charAt(i + 1), 16);
- bytes[pos++] = (byte)(hi * 16 + lo);
- }
+ /**
+ * Converts a hexadecimal string into a byte array.
+ *
+ * @param s the hexadecimal string to decode
+ * @return the decoded byte array, or null if input is null, has odd length, or contains non-hex characters
+ */
+ public static byte[] decode(final String s) {
+ return decode((CharSequence) s);
+ }
- return bytes;
- }
+ /**
+ * Converts a hexadecimal CharSequence into a byte array.
+ *
+ * @param s the hexadecimal CharSequence to decode
+ * @return the decoded byte array, or null if input is null, has odd length, or contains non-hex characters
+ */
+ public static byte[] decode(final CharSequence s) {
+ if (s == null) {
+ return null;
+ }
+ final int len = s.length();
+
+ // Security check: validate hex string length
+ int maxHexLength = getMaxHexStringLength();
+ if (maxHexLength > 0 && len > maxHexLength) {
+ throw new SecurityException("Hex string length exceeds maximum allowed: " + maxHexLength);
+ }
+
+ // Must be even length
+ if ((len & 1) != 0) {
+ return null;
+ }
+ byte[] bytes = new byte[len >> 1];
+ for (int i = 0, j = 0; i < len; i += 2) {
+ char c1 = s.charAt(i);
+ char c2 = s.charAt(i + 1);
+ // Check if the characters are within ASCII range
+ if (c1 >= HEX_LOOKUP.length || c2 >= HEX_LOOKUP.length) {
+ return null;
+ }
+ int hi = HEX_LOOKUP[c1];
+ int lo = HEX_LOOKUP[c2];
+ if (hi == -1 || lo == -1) {
+ return null;
+ }
+ bytes[j++] = (byte) ((hi << 4) | lo);
+ }
+ return bytes;
+ }
- /**
- * Convert a byte array into a printable format containing a String of hex
- * digit characters (two per byte).
- *
- * @param bytes array representation
- * @return String hex digits
- */
- public static String encode(final byte[] bytes)
- {
- StringBuilder sb = new StringBuilder(bytes.length << 1);
- for (byte aByte : bytes)
- {
- sb.append(convertDigit(aByte >> 4));
- sb.append(convertDigit(aByte & 0x0f));
- }
- return sb.toString();
- }
+ /**
+ * Converts a byte array into a string of hex digits.
+ *
+ * @param bytes the byte array to encode
+ * @return the hexadecimal string representation, or null if input is null
+ */
+ public static String encode(final byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+
+ // Security check: validate byte array size
+ int maxArraySize = getMaxArraySize();
+ if (maxArraySize > 0 && bytes.length > maxArraySize) {
+ throw new SecurityException("Byte array size exceeds maximum allowed: " + maxArraySize);
+ }
+ char[] hexChars = new char[bytes.length * 2];
+ for (int i = 0, j = 0; i < bytes.length; i++) {
+ int v = bytes[i] & 0xFF;
+ hexChars[j++] = HEX_ARRAY[v >>> 4];
+ hexChars[j++] = HEX_ARRAY[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
- /**
- * Convert the specified value (0 .. 15) to the corresponding hex digit.
- *
- * @param value
- * to be converted
- * @return '0'..'F' in char format.
- */
- private static char convertDigit(final int value)
- {
- return _hex[(value & 0x0f)];
- }
+ /**
+ * Checks if the byte array represents gzip-compressed data.
+ */
+ public static boolean isGzipped(byte[] bytes) {
+ return isGzipped(bytes, 0);
+ }
- /**
- * @param bytes byte[] of bytes to test
- * @return true if bytes are gzip compressed, false otherwise.
- */
- public static boolean isGzipped(byte[] bytes)
- {
- return bytes[0] == (byte)0x1f && bytes[1] == (byte)0x8b;
- }
-}
+ /**
+ * Checks if the byte array represents gzip-compressed data starting at the given offset.
+ *
+ * @param bytes the byte array to inspect
+ * @param offset the starting offset within the array
+ * @return true if the bytes appear to be GZIP compressed, false if bytes is null, offset is invalid, or not enough bytes
+ */
+ public static boolean isGzipped(byte[] bytes, int offset) {
+ if (bytes == null || offset < 0 || offset >= bytes.length) {
+ return false;
+ }
+ return bytes.length - offset >= 2 &&
+ bytes[offset] == GZIP_MAGIC[0] && bytes[offset + 1] == GZIP_MAGIC[1];
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/cedarsoftware/util/CaseInsensitiveMap.java b/src/main/java/com/cedarsoftware/util/CaseInsensitiveMap.java
index 3a3240fc5..ebe6fd610 100644
--- a/src/main/java/com/cedarsoftware/util/CaseInsensitiveMap.java
+++ b/src/main/java/com/cedarsoftware/util/CaseInsensitiveMap.java
@@ -1,30 +1,156 @@
package com.cedarsoftware.util;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
import java.util.AbstractMap;
import java.util.AbstractSet;
-import java.util.Arrays;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
/**
- * Useful Map that does not care about the case-sensitivity of keys
- * when the key value is a String. Other key types can be used.
- * String keys will be treated case insensitively, yet key case will
- * be retained. Non-string keys will work as they normally would.
+ * A Map implementation that provides case-insensitive key comparison for {@link String} keys, while preserving
+ * the original case of the keys. Non-String keys are treated as they would be in a regular {@link Map}.
+ *
+ *
This Map is conditionally thread-safe based on if the backing map implementation is a thread-safe.
+ *
+ *
When the backing map is a {@link MultiKeyMap}, this map also supports multi-key operations
+ * with case-insensitive String key handling. Works with 1D keys (no collections or arrays in keys)
+ *
+ *
ConcurrentMap Implementation: This class implements {@link ConcurrentMap} and provides
+ * all concurrent operations ({@code putIfAbsent}, {@code replace}, bulk operations, etc.) with case-insensitive
+ * semantics. Thread safety depends entirely on the backing map implementation:
+ *
+ *
Thread-Safe: When backed by concurrent maps ({@link ConcurrentHashMap}, {@link ConcurrentHashMapNullSafe},
+ * {@link java.util.concurrent.ConcurrentSkipListMap}, {@link ConcurrentNavigableMapNullSafe}, {@link MultiKeyMap}, etc.),
+ * all operations are thread-safe.
+ *
Not Thread-Safe: When backed by non-concurrent maps ({@link LinkedHashMap},
+ * {@link HashMap}, etc.), concurrent operations work correctly but without thread-safety guarantees.
+ *
+ *
Choose your backing map implementation based on your concurrency requirements.
+ *
+ *
Key Features
+ *
+ *
Case-Insensitive String Keys: {@link String} keys are internally stored as {@code CaseInsensitiveString}
+ * objects, enabling case-insensitive equality and hash code behavior.
+ *
Preserves Original Case: The original casing of String keys is maintained for retrieval and iteration.
+ *
Compatible with All Map Operations: Supports Java 8+ map methods such as {@code computeIfAbsent()},
+ * {@code computeIfPresent()}, {@code merge()}, and {@code forEach()}, with case-insensitive handling of String keys.
+ *
Concurrent Operations: Implements {@link ConcurrentMap} interface with full support for concurrent
+ * operations including {@code putIfAbsent()}, {@code replace()}, and bulk operations with parallelism control.
+ *
Customizable Backing Map: Allows developers to specify the backing map implementation or automatically
+ * chooses one based on the provided source map.
+ *
Thread-Safe Case-Insensitive String Cache: Efficiently reuses {@code CaseInsensitiveString} instances
+ * to minimize memory usage and improve performance.
+ *
+ *
+ *
Usage Examples
+ *
{@code
+ * // Create a case-insensitive map with default LinkedHashMap backing (not thread-safe)
+ * CaseInsensitiveMap map = new CaseInsensitiveMap<>();
+ * map.put("Key", "Value");
+ * LOG.info(map.get("key")); // Outputs: Value
+ * LOG.info(map.get("KEY")); // Outputs: Value
+ *
+ * // Create a thread-safe case-insensitive map with ConcurrentHashMap backing
+ * ConcurrentMap concurrentMap = CaseInsensitiveMap.concurrent();
+ * concurrentMap.putIfAbsent("Key", "Value");
+ * LOG.info(concurrentMap.get("key")); // Outputs: Value (thread-safe)
+ *
+ * // Alternative: explicit constructor approach
+ * ConcurrentMap explicitMap = new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMap<>());
+ *
+ * // Create a case-insensitive map from an existing map
+ * Map source = Map.of("Key1", "Value1", "Key2", "Value2");
+ * CaseInsensitiveMap copiedMap = new CaseInsensitiveMap<>(source);
+ *
+ * // Use with non-String keys
+ * CaseInsensitiveMap intKeyMap = new CaseInsensitiveMap<>();
+ * intKeyMap.put(1, "One");
+ * LOG.info(intKeyMap.get(1)); // Outputs: One
+ * }
+ *
+ *
Backing Map Selection
+ *
+ * The backing map implementation is automatically chosen based on the type of the source map or can be explicitly
+ * specified. For example:
+ *
+ *
+ *
If the source map is a {@link TreeMap}, the backing map will also be a {@link TreeMap}.
+ *
If no match is found, the default backing map is a {@link LinkedHashMap}.
+ *
Unsupported map types, such as {@link IdentityHashMap}, will throw an {@link IllegalArgumentException}.
+ *
+ *
+ *
Performance Considerations
+ *
+ *
The {@code CaseInsensitiveString} cache reduces object creation overhead for frequently used keys.
+ *
For extremely long keys, caching is bypassed to avoid memory exhaustion.
+ *
Performance is comparable to the backing map implementation used.
+ *
+ *
+ *
Thread Safety and ConcurrentMap Implementation
+ *
+ * CaseInsensitiveMap implements {@link ConcurrentMap} and provides all concurrent operations
+ * ({@code putIfAbsent}, {@code replace}, {@code remove(key, value)}, bulk operations, etc.) with
+ * case-insensitive semantics. Thread safety is determined by the backing map implementation:
+ *
+ *
+ *
Thread-Safe Backing Maps: When backed by concurrent implementations
+ * ({@link ConcurrentHashMap}, {@link java.util.concurrent.ConcurrentSkipListMap},
+ * {@link ConcurrentNavigableMapNullSafe}, etc.), all operations are fully thread-safe.
+ *
Non-Thread-Safe Backing Maps: When backed by non-concurrent implementations
+ * ({@link LinkedHashMap}, {@link HashMap}, {@link TreeMap}, etc.), concurrent operations work
+ * correctly but require external synchronization for thread safety.
+ *
String Cache: The case-insensitive string cache is thread-safe and can be
+ * safely accessed from multiple threads regardless of the backing map.
+ *
*
- * The internal CaseInsentitiveString is never exposed externally
- * from this class. When requesting the keys or entries of this map,
- * or calling containsKey() or get() for example, use a String as you
- * normally would. The returned Set of keys for the keySet() and
- * entrySet() APIs return the original Strings, not the internally
- * wrapped CaseInsensitiveString.
+ * Recommendation: For multi-threaded applications, explicitly choose a concurrent
+ * backing map implementation to ensure thread safety.
+ *
*
- * @author John DeRegnaucourt (john@cedarsoftware.com)
+ *
Additional Notes
+ *
+ *
String keys longer than 100 characters are not cached by default. This limit can be adjusted using
+ * {@link #setMaxCacheLengthString(int)}.
+ *
+ *
+ * @param the type of keys maintained by this map (String keys are case-insensitive)
+ * @param the type of mapped values
+ * @see Map
+ * @see ConcurrentMap
+ * @see AbstractMap
+ * @see LinkedHashMap
+ * @see TreeMap
+ * @see ConcurrentHashMap
+ * @see CaseInsensitiveString
+ * @see MultiKeyMap
+ *
+ * @author John DeRegnaucourt (jdereg@gmail.com)
*
* Copyright (c) Cedar Software LLC
*
@@ -32,7 +158,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -40,576 +166,1669 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-public class CaseInsensitiveMap implements Map
-{
- private Map map;
-
- public CaseInsensitiveMap()
- {
- map = new LinkedHashMap<>();
+public class CaseInsensitiveMap extends AbstractMap implements ConcurrentMap {
+ private final Map map;
+ private static final AtomicReference, Function>>>> mapRegistry;
+
+ static {
+ // Initialize the registry with default map types
+ List, Function>>> tempList = new ArrayList<>();
+ tempList.add(new AbstractMap.SimpleEntry<>(Hashtable.class, size -> new Hashtable<>()));
+ tempList.add(new AbstractMap.SimpleEntry<>(TreeMap.class, size -> new TreeMap<>()));
+ tempList.add(new AbstractMap.SimpleEntry<>(ConcurrentSkipListMap.class, size -> new ConcurrentSkipListMap<>()));
+ tempList.add(new AbstractMap.SimpleEntry<>(ConcurrentNavigableMapNullSafe.class, size -> new ConcurrentNavigableMapNullSafe<>()));
+ tempList.add(new AbstractMap.SimpleEntry<>(ConcurrentHashMapNullSafe.class, size -> new ConcurrentHashMapNullSafe<>(size)));
+ tempList.add(new AbstractMap.SimpleEntry<>(WeakHashMap.class, size -> new WeakHashMap<>(size)));
+ tempList.add(new AbstractMap.SimpleEntry<>(LinkedHashMap.class, size -> new LinkedHashMap<>(size)));
+ tempList.add(new AbstractMap.SimpleEntry<>(HashMap.class, size -> new HashMap<>(size)));
+ tempList.add(new AbstractMap.SimpleEntry<>(ConcurrentNavigableMap.class, size -> new ConcurrentSkipListMap<>()));
+ tempList.add(new AbstractMap.SimpleEntry<>(ConcurrentMap.class, size -> new ConcurrentHashMap<>(size)));
+ tempList.add(new AbstractMap.SimpleEntry<>(NavigableMap.class, size -> new TreeMap<>()));
+ tempList.add(new AbstractMap.SimpleEntry<>(SortedMap.class, size -> new TreeMap<>()));
+
+ validateMappings(tempList);
+
+ // Initialize the atomic reference with the immutable list
+ mapRegistry = new AtomicReference<>(Collections.unmodifiableList(new ArrayList<>(tempList)));
}
- public CaseInsensitiveMap(int initialCapacity)
- {
- map = new LinkedHashMap<>(initialCapacity);
+ /**
+ * Validates that collection type mappings are ordered correctly (most specific to most general)
+ * and ensures that unsupported map types like IdentityHashMap are not included.
+ * Throws IllegalStateException if mappings are incorrectly ordered or contain unsupported types.
+ *
+ * @param registry the registry list to validate
+ */
+ private static void validateMappings(List, Function>>> registry) {
+ for (int i = 0; i < registry.size(); i++) {
+ Class> current = registry.get(i).getKey();
+
+ // Check for unsupported map types
+ if (current.equals(IdentityHashMap.class)) {
+ throw new IllegalStateException("IdentityHashMap is not supported and cannot be added to the registry.");
+ }
+
+ for (int j = i + 1; j < registry.size(); j++) {
+ Class> next = registry.get(j).getKey();
+ if (current.isAssignableFrom(next)) {
+ throw new IllegalStateException("Mapping order error: " + next.getName() + " should come before " + current.getName());
+ }
+ }
+ }
}
- public CaseInsensitiveMap(Map extends K, ? extends V> map)
- {
- this(map.size());
- putAll(map);
+ /**
+ * Allows users to replace the entire registry with a new list of map type entries.
+ * This should typically be done at startup before any CaseInsensitiveMap instances are created.
+ *
+ * @param newRegistry the new list of map type entries
+ * @throws NullPointerException if newRegistry is null or contains null elements
+ * @throws IllegalArgumentException if newRegistry contains duplicate Class types or is incorrectly ordered
+ */
+ public static void replaceRegistry(List, Function>>> newRegistry) {
+ Objects.requireNonNull(newRegistry, "New registry list cannot be null");
+ for (Entry, Function>> entry : newRegistry) {
+ Objects.requireNonNull(entry, "Registry entries cannot be null");
+ Objects.requireNonNull(entry.getKey(), "Registry entry key (Class) cannot be null");
+ Objects.requireNonNull(entry.getValue(), "Registry entry value (Function) cannot be null");
+ }
+
+ // Check for duplicate Class types
+ Set> seen = new HashSet<>();
+ for (Entry, Function>> entry : newRegistry) {
+ if (!seen.add(entry.getKey())) {
+ throw new IllegalArgumentException("Duplicate map type in registry: " + entry.getKey());
+ }
+ }
+
+ // Validate mapping order
+ validateMappings(newRegistry);
+
+ // Replace the registry atomically with an unmodifiable copy
+ mapRegistry.set(Collections.unmodifiableList(new ArrayList<>(newRegistry)));
}
- public CaseInsensitiveMap(int initialCapacity, float loadFactor)
- {
- map = new LinkedHashMap<>(initialCapacity, loadFactor);
+ /**
+ * Replaces the current cache used for CaseInsensitiveString instances with a new cache.
+ * This operation is thread-safe due to the volatile nature of the cache field.
+ * When replacing the cache:
+ * - Existing CaseInsensitiveString instances in maps remain valid
+ * - The new cache will begin populating with strings as they are accessed
+ * - There may be temporary duplicate CaseInsensitiveString instances during transition
+ *
+ * @param lruCache the new LRUCache instance to use for caching CaseInsensitiveString objects
+ * @throws NullPointerException if the provided cache is null
+ */
+ public static void replaceCache(LRUCache lruCache) {
+ Objects.requireNonNull(lruCache, "Cache cannot be null");
+ CaseInsensitiveString.COMMON_STRINGS_REF.set(lruCache);
}
- public V get(Object key)
- {
- if (key instanceof String)
- {
- String keyString = (String) key;
- return map.get(new CaseInsensitiveString(keyString));
+ /**
+ * Sets the maximum string length for which CaseInsensitiveString instances will be cached.
+ * Strings longer than this length will not be cached but instead create new instances
+ * each time they are needed. This helps prevent memory exhaustion from very long strings.
+ *
+ * @param length the maximum length of strings to cache. Must be non-negative.
+ * @throws IllegalArgumentException if length is < 10.
+ */
+ public static void setMaxCacheLengthString(int length) {
+ if (length < 10) {
+ throw new IllegalArgumentException("Max cache String length must be at least 10.");
}
- return map.get(key);
+ CaseInsensitiveString.maxCacheLengthString = length;
+ }
+
+ /**
+ * Creates a new thread-safe CaseInsensitiveMap backed by a ConcurrentHashMap that can handle null as a
+ * key or value. This is equivalent to {@code new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMapNullSafe<>())}.
+ *
+ * @param the type of keys maintained by this map
+ * @param the type of mapped values
+ * @return a new thread-safe CaseInsensitiveMap
+ */
+ public static CaseInsensitiveMap concurrent() {
+ return new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMapNullSafe<>());
+ }
+
+ /**
+ * Creates a new thread-safe CaseInsensitiveMap backed by a ConcurrentHashMap that can handle null as a key or value
+ * with the specified initial capacity. This is equivalent to
+ * {@code new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMapNullSafe<>(initialCapacity))}.
+ *
+ * @param the type of keys maintained by this map
+ * @param the type of mapped values
+ * @param initialCapacity the initial capacity of the backing ConcurrentHashMap
+ * @return a new thread-safe CaseInsensitiveMap
+ * @throws IllegalArgumentException if the initial capacity is negative
+ */
+ public static CaseInsensitiveMap concurrent(int initialCapacity) {
+ return new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMapNullSafe<>(initialCapacity));
}
- public V put(K key, V value)
- {
- if (key instanceof String)
- { // Must remove entry because the key case can change
- final CaseInsensitiveString newKey = new CaseInsensitiveString((String) key);
- if (map.containsKey(newKey))
- {
- map.remove(newKey);
+ /**
+ * Creates a new thread-safe sorted CaseInsensitiveMap backed by a ConcurrentSkipListMap.
+ * This is equivalent to {@code new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentNavigableMapNullSafe<>())}.
+ *
+ * @param the type of keys maintained by this map
+ * @param the type of mapped values
+ * @return a new thread-safe sorted CaseInsensitiveMap
+ */
+ public static CaseInsensitiveMap concurrentSorted() {
+ return new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentNavigableMapNullSafe<>());
+ }
+
+ /**
+ * Determines the appropriate backing map based on the source map's type.
+ *
+ * @param source the source map to copy from
+ * @return a new Map instance with entries copied from the source
+ * @throws IllegalArgumentException if the source map is an IdentityHashMap
+ */
+ protected Map determineBackingMap(Map source) {
+ if (source instanceof IdentityHashMap) {
+ throw new IllegalArgumentException(
+ "Cannot create a CaseInsensitiveMap from an IdentityHashMap. " +
+ "IdentityHashMap compares keys by reference (==) which is incompatible.");
+ }
+
+ int size = source.size();
+
+ // Iterate through the registry and pick the first matching type
+ for (Entry, Function>> entry : mapRegistry.get()) {
+ if (entry.getKey().isInstance(source)) {
+ @SuppressWarnings("unchecked")
+ Map newMap = (Map) entry.getValue().apply(size);
+ return copy(source, newMap);
}
- return map.put((K) newKey, value);
}
- return map.put(key, value);
+
+ // If no match found, default to LinkedHashMap
+ return copy(source, new LinkedHashMap<>(size));
}
- public boolean containsKey(Object key)
- {
- if (key instanceof String)
- {
- String keyString = (String) key;
- return map.containsKey(new CaseInsensitiveString(keyString));
+ /**
+ * Constructs an empty CaseInsensitiveMap with a LinkedHashMap as the underlying
+ * implementation, providing predictable iteration order.
+ */
+ public CaseInsensitiveMap() {
+ map = new LinkedHashMap<>();
+ }
+
+ /**
+ * Constructs an empty CaseInsensitiveMap with the specified initial capacity
+ * and a LinkedHashMap as the underlying implementation.
+ *
+ * @param initialCapacity the initial capacity
+ * @throws IllegalArgumentException if the initial capacity is negative
+ */
+ public CaseInsensitiveMap(int initialCapacity) {
+ map = new LinkedHashMap<>(initialCapacity);
+ }
+
+ /**
+ * Constructs an empty CaseInsensitiveMap with the specified initial capacity
+ * and load factor, using a LinkedHashMap as the underlying implementation.
+ *
+ * @param initialCapacity the initial capacity
+ * @param loadFactor the load factor
+ * @throws IllegalArgumentException if the initial capacity is negative or the load factor is negative
+ */
+ public CaseInsensitiveMap(int initialCapacity, float loadFactor) {
+ map = new LinkedHashMap<>(initialCapacity, loadFactor);
+ }
+
+ /**
+ * Creates a CaseInsensitiveMap by copying entries from the specified source map into
+ * the specified destination map implementation.
+ *
+ * @param source the map containing entries to be copied
+ * @param mapInstance the empty map instance to use as the underlying implementation
+ * @throws NullPointerException if either map is null
+ * @throws IllegalArgumentException if mapInstance is not empty
+ */
+ public CaseInsensitiveMap(Map source, Map mapInstance) {
+ Objects.requireNonNull(source, "source map cannot be null");
+ Objects.requireNonNull(mapInstance, "mapInstance cannot be null");
+ if (!mapInstance.isEmpty()) {
+ throw new IllegalArgumentException("mapInstance must be empty");
}
- return map.containsKey(key);
+ map = copy(source, mapInstance);
}
- public void putAll(Map extends K, ? extends V> m)
- {
- if (m == null)
- {
- return;
+ /**
+ * Creates a case-insensitive map initialized with the entries from the specified source map.
+ * The created map preserves the characteristics of the source map by using a similar implementation type.
+ *
+ *
Concrete or known map types are matched to their corresponding internal maps (e.g. TreeMap to TreeMap).
+ * If no specific match is found, a LinkedHashMap is used by default.
+ *
+ * @param source the map whose mappings are to be placed in this map. Must not be null.
+ * @throws NullPointerException if the source map is null
+ */
+ public CaseInsensitiveMap(Map source) {
+ Objects.requireNonNull(source, "Source map cannot be null");
+ map = determineBackingMap(source);
+ }
+
+ /**
+ * Copies all entries from the source map to the destination map, wrapping String keys as needed.
+ *
+ * @param source the map whose entries are being copied
+ * @param dest the destination map
+ * @return the populated destination map
+ */
+ @SuppressWarnings("unchecked")
+ protected Map copy(Map source, Map dest) {
+ if (source.isEmpty()) {
+ return dest;
}
- for (Entry entry : m.entrySet())
- {
- put((K) entry.getKey(), (V) entry.getValue());
+ // OPTIMIZATION: If source is also CaseInsensitiveMap, keys are already normalized.
+ if (source instanceof CaseInsensitiveMap, ?>) {
+ // Directly copy from the wrapped map which has normalized keys
+ @SuppressWarnings("unchecked")
+ CaseInsensitiveMap ciSource = (CaseInsensitiveMap) source;
+ dest.putAll(ciSource.map);
+ } else {
+ // Original logic for general maps
+ for (Entry entry : source.entrySet()) {
+ dest.put(convertKey(entry.getKey()), entry.getValue());
+ }
}
+ return dest;
}
- public V remove(Object key)
- {
- if (key instanceof String)
- {
- String keyString = (String) key;
- return map.remove(new CaseInsensitiveString(keyString));
+ /**
+ * {@inheritDoc}
+ *
String keys are handled case-insensitively.
+ *
When backing map is MultiKeyMap, this method supports 1D Collections and Arrays with case-insensitive String handling.
+ */
+ @Override
+ public V get(Object key) {
+ if (map instanceof MultiKeyMap) {
+ return map.get(convertKeyForMultiKeyMap(key));
}
- return map.remove(key);
+ return map.get(convertKey(key));
}
- // delegates
- public int size()
- {
- return map.size();
+ /**
+ * {@inheritDoc}
+ *
String keys are handled case-insensitively.
+ *
When backing map is MultiKeyMap, this method supports 1D Collections and Arrays with case-insensitive String handling.
When backing map is MultiKeyMap, this method supports 1D Collections and Arrays with case-insensitive String handling.
+ */
+ @Override
+ public V put(K key, V value) {
+ if (map instanceof MultiKeyMap) {
+ return map.put((K) convertKeyForMultiKeyMap(key), value);
+ }
+ return map.put((K) convertKey(key), value);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
String keys are handled case-insensitively.
+ *
When backing map is MultiKeyMap, this method supports 1D Collections and Arrays with case-insensitive String handling.
+ */
+ @Override
+ public V remove(Object key) {
+ if (map instanceof MultiKeyMap) {
+ return map.remove(convertKeyForMultiKeyMap(key));
+ }
+ return map.remove(convertKey(key));
}
- public boolean equals(Object other)
- {
- if (other == this) return true;
- if (!(other instanceof Map)) return false;
+ // ===== PRIVATE HELPER METHODS =====
+
+ /**
+ * Handles array and collection keys for MultiKeyMap operations.
+ * Converts String keys to case-insensitive equivalents and handles different array types appropriately.
+ *
+ * @param key the key to process (can be array, collection, or single object)
+ * @param operation a function that takes the processed key and returns the result
+ * @return the result of the operation, or null if not a MultiKeyMap or not an array/collection
+ */
+
+ // ===== MULTI-KEY APIs =====
+
+ /**
+ * Stores a value with multiple keys, applying case-insensitive handling to String keys.
+ * This method is only supported when the backing map is a MultiKeyMap.
+ *
+ *
+ *
+ * @param value the value to store
+ * @param keys the key components (unlimited number, String keys are handled case-insensitively)
+ * @return the previous value associated with the key, or null if there was no mapping
+ * @throws IllegalStateException if the backing map is not a MultiKeyMap instance
+ */
+
+ /**
+ * {@inheritDoc}
+ *
Equality is based on case-insensitive comparison for String keys.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) { return true; }
+ if (!(other instanceof Map)) { return false; }
Map, ?> that = (Map, ?>) other;
- if (that.size() != size())
- {
- return false;
- }
+ if (that.size() != size()) { return false; }
- for (Entry entry : that.entrySet())
- {
- final Object thatKey = entry.getKey();
- if (!containsKey(thatKey))
- {
+ for (Entry, ?> entry : that.entrySet()) {
+ Object thatKey = entry.getKey();
+ if (!containsKey(thatKey)) {
return false;
}
Object thatValue = entry.getValue();
Object thisValue = get(thatKey);
-
- if (thatValue == null || thisValue == null)
- { // Perform null checks
- if (thatValue != thisValue)
- {
- return false;
- }
- }
- else if (!thisValue.equals(thatValue))
- {
+ if (!Objects.equals(thisValue, thatValue)) {
return false;
}
}
return true;
}
-
- public int hashCode()
- {
- int h = 0;
- for (Entry entry : map.entrySet())
- {
- Object key = entry.getKey();
- Object value = entry.getValue();
- int hKey = key == null ? 0 : key.hashCode();
- int hValue = value == null ? 0 : value.hashCode();
- h += hKey ^ hValue;
- }
- return h;
+
+ /**
+ * Returns the underlying wrapped map instance. This map contains the keys in their
+ * case-insensitive form (i.e., {@link CaseInsensitiveString} for String keys).
+ *
+ * @return the wrapped map
+ */
+ public Map getWrappedMap() {
+ return map;
}
- public String toString()
- {
- return map.toString();
- }
+ /**
+ * Returns a {@link Set} view of the keys contained in this map. The set is backed by the
+ * map, so changes to the map are reflected in the set, and vice versa. For String keys,
+ * the set contains the original Strings rather than their case-insensitive representations.
+ *
+ * @return a set view of the keys contained in this map
+ */
+ @Override
+ public Set keySet() {
+ return new AbstractSet() {
+ /**
+ * Returns an iterator over the keys in this set. For String keys, the iterator
+ * returns the original Strings rather than their case-insensitive representations.
+ *
+ * @return an iterator over the keys in this set
+ */
+ @Override
+ public Iterator iterator() {
+ return new ConcurrentAwareKeyIterator<>(map.keySet().iterator());
+ }
- public void clear()
- {
- map.clear();
- }
+ /**
+ * Computes a hash code for this set. The hash code of a set is defined as the
+ * sum of the hash codes of its elements. For null elements, no value is added
+ * to the sum. The hash code computation is case-insensitive, as it relies on
+ * the case-insensitive hash code implementation of the underlying keys.
+ *
+ * @return the hash code value for this set
+ */
+ @Override
+ public int hashCode() {
+ int h = 0;
+ for (Object key : map.keySet()) {
+ if (key != null) {
+ h += key.hashCode(); // CaseInsensitiveString's hashCode() is already case-insensitive
+ }
+ }
+ return h;
+ }
- public boolean containsValue(Object value)
- {
- return map.containsValue(value);
- }
+ /**
+ * Returns the number of elements in this set (its cardinality).
+ * This method delegates to the size of the underlying map.
+ *
+ * @return the number of elements in this set
+ */
+ @Override
+ public int size() {
+ return map.size();
+ }
- public Collection values()
- {
- return map.values();
- }
+ /**
+ * Returns true if this set contains the specified element.
+ * This operation is equivalent to checking if the specified object
+ * exists as a key in the map, using case-insensitive comparison.
+ *
+ * @param o element whose presence in this set is to be tested
+ * @return true if this set contains the specified element
+ */
+ @Override
+ public boolean contains(Object o) {
+ return containsKey(o);
+ }
+
+ /**
+ * Removes the specified element from this set if it is present.
+ * This operation removes the corresponding entry from the underlying map.
+ * The item to be removed is located case-insensitively if the element is a String.
+ * The method returns true if the set contained the specified element
+ * (or equivalently, if the map was modified as a result of the call).
+ *
+ * @param o object to be removed from this set, if present
+ * @return true if the set contained the specified element
+ */
+ @Override
+ public boolean remove(Object o) {
+ int size = map.size();
+ CaseInsensitiveMap.this.remove(o);
+ return map.size() != size;
+ }
+
+ /**
+ * Returns an array containing all the keys in this set; the runtime type of the returned
+ * array is that of the specified array. If the set fits in the specified array, it is
+ * returned therein. Otherwise, a new array is allocated with the runtime type of the
+ * specified array and the size of this set.
+ *
+ *
If the set fits in the specified array with room to spare (i.e., the array has more
+ * elements than the set), the element in the array immediately following the end of the set
+ * is set to null. This is useful in determining the length of the set only if the caller
+ * knows that the set does not contain any null elements.
+ *
+ *
String keys are returned in their original form rather than their case-insensitive
+ * representation used internally by the map.
+ *
+ *
This method could be removed and the parent class method would work, however, it's more efficient:
+ * It works directly with the backing map's keySet instead of using an iterator.
+ *
+ * @param a the array into which the elements of this set are to be stored,
+ * if it is big enough; otherwise, a new array of the same runtime
+ * type is allocated for this purpose
+ * @return an array containing the elements of this set
+ * @throws ArrayStoreException if the runtime type of the specified array
+ * is not a supertype of the runtime type of every element in this set
+ * @throws NullPointerException if the specified array is null
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public T[] toArray(T[] a) {
+ int size = size();
+ T[] result = a.length >= size ? a : (T[]) Array.newInstance(a.getClass().getComponentType(), size);
+
+ int i = 0;
+ for (K key : map.keySet()) {
+ result[i++] = (T) (key instanceof CaseInsensitiveString ? key.toString() : key);
+ }
+
+ if (result.length > size) {
+ result[size] = null;
+ }
+ return result;
+ }
+ /**
+ *
Retains only the elements in this set that are contained in the specified collection.
+ * In other words, removes from this set all of its elements that are not contained
+ * in the specified collection. The comparison is case-insensitive.
+ *
+ *
This operation creates a temporary CaseInsensitiveMap to perform case-insensitive
+ * comparison of elements, then removes all keys from the underlying map that are not
+ * present in the specified collection.
+ *
+ * @param c collection containing elements to be retained in this set
+ * @return true if this set changed as a result of the call
+ * @throws ClassCastException if the types of one or more elements in this set
+ * are incompatible with the specified collection
+ * @SuppressWarnings("unchecked") suppresses unchecked cast warnings as elements
+ * are assumed to be of type K
+ */
+ @Override
+ public boolean retainAll(Collection> c) {
+ // Normalize collection keys for case-insensitive comparison
+ Set normalizedRetainSet = new HashSet<>();
+ for (Object o : c) {
+ normalizedRetainSet.add(convertKey(o));
+ }
+
+ // Use state variable to track changes instead of computing size() twice
+ final boolean[] changed = {false};
+ map.keySet().removeIf(key -> {
+ boolean shouldRemove = !normalizedRetainSet.contains(key);
+ if (shouldRemove) {
+ changed[0] = true;
+ }
+ return shouldRemove;
+ });
+ return changed[0];
+ }
+ };
+ }
+
/**
- * Returns a {@link Set} view of the keys contained in this map.
- * The set is backed by the map, so changes to the map are
- * reflected in the set, and vice-versa. If the map is modified
- * while an iteration over the set is in progress (except through
- * the iterator's own remove operation), the results of
- * the iteration are undefined. The set supports element removal,
- * which removes the corresponding mapping from the map, via the
- * Iterator.remove, Set.remove,
- * removeAll, retainAll, and clear
- * operations. It does not support the add or addAll
- * operations.
+ * {@inheritDoc}
+ *
Returns a Set view of the entries contained in this map. Each entry returns its key in the
+ * original String form (if it was a String). Operations on this set affect the underlying map.
*/
- public Set keySet()
- {
- return new LocalSet();
- }
+ @Override
+ public Set> entrySet() {
+ return new AbstractSet>() {
+ /**
+ * {@inheritDoc}
+ *
Returns the number of entries in the underlying map.
+ */
+ @Override
+ public int size() {
+ return map.size();
+ }
- private class LocalSet extends AbstractSet
- {
- final Map localMap = CaseInsensitiveMap.this;
- Iterator iter;
+ /**
+ * {@inheritDoc}
+ *
Determines if the specified object is an entry present in the map. String keys are
+ * matched case-insensitively.
Returns an array containing all the entries in this set. Each entry returns its key in the
+ * original String form if it was originally a String.
+ */
+ @Override
+ public Object[] toArray() {
+ Object[] result = new Object[size()];
+ int i = 0;
+ for (Entry entry : map.entrySet()) {
+ result[i++] = new CaseInsensitiveEntry(entry);
+ }
+ return result;
+ }
- public boolean contains(Object o)
- {
- return localMap.containsKey(o);
- }
+ /**
+ * {@inheritDoc}
+ *
Returns an array containing all the entries in this set. The runtime type of the returned
+ * array is that of the specified array.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public T[] toArray(T[] a) {
+ int size = size();
+ T[] result = a.length >= size ? a : (T[]) Array.newInstance(a.getClass().getComponentType(), size);
+
+ Iterator> it = map.entrySet().iterator();
+ for (int i = 0; i < size; i++) {
+ result[i] = (T) new CaseInsensitiveEntry(it.next());
+ }
- public boolean remove(Object o)
- {
- boolean exists = localMap.containsKey(o);
- localMap.remove(o);
- return exists;
- }
+ if (result.length > size) {
+ result[size] = null;
+ }
- public boolean removeAll(Collection c)
- {
- int size = size();
+ return result;
+ }
- for (Object o : c)
- {
- if (contains(o))
- {
- remove(o);
+ /**
+ * {@inheritDoc}
+ *
Removes the specified entry from the underlying map if present.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean remove(Object o) {
+ if (!(o instanceof Entry)) {
+ return false;
}
+ final int size = map.size();
+ Entry that = (Entry) o;
+ CaseInsensitiveMap.this.remove(that.getKey());
+ return map.size() != size;
}
- return size() != size;
- }
- public boolean retainAll(Collection c)
- {
- Map other = new CaseInsensitiveMap();
- for (Object o : c)
- {
- other.put(o, null);
+ /**
+ * {@inheritDoc}
+ *
Removes all entries in the specified collection from the underlying map, if present.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean removeAll(Collection> c) {
+ final int size = map.size();
+ for (Object o : c) {
+ if (o instanceof Entry) {
+ try {
+ Entry that = (Entry) o;
+ CaseInsensitiveMap.this.remove(that.getKey());
+ } catch (ClassCastException ignored) {
+ // Ignore entries that cannot be cast
+ }
+ }
+ }
+ return map.size() != size;
}
- int origSize = size();
- Iterator> i = map.entrySet().iterator();
- while (i.hasNext())
- {
- Entry entry = i.next();
- if (!other.containsKey(entry.getKey()))
- {
- i.remove();
+ /**
+ * {@inheritDoc}
+ *
Retains only the entries in this set that are contained in the specified collection.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean retainAll(Collection> c) {
+ if (c.isEmpty()) {
+ int oldSize = size();
+ clear();
+ return oldSize > 0;
}
+
+ Map other = new CaseInsensitiveMap<>();
+ for (Object o : c) {
+ if (o instanceof Entry) {
+ Entry entry = (Entry) o;
+ other.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ int originalSize = size();
+ map.entrySet().removeIf(entry ->
+ !other.containsKey(entry.getKey()) ||
+ !Objects.equals(other.get(entry.getKey()), entry.getValue())
+ );
+ return size() != originalSize;
}
- return size() != origSize;
+ /**
+ * {@inheritDoc}
+ *
Returns an iterator over the entries in the map. Each returned entry will provide
+ * the key in its original form if it was originally a String.
+ */
+ @Override
+ public Iterator> iterator() {
+ return new ConcurrentAwareEntryIterator(map.entrySet().iterator());
+ }
+ };
+ }
+
+ /**
+ * Entry implementation that returns a String key rather than a CaseInsensitiveString
+ * when {@link #getKey()} is called.
+ */
+ public class CaseInsensitiveEntry extends AbstractMap.SimpleEntry {
+ /**
+ * Constructs a CaseInsensitiveEntry from the specified entry.
+ *
+ * @param entry the entry to wrap
+ */
+ public CaseInsensitiveEntry(Entry entry) {
+ super(entry);
}
- public boolean add(K o)
- {
- throw new UnsupportedOperationException("Cannot add() to a 'view' of a Map. See JavaDoc for Map.keySet()");
+ /**
+ * {@inheritDoc}
+ *
Returns the key in its original String form if it was originally stored as a String,
+ * otherwise returns the key as is.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public K getKey() {
+ K superKey = super.getKey();
+ if (superKey instanceof CaseInsensitiveString) {
+ return (K) ((CaseInsensitiveString) superKey).original;
+ }
+ return superKey;
}
- public boolean addAll(Collection c)
- {
- throw new UnsupportedOperationException("Cannot addAll() to a 'view' of a Map. See JavaDoc for Map.keySet()");
+ /**
+ * Returns the original key object used internally by the map. This may be a CaseInsensitiveString
+ * if the key was originally a String.
+ *
+ * @return the original key object
+ */
+ public K getOriginalKey() {
+ return super.getKey();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
Sets the value associated with this entry's key in the underlying map.
+ * For String keys, equality is based on the original String value rather than
+ * the case-insensitive representation. This ensures that entries with the same
+ * case-insensitive key but different original strings are considered distinct.
+ *
+ * @param o object to be compared for equality with this map entry
+ * @return true if the specified object is equal to this map entry
+ * @see Entry#equals(Object)
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Entry)) return false;
+ Entry, ?> e = (Entry, ?>) o;
+ return Objects.equals(getOriginalKey(), e.getKey()) &&
+ Objects.equals(getValue(), e.getValue());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * For String keys, the hash code is computed using the original String value
+ * rather than the case-insensitive representation.
+ *
+ * @return the hash code value for this map entry
+ * @see Entry#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(getOriginalKey()) ^ Objects.hashCode(getValue());
}
- public Object[] toArray()
- {
- Object[] items = new Object[size()];
- int i=0;
- for (Object key : map.keySet())
- {
- items[i++] = key instanceof CaseInsensitiveString ? key.toString() : key;
+ /**
+ * {@inheritDoc}
+ *
+ * Returns a string representation of this map entry. The string representation
+ * consists of this entry's key followed by the equals character ("=") followed
+ * by this entry's value. For String keys, the original string value is used.
+ *
+ * @return a string representation of this map entry
+ */
+ @Override
+ public String toString() {
+ return getKey() + "=" + getValue();
+ }
+ }
+
+ /**
+ * Wrapper class for String keys to enforce case-insensitive comparison.
+ * Implements CharSequence for compatibility with String operations and
+ * Serializable for persistence support.
+ */
+ public static final class CaseInsensitiveString implements Comparable, CharSequence, Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final String original;
+ private final int hash;
+
+ // Configuration values with system property overrides
+ private static final int DEFAULT_CACHE_SIZE = Integer.parseInt(
+ System.getProperty("caseinsensitive.cache.size", "5000"));
+ private static final int DEFAULT_MAX_STRING_LENGTH = Integer.parseInt(
+ System.getProperty("caseinsensitive.max.string.length", "100"));
+
+ // Add static cache for common strings - use AtomicReference for thread safety
+ private static final AtomicReference