Skip to content

Conversation

ottenhoff
Copy link

@ottenhoff ottenhoff commented Oct 2, 2025

  • src/main/java/org/azeckoski/reflectutils/FieldUtils.java:335-1052 now relies solely on standard collections, removes the BeanUtils adapter paths, and fixes indexed/mapped setters so populate-from-params
    works again on JDK 21.
  • src/main/java/org/azeckoski/reflectutils/DeepUtils.java:233-268 and ReflectUtilTest companions regained their behaviour by delegating to the updated FieldUtils, restoring all populate-from-params results.
  • src/main/java/org/azeckoski/reflectutils/ClassData.java:16-208 safely handles inaccessible JDK internals by catching InaccessibleObjectException, keeping reflective caches functional without illegal opens.

Dependency & Codec Refresh

  • pom.xml:93-112 adds Jackson databind/XML (and drops commons-beanutils) to power the new transcoders.
  • src/main/java/org/azeckoski/reflectutils/transcoders/JSONTranscoder.java:1-92 and XMLTranscoder.java:1-123 wrap Jackson APIs; the XML decoder now post-processes numeric/boolean strings back into their
    native types.
  • New JUnit 5 coverage for the codecs lives in src/test/java/org/azeckoski/reflectutils/TranscodersTest.java:1-68.

Collection Defaults & Converters

  • src/main/java/org/azeckoski/reflectutils/ConstructorUtils.java:432-448 selects ArrayList/LinkedHashSet/LinkedHashMap when instantiating interfaces; tests align in src/test/java/org/azeckoski/reflectutils/
    ConstructorUtilsTest.java:166-181.
  • src/main/java/org/azeckoski/reflectutils/ConversionUtils.java:117-160 and CollectionConverter.java:17-46 now use ConcurrentHashMap/ArrayList so conversions stay thread-safe without custom map
    implementations.

Docs & Tooling

  • README.md:1-45 and AGENTS.md:1-30 document the JDK 21 toolchain and Jackson-based workflow; .java-version pins the local runtime to 21.
  • GitHub Actions (.github/workflows/ci.yml:1-23) runs mvn clean verify on Temurin 21.

Tests

  • mvn clean verify

Everything now builds and the full JUnit suite passes on JDK 21 with no access violations.

Summary by CodeRabbit

  • Refactor

    • Rebuilt JSON/XML transcoders on Jackson; simplified reflection/accessibility and construction logic; replaced custom ordered/reference maps with standard collections/caches. Removed legacy adapters, lifecycle utilities, HTML transcoder, and other deprecated components (may be breaking).
  • Documentation

    • Overhauled README for Java 21+ with modern usage, quick start, and build/test info. Added repository guidelines. Removed outdated inline docs.
  • Tests

    • Migrated test suite to JUnit 5.
  • Chores

    • Modernized build: Java 21 requirement, dependency/plugin updates, coverage/reporting, new release profile, and version bump. Added CI workflow for builds on pushes/PRs.

 - src/main/java/org/azeckoski/reflectutils/FieldUtils.java:335-1052 now relies solely on standard collections, removes the BeanUtils adapter paths, and fixes indexed/mapped setters so populate-from-params
  works again on JDK 21.
  - src/main/java/org/azeckoski/reflectutils/DeepUtils.java:233-268 and ReflectUtilTest companions regained their behaviour by delegating to the updated FieldUtils, restoring all populate-from-params results.
  - src/main/java/org/azeckoski/reflectutils/ClassData.java:16-208 safely handles inaccessible JDK internals by catching InaccessibleObjectException, keeping reflective caches functional without illegal opens.

  Dependency & Codec Refresh

  - pom.xml:93-112 adds Jackson databind/XML (and drops commons-beanutils) to power the new transcoders.
  - src/main/java/org/azeckoski/reflectutils/transcoders/JSONTranscoder.java:1-92 and XMLTranscoder.java:1-123 wrap Jackson APIs; the XML decoder now post-processes numeric/boolean strings back into their
  native types.
  - New JUnit 5 coverage for the codecs lives in src/test/java/org/azeckoski/reflectutils/TranscodersTest.java:1-68.

  Collection Defaults & Converters

  - src/main/java/org/azeckoski/reflectutils/ConstructorUtils.java:432-448 selects ArrayList/LinkedHashSet/LinkedHashMap when instantiating interfaces; tests align in src/test/java/org/azeckoski/reflectutils/
  ConstructorUtilsTest.java:166-181.
  - src/main/java/org/azeckoski/reflectutils/ConversionUtils.java:117-160 and CollectionConverter.java:17-46 now use ConcurrentHashMap/ArrayList so conversions stay thread-safe without custom map
  implementations.

  Docs & Tooling

  - README.md:1-45 and AGENTS.md:1-30 document the JDK 21 toolchain and Jackson-based workflow; .java-version pins the local runtime to 21.
  - GitHub Actions (.github/workflows/ci.yml:1-23) runs mvn clean verify on Temurin 21.

  Tests

  - mvn clean verify

  Everything now builds and the full JUnit suite passes on JDK 21 with no access violations.
