Skip to content

fix:[NEXT-1943] Merge assets, finding matches in lower case#171

Merged
arunpaladin merged 1 commit into
mainfrom
NEXT-1943-handle-differently-cased-ids
Apr 9, 2026
Merged

fix:[NEXT-1943] Merge assets, finding matches in lower case#171
arunpaladin merged 1 commit into
mainfrom
NEXT-1943-handle-differently-cased-ids

Conversation

@kevin-paladin

@kevin-paladin kevin-paladin commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

This also fixes a null reference exception when dealing with some assets (it wasn't able to parse records with null values)

Summary by CodeRabbit

  • Bug Fixes

    • Improved asset management by enhancing the merge logic to correctly handle asset IDs with different character casing, ensuring consistent asset consolidation across the system
    • Strengthened data integrity by preventing null values from being stored in asset properties
  • Tests

    • Added comprehensive test coverage for case-insensitive asset ID reconciliation scenarios

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR enhances asset ID reconciliation by implementing case-insensitive lookups through a normalized primaryIdMap, prevents null values from entering asset properties, and introduces test coverage for case-insensitive ID scenarios. The .gitignore is updated to exclude AWS SAM build artifacts.

Changes

Cohort / File(s) Summary
Build Configuration
.gitignore
Added ignore pattern for .aws-sam directory in asset-delta-engine service and fixed file's ending newline.
Asset Property Handling
assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/AssetDTO.java
Modified addAdditionalProperty() to skip insertion of entries with null values into additionalProperties while preserving existing special-case handling for OPINIONS key.
Asset Merge Logic
assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/MergeAssets.java
Introduced primaryIdMap for case-insensitive asset ID normalization; refactored merge flow to resolve docId through normalized map before lookups; updated asset docId synchronization and primary asset resourceId conditional updates; adjusted post-processing to use resolved docId instead of iteration key.
Test Coverage
assets-management/services/svc-asset-delta-engine/src/test/java/com/paladincloud/commons/assets/MergeOpinionsTests.java
Added primaryAndOpinionDifferentCaseDocIds test for case-insensitive doc-id reconciliation scenarios and additional assertions in primaryAndOpinionUpdatedFromOpinion test.

Sequence Diagram(s)

sequenceDiagram
    participant Input as Input Assets
    participant Merge as MergeAssets Logic
    participant PrimaryMap as primaryIdMap
    participant Existing as existingAssets
    participant AssetDTO as AssetDTO Model
    participant Output as Updated State

    Input->>PrimaryMap: Register primary assets<br/>(normalized lower + original)
    Input->>Existing: Load existing assets
    Input->>Merge: Process opinion assets
    Merge->>PrimaryMap: Lookup by normalized docId<br/>(case-insensitive)
    PrimaryMap-->>Merge: Return mapped asset
    alt Asset found in existing
        Merge->>AssetDTO: Update docId (if mapped)
        Merge->>AssetDTO: Sync resourceId when<br/>matching (case-insensitive)
        AssetDTO-->>Merge: Updated asset
        Merge->>Existing: Record in latestAssetsDataMap
        Merge-->>Output: Track as updated
    else Asset not found
        Merge->>Existing: Check if missing/deleted
        Merge-->>Output: Track as new or deleted
    end
    Merge->>Output: Filter nulls from properties
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • arunpaladin
  • rnithinpaladin

Poem

🐰 A map of IDs, both high and low,
Case-insensitive, a natural flow,
Nulls begone from properties divine,
Assets merge in perfect design! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix:NEXT-1943 Merge assets, finding matches in lower case' directly and specifically describes the primary change — implementing case-insensitive asset ID matching in the merge logic. It aligns with the main objective stated in the PR description.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch NEXT-1943-handle-differently-cased-ids

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/MergeAssets.java (1)

79-83: ⚠️ Potential issue | 🔴 Critical

The remapped docId is not applied to the new opinion asset.

At line 71, when a primary asset exists, docId is remapped to the primary's canonical id. However, latestDoc is never updated with this remapped value. When line 83 calls assetHelper.createFrom(latestDoc), the helper internally rebuilds docId from latestDoc via buildDocId(), regenerating the original (uncorrected) id. This causes a mismatch: the response map key uses the canonical id, but the DTO's docId field retains the original casing, causing the asset to be indexed incorrectly.

Fix
            if (isNew) {
-                response.newAssets.put(docId, assetHelper.createFrom(latestDoc));
+                var newAsset = assetHelper.createFrom(latestDoc);
+                newAsset.setDocId(docId);
+                response.newAssets.put(docId, newAsset);
                 response.updatedAssets.remove(docId);
             } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/MergeAssets.java`
around lines 79 - 83, The new opinion asset's DTO is built from latestDoc before
latestDoc's docId is updated to the remapped canonical id, so
assetHelper.createFrom(latestDoc) regenerates the original (uncanonical) id;
update latestDoc with the remapped docId (the same value used as the
response.newAssets map key) before calling assetHelper.createFrom(latestDoc) —
i.e., after you detect/assign the canonical docId (the variable docId) when a
primary asset exists, set that value into latestDoc (the field used by
buildDocId()) so createFrom(latestDoc) produces a DTO whose docId matches the
canonical id in latestAssetsDataMap/response.newAssets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/MergeAssets.java`:
- Around line 49-57: Normalize all primary-id keys and lookups using
Locale.ROOT: when building primaryIdMap in MergeAssets (primaryAssets.forEach
block), call primaryId.toLowerCase(Locale.ROOT) instead of toLowerCase(); when
checking or retrieving entries use
primaryIdMap.get(docId.toLowerCase(Locale.ROOT)) and
primaryIdMap.containsKey(docId.toLowerCase(Locale.ROOT)) (normalize docId before
lookup), and in the deletion-path use the normalized lookup against primaryIdMap
for existingDocId (e.g.,
primaryIdMap.get(existingDocId.toLowerCase(Locale.ROOT))) instead of querying
primaryAssets directly so deletedPrimaryAssets is populated correctly.

---

Outside diff comments:
In
`@assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/MergeAssets.java`:
- Around line 79-83: The new opinion asset's DTO is built from latestDoc before
latestDoc's docId is updated to the remapped canonical id, so
assetHelper.createFrom(latestDoc) regenerates the original (uncanonical) id;
update latestDoc with the remapped docId (the same value used as the
response.newAssets map key) before calling assetHelper.createFrom(latestDoc) —
i.e., after you detect/assign the canonical docId (the variable docId) when a
primary asset exists, set that value into latestDoc (the field used by
buildDocId()) so createFrom(latestDoc) produces a DTO whose docId matches the
canonical id in latestAssetsDataMap/response.newAssets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a57c1f19-5c75-4417-975f-f900e79c42a0

📥 Commits

Reviewing files that changed from the base of the PR and between 88d4a90 and 1c294c7.

📒 Files selected for processing (4)
  • .gitignore
  • assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/AssetDTO.java
  • assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/MergeAssets.java
  • assets-management/services/svc-asset-delta-engine/src/test/java/com/paladincloud/commons/assets/MergeOpinionsTests.java

Comment on lines +49 to +57
// To handle secondary sources with all lowercase docId's, provide a mechanism to resolve
// both the lowercase & mixed case id to the primary asset
var primaryIdMap = new HashMap<String, AssetDTO>();
if (primaryAssets != null) {
primaryAssets.forEach((primaryId, primaryAsset) -> {
primaryIdMap.put(primaryId.toLowerCase(), primaryAsset);
primaryIdMap.put(primaryId, primaryAsset);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Is Java String.toLowerCase()locale-sensitive, and isLocale.ROOT the recommended choice when normalizing case-insensitive identifiers?

💡 Result:

Yes.

  • String.toLowerCase() is locale-sensitive: it uses the default locale (equivalent to toLowerCase(Locale.getDefault())) and can produce unexpected results for locale-independent text such as identifiers (classic example: Turkish dotted/dotless “I”). [1]
  • For normalizing case-insensitive identifiers/keys, you should pass an explicit locale and avoid the default locale. Locale.ROOT is the language-neutral (“base”) locale intended for locale-sensitive operations when you don’t want user-locale effects, and is widely recommended for this purpose (e.g., by static analysis guidance). [2] [3]

So, for identifier normalization, prefer:

s.toLowerCase(Locale.ROOT)

(or similarly toUpperCase(Locale.ROOT)). [2] [3]

Sources: Oracle String javadoc on locale sensitivity and identifier examples [1]; Oracle Locale.ROOT definition as language-neutral [2]; Error Prone guidance recommending Locale.ROOT for locale-independent casing [3].


🏁 Script executed:

cd assets-management/services/svc-asset-delta-engine && find . -name "MergeAssets.java" -type f

Repository: PaladinCloud/ce-extensions

Length of output: 133


🏁 Script executed:

cd assets-management/services/svc-asset-delta-engine && wc -l src/main/java/com/paladincloud/common/assets/MergeAssets.java

Repository: PaladinCloud/ce-extensions

Length of output: 135


🏁 Script executed:

cd assets-management/services/svc-asset-delta-engine && sed -n '40,120p' src/main/java/com/paladincloud/common/assets/MergeAssets.java

Repository: PaladinCloud/ce-extensions

Length of output: 4023


🏁 Script executed:

cd assets-management/services/svc-asset-delta-engine && sed -n '101,120p' src/main/java/com/paladincloud/common/assets/MergeAssets.java

Repository: PaladinCloud/ce-extensions

Length of output: 1198


🏁 Script executed:

cd assets-management/services/svc-asset-delta-engine && grep -n "Assets.java" src/main/java/com/paladincloud/common/assets/MergeAssets.java || echo "No direct reference found"

Repository: PaladinCloud/ce-extensions

Length of output: 95


Normalize every primary-id lookup with Locale.ROOT; fix the deletion-path lookup bypass.

The current implementation has three critical gaps in case normalization:

  1. Line 54: Uses String.toLowerCase() without specifying Locale.ROOT, which is locale-sensitive and can break identifier matching (example: Turkish dotted/dotless "I").

  2. Lines 69 & 89: Look up in primaryIdMap using the raw docId without case normalization:

    • Line 69: primaryIdMap.get(docId) will miss if the incoming docId case differs from the stored primaryId case
    • Line 89: primaryIdMap.containsKey(docId) fails for the same reason
  3. Line 114: Bypasses primaryIdMap entirely and looks up directly in primaryAssets using the raw existingDocId. A mixed-case primary asset will never be found here, leaving deletedPrimaryAssets incomplete and stale primary docs undeleted.

Suggested fixes
+import java.util.Locale;
+
         var primaryIdMap = new HashMap<String, AssetDTO>();
         if (primaryAssets != null) {
             primaryAssets.forEach((primaryId, primaryAsset) -> {
-                primaryIdMap.put(primaryId.toLowerCase(), primaryAsset);
-                primaryIdMap.put(primaryId, primaryAsset);
+                primaryIdMap.put(primaryId.toLowerCase(Locale.ROOT), primaryAsset);
             });
         }

-            var primaryAsset = primaryIdMap.get(docId);
+            var primaryAsset = primaryIdMap.get(docId.toLowerCase(Locale.ROOT));
             if (primaryAsset != null) {

-                if (primaryIdMap.containsKey(docId)) {
+                if (primaryAsset != null) {

-                        var primaryAsset = primaryAssets.get(existingDocId);
+                        var primaryAsset = primaryIdMap.get(existingDocId.toLowerCase(Locale.ROOT));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@assets-management/services/svc-asset-delta-engine/src/main/java/com/paladincloud/common/assets/MergeAssets.java`
around lines 49 - 57, Normalize all primary-id keys and lookups using
Locale.ROOT: when building primaryIdMap in MergeAssets (primaryAssets.forEach
block), call primaryId.toLowerCase(Locale.ROOT) instead of toLowerCase(); when
checking or retrieving entries use
primaryIdMap.get(docId.toLowerCase(Locale.ROOT)) and
primaryIdMap.containsKey(docId.toLowerCase(Locale.ROOT)) (normalize docId before
lookup), and in the deletion-path use the normalized lookup against primaryIdMap
for existingDocId (e.g.,
primaryIdMap.get(existingDocId.toLowerCase(Locale.ROOT))) instead of querying
primaryAssets directly so deletedPrimaryAssets is populated correctly.

@arunpaladin
arunpaladin merged commit bc9bdd1 into main Apr 9, 2026
7 checks passed
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.

2 participants