Java tutorial
/** * Copyright (c) 2018- Charles R. Severance * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. * * Author: Charles Severance <csev@umich.edu> */ package org.sakaiproject.lti13; import io.jsonwebtoken.Jws; import java.io.IOException; import java.io.PrintWriter; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPublicKey; import java.util.Map; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.json.simple.JSONValue; import org.json.simple.JSONObject; import org.apache.commons.io.IOUtils; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Claims; import java.security.Key; import java.security.KeyPairGenerator; import java.util.logging.Level; import java.util.logging.Logger; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeMap; import lombok.extern.slf4j.Slf4j; import org.json.simple.JSONArray; import org.sakaiproject.authz.api.AuthzGroupService; import org.sakaiproject.authz.api.Member; import org.sakaiproject.authz.api.Role; import org.sakaiproject.basiclti.util.LegacyShaUtil; import static org.sakaiproject.basiclti.util.SakaiBLTIUtil.getInt; import static org.sakaiproject.basiclti.util.SakaiBLTIUtil.getLongKey; import org.sakaiproject.basiclti.util.SakaiBLTIUtil; import static org.sakaiproject.basiclti.util.SakaiBLTIUtil.LTI13_PATH; import static org.sakaiproject.basiclti.util.SakaiBLTIUtil.getOurServerUrl; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.lti.api.LTIService; import static org.sakaiproject.lti13.LineItemUtil.getLineItem; import org.tsugi.basiclti.BasicLTIUtil; import org.tsugi.jackson.JacksonUtil; import org.tsugi.lti13.LTI13KeySetUtil; import org.tsugi.lti13.LTI13Util; import org.tsugi.lti13.LTI13JwtUtil; import org.tsugi.oauth2.objects.AccessToken; import org.tsugi.lti13.objects.Endpoint; import org.sakaiproject.lti13.util.SakaiAccessToken; import org.sakaiproject.service.gradebook.shared.AssessmentNotFoundException; import org.sakaiproject.service.gradebook.shared.Assignment; import org.sakaiproject.service.gradebook.shared.CommentDefinition; import org.sakaiproject.service.gradebook.shared.GradebookNotFoundException; import org.sakaiproject.service.gradebook.shared.GradebookService; import org.sakaiproject.site.api.Group; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.cover.SessionManager; import org.sakaiproject.user.api.User; import org.sakaiproject.user.cover.UserDirectoryService; import org.tsugi.ags2.objects.LineItem; import org.tsugi.ags2.objects.Result; import org.tsugi.lti13.objects.LaunchLIS; /** * */ @SuppressWarnings("deprecation") @Slf4j public class LTI13Servlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final String APPLICATION_JSON = "application/json; charset=utf-8"; private static final String ERROR_DETAIL = "X-Sakai-LTI13-Error-Detail"; protected static LTIService ltiService = null; // Used for signing and checking tokens // TODO: Rotate these after a while private KeyPair tokenKeyPair = null; @Override public void init(ServletConfig config) throws ServletException { super.init(config); if (ltiService == null) { ltiService = (LTIService) ComponentManager.get("org.sakaiproject.lti.api.LTIService"); } if (tokenKeyPair == null) { try { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); tokenKeyPair = keyGen.genKeyPair(); } catch (NoSuchAlgorithmException ex) { Logger.getLogger(LTI13Servlet.class.getName()).log(Level.SEVERE, "Unable to generate tokenKeyPair", ex); } } } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String uri = request.getRequestURI(); // /imsblis/lti13/keys // String launch_url = request.getParameter("launch_url"); String[] parts = uri.split("/"); LineItem filter = getLineItemFilter(request); // Get a keys for a client_id // /imsblis/lti13/keyset/{tool-id} if (parts.length == 5 && "keyset".equals(parts[3])) { String client_id = parts[4]; handleKeySet(client_id, request, response); return; } // Get the membership list for a placement // /imsblis/lti13/namesandroles/context:6 if (parts.length == 5 && "namesandroles".equals(parts[3])) { String signed_placement = parts[4]; handleNamesAndRoles(signed_placement, request, response); return; } // List lineitems created by a placement // /imsblis/lti13/lineitems/{signed-placement} if (parts.length == 5 && "lineitems".equals(parts[3])) { String signed_placement = parts[4]; handleLineItemsGet(signed_placement, true /* all */, filter, request, response); return; } // Get lineitem data // /imsblis/lti13/lineitem/{signed-placement} if (parts.length == 5 && "lineitem".equals(parts[3])) { String signed_placement = parts[4]; handleLineItemsGet(signed_placement, false /* all */, null /* filter */, request, response); return; } // /imsblis/lti13/lineitem/{signed-placement}/results if (parts.length == 6 && "lineitem".equals(parts[3]) && "results".equals(parts[5])) { String signed_placement = parts[4]; String lineItem = null; handleLineItemsDetail(signed_placement, lineItem, true /*results */, null /* user_id */, request, response); return; } // Handle lineitems created by the tool // /imsblis/lti13/lineitems/{signed-placement}/{lineitem-id} if (parts.length == 6 && "lineitems".equals(parts[3])) { String signed_placement = parts[4]; String lineItem = parts[5]; handleLineItemsDetail(signed_placement, lineItem, false /*results */, null /* user_id */, request, response); return; } // /imsblis/lti13/lineitems/{signed-placement}/{lineitem-id}/results if (parts.length == 7 && "lineitems".equals(parts[3]) && "results".equals(parts[6])) { String signed_placement = parts[4]; String lineItem = parts[5]; handleLineItemsDetail(signed_placement, lineItem, true /*results */, null /* user_id */, request, response); return; } // /imsblis/lti13/lineitems/{signed-placement}/{lineitem-id}/results/{user-id} if (parts.length == 8 && "lineitems".equals(parts[3]) && "results".equals(parts[6])) { String signed_placement = parts[4]; String lineItem = parts[5]; handleLineItemsDetail(signed_placement, lineItem, true /*results */, parts[7], request, response); return; } log.error("Unrecognized GET request parts={} request={}", parts.length, uri); LTI13Util.return400(response, "Unrecognized GET request parts=" + parts.length + " request=" + uri); } protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String uri = request.getRequestURI(); // /imsblis/lti13/keys String[] parts = uri.split("/"); // Handle lineitems created by the tool // /imsblis/lti13/lineitems/{signed-placement}/{lineitem-id} if (parts.length == 6 && "lineitems".equals(parts[3])) { String signed_placement = parts[4]; String lineItem = parts[5]; handleLineItemsDelete(signed_placement, lineItem, request, response); return; } log.error("Unrecognized DELETE request parts={} request={}", parts.length, uri); LTI13Util.return400(response, "Unrecognized DELETE request parts=" + parts.length + " request=" + uri); } protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String uri = request.getRequestURI(); // /imsblis/lti13/keys String[] parts = uri.split("/"); // Handle lineitems created by the tool // /imsblis/lti13/lineitems/{signed-placement}/{lineitem-id} if (parts.length == 6 && "lineitems".equals(parts[3])) { String signed_placement = parts[4]; String lineItem = parts[5]; handleLineItemsUpdate(signed_placement, lineItem, request, response); return; } log.error("Unrecognized DELETE request parts={} request={}", parts.length, uri); LTI13Util.return400(response, "Unrecognized DELETE request parts=" + parts.length + " request=" + uri); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String uri = request.getRequestURI(); // /imsblis/lti13/keys // String launch_url = request.getParameter("launch_url"); String[] parts = uri.split("/"); // Get an access token for a tool // /imsblis/lti13/token/{tool-id} if (parts.length == 5 && "token".equals(parts[3])) { String tool_id = parts[4]; handleTokenPost(tool_id, request, response); return; } // Set score for auto-created line item // /imsblis/lti13/lineitem/{signed-placement} if (parts.length == 6 && "lineitem".equals(parts[3]) && "scores".equals(parts[5])) { String signed_placement = parts[4]; String lineItem = null; handleLineItemScore(signed_placement, lineItem, request, response); return; } // Create a new lineitem for a placement // /imsblis/lti13/lineitems/{signed-placement} if (parts.length == 5 && "lineitems".equals(parts[3])) { String signed_placement = parts[4]; handleLineItemsPost(signed_placement, request, response); return; } // Set a score for a lineitem created by a placement // /imsblis/lti13/lineitems/{signed-placement}/12345/scores if (parts.length == 7 && "lineitems".equals(parts[3]) && "scores".equals(parts[6])) { String signed_placement = parts[4]; String lineItem = parts[5]; handleLineItemScore(signed_placement, lineItem, request, response); return; } log.error("Unrecognized POST request parts={} request={}", parts.length, uri); LTI13Util.return400(response, "Unrecognized POST request parts=" + parts.length + " request=" + uri); } protected void handleKeySet(String tool_id, HttpServletRequest request, HttpServletResponse response) { PrintWriter out = null; Long toolKey = SakaiBLTIUtil.getLongKey(tool_id); String siteId = null; // Full bypass mode Map<String, Object> tool = null; if (toolKey >= 0) { tool = ltiService.getToolDao(toolKey, siteId); } if (tool == null) { response.setHeader(ERROR_DETAIL, "Could not load keyset for client"); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); log.error("Could not load keyset for client_id={}", tool_id); return; } String publicSerialized = BasicLTIUtil.toNull((String) tool.get(LTIService.LTI13_PLATFORM_PUBLIC)); if (publicSerialized == null) { response.setHeader(ERROR_DETAIL, "Client has no public key"); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); log.error("Client_id={} has no public key", tool_id); return; } Key publicKey = LTI13Util.string2PublicKey(publicSerialized); if (publicKey == null) { response.setHeader(ERROR_DETAIL, "Client public key deserialization error"); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); log.error("Client_id={} deserialization error", tool_id); return; } // Cast should work :) RSAPublicKey rsaPublic = (RSAPublicKey) publicKey; String keySetJSON = null; try { keySetJSON = LTI13KeySetUtil.getKeySetJSON(rsaPublic); } catch (NoSuchAlgorithmException ex) { response.setHeader(ERROR_DETAIL, "NoSuchAlgorithmException"); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); log.error("Client_id={} NoSuchAlgorithmException", tool_id); return; } try { out = response.getWriter(); } catch (Exception e) { log.error(e.getMessage(), e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return; } response.setContentType(APPLICATION_JSON); try { out.println(keySetJSON); } catch (Exception e) { log.error(e.getMessage(), e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } } protected void handleTokenPost(String tool_id, HttpServletRequest request, HttpServletResponse response) { /* Parameters for /imsblis/lti13/token/9 Parameter Name - grant_type, Value - client_credentials Parameter Name - client_assertion_type, Value - urn:ietf:params:oauth:client-assertion-type:jwt-bearer Parameter Name - client_assertion, Value - eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODg4OFwvdHN1Z2kiLCJzdWIiOiJsdGkxM19odHRwczpcL1wvd3d3LnNha2FpcHJvamVjdC5vcmdcL181MmE2N2I2OS1jNTk4LTRjZWQtYWNiMy1kOTQ5MzJkZTJiMWEiLCJhdWQiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MFwvaW1zYmxpc1wvbHRpMTNcL3Rva2VuXC85IiwiaWF0IjoxNTM2NDMzODUwLCJleHAiOjE1MzY0MzM5MTAsImp0aSI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4ODg4XC90c3VnaTViOTQxZWJhNWVkNjQifQ.JhwwgUEVV85HLteYmmSykQkMkmP-mcbV0R99tvP69hTFBJf3ZAS_uyfdXZoeRJaS5_hzwNf_b9HXYJWmZvYQK2NLt3s5GsW3h2pD4S3lVybIRXbpajr8NgeKA3BfsRLDoyKCLYn16BDR5w7ULZj0om8avVSFMUNbQYouc6XaTUPCZGfxPn-OPFYxX7SlDfIZjvbPWFxQh-cS90m_mKIcSYitoKrg9az59K6iGu-pq1PmZYSdt4xabh0_WoOiracvvJE6N1Um7A5enS3iXuHbCufKySIO2ykYtdRgVqhxP5YYPlar55nNRqEZtDgBgMMsneNePfMrifOvvFLkxnpefA Parameter Name - scope, Value - http://imsglobal.org/ags/lineitem http://imsglobal.org/ags/result/read */ if (tokenKeyPair == null) { LTI13Util.return400(response, "No token key available to sign tokens"); log.error("No token key available to sign tokens"); return; } String grant_type = request.getParameter(AccessToken.GRANT_TYPE); String client_assertion = request.getParameter(AccessToken.CLIENT_ASSERTION); String scope = request.getParameter(AccessToken.SCOPE); String missing = ""; if (grant_type == null) { missing += " " + "grant_type"; } if (client_assertion == null) { missing += " " + "client_assertion"; } if (scope == null) { missing += " " + "scope"; } if (missing.length() > 0) { LTI13Util.return400(response, "Token request missing fields:" + missing); log.error("Token Request missing fields: {}", missing); return; } String body = LTI13JwtUtil.rawJwtBody(client_assertion); if (body == null) { LTI13Util.return400(response, "Could not find Jwt Body in client_assertion"); log.error("Could not find Jwt Body in client_assertion\n{}", client_assertion); return; } Long toolKey = getLongKey(tool_id); if (toolKey < 1) { LTI13Util.return400(response, "Invalid tool key"); log.error("Invalis tool key {}", tool_id); return; } // Load the tool Map<String, Object> tool = ltiService.getToolDao(toolKey, null, true); if (tool == null) { LTI13Util.return400(response, "Could not load tool"); log.error("Could not load tool {}", tool_id); return; } String tool_public = (String) tool.get(LTIService.LTI13_TOOL_PUBLIC); if (tool_public == null) { LTI13Util.return400(response, "Could not find tool public key"); log.error("Could not find tool public key {}", tool_id); return; } Key publicKey = LTI13Util.string2PublicKey(tool_public); if (publicKey == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); LTI13Util.return400(response, "Could not deserialize tool public key"); log.error("Could not deserialize tool public key {}", tool_id); return; } Jws<Claims> claims = Jwts.parser().setAllowedClockSkewSeconds(60).setSigningKey(publicKey) .parseClaimsJws(client_assertion); if (claims == null) { LTI13Util.return400(response, "Could not verify signature"); log.error("Could not verify signature {}", tool_id); return; } scope = scope.toLowerCase(); int allowOutcomes = getInt(tool.get(LTIService.LTI_ALLOWOUTCOMES)); int allowRoster = getInt(tool.get(LTIService.LTI_ALLOWROSTER)); int allowSettings = getInt(tool.get(LTIService.LTI_ALLOWSETTINGS)); int allowLineItems = getInt(tool.get(LTIService.LTI_ALLOWLINEITEMS)); SakaiAccessToken sat = new SakaiAccessToken(); sat.tool_id = toolKey; Long issued = new Long(System.currentTimeMillis() / 1000L); sat.expires = issued + 3600L; // Work through requested scopes if (scope.contains(Endpoint.SCOPE_LINEITEM_READONLY)) { if (allowLineItems != 1) { LTI13Util.return400(response, "invalid_scope", Endpoint.SCOPE_LINEITEM_READONLY); log.error("Scope lineitem not allowed {}", tool_id); return; } sat.addScope(SakaiAccessToken.SCOPE_LINEITEMS_READONLY); } if (scope.contains(Endpoint.SCOPE_LINEITEM)) { if (allowLineItems != 1) { LTI13Util.return400(response, "invalid_scope", Endpoint.SCOPE_LINEITEM); log.error("Scope lineitem not allowed {}", tool_id); return; } sat.addScope(SakaiAccessToken.SCOPE_LINEITEMS); sat.addScope(SakaiAccessToken.SCOPE_LINEITEMS_READONLY); } if (scope.contains(Endpoint.SCOPE_SCORE)) { if (allowOutcomes != 1 || allowLineItems != 1) { LTI13Util.return400(response, "invalid_scope", Endpoint.SCOPE_SCORE); log.error("Scope lineitem not allowed {}", tool_id); return; } sat.addScope(SakaiAccessToken.SCOPE_BASICOUTCOME); } if (scope.contains(Endpoint.SCOPE_RESULT_READONLY)) { if (allowOutcomes != 1 || allowLineItems != 1) { LTI13Util.return400(response, "invalid_scope", Endpoint.SCOPE_RESULT_READONLY); log.error("Scope lineitem not allowed {}", tool_id); return; } sat.addScope(SakaiAccessToken.SCOPE_BASICOUTCOME); } if (scope.contains(LaunchLIS.SCOPE_NAMES_AND_ROLES)) { if (allowOutcomes != 1) { LTI13Util.return400(response, "invalid_scope", LaunchLIS.SCOPE_NAMES_AND_ROLES); log.error("Scope lineitem not allowed {}", tool_id); return; } sat.addScope(SakaiAccessToken.SCOPE_ROSTER); } String payload = JacksonUtil.toString(sat); String jws = Jwts.builder().setPayload(payload).signWith(tokenKeyPair.getPrivate()).compact(); AccessToken at = new AccessToken(); at.access_token = jws; response.setContentType(APPLICATION_JSON); String atsp = JacksonUtil.prettyPrintLog(at); try { PrintWriter out = response.getWriter(); out.println(atsp); log.debug("Returning Token\n{}", atsp); } catch (IOException e) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); log.error(e.getMessage(), e); } } protected void handleLineItemScore(String signed_placement, String lineItem, HttpServletRequest request, HttpServletResponse response) { // Make sure the lineItem id is a long Long assignment_id = null; if (lineItem != null) { try { assignment_id = Long.parseLong(lineItem); } catch (NumberFormatException e) { LTI13Util.return400(response, "Bad value for assignment_id " + lineItem); log.error("Bad value for assignment_id " + lineItem); return; } } // Load the access token, checking the the secret SakaiAccessToken sat = getSakaiAccessToken(tokenKeyPair.getPublic(), request, response); log.debug("sat={}", sat); if (sat == null) { return; // Error already set } if (!sat.hasScope(SakaiAccessToken.SCOPE_BASICOUTCOME)) { LTI13Util.return400(response, "Scope basic outcome not in access token"); log.error("Scope basic outcome not in access token"); return; } String jsonString; try { // https://stackoverflow.com/questions/1548782/retrieving-json-object-literal-from-httpservletrequest jsonString = IOUtils.toString(request.getInputStream(), java.nio.charset.StandardCharsets.UTF_8); } catch (IOException ex) { log.error("Could not read POST Data {}", ex.getMessage()); LTI13Util.return400(response, "Could not read POST Data"); return; } log.debug("jsonString={}", jsonString); Object js = JSONValue.parse(jsonString); if (js == null || !(js instanceof JSONObject)) { LTI13Util.return400(response, "Badly formatted JSON"); return; } JSONObject jso = (JSONObject) js; Long scoreGiven = SakaiBLTIUtil.getLongNull(jso.get("scoreGiven")); Long scoreMaximum = SakaiBLTIUtil.getLongNull(jso.get("scoreMaximum")); String userId = (String) jso.get("userId"); // TODO: LTI13 quirk - should be subject String comment = (String) jso.get("comment"); log.debug("scoreGivenStr={} scoreMaximumStr={} userId={} comment={}", scoreGiven, scoreMaximum, userId, comment); if (scoreGiven == null || userId == null) { LTI13Util.return400(response, "Missing scoreGiven or userId"); return; } Map<String, Object> content = loadContentCheckSignature(signed_placement, response); if (content == null) { return; } String assignment_name = (String) content.get(LTIService.LTI_TITLE); if (assignment_name == null || assignment_name.length() < 1) { log.error("Could not determine assignment_name title {}", content.get(LTIService.LTI_ID)); LTI13Util.return400(response, "Could not determine assignment_name"); return; } Site site = loadSiteFromContent(content, signed_placement, response); if (site == null) { return; } String context_id = site.getId(); userId = SakaiBLTIUtil.parseSubject(userId); if (!checkUserInSite(site, userId)) { log.warn("User {} not found in siteId={}", userId, context_id); LTI13Util.return400(response, "User does not belong to site"); return; } Map<String, Object> tool = loadToolForContent(content, site, sat.tool_id, response); if (tool == null) { return; } Object retval; if (assignment_id == null) { retval = SakaiBLTIUtil.setGradeLTI13(site, sat.tool_id, content, userId, assignment_name, scoreGiven, scoreMaximum, comment); log.debug("Lineitem retval={}", retval); } else { // TODO: Could make a new method collapsing these tool calls into a single scan Assignment assnObj = LineItemUtil.getAssignmentByKeyDAO(context_id, sat.tool_id, assignment_id); if (assnObj == null || assnObj.getName() == null) { LTI13Util.return400(response, "Unable to load assignment " + assignment_id); return; } assignment_name = assnObj.getName(); retval = SakaiBLTIUtil.setGradeLTI13(site, sat.tool_id, content, userId, assignment_name, scoreGiven, scoreMaximum, comment); log.debug("Lineitem retval={}", retval); } } // https://github.com/IMSGlobal/LTI-spec-Names-Role-Provisioning/blob/develop/docs/names-role-provisioning-spec.md /* { "id" : "https://lms.example.com/sections/2923/memberships/?rlid=49566-rkk96", "context" : { "id": "2923-abc", "label": "CPS 435", "title": "CPS 435 Learning Analytics", } "members" : [ { "status" : "Active", "name": "Jane Q. Public", "picture" : "https://platform.example.edu/jane.jpg", "given_name" : "Jane", "family_name" : "Doe", "middle_name" : "Marie", "email": "jane@platform.example.edu", "user_id" : "0ae836b9-7fc9-4060-006f-27b2066ac545", "roles": [ "Instructor", "Mentor" ] "message" : [ { "https://purl.imsglobal.org/spec/lti/claim/message_type" : "ltiResourceLinkRequest", "https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome" : { "lis_result_sourcedid": "example.edu:71ee7e42-f6d2-414a-80db-b69ac2defd4", "lis_outcome_service_url": "https://www.example.com/2344" }, "https://purl.imsglobal.org/spec/lti/claim/custom": { "country" : "Canada", "user_mobile" : "123-456-7890" } } ] } ] } */ protected void handleNamesAndRoles(String signed_placement, HttpServletRequest request, HttpServletResponse response) throws java.io.IOException { // HttpUtil.printHeaders(request); // HttpUtil.printParameters(request); log.debug("signed_placement={}", signed_placement); // Load the access token, checking the the secret SakaiAccessToken sat = getSakaiAccessToken(tokenKeyPair.getPublic(), request, response); if (sat == null) { return; // Error already set } if (!sat.hasScope(SakaiAccessToken.SCOPE_ROSTER)) { LTI13Util.return400(response, "Scope roster not in access token"); log.error("Scope roster not in access token"); return; } Map<String, Object> content = loadContentCheckSignature(signed_placement, response); if (content == null) { LTI13Util.return400(response, "Could not load content from signed placement"); log.error("Could not load content from signed placement = {}", signed_placement); return; } Site site = loadSiteFromContent(content, signed_placement, response); if (site == null) { LTI13Util.return400(response, "Could not load site associated with content"); log.error("Could not load site associated with content={}", content.get(LTIService.LTI_ID)); return; } Map<String, Object> tool = loadToolForContent(content, site, sat.tool_id, response); if (tool == null) { log.error("Could not load tool={} associated with content={}", sat.tool_id, content.get(LTIService.LTI_ID)); return; } int releaseName = getInt(tool.get(LTIService.LTI_SENDNAME)); int releaseEmail = getInt(tool.get(LTIService.LTI_SENDEMAILADDR)); // int allowOutcomes = getInt(tool.get(LTIService.LTI_ALLOWOUTCOMES)); String assignment_name = (String) content.get(LTIService.LTI_TITLE); if (assignment_name == null || assignment_name.length() < 1) { assignment_name = null; } JSONObject context_obj = new JSONObject(); context_obj.put("id", site.getId()); context_obj.put("title", site.getTitle()); String maintainRole = site.getMaintainRole(); PrintWriter out = response.getWriter(); out.println("{"); out.println(" \"id\" : \"http://TODO.wtf.com/we_eliminated_json_ld_but_forgot_to_remove_this\","); out.println(" \"context\" : "); out.print(JacksonUtil.prettyPrint(context_obj)); out.println(","); out.println(" \"members\": ["); SakaiBLTIUtil.pushAdvisor(); try { boolean success = false; List<Map<String, Object>> lm = new ArrayList<>(); // Get users for each of the members. UserDirectoryService.getUsers will skip any undefined users. Set<Member> members = site.getMembers(); Map<String, Member> memberMap = new HashMap<>(); List<String> userIds = new ArrayList<>(); for (Member member : members) { userIds.add(member.getUserId()); memberMap.put(member.getUserId(), member); } List<User> users = UserDirectoryService.getUsers(userIds); boolean first = true; // TODO: Use LTISERVICE.LTI_ROLEMAP after SAK-40632 is completed and merged String roleMapProp = (String) tool.get("rolemap"); roleMapProp = "maintain:Dude"; Map<String, String> roleMap = SakaiBLTIUtil.convertRoleMapPropToMap(roleMapProp); for (User user : users) { JSONObject jo = new JSONObject(); jo.put("status", "Active"); String lti11_legacy_user_id = user.getId(); jo.put("lti11_legacy_user_id", lti11_legacy_user_id); String subject = SakaiBLTIUtil.getSubject(lti11_legacy_user_id, site.getId()); jo.put("user_id", subject); // TODO: Should be subject - LTI13 Quirk jo.put("lis_person_sourcedid", user.getEid()); if (releaseName != 0) { jo.put("name", user.getDisplayName()); } if (releaseEmail != 0) { jo.put("email", user.getEmail()); } Member member = memberMap.get(user.getId()); Map<String, Object> mm = new TreeMap<>(); Role role = member.getRole(); String ims_user_id = member.getUserId(); JSONArray roles = new JSONArray(); // If there is a role mapping, it has precedence over site.update if (roleMap.containsKey(role.getId())) { roles.add(roleMap.get(role.getId())); } else if (ComponentManager.get(AuthzGroupService.class).isAllowed(ims_user_id, SiteService.SECURE_UPDATE_SITE, "/site/" + site.getId())) { roles.add("Instructor"); } else { roles.add("Learner"); } jo.put("roles", roles); JSONObject sakai_ext = new JSONObject(); if (sat.hasScope(SakaiAccessToken.SCOPE_BASICOUTCOME) && assignment_name != null) { String placement_secret = (String) content.get(LTIService.LTI_PLACEMENTSECRET); String placement_id = getPlacementId(signed_placement); String result_sourcedid = SakaiBLTIUtil.getSourceDID(user, placement_id, placement_secret); if (result_sourcedid != null) sakai_ext.put("lis_result_sourcedid", result_sourcedid); } Collection groups = site.getGroupsWithMember(ims_user_id); if (groups.size() > 0) { JSONArray lgm = new JSONArray(); for (Iterator i = groups.iterator(); i.hasNext();) { Group group = (Group) i.next(); JSONObject groupObj = new JSONObject(); groupObj.put("id", group.getId()); groupObj.put("title", group.getTitle()); lgm.add(groupObj); } sakai_ext.put("sakai_groups", lgm); } jo.put("sakai_ext", sakai_ext); if (!first) { out.println(","); } first = false; out.print(JacksonUtil.prettyPrint(jo)); } out.println(""); out.println(" ] }"); } finally { SakaiBLTIUtil.popAdvisor(); } } protected SakaiAccessToken getSakaiAccessToken(Key publicKey, HttpServletRequest request, HttpServletResponse response) { String authorization = request.getHeader("authorization"); if (authorization == null || !authorization.startsWith("Bearer")) { log.error("Invalid authorization {}", authorization); LTI13Util.return400(response, "invalid_authorization"); return null; } // https://stackoverflow.com/questions/7899525/how-to-split-a-string-by-space/7899558 String[] parts = authorization.split("\\s+"); if (parts.length != 2 || parts[1].length() < 1) { log.error("Bad authorization {}", authorization); LTI13Util.return400(response, "invalid_authorization"); return null; } String jws = parts[1]; Claims claims; try { 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); LTI13Util.return400(response, "signature_error"); return null; } // Reconstruct the SakaiAccessToken // https://www.baeldung.com/jackson-deserialization SakaiAccessToken sat; try { ObjectMapper mapper = new ObjectMapper(); String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(claims); // System.out.println("jsonResult=" + jsonResult); sat = new ObjectMapper().readValue(jsonResult, SakaiAccessToken.class); } catch (IOException ex) { log.error("PARSE ERROR {}\n{}", ex.getMessage(), claims.toString()); LTI13Util.return400(response, "token_parse_failure", ex.getMessage()); return null; } // Validity check the access token if (sat.tool_id != null && sat.scope != null && sat.expires != null) { // All good } else { log.error("SakaiAccessToken missing required data {}", sat); LTI13Util.return400(response, "Missing required data in access_token"); return null; } return sat; } // Sanity check signed_placement // f093de4f8b98530abb0e7784c380ab1668510a7308cc454c79d4e7a0334ab268:::92e7ddf2-1c60-486c-97ae-bc2ffbde8e67:::content:6 /* String suffix = ":::" + context_id + ":::" + resource_link_id; String base_string = placementSecret + suffix; String signature = LegacyShaUtil.sha256Hash(base_string); return signature + suffix; */ protected static String[] splitSignedPlacement(String signed_placement, HttpServletResponse response) { String[] parts = signed_placement.split(":::"); if (parts.length != 3 || parts[0].length() < 1 || parts[0].length() > 10000 || parts[1].length() < 1 || parts[2].length() <= 8 || !parts[2].startsWith("content:")) { log.error("Bad signed_placement format {}", signed_placement); LTI13Util.return400(response, "bad signed_placement"); return null; } return parts; } // Assumes signature is correct and format is correct - call after loadContentCheckSignature protected static String getPlacementId(String signed_placement) { String[] parts = signed_placement.split(":::"); return parts.length == 3 ? parts[2] : null; } // Assumes signature is correct and format is correct - call after loadContentCheckSignature protected static String getContextId(String signed_placement, HttpServletResponse response) { String[] parts = signed_placement.split(":::"); return parts.length == 3 ? parts[1] : null; } protected static Map<String, Object> loadContentCheckSignature(String signed_placement, HttpServletResponse response) { String[] parts = splitSignedPlacement(signed_placement, response); if (parts == null) { return null; } String received_signature = parts[0]; String context_id = parts[1]; String placement_id = parts[2]; log.debug("signature={} context_id={} placement_id={}", received_signature, context_id, placement_id); String contentIdStr = placement_id.substring(8); Long contentKey = getLongKey(contentIdStr); if (contentKey < 0) { log.error("Bad placement format {}", signed_placement); LTI13Util.return400(response, "bad placement"); return null; } // Note that all of the above checking requires no database access :) // Now we have a valid access token and valid JSON, proceed with validating the signed_placement Map<String, Object> content = ltiService.getContentDao(contentKey); if (content == null) { log.error("Could not load Content Item {}", contentKey); LTI13Util.return400(response, "Could not load Content Item"); return null; } String placementSecret = (String) content.get(LTIService.LTI_PLACEMENTSECRET); if (placementSecret == null) { log.error("Could not load placementsecret {}", contentKey); LTI13Util.return400(response, "Could not load placementsecret"); return null; } // Validate the signed_placement signature before proceeding String suffix = ":::" + context_id + ":::" + placement_id; String base_string = placementSecret + suffix; String signature = LegacyShaUtil.sha256Hash(base_string); if (signature == null || !signature.equals(received_signature)) { log.error("Could not verify signed_placement {}", signed_placement); LTI13Util.return400(response, "Could not verify signed_placement"); return null; } return content; } protected Site loadSiteFromContent(Map<String, Object> content, String signed_placement, HttpServletResponse response) { String[] parts = splitSignedPlacement(signed_placement, response); if (parts == null) { return null; } String context_id = parts[1]; // Good signed_placement, lets load the site and tool String siteId = (String) content.get(LTIService.LTI_SITE_ID); if (siteId == null) { log.error("Could not find site content={}", content.get(LTIService.LTI_ID)); LTI13Util.return400(response, "Could not find site for content"); return null; } if (!siteId.equals(context_id)) { log.error("Found incorrect site for content={}", content.get(LTIService.LTI_ID)); LTI13Util.return400(response, "Found incorrect site for content"); return null; } Site site; try { site = SiteService.getSite(siteId); } catch (IdUnusedException e) { log.error("No site/page associated with content siteId={}", siteId); LTI13Util.return400(response, "Could not load site associated with content"); return null; } return site; } protected static boolean checkUserInSite(Site site, String userId) { // Make sure user exists in site // Make sure the user exists in the site boolean userExistsInSite = false; try { Member member = site.getMember(userId); if (member != null) { userExistsInSite = true; } } catch (Exception e) { userExistsInSite = false; } return userExistsInSite; } protected Map<String, Object> loadToolForContent(Map<String, Object> content, Site site, Long expected_tool_id, HttpServletResponse response) { Long toolKey = getLongKey(content.get(LTIService.LTI_TOOL_ID)); // System.out.println("toolKey="+toolKey+" sat.tool_id="+sat.tool_id); if (toolKey < 0 || !toolKey.equals(expected_tool_id)) { log.error("Content / Tool invalid content={} tool={}", content.get(LTIService.LTI_ID), toolKey); LTI13Util.return400(response, "Content / Tool mismatch"); return null; } Map<String, Object> tool = ltiService.getToolDao(toolKey, site.getId()); if (tool == null) { log.error("Could not load tool={}", toolKey); LTI13Util.return400(response, "Missing tool"); return null; } return tool; } protected String getPostData(HttpServletRequest request, HttpServletResponse response) { String jsonString; try { // https://stackoverflow.com/questions/1548782/retrieving-json-object-literal-from-httpservletrequest jsonString = IOUtils.toString(request.getInputStream(), java.nio.charset.StandardCharsets.UTF_8); } catch (IOException ex) { log.error("Could not read POST Data {}", ex.getMessage()); LTI13Util.return400(response, "Could not read POST Data"); return null; } log.debug("jsonString={}", jsonString); return jsonString; } protected Object getJSONFromPOST(HttpServletRequest request, HttpServletResponse response) { String jsonString = getPostData(request, response); if (jsonString == null) return null; // Error already set Object js = JSONValue.parse(jsonString); if (js == null || !(js instanceof JSONObject)) { log.error("Badly formatted JSON"); LTI13Util.return400(response, "Badly formatted JSON"); return null; } return js; } protected Object getObjectFromPOST(HttpServletRequest request, HttpServletResponse response, Class whichClass) { // https://www.baeldung.com/jackson-deserialization String jsonString = getPostData(request, response); if (jsonString == null) return null; // Error already set try { Object retval = new ObjectMapper().readValue(jsonString, whichClass); return retval; } catch (IOException ex) { String error = "Could not parse input as " + whichClass.getSimpleName(); log.error(error); LTI13Util.return400(response, error); return null; } } protected LineItem getLineItemFilter(HttpServletRequest request) { LineItem retval = new LineItem(); boolean found = false; String tag = request.getParameter("tag"); if (tag != null && tag.length() > 0) { found = true; retval.tag = tag; } String lti_link_id = request.getParameter("lti_link_id"); if (lti_link_id != null && lti_link_id.length() > 0) { found = true; retval.resourceLinkId = lti_link_id; } String resource_id = request.getParameter("resource_id"); if (resource_id != null && resource_id.length() > 0) { found = true; retval.resourceId = resource_id; } if (!found) return null; return retval; } /** * Add a new line item for this placement * * @param signed_placement * @param request * @param response */ private void handleLineItemsPost(String signed_placement, HttpServletRequest request, HttpServletResponse response) throws IOException { // Load the access token, checking the the secret SakaiAccessToken sat = getSakaiAccessToken(tokenKeyPair.getPublic(), request, response); log.debug("sat={}", sat); if (sat == null) { return; // No need - error is already set } if (!sat.hasScope(SakaiAccessToken.SCOPE_LINEITEMS)) { log.error("Scope lineitems not in access token"); LTI13Util.return400(response, "Scope lineitems not in access token"); return; } LineItem item = (LineItem) getObjectFromPOST(request, response, LineItem.class); if (item == null) { return; // Error alredy handled } Map<String, Object> content = loadContentCheckSignature(signed_placement, response); if (content == null) { return; } Site site = loadSiteFromContent(content, signed_placement, response); if (site == null) { return; } Map<String, Object> tool = loadToolForContent(content, site, sat.tool_id, response); if (tool == null) { return; } Assignment retval; try { retval = LineItemUtil.createLineItem(site, sat.tool_id, null /*content*/, item); } catch (Exception e) { log.error("Could not create lineitem: " + e.getMessage()); LTI13Util.return400(response, "Could not create lineitem: " + e.getMessage()); return; } // Add the link to this lineitem item.id = getOurServerUrl() + LTI13_PATH + "lineitems/" + signed_placement + "/" + retval.getId(); log.debug("Lineitem item={}", item); response.setContentType(LineItem.MIME_TYPE); PrintWriter out = response.getWriter(); String json_out = JacksonUtil.prettyPrint(item); log.debug("response={}", json_out); out.print(json_out); } /** * Add a new line item for this placement * * @param signed_placement * @param request * @param response */ private void handleLineItemsUpdate(String signed_placement, String lineItem, HttpServletRequest request, HttpServletResponse response) throws IOException { // Make sure the lineItem id is a long Long assignment_id; try { assignment_id = Long.parseLong(lineItem); } catch (NumberFormatException e) { LTI13Util.return400(response, "Bad value for assignment_id " + lineItem); log.error("Bad value for assignment_id " + lineItem); return; } // Load the access token, checking the the secret SakaiAccessToken sat = getSakaiAccessToken(tokenKeyPair.getPublic(), request, response); log.debug("sat={}", sat); if (sat == null) { return; // No need - error is already set } if (!sat.hasScope(SakaiAccessToken.SCOPE_LINEITEMS)) { LTI13Util.return400(response, "Scope lineitems not in access token"); log.error("Scope lineitems not in access token"); return; } LineItem item = (LineItem) getObjectFromPOST(request, response, LineItem.class); if (item == null) return; // Error alredy handled Map<String, Object> content = loadContentCheckSignature(signed_placement, response); if (content == null) { return; } Site site = loadSiteFromContent(content, signed_placement, response); if (site == null) { return; } Map<String, Object> tool = loadToolForContent(content, site, sat.tool_id, response); if (tool == null) { return; } Assignment retval; try { retval = LineItemUtil.updateLineItem(site, sat.tool_id, assignment_id, item); } catch (Exception e) { log.error(e.getMessage(), e); LTI13Util.return400(response, "Could not update lineitem: " + e.getMessage()); return; } // TODO: Does PUT need to return the entire line item - I think the code below // actually is wrong - we just need to do a GET to get the entire line // item after the PUT. It seems wasteful to always do the GET after PUT // when the tool can do it if it wants the newly updated item. So // For now I am sending nothing back for a pUT request. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT /* // Add the link to this lineitem item.id = getOurServerUrl() + LTI13_PATH + "lineitems/" + signed_placement + "/" + retval.getId(); log.debug("Lineitem item={}",item); response.setContentType(LineItem.MIME_TYPE); PrintWriter out = response.getWriter(); out.print(JacksonUtil.prettyPrint(item)); */ } /** * List all LineItems for this placement ore retrieve the single LineItem created for this placement * * @param signed_placement * @param all - Retrieve all the line items associated with the tool's placement. Otherwise return one LineItem. * @param filter - A LineItem with field upon which to filter, can also be null * @param request * @param response */ private void handleLineItemsGet(String signed_placement, boolean all, LineItem filter, HttpServletRequest request, HttpServletResponse response) throws IOException { log.debug("signed_placement={}", signed_placement); // Load the access token, checking the the secret SakaiAccessToken sat = getSakaiAccessToken(tokenKeyPair.getPublic(), request, response); if (sat == null) { return; } if (!(sat.hasScope(SakaiAccessToken.SCOPE_LINEITEMS_READONLY) || sat.hasScope(SakaiAccessToken.SCOPE_LINEITEMS))) { LTI13Util.return400(response, "Scope lineitems.readonly not in access token"); log.error("Scope lineitems.readonly not in access token"); return; } Map<String, Object> content = loadContentCheckSignature(signed_placement, response); if (content == null) { LTI13Util.return400(response, "Could not load content from signed placement"); log.error("Could not load content from signed placement = {}", signed_placement); return; } Site site = loadSiteFromContent(content, signed_placement, response); if (site == null) { LTI13Util.return400(response, "Could not load site associated with content"); log.error("Could not load site associated with content={}", content.get(LTIService.LTI_ID)); return; } Map<String, Object> tool = loadToolForContent(content, site, sat.tool_id, response); if (tool == null) { log.error("Could not load tool={} associated with content={}", sat.tool_id, content.get(LTIService.LTI_ID)); return; } // If we are only returning a single line item if (!all) { response.setContentType(LineItem.MIME_TYPE); LineItem item = LineItemUtil.getLineItem(content); PrintWriter out = response.getWriter(); out.print(JacksonUtil.prettyPrint(item)); return; } // Return all the line items for the tool List<LineItem> preItems = LineItemUtil.getPreCreatedLineItems(site, sat.tool_id, filter); List<LineItem> toolItems = LineItemUtil.getLineItemsForTool(signed_placement, site, sat.tool_id, filter); response.setContentType(LineItem.MIME_TYPE_CONTAINER); PrintWriter out = response.getWriter(); out.print("["); boolean first = true; for (LineItem item : preItems) { out.println(first ? "" : ","); first = false; out.print(JacksonUtil.prettyPrint(item)); } for (LineItem item : toolItems) { out.println(first ? "" : ","); first = false; out.print(JacksonUtil.prettyPrint(item)); } out.println(""); out.println("]"); } /** * Provide the detail or results for a tool created lineitem * @param signed_placement * @param lineItem - Can be null * @param results * @param request * @param response */ private void handleLineItemsDetail(String signed_placement, String lineItem, boolean results, String user_id, HttpServletRequest request, HttpServletResponse response) throws IOException { log.debug("signed_placement={}", signed_placement); // Make sure the lineItem id is a long Long assignment_id = null; if (lineItem != null) { try { assignment_id = Long.parseLong(lineItem); } catch (NumberFormatException e) { LTI13Util.return400(response, "Bad value for assignment_id " + lineItem); log.error("Bad value for assignment_id " + lineItem); return; } } // Load the access token, checking the the secret SakaiAccessToken sat = getSakaiAccessToken(tokenKeyPair.getPublic(), request, response); if (sat == null) { return; } /* if (! (sat.hasScope(SakaiAccessToken.SCOPE_LINEITEMS_READONLY) || sat.hasScope(SakaiAccessToken.SCOPE_LINEITEMS) )) { LTI13Util.return400(response, "Scope lineitems.readonly not in access token"); log.error("Scope lineitems.readonly not in access token"); return; } */ Map<String, Object> content = loadContentCheckSignature(signed_placement, response); if (content == null) { LTI13Util.return400(response, "Could not load content from signed placement"); log.error("Could not load content from signed placement = {}", signed_placement); return; } Site site = loadSiteFromContent(content, signed_placement, response); if (site == null) { LTI13Util.return400(response, "Could not load site associated with content"); log.error("Could not load site associated with content={}", content.get(LTIService.LTI_ID)); return; } String context_id = site.getId(); Map<String, Object> tool = loadToolForContent(content, site, sat.tool_id, response); if (tool == null) { log.error("Could not load tool={} associated with content={}", sat.tool_id, content.get(LTIService.LTI_ID)); return; } Assignment a; if (assignment_id != null) { a = LineItemUtil.getAssignmentByKeyDAO(context_id, sat.tool_id, assignment_id); } else { String assignment_label = (String) content.get(LTIService.LTI_TITLE); a = LineItemUtil.getAssignmentByLabelDAO(context_id, sat.tool_id, assignment_label); } if (a == null) { LTI13Util.return400(response, "Could not load assignment"); log.error("Could not load assignment_id={}", assignment_id); return; } // Return the line item metadata if (!results) { LineItem item = getLineItem(signed_placement, a); response.setContentType(LineItem.MIME_TYPE); String json_out = JacksonUtil.prettyPrint(item); log.debug("Returning {}", json_out); PrintWriter out = response.getWriter(); out.print(json_out); return; } resultsForAssignment(signed_placement, site, a, assignment_id, user_id, request, response); } private void resultsForAssignment(String signed_placement, Site site, Assignment a, Long assignment_id, String user_id, HttpServletRequest request, HttpServletResponse response) { log.debug("signed_placement={} user_id={}", signed_placement, user_id); // TODO: Is the outer container an array or an object - the spec and swagger doc disagree /* [{ "id": "https://lms.example.com/context/2923/lineitems/1/results/5323497", "scoreOf": "https://lms.example.com/context/2923/lineitems/1", "userId": "5323497", "resultScore": 0.83, "resultMaximum": 1, "comment": "This is exceptional work." }] */ response.setContentType(Result.MIME_TYPE_CONTAINER); // Look up the assignment so we can find the max points GradebookService g = (GradebookService) ComponentManager .get("org.sakaiproject.service.gradebook.GradebookService"); Session sess = SessionManager.getCurrentSession(); // Indicate "who" is reading this grade - needs to be a real user account String gb_user_id = ServerConfigurationService.getString("basiclti.outcomes.userid", "admin"); String gb_user_eid = ServerConfigurationService.getString("basiclti.outcomes.usereid", gb_user_id); sess.setUserId(gb_user_id); sess.setUserEid(gb_user_eid); String context_id = site.getId(); SakaiBLTIUtil.pushAdvisor(); try { boolean success = false; List<Map<String, Object>> lm = new ArrayList<>(); // Get users for each of the members. UserDirectoryService.getUsers will skip any undefined users. Map<String, Member> memberMap = new HashMap<>(); List<String> userIds = new ArrayList<>(); // TODO: Make this faster Set<Member> members = site.getMembers(); for (Member member : members) { if (user_id != null && !user_id.equals(member.getUserId())) continue; userIds.add(member.getUserId()); memberMap.put(member.getUserId(), member); } List<User> users = UserDirectoryService.getUsers(userIds); boolean first = true; PrintWriter out = response.getWriter(); if (user_id == null) out.println("["); for (User user : users) { Result result = new Result(); String lti11_legacy_user_id = user.getId(); String subject = SakaiBLTIUtil.getSubject(lti11_legacy_user_id, context_id); result.userId = subject; result.resultMaximum = a.getPoints(); if (signed_placement != null) { if (assignment_id != null) { result.id = getOurServerUrl() + LTI13_PATH + "lineitems/" + signed_placement + "/" + assignment_id + "/results/" + user.getId(); result.scoreOf = getOurServerUrl() + LTI13_PATH + "lineitems/" + signed_placement + "/" + assignment_id; } else { result.id = getOurServerUrl() + LTI13_PATH + "lineitem/" + signed_placement + "/results/" + user.getId(); result.scoreOf = getOurServerUrl() + LTI13_PATH + "lineitem/" + signed_placement; } } try { CommentDefinition commentDef = g.getAssignmentScoreComment(context_id, a.getId(), user.getId()); if (commentDef != null) { result.comment = commentDef.getCommentText(); } } catch (AssessmentNotFoundException | GradebookNotFoundException e) { log.error(e.getMessage(), e); // Unexpected break; } String actualGrade = null; result.resultScore = null; try { actualGrade = g.getAssignmentScoreString(context_id, a.getId(), user.getId()); } catch (AssessmentNotFoundException | GradebookNotFoundException e) { log.error(e.getMessage(), e); // Unexpected break; } if (actualGrade != null) { try { Double dGrade = new Double(actualGrade); result.resultScore = dGrade; } catch (NumberFormatException e) { log.error("Could not parse grade=" + actualGrade); result.resultScore = null; } } if (!first) { out.println(","); } first = false; String json_out = JacksonUtil.prettyPrint(result); out.print(json_out); } if (user_id == null) { out.println("]"); } } catch (Throwable t) { log.error(t.getMessage(), t); } finally { SakaiBLTIUtil.popAdvisor(); } } /** * Delete a tool created lineitem * @param signed_placement * @param lineItem * @param results * @param request * @param response */ private void handleLineItemsDelete(String signed_placement, String lineItem, HttpServletRequest request, HttpServletResponse response) throws IOException { log.debug("signed_placement={}", signed_placement); // Make sure the lineItem id is a long Long assignment_id; try { assignment_id = Long.parseLong(lineItem); } catch (NumberFormatException e) { LTI13Util.return400(response, "Bad value for assignment_id " + lineItem); log.error("Bad value for assignment_id " + lineItem); return; } // Load the access token, checking the the secret SakaiAccessToken sat = getSakaiAccessToken(tokenKeyPair.getPublic(), request, response); if (sat == null) { return; } if (!sat.hasScope(SakaiAccessToken.SCOPE_LINEITEMS)) { LTI13Util.return400(response, "Scope lineitems not in access token"); log.error("Scope lineitems not in access token"); return; } Map<String, Object> content = loadContentCheckSignature(signed_placement, response); if (content == null) { LTI13Util.return400(response, "Could not load content from signed placement"); log.error("Could not load content from signed placement = {}", signed_placement); return; } Site site = loadSiteFromContent(content, signed_placement, response); if (site == null) { LTI13Util.return400(response, "Could not load site associated with content"); log.error("Could not load site associated with content={}", content.get(LTIService.LTI_ID)); return; } Map<String, Object> tool = loadToolForContent(content, site, sat.tool_id, response); if (tool == null) { log.error("Could not load tool={} associated with content={}", sat.tool_id, content.get(LTIService.LTI_ID)); return; } String context_id = site.getId(); if (LineItemUtil.deleteAssignmentByKeyDAO(context_id, sat.tool_id, assignment_id)) { return; } LTI13Util.return400(response, "Could not delete assignment " + assignment_id); log.error("Could delete assignment={}", assignment_id); } }