Copy link

coderabbitai bot commented Oct 2, 2025

Walkthrough

Modernizes project to Java 21, replaces legacy BeanUtils/refmap/lifecycle features, adopts Jackson for JSON/XML, updates collections to standard JDK types, adds GitHub Actions CI, refreshes documentation, and migrates tests to JUnit 5. Internal reflection/access and construction logic refined; numerous obsolete classes, HTML docs, and custom maps removed.

Changes

Cohort / File(s) Summary
CI Workflow
.github/workflows/ci.yml
Adds GitHub Actions to build with JDK 21 and Maven verify on pushes/PRs to master.
Project Docs
README.md, AGENTS.md, readme.txt
Rewrites README for Java 21/Jackson focus; adds contributor guidelines; removes legacy readme.txt.
Build/Dependencies
pom.xml
Bumps version; targets Java 21; switches to Jackson and JUnit 5; adds enforcer, JaCoCo, modern plugin management; updates SCM/distribution; adds release profile.
Reflection internals
src/main/java/org/azeckoski/reflectutils/ClassData.java, .../ClassDataCacher.java, .../ConstructorUtils.java, .../FieldUtils.java, .../ClassFields.java, .../ClassProperty.java, .../ConversionUtils.java, .../DeepUtils.java, .../ReflectUtils.java
Refactors accessibility handling, caching (WeakHashMap/ConcurrentHashMap), constructor instantiation, removal of FieldAdapter pathways, and replaces custom ordered maps with LinkedHashMap/ArrayList. Minor utilities cleanups.
Adapters removal (BeanUtils)
src/main/java/org/azeckoski/reflectutils/beanutils/*
Removes FieldAdapter API, manager, and DynaBean adapter; deletes DefaultFieldAdapter and package docs.
Reference map removal
src/main/java/org/azeckoski/reflectutils/refmap/*
Deletes ReferenceMap, ReferenceType, Finalizer/queues, and related interfaces/classes and docs.
Lifecycle removal
src/main/java/org/azeckoski/reflectutils/Lifecycle.java, .../LifecycleManager.java
Removes Lifecycle interface and manager utilities.
Custom map removal
src/main/java/org/azeckoski/reflectutils/map/ArrayOrderedMap.java, .../OrderedMap.java, .../map/package.html
Deletes custom ordered map implementation and its interface; removes package docs.
Converters updates
src/main/java/org/azeckoski/reflectutils/converters/CollectionConverter.java, .../MapConverter.java, .../NumberConverter.java, .../DateConverter.java, .../converters/package.html
Switches defaults to ArrayList/LinkedHashMap; uses valueOf/parseLong; removes package docs.
Transcoders refactor
src/main/java/org/azeckoski/reflectutils/transcoders/JSONTranscoder.java, .../XMLTranscoder.java, .../HTMLTranscoder.java, .../Transcoder.java, .../TranscoderUtils.java, .../transcoders/package.html
Replaces JSON/XML encoding/decoding with Jackson; removes HTML transcoder and helper utils; simplifies Transcoder interface (adds default maxDepth overload; drops explicit getHandledFormat in interface).
HTML/package docs cleanup
src/main/java/org/azeckoski/reflectutils/annotations/package.html, .../package.html, src/main/java/overview.html
Removes static HTML documentation files across packages.
Tests migration to JUnit 5
src/test/java/org/azeckoski/reflectutils/*Test.java, src/test/java/org/azeckoski/reflectutils/map/ArrayOrderedMapTest.java
Converts tests from JUnit 3 to JUnit 5 annotations/assertions; updates expectations for new defaults and collections; removes ArrayOrderedMap tests.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.70% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “JDK 21 modernization” succinctly and accurately conveys the core objective of the pull request—updating the project for JDK 21 compatibility and modernizing internals—without unnecessary detail, and aligns with the extensive changes detailed in the PR.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/org/azeckoski/reflectutils/ConversionUtils.java (1)

185-193: Restore thread-safe variable converter list

variableConverters used to be backed by a thread-safe Vector. Replacing it with a plain ArrayList introduces data races: concurrent convert(...) calls iterate the list while other threads may add/clear converters, leading to stale reads or ConcurrentModificationException. We need a concurrency-safe collection here.

Apply this diff to switch to CopyOnWriteArrayList:

-import java.util.ArrayList;
+import java.util.ArrayList;
...
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
...
-        if (variableConverters == null) {
-            variableConverters = new ArrayList<VariableConverter>();
+        if (variableConverters == null) {
+            variableConverters = new CopyOnWriteArrayList<>();
🧹 Nitpick comments (6)
src/main/java/org/azeckoski/reflectutils/ClassDataCacher.java (1)

119-119: Consider ConcurrentHashMap for better concurrent read performance.

The Collections.synchronizedMap(new WeakHashMap<>()) approach is correct and provides thread-safety. However, ConcurrentHashMap with weak keys (via a custom WeakReference wrapper or a library like Guava's MapMaker) would offer better concurrent read performance by allowing multiple threads to read without blocking.

For a reflection cache with mostly reads after warmup, this could improve throughput in multi-threaded scenarios. That said, the current implementation is functionally correct and simpler.

Example refactor using ConcurrentHashMap with custom weak key handling:

-reflectionCache = Collections.synchronizedMap(new WeakHashMap<Class<?>, ClassFields>());
+// Note: This requires a custom WeakReference key wrapper or a library like Guava's Cache with weak keys
+reflectionCache = new ConcurrentHashMap<>();

Alternatively, if staying with standard JDK without custom wrappers, the current approach is reasonable.

src/test/java/org/azeckoski/reflectutils/ArrayUtilsTest.java (1)

35-69: Optional: prefer assertNull for null checks.

Use assertNull(...) instead of assertEquals(null, ...) for clearer failure messages in this test.

src/main/java/org/azeckoski/reflectutils/DeepUtils.java (1)

346-356: Minor: pre-size LinkedHashMap when cloning beans to MAP to reduce rehashing.

Build the map after you have the types to size it appropriately.

Apply this diff near the bean->MAP branch:

-                            if (CopyDestination.MAP.equals(dest)) {
-                                copy = new LinkedHashMap<String, Object>();
+                            if (CopyDestination.MAP.equals(dest)) {
                                 // maps should pick up all readable fields
                                 if (ignoreTransient) {
                                     filter = FieldsFilter.SERIALIZABLE;
                                 } else {
                                     filter = FieldsFilter.READABLE;
                                 }
                             } else {
                                 copy = getConstructorUtils().constructClass(beanClass); // make new bean
                             }
                             ClassFields<?> cf = getFieldUtils().analyzeClass(beanClass);
                             Map<String, Class<?>> types = cf.getFieldTypes(filter);
+                            if (CopyDestination.MAP.equals(dest)) {
+                                copy = new LinkedHashMap<String, Object>(Math.max(16, types.size()));
+                            }

Also applies to: 359-361

src/test/java/org/azeckoski/reflectutils/FieldUtilsTest.java (1)

105-141: Prefer assertThrows and assertNull in tests.

Replace try/catch with assertThrows(...) and assertEquals(null, ...) with assertNull(...) to modernize and improve clarity.

src/test/java/org/azeckoski/reflectutils/DeepUtilsTest.java (1)

318-319: Optional: adopt assertThrows and assertNull across this class.

Modernize exception assertions and null checks for clearer failure output.

src/test/java/org/azeckoski/reflectutils/ReflectUtilTest.java (1)

133-141: Prefer assertThrows and assertNull style in tests.

Replace try/catch with assertThrows(...) and assertEquals(null, ...) with assertNull(...) for cleaner tests.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d50b10 and d531ceb.

📒 Files selected for processing (55)
  • .github/workflows/ci.yml (1 hunks)
  • AGENTS.md (1 hunks)
  • README.md (1 hunks)
  • pom.xml (1 hunks)
  • readme.txt (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/ClassData.java (4 hunks)
  • src/main/java/org/azeckoski/reflectutils/ClassDataCacher.java (3 hunks)
  • src/main/java/org/azeckoski/reflectutils/ClassFields.java (7 hunks)
  • src/main/java/org/azeckoski/reflectutils/ClassProperty.java (2 hunks)
  • src/main/java/org/azeckoski/reflectutils/ConstructorUtils.java (5 hunks)
  • src/main/java/org/azeckoski/reflectutils/ConversionUtils.java (4 hunks)
  • src/main/java/org/azeckoski/reflectutils/DeepUtils.java (3 hunks)
  • src/main/java/org/azeckoski/reflectutils/FieldUtils.java (12 hunks)
  • src/main/java/org/azeckoski/reflectutils/Lifecycle.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/LifecycleManager.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/ReflectUtils.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/annotations/package.html (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/beanutils/DefaultFieldAdapter.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/beanutils/DynaBeanAdapter.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/beanutils/FieldAdapter.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/beanutils/FieldAdapterManager.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/beanutils/package.html (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/converters/CollectionConverter.java (3 hunks)
  • src/main/java/org/azeckoski/reflectutils/converters/DateConverter.java (2 hunks)
  • src/main/java/org/azeckoski/reflectutils/converters/MapConverter.java (3 hunks)
  • src/main/java/org/azeckoski/reflectutils/converters/NumberConverter.java (4 hunks)
  • src/main/java/org/azeckoski/reflectutils/converters/package.html (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/map/ArrayOrderedMap.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/map/OrderedMap.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/map/package.html (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/package.html (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/refmap/FinalizableReference.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/refmap/FinalizableReferenceQueue.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/refmap/FinalizableSoftReference.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/refmap/FinalizableWeakReference.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/refmap/Finalizer.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/refmap/ReferenceMap.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/refmap/ReferenceType.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/refmap/package.html (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/transcoders/HTMLTranscoder.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/transcoders/JSONTranscoder.java (1 hunks)
  • src/main/java/org/azeckoski/reflectutils/transcoders/Transcoder.java (1 hunks)
  • src/main/java/org/azeckoski/reflectutils/transcoders/TranscoderUtils.java (0 hunks)
  • src/main/java/org/azeckoski/reflectutils/transcoders/XMLTranscoder.java (1 hunks)
  • src/main/java/org/azeckoski/reflectutils/transcoders/package.html (0 hunks)
  • src/main/java/overview.html (0 hunks)
  • src/test/java/org/azeckoski/reflectutils/ArrayUtilsTest.java (2 hunks)
  • src/test/java/org/azeckoski/reflectutils/ClassFieldsTest.java (36 hunks)
  • src/test/java/org/azeckoski/reflectutils/ConstructorUtilsTest.java (19 hunks)
  • src/test/java/org/azeckoski/reflectutils/ConversionUtilsTest.java (10 hunks)
  • src/test/java/org/azeckoski/reflectutils/DeepUtilsTest.java (13 hunks)
  • src/test/java/org/azeckoski/reflectutils/FieldUtilsTest.java (22 hunks)
  • src/test/java/org/azeckoski/reflectutils/ReflectUtilTest.java (36 hunks)
  • src/test/java/org/azeckoski/reflectutils/TranscodersTest.java (1 hunks)
  • src/test/java/org/azeckoski/reflectutils/map/ArrayOrderedMapTest.java (0 hunks)
💤 Files with no reviewable changes (28)
  • src/main/java/org/azeckoski/reflectutils/ReflectUtils.java
  • src/main/java/org/azeckoski/reflectutils/package.html
  • src/main/java/org/azeckoski/reflectutils/map/OrderedMap.java
  • src/main/java/org/azeckoski/reflectutils/beanutils/FieldAdapterManager.java
  • readme.txt
  • src/test/java/org/azeckoski/reflectutils/map/ArrayOrderedMapTest.java
  • src/main/java/org/azeckoski/reflectutils/LifecycleManager.java
  • src/main/java/org/azeckoski/reflectutils/transcoders/TranscoderUtils.java
  • src/main/java/org/azeckoski/reflectutils/transcoders/package.html
  • src/main/java/overview.html
  • src/main/java/org/azeckoski/reflectutils/beanutils/FieldAdapter.java
  • src/main/java/org/azeckoski/reflectutils/refmap/FinalizableSoftReference.java
  • src/main/java/org/azeckoski/reflectutils/refmap/FinalizableReference.java
  • src/main/java/org/azeckoski/reflectutils/refmap/FinalizableReferenceQueue.java
  • src/main/java/org/azeckoski/reflectutils/refmap/FinalizableWeakReference.java
  • src/main/java/org/azeckoski/reflectutils/Lifecycle.java
  • src/main/java/org/azeckoski/reflectutils/beanutils/DefaultFieldAdapter.java
  • src/main/java/org/azeckoski/reflectutils/refmap/Finalizer.java
  • src/main/java/org/azeckoski/reflectutils/transcoders/HTMLTranscoder.java
  • src/main/java/org/azeckoski/reflectutils/beanutils/DynaBeanAdapter.java
  • src/main/java/org/azeckoski/reflectutils/map/package.html
  • src/main/java/org/azeckoski/reflectutils/map/ArrayOrderedMap.java
  • src/main/java/org/azeckoski/reflectutils/beanutils/package.html
  • src/main/java/org/azeckoski/reflectutils/refmap/ReferenceType.java
  • src/main/java/org/azeckoski/reflectutils/refmap/package.html
  • src/main/java/org/azeckoski/reflectutils/converters/package.html
  • src/main/java/org/azeckoski/reflectutils/annotations/package.html
  • src/main/java/org/azeckoski/reflectutils/refmap/ReferenceMap.java
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/org/azeckoski/reflectutils/FieldUtils.java (4)
src/main/java/org/azeckoski/reflectutils/ConstructorUtils.java (1)
  • ConstructorUtils (53-859)
src/main/java/org/azeckoski/reflectutils/ClassFields.java (1)
  • ClassFields (54-1170)
src/main/java/org/azeckoski/reflectutils/ArrayUtils.java (1)
  • ArrayUtils (28-321)
src/main/java/org/azeckoski/reflectutils/ReflectUtils.java (1)
  • ReflectUtils (70-763)
src/test/java/org/azeckoski/reflectutils/ReflectUtilTest.java (1)
src/main/java/org/azeckoski/reflectutils/ReflectUtils.java (1)
  • ReflectUtils (70-763)
src/main/java/org/azeckoski/reflectutils/transcoders/XMLTranscoder.java (2)
src/main/java/org/azeckoski/reflectutils/TypeReference.java (1)
  • TypeReference (27-51)
src/main/java/org/azeckoski/reflectutils/converters/MapConverter.java (1)
  • SuppressWarnings (34-86)
src/test/java/org/azeckoski/reflectutils/ConstructorUtilsTest.java (2)
src/test/java/org/azeckoski/reflectutils/classes/TestNoPubConstructor.java (1)
  • TestNoPubConstructor (22-38)
src/main/java/org/azeckoski/reflectutils/ConstructorUtils.java (1)
  • ConstructorUtils (53-859)
src/main/java/org/azeckoski/reflectutils/transcoders/JSONTranscoder.java (1)
src/main/java/org/azeckoski/reflectutils/TypeReference.java (1)
  • TypeReference (27-51)
🔇 Additional comments (16)
src/main/java/org/azeckoski/reflectutils/ClassDataCacher.java (2)

19-21: LGTM! Standard library imports replace custom refmap infrastructure.

The addition of Collections and WeakHashMap imports aligns with the PR's goal of modernizing to standard JDK collections and removing the custom ReferenceMap implementation.


134-136: LGTM! Null-check prevents potential NPE.

The addition of the null-check before clearing this.reflectionCache is a defensive programming improvement that prevents a potential NullPointerException if the cache hasn't been initialized yet when setReflectionCache is called with a non-null argument.

src/main/java/org/azeckoski/reflectutils/converters/MapConverter.java (1)

19-19: Approve LinkedHashMap migration
Confirmed no remaining ArrayOrderedMap references and no map.get(<numeric>) usages—LinkedHashMap preserves insertion order and is safe here.

src/main/java/org/azeckoski/reflectutils/converters/CollectionConverter.java (2)

17-18: LGTM! Modernized import from Vector to ArrayList.

This change aligns with the broader JDK 21 modernization effort, replacing the legacy synchronized Vector with the standard ArrayList.


36-36: LGTM! Default collection type modernized from Vector to ArrayList.

The switch from Vector to ArrayList is appropriate:

  • Vector's synchronization overhead is unnecessary for most use cases
  • ArrayList is the standard choice for non-thread-safe lists
  • Return type remains the Collection interface, preserving API compatibility

Also applies to: 48-48

src/main/java/org/azeckoski/reflectutils/converters/DateConverter.java (2)

133-133: LGTM! Replaced deprecated Long constructor with parseLong.

The change from new Long(stringValue) to Long.parseLong(stringValue) correctly modernizes the code by avoiding the deprecated constructor (since JDK 9) and returns a primitive long suitable for the subsequent numeric comparisons.


160-160: LGTM! Cleaner use of local variable.

Using the calendar local variable (assigned at line 158) instead of re-casting is a minor readability improvement with no functional change.

src/main/java/org/azeckoski/reflectutils/converters/NumberConverter.java (1)

145-145: LGTM! Replaced deprecated wrapper constructors with valueOf.

All six wrapper type conversions correctly modernized:

  • new Byte(...)Byte.valueOf(...)
  • new Short(...)Short.valueOf(...)
  • new Integer(...)Integer.valueOf(...)
  • new Long(...)Long.valueOf(...)
  • new Float(...)Float.valueOf(...)
  • new Double(...)Double.valueOf(...)

The valueOf factory methods are the recommended replacements for the deprecated constructors (since JDK 9) and enable caching for small values. This change aligns with the existing String-to-Number conversion logic (lines 242-267) which already uses valueOf.

Also applies to: 159-159, 173-173, 178-178, 187-187, 192-192

src/test/java/org/azeckoski/reflectutils/ArrayUtilsTest.java (1)

20-36: JUnit 5 migration looks good.

Imports, annotations, and class declaration are correct and consistent with Jupiter.

src/test/java/org/azeckoski/reflectutils/FieldUtilsTest.java (1)

17-20: JUnit 5 migration LGTM.

Imports, annotations, and class declaration are consistent across the file.

Also applies to: 34-34

src/test/java/org/azeckoski/reflectutils/DeepUtilsTest.java (3)

27-30: JUnit 5 migration looks good.

Imports, annotations, and class declaration are correct.

Also applies to: 45-45


455-456: Using LinkedHashMap in tests matches production changes.

Good alignment with DeepUtils’s MAP cloning behavior.


247-257: Wrapper-based expectations for numeric fields are correct.

Using Long.valueOf(...) avoids primitive/wrapper surprises in assertions.

src/test/java/org/azeckoski/reflectutils/ReflectUtilTest.java (2)

55-55: Numeric wrapper assertions are correct post-modernization.

Using Long.valueOf/Integer.valueOf in expectations aligns with API returning wrappers.

Also applies to: 64-64, 116-116, 267-269, 308-308, 559-559, 1028-1029, 1069-1069


17-20: JUnit 5 migration LGTM – no legacy imports found
Automated scans found no import junit.framework or import org.junit (non-Jupiter); please manually confirm no JUnit 3/4 annotations remain.

src/main/java/org/azeckoski/reflectutils/DeepUtils.java (1)

309-311: Approve LinkedHashMap replacement. No remaining references to ArrayOrderedMap or commons-beanutils found.

Comment on lines +994 to +1013
try {
int length = ArrayUtils.size((Object[]) indexedObject);
if (index >= length) {
indexedObject = ArrayUtils.resize((Object[]) indexedObject, index + 1);
setSimpleValue(obj, name, indexedObject);
}
} else {
// this better be a list
if (indexedObject == null
|| ! List.class.isAssignableFrom(indexedObject.getClass())) {
throw new IllegalArgumentException("Field (" + name + ") does not appear to be indexed (not an array or a list): "
+ (indexedObject == null ? "NULL" : indexedObject.getClass()) );
Class<?> componentType = ArrayUtils.type((Object[]) indexedObject);
Object convert = ReflectUtils.getInstance().convert(value, componentType);
Array.set(indexedObject, index, convert);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to set index (" + index + ") for array of size (" + Array.getLength(indexedObject) + ") to value: " + value, e);
}
} else {
if (!List.class.isAssignableFrom(indexedObject.getClass())) {
throw new IllegalArgumentException("Field (" + name + ") does not appear to be indexed (not an array or a list): " + indexedObject.getClass());
}
List<Object> list = (List<Object>) indexedObject;
try {
if (index < 0) {
list.add(value);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Primitive arrays now throw ClassCastException in setIndexedValue.

Line 996 onwards casts indexedObject to Object[], but primitive arrays (e.g., int[]) are not Object[]. Any write through setFieldValue(bean, "ints[0]", 1) will now explode with ClassCastException, breaking previously working code. Please restore component handling via java.lang.reflect.Array so both primitive and object arrays remain supported.

-                int length = ArrayUtils.size((Object[]) indexedObject);
+                int length = Array.getLength(indexedObject);
                 if (index >= length) {
-                    indexedObject = ArrayUtils.resize((Object[]) indexedObject, index + 1);
-                    setSimpleValue(obj, name, indexedObject);
+                    Object resized = Array.newInstance(indexedObject.getClass().getComponentType(), index + 1);
+                    System.arraycopy(indexedObject, 0, resized, 0, length);
+                    indexedObject = resized;
+                    setSimpleValue(obj, name, indexedObject);
                 }
-                Class<?> componentType = ArrayUtils.type((Object[]) indexedObject);
+                Class<?> componentType = indexedObject.getClass().getComponentType();
                 Object convert = ReflectUtils.getInstance().convert(value, componentType);
                 Array.set(indexedObject, index, convert);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
int length = ArrayUtils.size((Object[]) indexedObject);
if (index >= length) {
indexedObject = ArrayUtils.resize((Object[]) indexedObject, index + 1);
setSimpleValue(obj, name, indexedObject);
}
} else {
// this better be a list
if (indexedObject == null
|| ! List.class.isAssignableFrom(indexedObject.getClass())) {
throw new IllegalArgumentException("Field (" + name + ") does not appear to be indexed (not an array or a list): "
+ (indexedObject == null ? "NULL" : indexedObject.getClass()) );
Class<?> componentType = ArrayUtils.type((Object[]) indexedObject);
Object convert = ReflectUtils.getInstance().convert(value, componentType);
Array.set(indexedObject, index, convert);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to set index (" + index + ") for array of size (" + Array.getLength(indexedObject) + ") to value: " + value, e);
}
} else {
if (!List.class.isAssignableFrom(indexedObject.getClass())) {
throw new IllegalArgumentException("Field (" + name + ") does not appear to be indexed (not an array or a list): " + indexedObject.getClass());
}
List<Object> list = (List<Object>) indexedObject;
try {
if (index < 0) {
list.add(value);
try {
int length = Array.getLength(indexedObject);
if (index >= length) {
Object resized = Array.newInstance(
indexedObject.getClass().getComponentType(),
index + 1
);
System.arraycopy(indexedObject, 0, resized, 0, length);
indexedObject = resized;
setSimpleValue(obj, name, indexedObject);
}
Class<?> componentType = indexedObject.getClass().getComponentType();
Object convert = ReflectUtils.getInstance().convert(value, componentType);
Array.set(indexedObject, index, convert);
} catch (Exception e) {
throw new IllegalArgumentException(
"Failed to set index (" + index + ") for array of size ("
Array.getLength(indexedObject)
") to value: " + value, e
);
}

Comment on lines +112 to +134
if (value instanceof String s) {
if (s.matches("[-+]?\\d+")) {
try {
ReflectUtils.getInstance().setFieldValue(container.getContainer(), key, value);
} catch (RuntimeException e) {
throw new RuntimeException("Unknown container type ("+type+") and could not set field on container: " + container, e);
}
}
}

private Stack<String> tagStack = new Stack<String>();
private Stack<Container> containerStack = new Stack<Container>();

private CharArrayWriter contents = new CharArrayWriter();
private Types currentType = null;
// this should be false when there are no contents to read
private boolean currentContents = false;

// Event Handlers
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
contents.reset();
tagStack.push(localName);
if (tagStack.size() > containerStack.size() + 1) {
// add a new container to the stack, use the types info from the parent
Container lastContainer = containerStack.peek();
Object newContainerObject = makeContainerObject(currentType);
String parentName = ( tagStack.size() > 1 ? tagStack.get(tagStack.size()-2) : tagStack.peek() );
containerStack.push( new Container(lastContainer.getContainer(), parentName, newContainerObject) );
add(lastContainer, parentName, newContainerObject);
}
currentType = getDataType(attributes);
currentContents = false;
}

@Override
public void endElement(String uri, String localName, String name) throws SAXException {
if (tagStack.size() > containerStack.size()) {
// only add data when we are above a container
Object val = null;
if (currentContents) {
String content = unescapeXML(contents.toString().trim());
val = content;
if (Types.BOOLEAN.equals(currentType)) {
val = Boolean.valueOf(content);
} else if (Types.NUMBER.equals(currentType)) {
try {
val = number(content);
} catch (NumberFormatException e) {
val = content;
}
} else if (Types.DATE.equals(currentType)) {
try {
val = new Date(Long.valueOf(content));
} catch (NumberFormatException e) {
val = content;
}
if (s.length() < 10) {
return Integer.valueOf(s);
}
return Long.valueOf(s);
} catch (NumberFormatException ignored) {
// fall back to double below
}
// put the value into the current container
add(containerStack.peek(), localName, val);
}
if (tagStack.isEmpty()) {
throw new IllegalStateException("tag stack is out of sync, empty while still processing tags: " + localName);
} else {
tagStack.pop();
}
// now we need to remove the current container if we are done with it
while (tagStack.size() < containerStack.size()) {
if (containerStack.size() <= 1) break;
containerStack.pop();
}
contents.reset();
}

@Override
public void characters(char[] ch, int start, int length) throws SAXException {
// get the text out of the element
contents.write(ch, start, length);
currentContents = true;
}

@Override
public String toString() {
return "parser: " + xml + " => " + map;
}
}

public static String unescapeXML(String string) {
if (string != null && string.length() > 0) {
string = string.replace("&lt;","<").replace("&gt;", ">").replace("&quot;", "\"").replace("&amp;", "&").replace("&apos;","'");
}
return string;
}

/**
* This will force a tag or attribute to be valid in XML by replacing the invalid chars with "_",
* invalid chars are ' ' (space), =, ', ", >, <, &
* @param string any string
* @return a valid string
*/
public static String convertInvalidChars(String string) {
if (string != null && string.length() > 0) {
string = string.replace(' ','_').replace('=','_').replace('"','_').replace('\'','_').replace('<','_').replace('>','_').replace('&','_');
}
return string;
}

