Repository: sakaiproject/sakai
Pull request #13423: SAK-51055 Samigo cc+ import issues with embedded image in CKEditor
Author: login: csev
Created: 2025-03-08T16:10:04Z | Merged: 2025-03-11T15:43:28Z

There are three steps to this change - they are separate commits below:

1.  Make it so that "Import from Site" fixes embedded images in the correct and incorrect feedback.
2. Make it so that "Import from Site" handles embedded LTI links (shipping cart) in all RTE areas
3. Make it so that "Lessons - Import from CC+" also works across all RTE areas and attachments

To do this, I extended the calling sequences for internal methods for updateEntityReferences() and migrateText() so they can handle the "Import from Site" or "merge()" use cases. The method signatures got a little long but I really wanted to avoid duplicating the complex code that goes through and finds all the RTE areas post copy or post merge.  It seemed more reliable in the long run the have a few more parameters and if tests that a lot of intricate code duplicated.

@stetsche Since this touched your work on performance improvement on Samigo site copy - I think it needs a review from you.

Comments welcome.


Diff:
diff --git a/lti/lti-impl/src/java/org/sakaiproject/lti/impl/BaseLTIService.java b/lti/lti-impl/src/java/org/sakaiproject/lti/impl/BaseLTIService.java
index bcfc55d1da21..6ae226b33cdd 100644
--- a/lti/lti-impl/src/java/org/sakaiproject/lti/impl/BaseLTIService.java
+++ b/lti/lti-impl/src/java/org/sakaiproject/lti/impl/BaseLTIService.java
@@ -1313,7 +1313,7 @@ protected String fixLtiLaunchUrls(String text, String fromContext, String toCont
 
 				// If we cannot find the content item and tool on in this server, get skeleton data
 				// from the basiclti.xml import
-				if ( content == null && mcx.ltiContentItems != null ) {
+				if ( content == null && mcx != null && mcx.ltiContentItems != null ) {
 					log.debug("Could not find content item {} / {} in site {}, checking ltiContentItems", linkContentId, contentKey, linkSiteId);
 					content = mcx.ltiContentItems.get(contentKey);
 					tool = null;  // force creation of a new tool in findOrCreateToolForContentItem
diff --git a/samigo/samigo-pack/src/webapp/WEB-INF/components.xml b/samigo/samigo-pack/src/webapp/WEB-INF/components.xml
index 9bde0827ac08..b4834d16c447 100644
--- a/samigo/samigo-pack/src/webapp/WEB-INF/components.xml
+++ b/samigo/samigo-pack/src/webapp/WEB-INF/components.xml
@@ -390,6 +390,7 @@
     <property name="publishedAssessmentFacadeQueries" ref="PublishedAssessmentFacadeQueries" />
     <property name="qtiService" ref="QTIService"/>
     <property name="linkMigrationHelper" ref="org.sakaiproject.util.api.LinkMigrationHelper"/>
+    <property name="ltiService" ref="org.sakaiproject.lti.api.LTIService"/>
   </bean>
 
 
diff --git a/samigo/samigo-services/pom.xml b/samigo/samigo-services/pom.xml
index 1e88a3715fb0..3e5a158aa572 100644
--- a/samigo/samigo-services/pom.xml
+++ b/samigo/samigo-services/pom.xml
@@ -155,6 +155,10 @@
             <groupId>org.sakaiproject.scheduler</groupId>
             <artifactId>scheduler-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.sakaiproject.lti</groupId>
+            <artifactId>lti-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.mariuszgromada.math</groupId>
             <artifactId>MathParser.org-mXparser</artifactId>
diff --git a/samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java b/samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
index b3a8291fed0b..21041a61e1ef 100644
--- a/samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
+++ b/samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
@@ -34,6 +34,7 @@
 import java.util.Set;
 import java.util.Stack;
 import java.util.TreeSet;
+import java.util.Iterator;
 import java.util.stream.Collectors;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -63,6 +64,7 @@
 import org.sakaiproject.exception.TypeException;
 import org.sakaiproject.samigo.api.SamigoReferenceReckoner;
 import org.sakaiproject.samigo.util.SamigoConstants;
+import org.sakaiproject.lti.api.LTIService;
 import org.sakaiproject.site.api.Site;
 import org.sakaiproject.site.api.SiteService;
 import org.sakaiproject.site.api.ToolConfiguration;
@@ -71,6 +73,7 @@
 import org.sakaiproject.tool.assessment.data.ifc.assessment.AnswerIfc;
 import org.sakaiproject.tool.assessment.data.ifc.assessment.AssessmentIfc;
 import org.sakaiproject.tool.assessment.data.ifc.assessment.AssessmentMetaDataIfc;
+import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemFeedbackIfc;
 import org.sakaiproject.tool.assessment.data.ifc.questionpool.QuestionPoolDataIfc;
 import org.sakaiproject.tool.assessment.data.dao.assessment.*;
 import org.sakaiproject.tool.assessment.data.dao.questionpool.QuestionPoolItemData;
@@ -81,6 +84,7 @@
 import org.sakaiproject.tool.assessment.facade.SectionFacade;
 import org.sakaiproject.tool.assessment.shared.api.questionpool.QuestionPoolServiceAPI;
 import org.sakaiproject.util.api.LinkMigrationHelper;
+import org.sakaiproject.util.MergeConfig;
 import org.sakaiproject.tool.assessment.shared.api.qti.QTIServiceAPI;
 import org.sakaiproject.user.api.UserDirectoryService;
 import org.sakaiproject.user.api.UserNotDefinedException;
@@ -109,6 +113,7 @@ public class AssessmentEntityProducer implements EntityTransferrer, EntityProduc
     @Getter @Setter protected UserDirectoryService userDirectoryService;
     @Getter @Setter protected PublishedAssessmentFacadeQueriesAPI publishedAssessmentFacadeQueries;
     @Setter protected LinkMigrationHelper linkMigrationHelper;
+    @Setter protected LTIService ltiService;
 
 	public void init() {
 		log.info("init()");
@@ -325,9 +330,9 @@ public String getLabel() {
 		return "samigo";
 	}
 
-	public String merge(String siteId, Element root, String archivePath,
-			String fromSiteId, Map attachmentNames, Map userIdTrans,
-			Set userListAllowImport) {
+	@Override
+	public String merge(String siteId, Element root, String archivePath, String fromSiteId, MergeConfig mcx) {
+
 	if (log.isDebugEnabled()) log.debug("merging " + getLabel());
         StringBuilder results = new StringBuilder();
         String qtiPath = (new File(archivePath)).getParent() 
@@ -356,8 +361,12 @@ public String merge(String siteId, Element root, String archivePath,
                                + id + ": " + t.getMessage() + "\n");
             }
         }
+
+        // Update the RTE text areas
+        Map<String, String> transversalMap = new HashMap<> ();
+        updateEntityReferencesInternal(siteId, transversalMap, mcx);
         return results.toString();
-	}
+    }
 
 	public boolean parseEntityReference(String reference, Reference ref) {
 		if (StringUtils.startsWith(reference, REFERENCE_ROOT)) {
@@ -397,7 +406,13 @@ public Map<String, String> transferCopyEntities(String fromContext, String toCon
 
 	@Override
 	public void updateEntityReferences(String toContext, Map<String, String> transversalMap){
-		if (transversalMap != null && !transversalMap.isEmpty()) {
+		MergeConfig mcx = null;
+		updateEntityReferencesInternal(toContext, transversalMap, mcx);
+	}
+
+	// Internal, usable in either transferCopyEntities or merge()
+	public void updateEntityReferencesInternal(String toContext, Map<String, String> transversalMap, MergeConfig mcx){
+		if (mcx != null || (transversalMap != null && !transversalMap.isEmpty()) ) {
 			Set<Entry<String, String>> entrySet = transversalMap.entrySet();
 
 			AssessmentService service = new AssessmentService();
@@ -421,7 +436,9 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 
 				String assessmentDesc = assessmentFacade.getDescription();
 				if(StringUtils.isNotBlank(assessmentDesc)){
+					log.debug("before migrate assessmentDesc: {}", assessmentDesc);
 					assessmentDesc = org.sakaiproject.util.cover.LinkMigrationHelper.migrateAllLinks(entrySet, assessmentDesc);
+					log.debug("after migrate assessmentDesc: {}", assessmentDesc);
 					if(!assessmentDesc.equals(assessmentFacade.getDescription())){
 						//need to save since a ref has been updated:
 						needToUpdate = true;
@@ -434,7 +451,9 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 					SectionFacade section = (SectionFacade) sectionList.get(i);
 					String sectionDesc = section.getDescription();
 					if(StringUtils.isNotBlank(sectionDesc)){
+						log.debug("before migrate sectionDesc: {}", sectionDesc);
 						sectionDesc = org.sakaiproject.util.cover.LinkMigrationHelper.migrateAllLinks(entrySet, sectionDesc);
+						log.debug("after migrate sectionDesc: {}", sectionDesc);
 						if(!sectionDesc.equals(section.getDescription())){
 							//need to save since a ref has been updated:
 							needToUpdate = true;
@@ -453,25 +472,28 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 							continue;
 						}
 
+						// TODO: why are these copyAttachments = false?
 						boolean instructionChanged = migrateText(service, toContext, item, itemHash, hasCaches, hasDuplicates, false,
-								"inst", itemContentCache, entrySet, ItemData::getInstruction, ItemData::setInstruction);
+								"inst", itemContentCache, entrySet, transversalMap, mcx, ItemData::getInstruction, ItemData::setInstruction);
 
 						boolean descriptionChanged = migrateText(service, toContext, item, itemHash, hasCaches, hasDuplicates, false,
-								"desc", itemContentCache, entrySet, ItemData::getDescription, ItemData::setDescription);
+								"desc", itemContentCache, entrySet, transversalMap, mcx, ItemData::getDescription, ItemData::setDescription);
 
 						boolean itemTextsChanged = false;
 						List<ItemTextIfc> itemTexts = item.getItemTextArray();
 						if (itemTexts != null) {
 							for (ItemTextIfc itemText : itemTexts) {
 								boolean itemTextChanged = migrateText(service, toContext, itemText, itemHash, hasCaches, hasDuplicates, true,
-										"it-" + itemText.getSequence(), itemContentCache, entrySet, ItemTextIfc::getText, ItemTextIfc::setText);
+										"it-" + itemText.getSequence(), itemContentCache, entrySet, transversalMap,
+										mcx, ItemTextIfc::getText, ItemTextIfc::setText);
 
 								boolean answersChanged = false;
 								List<AnswerIfc> answers =  itemText.getAnswerArray();
 								if (answers != null) {
 									for (AnswerIfc answer : answers) {
 										boolean answerChanged = migrateText(service, toContext, answer, itemHash, hasCaches, hasDuplicates, true,
-												"at-" + itemText.getSequence() + "-"+ answer.getSequence() , itemContentCache, entrySet, AnswerIfc::getText, AnswerIfc::setText);
+												"at-" + itemText.getSequence() + "-"+ answer.getSequence() , itemContentCache, entrySet, transversalMap,
+												mcx, AnswerIfc::getText, AnswerIfc::setText);
 
 										answersChanged = answersChanged || answerChanged;
 									}
@@ -481,9 +503,21 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 							}
 						}
 
+						boolean itemFeedbacksChanged = false;
+						if ( item.getItemFeedbackSet() != null && !item.getItemFeedbackSet().isEmpty() ) {
+							for (ItemFeedbackIfc itemFeedback : item.getItemFeedbackSet()) {
+								boolean itemFeedbackCHanged = migrateText(service, toContext, itemFeedback, itemHash, hasCaches, hasDuplicates, true,
+										"feedback" + itemFeedback.getTypeId(), itemContentCache, entrySet, transversalMap,
+										mcx, ItemFeedbackIfc::getText, ItemFeedbackIfc::setText);
+
+								itemFeedbacksChanged = itemFeedbacksChanged || itemFeedbackCHanged;
+							}
+						}
+
 						boolean needToUpdateItem = instructionChanged
 								|| descriptionChanged
-								|| itemTextsChanged;
+								|| itemTextsChanged
+								|| itemFeedbacksChanged;
 						needToUpdateCache.put(itemHash, needToUpdateItem);
 
 						needToUpdate = needToUpdate || needToUpdateItem;
@@ -776,34 +810,41 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
 			String migratedText;
-			if (copyAttachments) {
-				migratedText = assessmentService.copyContentHostingAttachments(itemText, toContext);
+			log.debug("itemText before {}", itemText);
+			if ( mcx != null ) {
+				migratedText = ltiService.fixLtiLaunchUrls(itemText, toContext, mcx);
+				migratedText = linkMigrationHelper.migrateLinksInMergedRTE(toContext, mcx, migratedText);
 			} else {
-				migratedText = itemText;
+				if (copyAttachments) {
+					migratedText = assessmentService.copyContentHostingAttachments(itemText, toContext);
+				} else {
+					migratedText = itemText;
+				}
+				migratedText = linkMigrationHelper.migrateAllLinks(entrySet, migratedText);
+				migratedText = ltiService.fixLtiLaunchUrls(migratedText, null, toContext, transversalMap);
 			}
-
-			migratedText = linkMigrationHelper.migrateAllLinks(entrySet, migratedText);
+			log.debug("migratedText after {}", migratedText);
 
 			// Check if there has been a change
 			if (!StringUtils.equals(itemText, migratedText)) {
 				setter.accept(item, migratedText);
-
 				if (hasDuplicates) {
 					textCache.put(cacheKey, migratedText);
 				}
-
 				return true;
 			}
 		}


Reviews:
user: login: ottenhoff
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T18:59:33Z
user: login: stetsche
body: @csev , thanks for the improvements! I left a few comments regarding some context and your changes. Do you think we should do another iteration of performance testing with the added code, or do you think it's really light additions?
CC: @bgarciaentornos 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T13:51:20Z
user: login: csev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T19:43:03Z
user: login: csev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T19:43:53Z
user: login: csev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T20:07:42Z
user: login: csev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T20:10:55Z
user: login: stetsche
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T22:02:23Z
user: login: stetsche
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T22:16:42Z
user: login: csev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T03:52:39Z
user: login: stetsche
body: Looks pretty good now, the diff is also pretty readable now. One suggestion and a doubt:
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:08:54Z
user: login: csev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:45:20Z
user: login: csev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:51:12Z

Review comments:
diff_hunk: @@ -453,25 +472,26 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 							continue;
 						}
 
+						// TODO: why are these copyAttachments = false?
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: stetsche
body: While I can't answer the _why_ definitely, what `copyAttachments` does, did not happen for instructions and descriptions, so that boolean exists to keep the previous behavior. My focus was on improving performance while maintaining functionality, I didn't want to add more process time, especially without fully understanding this _why_. 
created_at: 2025-03-10T13:30:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -481,9 +501,23 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 							}
 						}
 
+						boolean itemFeedbacksChanged = false;
+						if ( item.getItemFeedbackSet() != null && !item.getItemFeedbackSet().isEmpty() ) {
+							for (Iterator<ItemFeedbackIfc> j = item.getItemFeedbackSet().iterator(); j.hasNext(); ) {
+								ItemFeedback itemFeedback = (ItemFeedback) j.next();
+								log.debug("itemFeedback: {} {}", itemFeedback.getText(), itemFeedback.getTypeId());
+								String migratedText = migrateTextString(service, toContext, itemHash, hasCaches, hasDuplicates, true,
+										"feedback" + itemFeedback.getTypeId(), itemContentCache, entrySet, transversalMap, mcx, itemFeedback.getText());
+								log.debug("after migratedText: {}", migratedText);
+								itemFeedback.setText(migratedText);
+								itemFeedbacksChanged = true;
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: stetsche
body: Should we check for a change?
created_at: 2025-03-10T13:33:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -776,38 +810,67 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
+
+			String migratedText = migrateTextString(assessmentService, toContext, itemHash, hasCaches, hasDuplicates,
+				copyAttachments, cacheCode, textCache, entrySet, transversalMap, mcx, itemText);
+
+			if (!StringUtils.equals(itemText, migratedText)) {
+				setter.accept(item, migratedText);
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private String migrateTextString(AssessmentService assessmentService, String toContext, String itemHash,
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: stetsche
body: I don't fully understand this yet. It looks like you added some code to the part where the migration happens and then moved that into it's own method. But why does that also include the caching part? We would be double checking the caches?
If I understand the changes right, I think `migrateTextString` should just migrate without checking the cache and `migrateText` would call it after checking caches.
created_at: 2025-03-10T13:46:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -481,9 +501,23 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 							}
 						}
 
+						boolean itemFeedbacksChanged = false;
+						if ( item.getItemFeedbackSet() != null && !item.getItemFeedbackSet().isEmpty() ) {
+							for (Iterator<ItemFeedbackIfc> j = item.getItemFeedbackSet().iterator(); j.hasNext(); ) {
+								ItemFeedback itemFeedback = (ItemFeedback) j.next();
+								log.debug("itemFeedback: {} {}", itemFeedback.getText(), itemFeedback.getTypeId());
+								String migratedText = migrateTextString(service, toContext, itemHash, hasCaches, hasDuplicates, true,
+										"feedback" + itemFeedback.getTypeId(), itemContentCache, entrySet, transversalMap, mcx, itemFeedback.getText());
+								log.debug("after migratedText: {}", migratedText);
+								itemFeedback.setText(migratedText);
+								itemFeedbacksChanged = true;
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: csev
body: Yes.  Thanks.
created_at: 2025-03-10T19:43:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -776,38 +810,67 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
+
+			String migratedText = migrateTextString(assessmentService, toContext, itemHash, hasCaches, hasDuplicates,
+				copyAttachments, cacheCode, textCache, entrySet, transversalMap, mcx, itemText);
+
+			if (!StringUtils.equals(itemText, migratedText)) {
+				setter.accept(item, migratedText);
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private String migrateTextString(AssessmentService assessmentService, String toContext, String itemHash,
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: csev
body: Thanks - I will look into that.
created_at: 2025-03-10T19:43:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -453,25 +472,26 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 							continue;
 						}
 
+						// TODO: why are these copyAttachments = false?
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: csev
body: I feel the same - it looks like `copyAttachments` makes _another_ copy of embedded things like images attachments and only is used for the questions and answers and not the assessment-level text - so I will follow that pattern as well.
created_at: 2025-03-10T20:07:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -776,38 +810,67 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
+
+			String migratedText = migrateTextString(assessmentService, toContext, itemHash, hasCaches, hasDuplicates,
+				copyAttachments, cacheCode, textCache, entrySet, transversalMap, mcx, itemText);
+
+			if (!StringUtils.equals(itemText, migratedText)) {
+				setter.accept(item, migratedText);
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private String migrateTextString(AssessmentService assessmentService, String toContext, String itemHash,
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: csev
body: Actually I will move all caching into `migrateTextString()`.  The way I first did it avoided allocating one string that would be quickly deallocated.  But making the code simpler and clearer and not duplicating logic is more important.  Now `migrateText()` relies on `migrateTextString()` completely including all cache processing except for the check for differences, and using the setter and getter.  I wanted the instruction text to be cached as well.
created_at: 2025-03-10T20:10:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -776,38 +810,67 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
+
+			String migratedText = migrateTextString(assessmentService, toContext, itemHash, hasCaches, hasDuplicates,
+				copyAttachments, cacheCode, textCache, entrySet, transversalMap, mcx, itemText);
+
+			if (!StringUtils.equals(itemText, migratedText)) {
+				setter.accept(item, migratedText);
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private String migrateTextString(AssessmentService assessmentService, String toContext, String itemHash,
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: stetsche
body: If you move caching to `migrateTextString` what purpose is left for `migrateText`? Why do we actually need two methods?
Either way, please ping me when you want me to look at it again.
created_at: 2025-03-10T22:02:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -776,38 +810,67 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
+
+			String migratedText = migrateTextString(assessmentService, toContext, itemHash, hasCaches, hasDuplicates,
+				copyAttachments, cacheCode, textCache, entrySet, transversalMap, mcx, itemText);
+
+			if (!StringUtils.equals(itemText, migratedText)) {
+				setter.accept(item, migratedText);
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private String migrateTextString(AssessmentService assessmentService, String toContext, String itemHash,
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: stetsche
body: Looking at the use of `migrateTextString` again, it looks like the only difference is that you apply getter and setter manually. If you pass `ItemFeedback::getText` and `ItemFeedback::setText` to `migrateText` it should do that for you, also returning the value for of `itemFeedbacksChanged`.
created_at: 2025-03-10T22:16:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -776,38 +810,67 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
+
+			String migratedText = migrateTextString(assessmentService, toContext, itemHash, hasCaches, hasDuplicates,
+				copyAttachments, cacheCode, textCache, entrySet, transversalMap, mcx, itemText);
+
+			if (!StringUtils.equals(itemText, migratedText)) {
+				setter.accept(item, migratedText);
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private String migrateTextString(AssessmentService assessmentService, String toContext, String itemHash,
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: csev
body: Good point.  I will go back to one migrate text function.
created_at: 2025-03-11T03:52:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -481,9 +503,22 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 							}
 						}
 
+						boolean itemFeedbacksChanged = false;
+						if ( item.getItemFeedbackSet() != null && !item.getItemFeedbackSet().isEmpty() ) {
+							for (Iterator<ItemFeedbackIfc> j = item.getItemFeedbackSet().iterator(); j.hasNext(); ) {
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: stetsche
body: Could we do `for (ItemFeedbackIfc feedback : itemFeedbacks)`? We also don't need to explicitly check for the collection to be empty.
created_at: 2025-03-11T13:03:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -776,34 +811,42 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
 			String migratedText;
-			if (copyAttachments) {
-				migratedText = assessmentService.copyContentHostingAttachments(itemText, toContext);
+			log.debug("itemText before {}", itemText);
+			if ( mcx != null ) {
+				migratedText = ltiService.fixLtiLaunchUrls(itemText, toContext, mcx);
+				migratedText = linkMigrationHelper.migrateLinksInMergedRTE(toContext, mcx, migratedText);
 			} else {
-				migratedText = itemText;
+				if (copyAttachments) {
+					migratedText = assessmentService.copyContentHostingAttachments(itemText, toContext);
+				} else {
+					migratedText = itemText;
+				}
+				migratedText = linkMigrationHelper.migrateAllLinks(entrySet, migratedText);
+				String fromContext = null;
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: stetsche
body: Is that correct? If yes, we might just pass null, I think that would be less confusing.
created_at: 2025-03-11T13:07:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -481,9 +503,22 @@ public void updateEntityReferences(String toContext, Map<String, String> transve
 							}
 						}
 
+						boolean itemFeedbacksChanged = false;
+						if ( item.getItemFeedbackSet() != null && !item.getItemFeedbackSet().isEmpty() ) {
+							for (Iterator<ItemFeedbackIfc> j = item.getItemFeedbackSet().iterator(); j.hasNext(); ) {
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: csev
body: Ha ha - I looked at that code and had the same thought - I will fix :)
created_at: 2025-03-11T13:45:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -776,34 +811,42 @@ private List<String> getAttachmentResourceIds(NodeList list) {
 	
 	private <T> boolean migrateText(AssessmentService assessmentService, String toContext, T item, String itemHash,
 			boolean hasCaches,boolean hasDuplicates, boolean copyAttachments, String cacheCode, Map<String, String> textCache,
-			Set<Entry<String, String>> entrySet, Function<T, String> getter, BiConsumer<T, String> setter) {
+			Set<Entry<String, String>> entrySet, Map<String, String> transversalMap, MergeConfig mcx,
+			Function<T, String> getter, BiConsumer<T, String> setter) {
 
+		log.debug("migrateText: {} {}", itemHash, copyAttachments);
 		String cacheKey = itemHash + "-" + cacheCode;
 
 		if (hasCaches && textCache.containsKey(cacheKey)) {
-			// Item instruction has been cashed, lets get it form the cache
+			// Item instruction has been cached, lets get it from the cache
 			setter.accept(item, textCache.get(cacheKey));
 			return true;
 		} else {
 			// Item instruction has not been cached, lets try migrating
 			String itemText = StringUtils.trimToEmpty(getter.apply(item));
 			String migratedText;
-			if (copyAttachments) {
-				migratedText = assessmentService.copyContentHostingAttachments(itemText, toContext);
+			log.debug("itemText before {}", itemText);
+			if ( mcx != null ) {
+				migratedText = ltiService.fixLtiLaunchUrls(itemText, toContext, mcx);
+				migratedText = linkMigrationHelper.migrateLinksInMergedRTE(toContext, mcx, migratedText);
 			} else {
-				migratedText = itemText;
+				if (copyAttachments) {
+					migratedText = assessmentService.copyContentHostingAttachments(itemText, toContext);
+				} else {
+					migratedText = itemText;
+				}
+				migratedText = linkMigrationHelper.migrateAllLinks(entrySet, migratedText);
+				String fromContext = null;
path: samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/assessment/AssessmentEntityProducer.java
user: login: csev
body: I like that pattern because it makes it really clear that sending null is a 100% OK thing.   I really dislike calls with `null, null, null` in their calling sequence.   We lose why each of those is OK to be null and what each of the null s really is.  I know that in modern IDEs - it tells you what each of the nulls represents.  So I will change it :)
created_at: 2025-03-11T13:51:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: csev
created_at: 2025-03-09T16:45:31Z
body: @stetsche Since this touched your work on performance improvement on Samigo site copy - I think it needs a review from you.

author_association: CONTRIBUTOR
user: login: csev
created_at: 2025-03-11T15:13:11Z
body: @stetsche I do think that a quick test of the performance fixes is in order.  I do not think that my changes change it in any material way.  I think that the test is more to insure no regressions.  cc: @bgarciaentornos

author_association: CONTRIBUTOR