Skip to content

Block Editor: propagate nested content updates through relationship-chain hydration#35412

Merged
fmontes merged 11 commits intomainfrom
copilot/fix-nested-contentlet-updates
Apr 23, 2026
Merged

Block Editor: propagate nested content updates through relationship-chain hydration#35412
fmontes merged 11 commits intomainfrom
copilot/fix-nested-contentlet-updates

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 22, 2026

nested-block-editor.mp4
  • Diagnose root cause from CI failure logs
  • Fix refreshContentlet to not throw JsonParseException on non-JSON _raw default values
  • Parallel validation (CodeQL clean; code review false-positive addressed in comments)

Copilot AI changed the title [WIP] Fix Block Editor nested contentlet updates propagation Block Editor: propagate nested content updates through relationship-chain hydration Apr 22, 2026
Copilot AI requested a review from fmontes April 22, 2026 00:31
@fmontes fmontes marked this pull request as ready for review April 22, 2026 03:45
@fmontes fmontes requested a review from Copilot April 22, 2026 03:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates Story Block hydration so that nested Story Block references embedded inside hydrated relationship payloads are also refreshed, enabling relationship-chain updates to propagate correctly.

Changes:

  • Add recursive traversal to refresh Story Block values found inside nested relationship payload maps/lists (with a bounded traversal depth).
  • Reduce the nested refresh traversal ceiling and document why it’s safe given the existing relationship depth cap (0–3).
  • Add an integration test covering nested publish propagation through a Story Block → relationship → Story Block chain.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java Adds bounded recursive refresh of nested Story Block values within relationship-hydrated payload structures.
dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java Adds coverage asserting nested Story Block content refreshes after publishing a deeply nested referenced contentlet.

Comment thread dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPIImpl.java Outdated
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Apr 22, 2026
…in StoryBlockAPIImpl

Agent-Logs-Url: https://github.com/dotCMS/core/sessions/adf886a0-14d8-44b9-a5c9-85b7680f63f5

Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
… avoid unnecessary allocations

Agent-Logs-Url: https://github.com/dotCMS/core/sessions/44a24ef3-b512-4211-beba-a67444fb06b6

Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
@dsilvam dsilvam enabled auto-merge April 22, 2026 20:16
@fmontes
Copy link
Copy Markdown
Member

fmontes commented Apr 22, 2026

@copilot fix conflicts

@fmontes
Copy link
Copy Markdown
Member

fmontes commented Apr 22, 2026

@copilot resolve the merge conflicts in this pull request

Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
auto-merge was automatically disabled April 22, 2026 21:28

Head branch was pushed to by a user without write access

Copilot AI requested a review from fmontes April 22, 2026 21:28
…cycle detection

Agent-Logs-Url: https://github.com/dotCMS/core/sessions/85256f4d-c5e2-41e4-b7c3-fc71bd3e079a

Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
@fmontes fmontes removed their request for review April 22, 2026 23:22
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 22, 2026

Claude finished @fmontes's task in 6m 6s —— View job


Review

  • Read changed files and understand the approach
  • Analyze for bugs, edge cases, and design issues

Overall: The StackOverflow fix (ThreadLocal cycle detection) and the nested Story Block refresh are both correct in their core logic. Three things worth looking at:


1. refreshNestedStoryBlockValues is called for every non-StoryBlock field, not just relationship fields

StoryBlockAPIImpl.java:756–758

} else {
    dataMap.putIfAbsent(field.variable(),
            this.refreshNestedStoryBlockValues(value, contentlet.getIdentifier(),
                    MAX_NESTED_STORY_BLOCK_REFRESH_DEPTH));
}

This runs for text fields, binary fields, date fields, tag fields — everything that isn't a StoryBlockField. For each String-typed field value, the code falls through to the String branch at line 841 and calls refreshStoryBlockValueReferences, which calls JsonUtil.isValidJSON(value.toString()) on every string — including large WYSIWYG bodies. Only RelationshipField values can hold nested contentlet maps that might embed a Story Block. The remaining field types can never match isStoryBlockMap, so they're pure wasted work.

