Skip to content

Conversation

@st-manu
Copy link
Contributor

@st-manu st-manu commented Dec 19, 2025

https://sakaiproject.atlassian.net/browse/SAK-52251

Summary by CodeRabbit

  • Bug Fixes

    • Improved reconstruction of page hierarchies during import/merge, preserving parent/child/top-parent relationships and skipping placeholder/orphaned pages.
    • Ensured tool associations are synchronized across imported page trees for consistent behavior after merges.
    • Enhanced import/merge reporting with clearer logs and counters for applied hierarchy and tool updates.
  • Tests

    • Added tests validating referenced-page lookup, hierarchy calculation, orphan skipping, and selective-import propagation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 19, 2025

Walkthrough

Expose referenced-page discovery, skip orphaned pages during traversal, and add hierarchy computation and application during merge/import including parent/topParent mapping, toolId synchronization, and related test coverage.

Changes

Cohort / File(s) Summary
Page hierarchy & merge flow
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java
Add multi-pass merge: collect subpageRefs, build calculatedParentMap and calculatedTopParentMap, apply parent/topParent to imported pages, persist updates, and log hierarchy updates.
Referenced-page discovery & orphan handling
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java
Change findReferencedPagesByItems(String siteId) visibility to public; skip pages whose toolId equals "0" (treated as orphans) during reference aggregation.
ToolId synchronization
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java
After hierarchy is applied, update child pages' toolId based on computed topparent relationships and persist/log those changes.
Page creation adjustments
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java
New pages created during merge use null for parent/topParent (instead of 0L) so linkage is deferred to the hierarchy application phase.
Tests: references, hierarchy, orphans
lessonbuilder/tool/src/test/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducerTest.java
Add tests: testFindReferencedPagesByItems, testFindReferencedPagesByItemsSkipsOrphans, testHierarchyCalculationFromReferences, testHierarchyCalculationWithSelectiveImport exercising reference discovery, orphan skipping, hierarchy derivation, and selective import behavior.
Imports & logging
.../LessonBuilderEntityProducer.java
Adjust imports and add logging/instrumentation to report hierarchy calculation and toolId update progress.

Possibly related PRs

Suggested reviewers

  • ern
  • jesusmmp
🚥 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 title 'SAK-52251 Lessons Subpages of a Lessons are imported as pseudo-orphans' directly matches the pull request's core objective: fixing the issue where lesson subpages are imported as pseudo-orphans by implementing hierarchy calculation and synchronization logic.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% 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

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
Contributor

@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: 1

🧹 Nitpick comments (1)
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java (1)

1739-1751: Consider adding cycle detection to the parent chain traversal.

The while loop walks up the parent chain to find the top parent. While cycles should not exist in valid data, a defensive safeguard would prevent an infinite loop in case of corrupted data.

