Repository: sakaiproject/sakai
Pull request #13625: SAK-51078 LTI13 Fix UpdateLineItem API call losing resourceLinkId
Author: login: csev
Created: 2025-04-18T19:54:05Z | Merged: 2025-05-08T12:32:46Z



Diff:
diff --git a/lti/lti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java b/lti/lti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java
index 0ef4ef91c741..e046194cfaab 100644
--- a/lti/lti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java
+++ b/lti/lti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java
@@ -1693,7 +1693,9 @@ protected void handleGroupService(String site_id, HttpServletRequest request, Ht
 	}
 
 	protected SakaiAccessToken getSakaiAccessToken(Key publicKey, HttpServletRequest request, HttpServletResponse response) {
-		String authorization = request.getHeader("authorization");
+	        log.debug("publicKey={}", publicKey);
+
+	        String authorization = request.getHeader("authorization");
 
 		if (authorization == null || !authorization.startsWith("Bearer")) {
 			log.error("Invalid authorization {}", authorization);
@@ -1715,7 +1717,7 @@ protected SakaiAccessToken getSakaiAccessToken(Key publicKey, HttpServletRequest
 			claims = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(jws).getBody();
 		} catch (ExpiredJwtException | MalformedJwtException | UnsupportedJwtException
 				| io.jsonwebtoken.security.SignatureException | IllegalArgumentException e) {
-			log.error("Signature error {}\n{}", e.getMessage(), jws);
+			log.error("{} Signature error {}\n{}", e.getClass().getName(), e.getMessage(), jws, e);
 			LTI13Util.return403(response, "signature_error");
 			return null;
 		}
@@ -2185,7 +2187,7 @@ private void handleLineItemsUpdate(String signed_placement, String lineItem, Htt
 	 */
 	private void handleLineItemsGet(String signed_placement, boolean all, SakaiLineItem filter,
 			HttpServletRequest request, HttpServletResponse response) throws IOException {
-		log.debug("signed_placement={}", signed_placement);
+		log.debug("signed_placement={}; all={}", signed_placement, all);
 
 		// Load the access token, checking the the secret
 		SakaiAccessToken sat = getSakaiAccessToken(tokenKeyPair.getPublic(), request, response);
@@ -2249,6 +2251,7 @@ private void handleLineItemsGet(String signed_placement, boolean all, SakaiLineI
 			if ( content != null ) {
 				response.setContentType(SakaiLineItem.CONTENT_TYPE);
 				SakaiLineItem item = LineItemUtil.getDefaultLineItem(site, content);
+				log.debug("single line item = {}", JacksonUtil.prettyPrint(item));
 				PrintWriter out = response.getWriter();
 				out.print(JacksonUtil.prettyPrint(item));
 				return;
@@ -2259,6 +2262,7 @@ private void handleLineItemsGet(String signed_placement, boolean all, SakaiLineI
 		}
 
 		// Find the line items created for this tool
+		log.debug("filter={}", JacksonUtil.prettyPrint(filter));
 		List<SakaiLineItem> toolItems = LineItemUtil.getLineItemsForTool(signed_placement, site, sat.tool_id, filter);
 
 		response.setContentType(SakaiLineItem.CONTENT_TYPE_CONTAINER);
@@ -2269,6 +2273,7 @@ private void handleLineItemsGet(String signed_placement, boolean all, SakaiLineI
 		for (SakaiLineItem item : toolItems) {
 			out.println(first ? "" : ",");
 			first = false;
+			log.debug("line item = {}", JacksonUtil.prettyPrint(item));
 			out.print(JacksonUtil.prettyPrint(item));
 		}
 		out.println("");
diff --git a/lti/lti-common/src/java/org/sakaiproject/lti/util/SakaiLTIUtil.java b/lti/lti-common/src/java/org/sakaiproject/lti/util/SakaiLTIUtil.java
index 1b306d32aa79..bc6974497a1e 100644
--- a/lti/lti-common/src/java/org/sakaiproject/lti/util/SakaiLTIUtil.java
+++ b/lti/lti-common/src/java/org/sakaiproject/lti/util/SakaiLTIUtil.java
@@ -2576,11 +2576,16 @@ private static Object handleGradebook(String sourcedid, HttpServletRequest reque
 		}
 
 		// Now read, set, or delete the non-assignment grade...
+		// For LTI 1.1 columns we don't need to mark them for AGS LineItems retrieval
+		Long tool_id = null;
+		Map<String, Object> content = null;
+
 		Session sess = SessionManager.getCurrentSession();
 
 		SakaiLineItem lineItem = new SakaiLineItem();
 		lineItem.scoreMaximum = 100.0D;
-		org.sakaiproject.grading.api.Assignment gradebookColumn = getGradebookColumn(site, user_id, title, lineItem);
+
+		org.sakaiproject.grading.api.Assignment gradebookColumn = getGradebookColumn(site, user_id, title, lineItem, tool_id, content);
 		if (gradebookColumn == null) {
 			log.warn("gradebookColumn or Id is null, cannot proceed with grading in site {} for column {}", siteId, title);
 			return "Grade failure siteId=" + siteId;
@@ -2670,7 +2675,7 @@ public static Object handleGradebookLTI13(Site site,  Long tool_id, Map<String,
 				log.error("Could not determine content title {}", content.get(LTIService.LTI_ID));
 				return "Could not determine content title key="+content.get(LTIService.LTI_ID);
 			}
-			gradebookColumn = getGradebookColumn(site, userId, title, lineItem);
+			gradebookColumn = getGradebookColumn(site, userId, title, lineItem, tool_id, content);
 		} else {
 			gradebookColumn = LineItemUtil.getColumnByKeyDAO(siteId, tool_id, lineitem_key);
 			if ( gradebookColumn == null || gradebookColumn.getName() == null ) {
@@ -2931,7 +2936,7 @@ private static String getNextSubmissionLogKey(org.sakaiproject.assignment.api.mo
 		return keyPrefix + next;
 	}
 
-	public static org.sakaiproject.grading.api.Assignment getGradebookColumn(Site site, String userId, String title, SakaiLineItem lineItem) {
+	public static org.sakaiproject.grading.api.Assignment getGradebookColumn(Site site, String userId, String title, SakaiLineItem lineItem, Long tool_id, Map<String, Object> content) {
 		// Look up the gradebook columns so we can find the max points
 		GradingService gradingService = (GradingService) ComponentManager.get("org.sakaiproject.grading.api.GradingService");
 
@@ -2973,6 +2978,11 @@ public static org.sakaiproject.grading.api.Assignment getGradebookColumn(Site si
 				returnColumn.setPoints(scoreMaximum);
 				returnColumn.setExternallyMaintained(false);
 				returnColumn.setName(title);
+				if ( tool_id != null && content != null ) {
+					String external_id = LineItemUtil.constructExternalId(tool_id, content, lineItem);
+					returnColumn.setExternalAppName(LineItemUtil.GB_EXTERNAL_APP_NAME);
+					returnColumn.setExternalId(external_id);
+				}
 				// SAK-40043
 				Boolean releaseToStudent = lineItem.releaseToStudent == null ? Boolean.TRUE : lineItem.releaseToStudent; // Default to true
 				Boolean includeInComputation = lineItem.includeInComputation == null ? Boolean.TRUE : lineItem.includeInComputation; // Default true
diff --git a/lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java b/lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
index a92eb1b528af..90e7f41e7bca 100644
--- a/lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
+++ b/lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
@@ -55,6 +55,7 @@
 import org.sakaiproject.lti13.util.SakaiLineItem;
 
 import org.tsugi.lti.LTIUtil;
+import org.tsugi.lti13.LTI13Util;
 
 /**
  * Some Sakai Utility code for IMS LTI This is mostly code to support the
@@ -120,13 +121,16 @@ public static String constructExternalId(Map<String, Object> content, SakaiLineI
 	 */
 	// TODO: Remember to dream that someday this will be JSON :)
 	public static String constructExternalId(Long tool_id, Map<String, Object> content, SakaiLineItem lineItem)
+	{
+		Long content_id = (content == null) ? null : LTIUtil.toLongNull(content.get(LTIService.LTI_ID));
+		log.debug("content_id={}", content_id);
+		return constructExternalIdImpl(tool_id, content_id, lineItem);
+	}
+
+	private static String constructExternalIdImpl(Long tool_id, Long content_id, SakaiLineItem lineItem)
 	{
 		String retval = tool_id.toString();
-		if ( content == null ) {
-			retval += ID_SEPARATOR + "0" + ID_SEPARATOR;
-		} else {
-			retval += ID_SEPARATOR + content.get(LTIService.LTI_ID) + "|";
-		}
+		retval += ID_SEPARATOR + ((content_id == null) ? "0" : content_id.toString()) + ID_SEPARATOR;
 		if ( lineItem != null ) {
 			if ( lineItem.resourceId == null ) {
 				retval += ID_SEPARATOR;
@@ -139,9 +143,18 @@ public static String constructExternalId(Long tool_id, Map<String, Object> conte
 				retval += URLEncode(lineItem.tag.replace("|", "")) + ID_SEPARATOR;
 			}
 		}
+		log.debug("retval={}", retval);
 		return retval;
 	}
 
+	private static Long deriveContentIdFromGradebookExternalId(String external_id) {
+		if (external_id == null) {
+			return null;
+		}
+		String[] parts = StringUtils.split(external_id, ID_SEPARATOR_REGEX);
+		return (parts == null || parts.length < 2) ? null : Long.valueOf(parts[1]);
+	}
+
 	public static Assignment createLineItem(Site site, Long tool_id, Map<String, Object> content, SakaiLineItem lineItem) {
 		String context_id = site.getId();
 		return createLineItem(context_id, tool_id, content, lineItem);
@@ -270,7 +283,12 @@ public static Assignment updateLineItem(Site site, Long tool_id, Long column_id,
 			gradebookColumn.setPoints(Double.valueOf(lineItem.scoreMaximum));
 		}
 
-		String external_id = constructExternalId(tool_id, null, lineItem);
+		Long content_id = deriveContentIdFromGradebookExternalId(gradebookColumn.getExternalId());
+		String external_id = (content_id != null) ? constructExternalIdImpl(tool_id, content_id, lineItem) : constructExternalId(tool_id, null, lineItem);
+
+		log.debug("gb item id={}; gb item title={}; external_id={}; prior external_id={}; derived content id={}", gradebookColumn.getId(),
+			  gradebookColumn.getName(), external_id, gradebookColumn.getExternalId(), content_id);
+
 		gradebookColumn.setExternalId(external_id);
 		if ( lineItem.label != null ) {
 			gradebookColumn.setName(lineItem.label);
@@ -501,6 +519,8 @@ public static boolean isAssignmentColumn(String external_id) {
 	 */
 	public static List<SakaiLineItem> getLineItemsForTool(String signed_placement, Site site, Long tool_id, SakaiLineItem filter) {
 
+		log.debug("signed_placement={}; site id={}; tool_id={}", signed_placement, site.getId(), tool_id);
+
 		String context_id = site.getId();
 		if ( tool_id == null ) {
 			throw new RuntimeException("tool_id is required");
@@ -534,11 +554,13 @@ public static List<SakaiLineItem> getLineItemsForTool(String signed_placement, S
 				// tool_id|content_id|resourceLink|tag|assignmentRef (optional)
 				if ( external_id == null || external_id.length() < 1 ) continue;
 
+				log.debug("gb column id={}; title={}; external_id={}", gbColumn.getId(), gbColumn.getName(), external_id);
+
 				String[] parts = external_id.split(ID_SEPARATOR_REGEX);
 				if ( parts.length < 1 || ! parts[0].equals(tool_id.toString()) ) continue;
 
 				SakaiLineItem item = getLineItem(signed_placement, gbColumn);
-				if ( parts.length > 1 ) {
+				if ( parts.length > 1 && ! StringUtils.equals("0", parts[1]) ) {
 					item.resourceLinkId = "content:" + parts[1];
 				}
 
@@ -609,6 +631,7 @@ public static SakaiLineItem constructLineItem(Map<String, Object> content) {
 	public static SakaiLineItem getDefaultLineItem(Site site, Map<String, Object> content) {
 		String signed_placement = SakaiLTIUtil.getSignedPlacement(content);
 		Long tool_id = LTIUtil.toLongKey(content.get(LTIService.LTI_TOOL_ID));
+		log.debug("signed_placement={}; site id={}; tool_id={}", signed_placement, site.getId(), tool_id);
 		List<SakaiLineItem> toolItems = LineItemUtil.getLineItemsForTool(signed_placement, site, tool_id, null /* filter */);
 		String title = (String) content.get(LTIService.LTI_TITLE);
 		for (SakaiLineItem item : toolItems) {
diff --git a/lti/lti-tool/src/java/org/sakaiproject/lti/tool/LTIAdminTool.java b/lti/lti-tool/src/java/org/sakaiproject/lti/tool/LTIAdminTool.java
index 0120f168a909..9d69c5cdfaf5 100644
--- a/lti/lti-tool/src/java/org/sakaiproject/lti/tool/LTIAdminTool.java
+++ b/lti/lti-tool/src/java/org/sakaiproject/lti/tool/LTIAdminTool.java
@@ -1994,7 +1994,7 @@ private void handleLineItem(SessionState state, SakaiLineItem sakaiLineItem, Lon
 				log.warn("Could not create gradebook column while processing LineItem");
 			}
 		} catch (Exception e) { // Probably a duplicate title
-			log.info("Could not create gradebook column while processing LineItem");
+			log.warn("Could not create gradebook column while processing LineItem");
 		}
 	}
 
diff --git a/lti/tsugi-util/src/java/org/tsugi/lti13/LTI13Util.java b/lti/tsugi-util/src/java/org/tsugi/lti13/LTI13Util.java
index 1a75ac97f309..49c259d13f7e 100644
--- a/lti/tsugi-util/src/java/org/tsugi/lti13/LTI13Util.java
+++ b/lti/tsugi-util/src/java/org/tsugi/lti13/LTI13Util.java
@@ -473,6 +473,7 @@ public static void return4XX(HttpServletResponse response, String error, String
 		job.put("error", error);
 		String retval = org.tsugi.jackson.JacksonUtil.toString(job);
 		try {
+			log.debug("httpStatus={}; retval={}", httpStatus, retval);
 			PrintWriter out = response.getWriter();
 			out.println(retval);
 		} catch (IOException e) {


Reviews:
user: login: hornersa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T22:28:46Z
user: login: hornersa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T22:30:11Z
user: login: hornersa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T22:31:33Z
user: login: hornersa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T22:33:21Z
user: login: hornersa
body: I've manually backported this PR to my use case in Sakai 23 and it fixed that bug as is. Since attaching the 23.x patch to the jira a while ago, I've modified a few lines to leverage StringUtil method and made another change to further NPE-proof  deriveContentIdFromGradebookExternalId. The suggested changes mostly reflect this. 

Finally, I'm not sure if my case exercises the changes introduced with SakaiLTIUtil (or SakaiBLTIUtil in 23.x). Otherwise, this looks good to go to me. I'll await your response before committing to "Approve" for my review.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T22:40:51Z
user: login: jesusmmp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T18:46:43Z
user: login: hornersa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T18:49:16Z
user: login: jesusmmp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T19:27:39Z
user: login: hornersa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T19:59:23Z
user: login: hornersa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T22:50:49Z
user: login: hornersa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T21:26:51Z
user: login: hornersa
body: I observe now that the toLongNull method(s) seem to have been introduced with some recent commit. I'll bypass testing these new changes since they don't yet have analogs in 23.x (the only platform where I currently have the infrastructure to test LTI stuff outside our institution's firewall).
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:24:31Z

Review comments:
diff_hunk: @@ -140,9 +143,18 @@ public static String constructExternalId(Long tool_id, Map<String, Object> conte
 				retval += URLEncode(lineItem.tag.replace("|", "")) + ID_SEPARATOR;
 			}
 		}
+		log.debug("retval={}", retval);
 		return retval;
 	}
 
+	private static Long deriveContentIdFromGradebookExternalId(String external_id) {
+		if (external_id == null) {
+			return null;
+		}
+		String[] parts = external_id.split(ID_SEPARATOR_REGEX);
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: hornersa
body: ```suggestion
		String[] parts = StringUtils.split(external_id, ID_SEPARATOR_REGEX);
```
created_at: 2025-04-23T22:28:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,9 +143,18 @@ public static String constructExternalId(Long tool_id, Map<String, Object> conte
 				retval += URLEncode(lineItem.tag.replace("|", "")) + ID_SEPARATOR;
 			}
 		}
+		log.debug("retval={}", retval);
 		return retval;
 	}
 
+	private static Long deriveContentIdFromGradebookExternalId(String external_id) {
+		if (external_id == null) {
+			return null;
+		}
+		String[] parts = external_id.split(ID_SEPARATOR_REGEX);
+		return (parts.length < 2) ? null : Long.valueOf(parts[1]);
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: hornersa
body: ```suggestion
		return (parts == null || parts.length < 2) ? null : Long.valueOf(parts[1]);
```
created_at: 2025-04-23T22:30:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -535,11 +554,13 @@ public static List<SakaiLineItem> getLineItemsForTool(String signed_placement, S
 				// tool_id|content_id|resourceLink|tag|assignmentRef (optional)
 				if ( external_id == null || external_id.length() < 1 ) continue;
 
+				log.debug("gb column id={}; title={}; external_id={}", gbColumn.getId(), gbColumn.getName(), external_id);
+
 				String[] parts = external_id.split(ID_SEPARATOR_REGEX);
 				if ( parts.length < 1 || ! parts[0].equals(tool_id.toString()) ) continue;
 
 				SakaiLineItem item = getLineItem(signed_placement, gbColumn);
-				if ( parts.length > 1 ) {
+				if ( parts.length > 1 && ! "0".equals(parts[1]) ) {
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: hornersa
body: ```suggestion
				if ( parts.length > 1 && ! StringUtils.equals("0", parts[1]) ) {
```
created_at: 2025-04-23T22:31:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2650,6 +2655,7 @@ public static Object handleGradebookLTI13(Site site,  Long tool_id, Map<String,
 		String title;
 
 		log.debug("siteid: {} tool_id: {} lineitem_key: {} userId: {} scoreObj: {}", site.getId(), tool_id, lineitem_key, userId, scoreObj);
+System.out.println("content="+content);
path: lti/lti-common/src/java/org/sakaiproject/lti/util/SakaiLTIUtil.java
user: login: hornersa
body: Is this println intended? If so, I recommend folding the content var into the log.debug line above.
created_at: 2025-04-23T22:33:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -120,13 +120,16 @@ public static String constructExternalId(Map<String, Object> content, SakaiLineI
 	 */
 	// TODO: Remember to dream that someday this will be JSON :)
 	public static String constructExternalId(Long tool_id, Map<String, Object> content, SakaiLineItem lineItem)
+	{
+		Long content_id = (content == null) ? null : ((Integer) content.get(LTIService.LTI_ID)).longValue();
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: jesusmmp
body: I think this is not going to work on Oracle...
created_at: 2025-05-05T18:46:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -120,13 +120,16 @@ public static String constructExternalId(Map<String, Object> content, SakaiLineI
 	 */
 	// TODO: Remember to dream that someday this will be JSON :)
 	public static String constructExternalId(Long tool_id, Map<String, Object> content, SakaiLineItem lineItem)
+	{
+		Long content_id = (content == null) ? null : ((Integer) content.get(LTIService.LTI_ID)).longValue();
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: hornersa
body: @jesusmmp Could you expand on what would/might break for Oracle?
created_at: 2025-05-05T18:49:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -120,13 +120,16 @@ public static String constructExternalId(Map<String, Object> content, SakaiLineI
 	 */
 	// TODO: Remember to dream that someday this will be JSON :)
 	public static String constructExternalId(Long tool_id, Map<String, Object> content, SakaiLineItem lineItem)
+	{
+		Long content_id = (content == null) ? null : ((Integer) content.get(LTIService.LTI_ID)).longValue();
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: jesusmmp
body: The casting to Integer:

https://github.com/sakaiproject/sakai/pull/13661/files#diff-17263379da42a2007b1b53f17db8aea84add508a5c71d29c2a318d985ad70500R748
created_at: 2025-05-05T19:27:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -120,13 +120,16 @@ public static String constructExternalId(Map<String, Object> content, SakaiLineI
 	 */
 	// TODO: Remember to dream that someday this will be JSON :)
 	public static String constructExternalId(Long tool_id, Map<String, Object> content, SakaiLineItem lineItem)
+	{
+		Long content_id = (content == null) ? null : ((Integer) content.get(LTIService.LTI_ID)).longValue();
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: hornersa
body: Ok, thanks. I'll try the cited alternative in testing. If I recall correctly, I originally set the cast to Long but encountered a runtime exception when testing against mariadb.
created_at: 2025-05-05T19:59:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -120,13 +120,16 @@ public static String constructExternalId(Map<String, Object> content, SakaiLineI
 	 */
 	// TODO: Remember to dream that someday this will be JSON :)
 	public static String constructExternalId(Long tool_id, Map<String, Object> content, SakaiLineItem lineItem)
+	{
+		Long content_id = (content == null) ? null : ((Integer) content.get(LTIService.LTI_ID)).longValue();
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: hornersa
body: ```suggestion
		Long content_id = (content == null) ? null : LTI13Util.getLongNull(content.get(LTIService.LTI_ID));
```
@csev Also, at the top of the source file, you'll need to import org.tsugi.lti13.LTI13Util. I've tested this suggested change with McGrawHill, and it works. It's adapted from the Oracle issue raised by @jesusmmp .
created_at: 2025-05-05T22:50:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -120,13 +121,16 @@ public static String constructExternalId(Map<String, Object> content, SakaiLineI
 	 */
 	// TODO: Remember to dream that someday this will be JSON :)
 	public static String constructExternalId(Long tool_id, Map<String, Object> content, SakaiLineItem lineItem)
+	{
+		Long content_id = (content == null) ? null : LTIUtil.toLongNull(content.get(LTIService.LTI_ID));
path: lti/lti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
user: login: hornersa
body: I'm not sure where toLongNull is defined as I can't locate it in LTIUtil.java. Earlier this line used LTI13Util.getLongNull which had compelled the import of that class on line 58.
created_at: 2025-05-06T21:26:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: hornersa
created_at: 2025-05-06T18:21:40Z
body: @csev - Once you add the following line to the LineItemUtil.java, I think we should be good to go:
`import org.tsugi.lti13.LTI13Util;`
author_association: CONTRIBUTOR
user: login: csev
created_at: 2025-05-06T22:56:41Z
body: @hornersa I think we should merge this into master, back port it to 23.x and then if something else comes up do it in a separate JIRA.  The round trip for testing just takes too long given that a patch is need to `toLong()` each time we back port.
author_association: CONTRIBUTOR