Consider restricting the call:

} else if (field instanceof RelationshipField) {
    dataMap.putIfAbsent(field.variable(),
            this.refreshNestedStoryBlockValues(value, contentlet.getIdentifier(),
                    MAX_NESTED_STORY_BLOCK_REFRESH_DEPTH));
} else {
    dataMap.putIfAbsent(field.variable(), value);
}

Fix this →


2. Map copy-on-write rebuilds from a second entrySet() iteration — assumes stable order

StoryBlockAPIImpl.java:804–811

refreshedMap = new LinkedHashMap<>(valueMap.size());
for (final Map.Entry<String, Object> existingEntry : valueMap.entrySet()) {
    if (existingEntry.getKey().equals(entry.getKey())) {
        break;
    }
    refreshedMap.put(existingEntry.getKey(), existingEntry.getValue());
}

The inner loop iterates valueMap.entrySet() a second time to copy entries that precede the changed one. HashMap iteration order is stable within a single JVM session (no structural modifications), so this is safe in practice — and in practice these maps come from Jackson deserialization (which uses LinkedHashMap). But the method signature accepts Map<String, Object> with no ordering contract, so a caller passing a HashMap would see entries silently reordered. A safer alternative is to collect entrySet() to a List once before the outer loop, then index into it for the rebuild.


3. isCurrentDepthEmpty computed outside the try block — NPE skips finally

StoryBlockAPIImpl.java:596–599

final HttpServletRequest request = HttpServletRequestThreadLocal.INSTANCE.getRequest();
final boolean isCurrentDepthEmpty = request.getAttribute(CURRENT_DEPTH_ATTR) == null;
try {
    ...
} finally {
    if (isCurrentDepthEmpty) {
        ...
        REFRESH_PROCESSING_IDENTIFIERS.remove();
    }
}

If getRequest() returns null (programmatic/background invocation with no servlet request on the thread), the NPE on line 599 is thrown before the try block, so the finally does not execute. In the original code both lines were inside the try. Since no ThreadLocal entries are added before this point, there's no actual leak — but the finally placement is now load-bearing for the new ThreadLocal cleanup, and "NPE before try skips finally" is a silent contract that future maintainers may not notice. At minimum, add a null check for request and return early, or move both lines back inside the try.


Minor

  • Javadoc on refreshNestedStoryBlockValues (line 770) says parentContentletIdentifier is "used to prevent self-refresh loops." It's passed through to refreshStoryBlockValueReferences only for logging/error messages — the actual cycle prevention is the ThreadLocal in refreshBlockEditorDataMap. The comment is misleading.
  • No test covers the case where nested is an unpublished draft (only live version). Worth adding, but not blocking.

…BlockField handling

When a content type has a StoryBlockField, the FieldDataGen test utility
auto-creates a companion _raw TextField with a plain-text default value like
'testDefaultValue1776903544700'. refreshContentlet() was unconditionally calling
toMap() on this _raw value, which threw JsonParseException and aborted the method
before processing RelationshipFields. This left the relationship data absent from
the story block data map, causing initialNewsData/initialNewsList to be null in
the new integration tests.

Fix: check isValidJSON on both _raw and the primary value, falling back
gracefully if neither is valid JSON.

Agent-Logs-Url: https://github.com/dotCMS/core/sessions/f41bd29d-ba42-4f34-bea3-41a7a5e519da

Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Copilot AI requested a review from fmontes April 23, 2026 01:08
@fmontes fmontes requested review from nollymar and removed request for fmontes April 23, 2026 03:30
@fmontes fmontes added this pull request to the merge queue Apr 23, 2026
Merged via the queue into main with commit d130af3 Apr 23, 2026
47 checks passed
@fmontes fmontes deleted the copilot/fix-nested-contentlet-updates branch April 23, 2026 18:47
@zJaaal zJaaal restored the copilot/fix-nested-contentlet-updates branch April 24, 2026 15:46
riccardoruocco pushed a commit to riccardoruocco/core that referenced this pull request Apr 27, 2026
We had an incident on the merge queue and we lost these changes

Main PR dotCMS#35412


Closes: dotCMS#35408

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Co-authored-by: Freddy Montes <freddymontes@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Block Editor: Nested contentlet updates don't propagate through relationship chains

6 participants