🔎 Suggested defensive safeguard
 // Calculate top parents by walking up the tree
 for (Long pageId : calculatedParentMap.keySet()) {
     Long currentPageId = pageId;
     Long topParent = null;
+    Set<Long> visited = new HashSet<>();
 
-    while (calculatedParentMap.containsKey(currentPageId)) {
+    while (calculatedParentMap.containsKey(currentPageId) && !visited.contains(currentPageId)) {
+        visited.add(currentPageId);
         topParent = calculatedParentMap.get(currentPageId);
         currentPageId = topParent;
     }
 
     if (topParent != null) {
         calculatedTopParentMap.put(pageId, topParent);
     }
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57fcc2a and d47debf.

📒 Files selected for processing (1)
  • lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.java

📄 CodeRabbit inference engine (.cursor/rules/logging-rule.mdc)

**/*.java: Use SLF4J parameterized logging (logger.info("Value is: {}", value)) instead of string concatenation (logger.info("Value is: " + value))
Log messages and code comments should be in English. Log messages should never be translated.

**/*.java: Java: Never use local variable type inference (var). Always declare explicit types. Yes: Map<String, Integer> counts = new HashMap<>(); No: var counts = new HashMap<String, Integer>();
When proposing Java code, spell out full types in local variable declarations, for loops, and try-with-resources
When editing Java, prefer clarity over brevity; avoid introducing language features that aren't widely used in the repo
Treat any PR or suggestion containing Java var as non-compliant. Recommend replacing with explicit types before merge

**/*.java: Use Java 17 for trunk development (Java 11 was used for Sakai 22 and Sakai 23)
Do not use local variable type inference (var) in Java code. Always declare explicit types (e.g., List<String> names = new ArrayList<>(); not var names = new ArrayList<String>();). Enforced by Checkstyle rule during mvn validate

Files:

  • lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java
🧠 Learnings (1)
📚 Learning: 2025-10-07T16:11:33.008Z
Learnt from: CR
Repo: sakaiproject/sakai PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T16:11:33.008Z
Learning: For new tools, prefer Spring MVC/Boot with Hibernate and ThymeLeaf; avoid RSF for new development and consider modernization when changing legacy tools

Applied to files:

  • lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java
🧬 Code graph analysis (1)
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java (2)
kernel/api/src/main/java/org/sakaiproject/util/MergeConfig.java (1)
  • MergeConfig (31-41)
kernel/api/src/main/java/org/sakaiproject/util/cover/LinkMigrationHelper.java (1)
  • LinkMigrationHelper (23-52)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: maven-build
  • GitHub Check: maven-build
  • GitHub Check: sakai-deploy
🔇 Additional comments (4)
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java (4)

101-134: Import additions are appropriate.

The added imports support the new hierarchy reconstruction functionality:

  • SakaiLTIUtil for LTI content handling
  • MergeConfig and LinkMigrationHelper for merge operations

These are all used within the file and follow proper organization.


1650-1651: Deferred parent relationship assignment is a reasonable approach.

Creating pages with a placeholder toolId of "0" and setting parent relationships in a later pass allows proper hierarchy reconstruction after all pages are created. This is cleaner than trying to resolve relationships during initial creation.


1753-1792: Hierarchy update logic looks correct.

The code properly:

  1. Iterates through the page map using old page IDs as keys
  2. Looks up calculated parent/topparent relationships
  3. Maps old IDs to new IDs via pageMap
  4. Updates and persists only when changes are needed

The logging at the end provides good visibility into the operation.


1970-1999: ToolId propagation logic is correct.

The code properly propagates the toolId from top-level pages to their descendants. The checks at line 1986-1987 correctly handle:

  • Null pages
  • Top parent with null or placeholder "0" toolId
  • Avoiding unnecessary updates when toolId already matches

Note: For very large imports, the two getPage() calls per iteration (lines 1983-1984) could be optimized by caching pages from the earlier pass, but this is acceptable for typical import scenarios.

@st-manu st-manu marked this pull request as draft December 19, 2025 10:10
@st-manu
Copy link
Contributor Author

st-manu commented Dec 19, 2025

Converted to draft: continues the work from #14300

@github-actions
Copy link

This PR has been automatically marked as stale due to 21 days of inactivity. It will be closed in 7 days unless there is new activity.

@st-manu st-manu marked this pull request as ready for review January 27, 2026 16:43
Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In
`@lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java`:
- Around line 1893-1969: The hierarchy reconstruction loses parent/topparent
info for cross-server imports because findReferencedPagesByItems(fromSiteId)
returns empty when fromSite == null; update the logic after subpageRefs is
computed to fall back to the XML attributes by iterating pageElementMap (the
parsed <page> Elements), reading the "parent" attribute (use NumberUtils.toLong
or equivalent) and populating calculatedParentMap for any parentId > 0 when
!isSameServer and subpageRefs.isEmpty(), then compute calculatedTopParentMap as
before and proceed to apply them to pages; this uses the existing symbols
findReferencedPagesByItems, pageElementMap, calculatedParentMap,
calculatedTopParentMap, and isSameServer.
🧹 Nitpick comments (3)
lessonbuilder/tool/src/test/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducerTest.java (3)

126-164: Tests verify mock configuration rather than production logic.

These tests create mocks with when(subpage1.getToolId()).thenReturn("tool-id-123") and then assert assertNotNull(subpage1.getToolId()). This only validates that Mockito returns configured values, not that LessonBuilderEntityProducer correctly reconstructs hierarchy during import.

To actually test the hierarchy reconstruction:

`@Test`
public void testSubpagesImportedWithCorrectHierarchy() {
    // Setup: Create XML archive element or mock the merge inputs
    // ...
    
    // Act: Call the actual merge method
    // producer.mergeInternal(toSiteId, rootElement, archivePath, fromSiteId, mcx, entityMap, false);
    
    // Assert: Verify pages in destination site have correct hierarchy
    // SimplePage importedSubpage = dao.getPage(newPageId);
    // assertNotNull(importedSubpage.getParent());
    // assertEquals(expectedParentId, importedSubpage.getParent());
}

Consider refactoring to test actual production code behavior, or add integration tests that exercise the full import flow.


178-220: Test doesn't verify the recovery mechanism it documents.

The test comment at lines 214-215 states "the findReferencedPagesByItems method should find this relationship" but the test never calls findReferencedPagesByItems (which is a private method in LessonBuilderEntityProducer). The test only verifies that dao.findItemsOnPage() returns the mocked items.

To properly test orphaned subpage recovery, consider:

  1. Making findReferencedPagesByItems package-private or exposing through a testable interface
  2. Testing the public merge method with orphaned pages in the input data
  3. Verifying the resulting pages have correct parent/topParent values after import

225-272: Test serves as documentation but doesn't exercise production code.

This test validates expected data structures (PAGE items referencing subpages) but only exercises mock configuration and iteration, not the actual findReferencedPagesByItems method.

The test has value as documentation of the expected data model. To improve test coverage, consider:

  1. Using reflection or package-private access to test findReferencedPagesByItems directly
  2. Testing through public getEntityMap() which calls findReferencedPagesByItems internally

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In
`@lessonbuilder/tool/src/test/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducerTest.java`:
- Around line 194-235: The test testFindReferencedPagesByItemsSkipsOrphans
currently only asserts the result is non-null; change it to assert that the
orphan parent (pageId 200L) is not included in the produced mapping from
producer.findReferencedPagesByItems(siteId). Specifically, in
LessonBuilderEntityProducerTest::testFindReferencedPagesByItemsSkipsOrphans add
an assertion that result does not contain key 200L (e.g.
assertFalse(result.containsKey(200L))) or that 200L is not present in any of the
aggregated referenced ID lists, so the orphan parent is explicitly verified as
skipped.
🧹 Nitpick comments (2)
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java (1)

1827-1828: Minor: Clarify comment to match deferred assignments.

The comment mentions "parent relationships" but the makePage signature has toolId as the first parameter. The parent relationships are set via the 4th and 5th parameters which are already null. Consider updating the comment to reflect what's actually being deferred.

💡 Suggested clarification
-					// Create page with initial toolId, parent relationships will be set later
+					// Create page with placeholder toolId; toolId and parent relationships will be set by hierarchy calculation
 					page = simplePageToolDao.makePage("0", siteId, title, null, null);
lessonbuilder/tool/src/test/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducerTest.java (1)

237-330: Avoid re-implementing hierarchy logic in tests.

These two tests re-create the hierarchy algorithm inline, which can drift from production behavior. Consider extracting the hierarchy calculation into a package-private helper in LessonBuilderEntityProducer and invoking that directly in tests.

Copy link
Contributor

@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: 1

🤖 Fix all issues with AI agents
In
`@lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java`:
- Around line 1947-1960: The loop that walks parent chains using
calculatedParentMap (iterating pageId, currentPageId, topParent) must guard
against cycles: for each pageId traversal, track visited IDs (e.g., a local
Set<Long>) and break if currentPageId is seen again (or after a reasonable max
iteration count), then skip setting calculatedTopParentMap for that pageId and
optionally log a warning; update the while loop to check the visited set before
advancing currentPageId to prevent an infinite loop caused by cyclic parent
relationships.
🧹 Nitpick comments (2)
lessonbuilder/tool/src/test/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducerTest.java (2)

237-287: Consider testing the actual production code instead of duplicating the algorithm.

This test duplicates the hierarchy calculation algorithm inline rather than invoking the production code. While useful as a specification, it won't catch bugs if the production implementation diverges. Consider refactoring to call the actual merge methods with appropriate mocks, or at minimum, extract this algorithm into a shared utility that both test and production code use.


289-330: Same observation as the previous test - algorithm duplication.

Like testHierarchyCalculationFromReferences, this test validates the algorithm logic but doesn't exercise the production code path. The selective import filtering logic appears correct, but changes to the production implementation wouldn't be caught by this test.

Copy link
Contributor

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java (1)

850-856: Guard against blank/null siteId in public API.

Line 850: this method is now public and is called during merge; a blank siteId will pass through to DAO and can fail. Add a short guard to return an empty map.

🛠️ Proposed fix
 public Map<Long, List<Long>> findReferencedPagesByItems(String siteId) {
     Map<Long, List<Long>> pageToReferencedPages = new HashMap<>();
+    if (StringUtils.isBlank(siteId)) {
+        log.warn("findReferencedPagesByItems called with blank siteId");
+        return pageToReferencedPages;
+    }
 
     List<SimplePage> allPages = simplePageToolDao.getSitePages(siteId);
🤖 Fix all issues with AI agents
In
`@lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/service/LessonBuilderEntityProducer.java`:
- Around line 918-934: The calculateTopParentMap method may write an incorrect
top parent after breaking out of a detected cycle; modify the loop to track when
a cycle is detected (e.g., boolean cycleDetected set when visited.add returns
false) and only put into calculatedTopParentMap for pageId if no cycle was
detected and topParent != null. Update the logic around variables currentPageId,
topParent, visited to avoid storing any top parent when a cycle was encountered.

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