private static enum Types {STRING,NUMBER,BOOLEAN,DATE,ARRAY,COLLECTION,MAP,BEAN};

protected static Types getDataType(Attributes attributes) {
Types elementType = Types.STRING;
String value = attributes.getValue("", "type");
if (value != null) {
if ("boolean".equals(value)) {
elementType = Types.BOOLEAN;
} else if ("number".equals(value)) {
elementType = Types.NUMBER;
} else if ("date".equals(value)) {
elementType = Types.DATE;
} else if ("array".equals(value)) {
elementType = Types.ARRAY;
} else if ("collection".equals(value)) {
elementType = Types.COLLECTION;
} else if ("map".equals(value)) {
elementType = Types.MAP;
} else if ("bean".equals(value)) {
elementType = Types.BEAN;
}
}
return elementType;
}

protected static Class<?> getDataClass(Attributes attributes) {
Class<?> type = String.class;
String value = attributes.getValue("", "type");
if (value != null) {
if (value.startsWith("class ")) {
value = value.substring(6);
}
// TODO handle the classes?
}
return type;
}

protected static Object makeContainerObject(Types type) {
Object newContainer = null;
if (Types.ARRAY.equals(type)
|| Types.COLLECTION.equals(type)) {
newContainer = new Vector<Object>();
} else {
// bean, map, unknown
newContainer = new ArrayOrderedMap<String, Object>();
}
return newContainer;
}

/**
* Converts a string into a number
* @param s the string
* @return the number
* @throws NumberFormatException if the string is not a number
*/
@SuppressWarnings("fallthrough")
protected static Number number(String s) {
int length = s.length();
boolean isFloatingPoint = false;

for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '.':
case 'e':
case 'E':
isFloatingPoint = true;
case '-':
case '+':
length--;
}
}

// more friendly handling of numbers
Number num = null;
if (isFloatingPoint) {
if (length < 10) {
num = Float.valueOf(s);
} else if (length < 17) {
num = Double.valueOf(s);
} else {
num = new BigDecimal(s);
}
} else {
if (length < 10) {
num = Integer.valueOf(s);
} else if (length < 19) {
num = Long.valueOf(s);
} else {
num = new BigInteger(s);
}
}
return num;
}

protected static class Container {
private boolean root = false;
public void setRoot(boolean root) {
this.root = root;
}
public boolean isRoot() {
return root;
}
public Object parent;
public Object getParent() {
return parent;
}
public String key;
public String getKey() {
return key;
}
public int index;
public int getIndex() {
return index;
}
public Object container;
public Object getContainer() {
return container;
}
public void updateIndex(int index) {
if (index < 0) {
throw new IllegalArgumentException("invalid index: " + index);
}
this.index = index;
key = null;
}
/**
* Replace the container with a new one based on the parent and settings in this Container
*/
public void replaceContainer(Object container) {
if (container == null) {
throw new IllegalArgumentException("No null params allowed");
}
if (key != null) {
FieldUtils.getInstance().setFieldValue(parent, key, container);
} else if (index >= 0) {
FieldUtils.getInstance().setIndexedValue(parent, index, container);
}
// if not key or index then do nothing except replacing the value
this.container = container;
}
/**
* Use if parent is non-existent (i.e. this is the root)
*/
public Container(Object container) {
if (container == null) {
throw new IllegalArgumentException("No null params allowed");
}
this.container = container;
this.root = true;
}
/**
* Use if parent is keyed
*/
public Container(Object parent, String key, Object container) {
if (parent == null || key == null || container == null) {
throw new IllegalArgumentException("No null params allowed");
if (s.matches("[-+]?\\d*\\.\\d+")) {
try {
return Double.valueOf(s);
} catch (NumberFormatException ignored) {
return s;
}
}
this.container = container;
this.key = key;
this.parent = parent;
}
/**
* Use if parent is indexed
*/
public Container(Object parent, int index, Object container) {
if (parent == null || index < 0 || container == null) {
throw new IllegalArgumentException("No null params or index < 0 allowed");
if ("true".equalsIgnoreCase(s) || "false".equalsIgnoreCase(s)) {
return Boolean.valueOf(s);
}
this.container = container;
this.index = index;
this.parent = parent;
}
@Override
public String toString() {
return "C:root="+root+":parent="+(parent==null?parent:parent.getClass().getSimpleName())+":key="+key+":index="+index+":container="+(container==null?container:container.getClass().getSimpleName());
}
return value;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not auto-coerce digit-only XML text into numbers

coerceValue mutates every digit-only string into Integer/Long (and decimals into Double). That breaks round-trips for legitimate string data. For example:

Map<String, Object> payload = Map.of("zip", "02138");
XMLTranscoder transcoder = new XMLTranscoder();
String xml = transcoder.encode(payload, null, null);
Map<String, Object> decoded = transcoder.decode(xml);

decoded.get("zip") now yields the integer 2138, losing the leading zero. Any textual numeric ID, SKU, or version string is similarly corrupted. Please drop the blanket regex coercion or replace it with a metadata-driven strategy that preserves exact values on encode/decode.

🤖 Prompt for AI Agents
In src/main/java/org/azeckoski/reflectutils/transcoders/XMLTranscoder.java
around lines 112 to 134, the current coerceValue logic automatically converts
digit-only or decimal-only Strings into Integer/Long/Double/Boolean which
corrupts legitimate textual numeric values (e.g., ZIP codes with leading zeros).
Remove the blanket regex-based numeric/boolean coercion and instead preserve
String values by default; if coercion must be supported, implement a
metadata-driven or explicit flag-based pathway so only values annotated or
encoded with type hints are converted during decode. Ensure existing callers
that expect Strings continue to receive String objects and add a configurable
switch or metadata check to opt-in to coercion where necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant