/**
* $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-blis/src/java/org/sakaiproject/blti/ServiceServlet.java $
* $Id: ServiceServlet.java 131688 2013-11-17 02:14:16Z csev@umich.edu $
*
* Copyright (c) 2009 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.
*/
package org.sakaiproject.blti;
import java.lang.StringBuffer;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.TreeMap;
import java.util.Properties;
import java.util.Enumeration;
import java.util.Set;
import java.util.Iterator;
import java.util.UUID;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;
import net.oauth.OAuthValidator;
import net.oauth.SimpleOAuthValidator;
import net.oauth.server.OAuthServlet;
import net.oauth.signature.OAuthSignatureMethod;
import org.imsglobal.basiclti.XMLMap;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathConstants;
import org.sakaiproject.component.cover.ComponentManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.imsglobal.basiclti.BasicLTIUtil;
import org.sakaiproject.authz.api.Member;
import org.sakaiproject.authz.api.Role;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.event.cover.UsageSessionService;
import org.sakaiproject.id.cover.IdManager;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SitePage;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.site.api.ToolConfiguration;
import org.sakaiproject.site.cover.SiteService;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.Tool;
import org.sakaiproject.tool.cover.SessionManager;
import org.sakaiproject.tool.cover.ToolManager;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.cover.UserDirectoryService;
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.basiclti.util.SakaiBLTIUtil;
import org.imsglobal.basiclti.BasicLTIConstants;
import org.sakaiproject.basiclti.util.ShaUtil;
import org.sakaiproject.util.FormattedText;
import org.sakaiproject.service.gradebook.shared.AssignmentHasIllegalPointsException;
import org.sakaiproject.service.gradebook.shared.CategoryDefinition;
import org.sakaiproject.service.gradebook.shared.GradebookService;
import org.sakaiproject.service.gradebook.shared.GradebookExternalAssessmentService;
import org.sakaiproject.service.gradebook.shared.ConflictingAssignmentNameException;
import org.sakaiproject.service.gradebook.shared.ConflictingExternalIdException;
import org.sakaiproject.service.gradebook.shared.GradebookNotFoundException;
import org.sakaiproject.service.gradebook.shared.Assignment;
import org.sakaiproject.lessonbuildertool.SimplePageItem;
import org.imsglobal.pox.IMSPOXRequest;
import org.sakaiproject.lti.api.LTIService;
import org.sakaiproject.util.foorm.SakaiFoorm;
import org.sakaiproject.util.foorm.FoormUtil;
import org.sakaiproject.blti.LessonsFacade;
/**
* Notes:
*
* This program is directly exposed as a URL to receive IMS Basic LTI messages
* so it must be carefully reviewed and any changes must be looked at carefully.
* Here are some issues:
*
* - This will only function when it is enabled via sakai.properties
*
* - This servlet makes use of security advisors - once an advisor has been
* added, it must be removed - often in a finally. Also the code below only adds
* the advisor for very short segments of code to allow for easier review.
*
* Implemented using a SHA-1 hash of the effective context_id and then stores
* the original context_id in a site.property "lti_context_id" which will be
* useful for later reference. Since SHA-1 hashes to 40 chars, that would leave
* us 59 chars (i.e. 58 + ":") to use for LTI key. This also means that the new
* maximum supported size of an effective context_id is the maximum message size
* of SHA-1: maximum length of (264 ? 1) bits.
*/
@SuppressWarnings("deprecation")
public class ServiceServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static Log M_log = LogFactory.getLog(ServiceServlet.class);
private static ResourceLoader rb = new ResourceLoader("blis");
protected static SakaiFoorm foorm = new SakaiFoorm();
protected static LTIService ltiService = null;
protected static XPath xpath = null;
protected static XPathExpression LESSONS_RESOURCES_EXPR = null;
protected static XPathExpression LESSONS_FOLDER_EXPR = null;
protected static XPathExpression LESSONS_TYPE_EXPR = null;
protected static XPathExpression LESSONS_TITLE_EXPR = null;
protected static XPathExpression LESSONS_TEMPID_EXPR = null;
protected static XPathExpression LESSONS_URL_EXPR = null;
protected static XPathExpression LESSONS_CUSTOM_EXPR = null;
private final String returnHTML =
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n" +
" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" +
"<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">\n" +
"<body>\n" +
"<script language=\"javascript\">\n" +
"$message = '<div align=\"center\" style=\"text-align:left;width:80%;margin-top:5px;margin-left:auto;margin-right:auto;border-width:1px 1px 1px 1px;border-style:solid;border-color: gray;padding:.5em;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:.8em\"><p>MESSAGE</p>';\n" +
"$closeText = '<p><a href=\"javascript: self.close()\">CLOSETEXT</a></p>';\n" +
"$gotMessage = GOTMESSAGE;\n" +
"if(self.location==top.location) {\n" +
" if ( $gotMessage ) {\n" +
" document.write($message);\n" +
" document.write($closeText);\n" +
" } else {\n" +
" self.close();\n" +
" }\n" +
"} else {\n" +
" document.write($message);\n" +
"}\n" +
"</script>\n" +
"</div></body>\n" +
"</html>\n";
public void doError(HttpServletRequest request,HttpServletResponse response,
Map<String, Object> theMap, String s, String message, Exception e)
throws java.io.IOException
{
if (e != null) {
M_log.error(e.getLocalizedMessage(), e);
}
theMap.put("/message_response/statusinfo/codemajor", "Fail");
theMap.put("/message_response/statusinfo/severity", "Error");
String msg = rb.getString(s) + ": " + message;
M_log.info(msg);
theMap.put("/message_response/statusinfo/description", FormattedText.escapeHtmlFormattedText(msg));
String theXml = XMLMap.getXML(theMap, true);
PrintWriter out = response.getWriter();
out.println(theXml);
System.out.println("doError="+theXml);
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
LessonsFacade.init();
if ( ltiService == null ) ltiService = (LTIService) ComponentManager.get("org.sakaiproject.lti.api.LTIService");
try {
xpath = XPathFactory.newInstance().newXPath();
LESSONS_RESOURCES_EXPR = xpath.compile("params/resources/*");
LESSONS_FOLDER_EXPR = xpath.compile("resources/*");
LESSONS_TYPE_EXPR = xpath.compile("type");
LESSONS_TITLE_EXPR = xpath.compile("title");
LESSONS_TEMPID_EXPR = xpath.compile("tempId");
LESSONS_URL_EXPR = xpath.compile("launchUrl");
LESSONS_CUSTOM_EXPR = xpath.compile("launchParams");
} catch (Exception e) {
M_log.error("Error compiling XPath expressions.");
throw new ServletException();
}
}
/* launch_presentation_return_url=http://lmsng.school.edu/portal/123/page/988/
The TP may add a parameter called lti_errormsg that includes some detail as to
the nature of the error. The lti_errormsg value should make sense if displayed
to the user. If the tool has displayed a message to the end user and only wants
to give the TC a message to log, use the parameter lti_errorlog instead of
lti_errormsg. If the tool is terminating normally, and wants a message displayed
to the user it can include a text message as the lti_msg parameter to the
return URL. If the tool is terminating normally and wants to give the TC a
message to log, use the parameter lti_log.
http://localhost:8080/imsblis/service/return-url/site/12345
http://localhost:8080/imsblis/service/return-url/pda/12345
*/
protected void handleReturnUrl(HttpServletRequest request, HttpServletResponse response)
throws IOException
{
String lti_errorlog = request.getParameter("lti_errorlog");
if ( lti_errorlog != null ) M_log.error(lti_errorlog);
String lti_errormsg = request.getParameter("lti_errormsg");
if ( lti_errormsg != null ) M_log.error(lti_errormsg);
String lti_log = request.getParameter("lti_log");
if ( lti_log != null ) M_log.info(lti_log);
String lti_msg = request.getParameter("lti_msg");
if ( lti_msg != null ) M_log.info(lti_msg);
String message = rb.getString("outcome.tool.finished");
String gotMessage = "false";
if ( lti_msg != null ) {
message = rb.getString("outcome.tool.lti_msg") + " " + lti_msg;
gotMessage = "true";
} else if ( lti_errormsg != null ) {
message = rb.getString("outcome.tool.lti_errormsg") + " " + lti_errormsg;
gotMessage = "true";
}
String rpi = request.getPathInfo();
if ( rpi.length() > 11 ) rpi = rpi.substring(11);
String portalUrl = ServerConfigurationService.getPortalUrl();
portalUrl = portalUrl + rpi;
String output = returnHTML.replace("URL",portalUrl);
output = output.replace("GOTMESSAGE",gotMessage);
output = output.replace("MESSAGE",message);
output = output.replace("CLOSETEXT",rb.getString("outcome.tool.close.window"));
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println(output);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String rpi = request.getPathInfo();
if ( rpi.startsWith("/return-url") ) {
handleReturnUrl(request, response);
return;
}
doPost(request, response);
}
@SuppressWarnings("unchecked")
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String contentType = request.getContentType();
if ( contentType != null && contentType.startsWith("application/json") ) {
doPostJSON(request, response);
} else if ( contentType != null && contentType.startsWith("application/xml") ) {
doPostXml(request, response);
} else {
doPostForm(request, response);
}
}
@SuppressWarnings("unchecked")
protected void doPostForm(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String ipAddress = request.getRemoteAddr();
M_log.debug("Basic LTI Service request from IP=" + ipAddress);
String allowOutcomes = ServerConfigurationService.getString(
SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED, SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED_DEFAULT);
if ( ! "true".equals(allowOutcomes) ) allowOutcomes = null;
String allowSettings = ServerConfigurationService.getString(
SakaiBLTIUtil.BASICLTI_SETTINGS_ENABLED, SakaiBLTIUtil.BASICLTI_SETTINGS_ENABLED_DEFAULT);
if ( ! "true".equals(allowSettings) ) allowSettings = null;
String allowRoster = ServerConfigurationService.getString(
SakaiBLTIUtil.BASICLTI_ROSTER_ENABLED, SakaiBLTIUtil.BASICLTI_ROSTER_ENABLED_DEFAULT);
if ( ! "true".equals(allowRoster) ) allowRoster = null;
if (allowOutcomes == null && allowSettings == null && allowRoster == null ) {
M_log.warn("LTI Services are disabled IP=" + ipAddress);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
// Lets return an XML Response
Map<String,Object> theMap = new TreeMap<String,Object>();
Map<String,String[]> params = (Map<String,String[]>)request.getParameterMap();
for (Map.Entry<String,String[]> param : params.entrySet()) {
M_log.debug(param.getKey() + ":" + param.getValue()[0]);
}
//check lti_message_type
String lti_message_type = request.getParameter(BasicLTIConstants.LTI_MESSAGE_TYPE);
theMap.put("/message_response/lti_message_type", lti_message_type);
String sourcedid = null;
String message_type = null;
if( BasicLTIUtil.equals(lti_message_type, "basic-lis-replaceresult") ||
BasicLTIUtil.equals(lti_message_type, "basic-lis-createresult") ||
BasicLTIUtil.equals(lti_message_type, "basic-lis-updateresult") ||
BasicLTIUtil.equals(lti_message_type, "basic-lis-deleteresult") ||
BasicLTIUtil.equals(lti_message_type, "basic-lis-readresult") ) {
sourcedid = request.getParameter("sourcedid");
if ( allowOutcomes != null ) message_type = "basicoutcome";
} else if( BasicLTIUtil.equals(lti_message_type, "basic-lti-loadsetting") ||
BasicLTIUtil.equals(lti_message_type, "basic-lti-savesetting") ||
BasicLTIUtil.equals(lti_message_type, "basic-lti-deletesetting") ) {
sourcedid = request.getParameter("id");
if ( allowSettings != null ) message_type = "toolsetting";
} else if( BasicLTIUtil.equals(lti_message_type, "basic-lis-readmembershipsforcontext") ) {
sourcedid = request.getParameter("id");
if ( allowRoster != null ) message_type = "roster";
} else {
doError(request, response, theMap, "outcomes.invalid", "lti_message_type="+lti_message_type, null);
return;
}
// If we have not gotten one of our allowed message types, stop now
if ( message_type == null ) {
doError(request, response, theMap, "outcomes.invalid", "lti_message_type="+lti_message_type, null);
return;
}
// No point continuing without a sourcedid
if(BasicLTIUtil.isBlank(sourcedid)) {
doError(request, response, theMap, "outcomes.missing", "sourcedid", null);
return;
}
String lti_version = request.getParameter(BasicLTIConstants.LTI_VERSION);
if(!BasicLTIUtil.equals(lti_version, "LTI-1p0")) {
doError(request, response, theMap, "outcomes.invalid", "lti_version="+lti_version, null);
return;
}
String oauth_consumer_key = request.getParameter("oauth_consumer_key");
if(BasicLTIUtil.isBlank(oauth_consumer_key)) {
doError(request, response, theMap, "outcomes.missing", "oauth_consumer_key", null);
return;
}
// Sadly not supported easily using the Gradebook API - may have to dig
// deeper later
if ( BasicLTIUtil.equals(lti_message_type, "basic-lis-deleteresult") ) {
theMap.put("/message_response/statusinfo/codemajor", "Unsupported");
theMap.put("/message_response/statusinfo/severity", "Error");
theMap.put("/message_response/statusinfo/codeminor", "cannotdelete");
String theXml = XMLMap.getXML(theMap, true);
PrintWriter out = response.getWriter();
out.println(theXml);
return;
}
// Truncate this to the maximum length to insure no cruft at the end
if ( sourcedid.length() > 2048) sourcedid = sourcedid.substring(0,2048);
// Attempt to parse the sourcedid, any failure is fatal
String placement_id = null;
String signature = null;
String user_id = null;
try {
int pos = sourcedid.indexOf(":::");
if ( pos > 0 ) {
signature = sourcedid.substring(0, pos);
String dec2 = sourcedid.substring(pos+3);
pos = dec2.indexOf(":::");
user_id = dec2.substring(0,pos);
placement_id = dec2.substring(pos+3);
}
} catch (Exception e) {
// Log some detail for ourselves
M_log.warn("Unable to decrypt result_sourcedid IP=" + ipAddress + " Error=" + e.getMessage(),e);
signature = null;
placement_id = null;
user_id = null;
}
// Send a more generic message back to the caller
if ( placement_id == null || user_id == null ) {
doError(request, response, theMap, "outcomes.sourcedid", "sourcedid", null);
return;
}
M_log.debug("signature="+signature);
M_log.debug("user_id="+user_id);
M_log.debug("placement_id="+placement_id);
Properties pitch = SakaiBLTIUtil.getPropertiesFromPlacement(placement_id, ltiService);
if ( pitch == null ) {
M_log.debug("Error retrieving result_sourcedid information");
doError(request, response, theMap, "outcomes.sourcedid", "sourcedid", null);
return;
}
String siteId = pitch.getProperty(LTIService.LTI_SITE_ID);
Site site = null;
try {
site = SiteService.getSite(siteId);
} catch (Exception e) {
M_log.debug("Error retrieving result_sourcedid site: "+e.getLocalizedMessage(), e);
}
// Send a more generic message back to the caller
if ( site == null ) {
doError(request, response, theMap, "outcomes.sourcedid", "sourcedid", null);
return;
}
// Check the message signature using OAuth
String oauth_secret = pitch.getProperty(LTIService.LTI_SECRET);
M_log.debug("oauth_secret: "+oauth_secret);
oauth_secret = SakaiBLTIUtil.decryptSecret(oauth_secret);
M_log.debug("oauth_secret (decrypted): "+oauth_secret);
String URL = SakaiBLTIUtil.getOurServletPath(request);
OAuthMessage oam = OAuthServlet.getMessage(request, URL);
OAuthValidator oav = new SimpleOAuthValidator();
OAuthConsumer cons = new OAuthConsumer("about:blank#OAuth+CallBack+NotUsed", oauth_consumer_key,oauth_secret, null);
OAuthAccessor acc = new OAuthAccessor(cons);
String base_string = null;
try {
base_string = OAuthSignatureMethod.getBaseString(oam);
} catch (Exception e) {
M_log.error(e.getLocalizedMessage(), e);
base_string = null;
}
try {
oav.validateMessage(oam, acc);
} catch (Exception e) {
M_log.warn("Provider failed to validate message");
M_log.warn(e.getLocalizedMessage(), e);
if (base_string != null) {
M_log.warn(base_string);
}
doError(request, response, theMap, "outcome.no.validate", oauth_consumer_key, null);
return;
}
// Check the signature of the sourcedid to make sure it was not altered
String placement_secret = pitch.getProperty(LTIService.LTI_PLACEMENTSECRET);
// Send a generic message back to the caller
if ( placement_secret == null ) {
doError(request, response, theMap, "outcomes.sourcedid", "sourcedid", null);
return;
}
String pre_hash = placement_secret + ":::" + user_id + ":::" + placement_id;
String received_signature = ShaUtil.sha256Hash(pre_hash);
M_log.debug("Received signature="+signature+" received="+received_signature);
boolean matched = signature.equals(received_signature);
String old_placement_secret = pitch.getProperty(LTIService.LTI_OLDPLACEMENTSECRET);
if ( old_placement_secret != null && ! matched ) {
pre_hash = placement_secret + ":::" + user_id + ":::" + placement_id;
received_signature = ShaUtil.sha256Hash(pre_hash);
M_log.debug("Received signature II="+signature+" received="+received_signature);
matched = signature.equals(received_signature);
}
// Send a message back to the caller
if ( ! matched ) {
doError(request, response, theMap, "outcomes.sourcedid", "sourcedid", null);
return;
}
// Perform the message-specific handling
if ( "basicoutcome".equals(message_type) ) processOutcome(request, response, lti_message_type, site, siteId, placement_id, pitch, user_id, theMap);
if ( "toolsetting".equals(message_type) ) processSetting(request, response, lti_message_type, site, siteId, placement_id, pitch, user_id, theMap);
if ( "roster".equals(message_type) ) processRoster(request, response, lti_message_type, site, siteId, placement_id, pitch, user_id, theMap);
}
protected void processSetting(HttpServletRequest request, HttpServletResponse response,
String lti_message_type,
Site site, String siteId, String placement_id, Properties pitch,
String user_id, Map<String, Object> theMap)
throws java.io.IOException
{
String setting = null;
// Check for permission in placement
String allowSetting = pitch.getProperty(LTIService.LTI_ALLOWSETTINGS);
if ( ! "on".equals(allowSetting) ) {
doError(request, response, theMap, "outcomes.invalid", "lti_message_type="+lti_message_type, null);
return;
}
SakaiBLTIUtil.pushAdvisor();
boolean success = false;
try {
if ( "basic-lti-loadsetting".equals(lti_message_type) ) {
setting = pitch.getProperty(LTIService.LTI_SETTINGS);
if ( setting != null ) {
theMap.put("/message_response/setting/value", setting);
}
success = true;
} else {
if ( SakaiBLTIUtil.isPlacement(placement_id) ) {
ToolConfiguration placement = SiteService.findTool(placement_id);
if ( "basic-lti-savesetting".equals(lti_message_type) ) {
setting = request.getParameter("setting");
if ( setting == null ) {
M_log.warn("No setting parameter");
doError(request, response, theMap, "setting.empty", "", null);
} else {
if ( setting.length() > 8096) setting = setting.substring(0,8096);
placement.getPlacementConfig().setProperty("toolsetting", setting);
}
} else if ( "basic-lti-deletesetting".equals(lti_message_type) ) {
placement.getPlacementConfig().remove("toolsetting");
}
try {
placement.save();
success = true;
} catch(Exception e) {
doError(request, response, theMap, "setting.save.fail", "", e);
}
} else {
Map<String,Object> content = null;
String contentStr = pitch.getProperty("contentKey");
Long contentKey = foorm.getLongKey(contentStr);
if ( contentKey > 0 ) content = ltiService.getContentDao(contentKey, siteId);
if ( content != null ) {
if ( "basic-lti-savesetting".equals(lti_message_type) ) {
setting = request.getParameter("setting");
if ( setting == null ) {
M_log.warn("No setting parameter");
doError(request, response, theMap, "setting.empty", "", null);
} else {
if ( setting.length() > 8096) setting = setting.substring(0,8096);
content.put(LTIService.LTI_SETTINGS,setting);
success = true;
}
} else if ( "basic-lti-deletesetting".equals(lti_message_type) ) {
content.put(LTIService.LTI_SETTINGS,null);
success = true;
}
if ( success ) {
Object result = ltiService.updateContentDao(contentKey,content, siteId);
if ( result instanceof String ) {
M_log.warn("Setting update failed: "+result);
doError(request, response, theMap, "setting.fail", "", null);
success = false;
}
}
}
}
}
} catch (Exception e) {
doError(request, response, theMap, "setting.fail", "", e);
} finally {
SakaiBLTIUtil.popAdvisor();
}
if ( ! success ) return;
theMap.put("/message_response/statusinfo/codemajor", "Success");
theMap.put("/message_response/statusinfo/severity", "Status");
theMap.put("/message_response/statusinfo/codeminor", "fullsuccess");
String theXml = XMLMap.getXML(theMap, true);
PrintWriter out = response.getWriter();
out.println(theXml);
}
protected void processOutcome(HttpServletRequest request, HttpServletResponse response,
String lti_message_type,
Site site, String siteId, String placement_id, Properties pitch,
String user_id, Map<String, Object> theMap)
throws java.io.IOException
{
// Make sure the user exists in the site
boolean userExistsInSite = false;
try {
Member member = site.getMember(user_id);
if(member != null ) userExistsInSite = true;
} catch (Exception e) {
M_log.warn(e.getLocalizedMessage() + " siteId="+siteId, e);
doError(request, response, theMap, "outcome.site.membership", "", e);
return;
}
// Make sure the placement is configured to receive grades
String assignment = pitch.getProperty("assignment");
M_log.debug("ASSN="+assignment);
if ( assignment == null ) {
doError(request, response, theMap, "outcome.no.assignment", "", null);
return;
}
// Look up the assignment so we can find the max points
GradebookService g = (GradebookService) ComponentManager
.get("org.sakaiproject.service.gradebook.GradebookService");
SakaiBLTIUtil.pushAdvisor();
Assignment assignmentObject = getOrMakeAssignment(assignment, siteId, g);
SakaiBLTIUtil.popAdvisor();
if ( assignmentObject == null ) {
doError(request, response, theMap, "outcome.no.assignment", "", null);
return;
}
// Things look good - time to process the grade
boolean isRead = BasicLTIUtil.equals(lti_message_type, "basic-lis-readresult");
String result_resultscore_textstring = request.getParameter("result_resultscore_textstring");
if(BasicLTIUtil.isBlank(result_resultscore_textstring) && ! isRead ) {
doError(request, response, theMap, "outcomes.missing", "result_resultscore_textstring", null);
return;
}
// We don't need to retrieve the assignments and check if it
// is a valid column because if the column is wrong, we
// will get an exception below
// Lets store or retrieve the grade using the securityadvisor
Session sess = SessionManager.getCurrentSession();
String theGrade = null;
SakaiBLTIUtil.pushAdvisor();
boolean success = false;
try {
// Indicate "who" is setting 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);
Double dGrade;
if ( isRead ) {
theGrade = g.getAssignmentScoreString(siteId, assignment, user_id);
dGrade = new Double(theGrade);
dGrade = dGrade / assignmentObject.getPoints();
theMap.put("/message_response/result/resultscore/textstring", dGrade.toString());
} else {
dGrade = new Double(result_resultscore_textstring);
dGrade = dGrade * assignmentObject.getPoints();
g.setAssignmentScore(siteId, assignment, user_id, dGrade, "External Outcome");
M_log.info("Stored Score=" + siteId + " assignment="+ assignment + " user_id=" + user_id +
" score="+ result_resultscore_textstring);
}
success = true;
theMap.put("/message_response/statusinfo/codemajor", "Success");
theMap.put("/message_response/statusinfo/severity", "Status");
theMap.put("/message_response/statusinfo/codeminor", "fullsuccess");
} catch (Exception e) {
doError(request, response, theMap, "outcome.grade.fail", "siteId="+siteId, e);
} finally {
sess.invalidate(); // Make sure to leave no traces
SakaiBLTIUtil.popAdvisor();
}
if ( ! success ) return;
String theXml = XMLMap.getXML(theMap, true);
PrintWriter out = response.getWriter();
out.println(theXml);
}
protected void processRoster(HttpServletRequest request, HttpServletResponse response,
String lti_message_type,
Site site, String siteId, String placement_id, Properties pitch,
String user_id, Map<String, Object> theMap)
throws java.io.IOException
{
// Check for permission in placement
String allowRoster = pitch.getProperty(LTIService.LTI_ALLOWROSTER);
if ( ! "on".equals(allowRoster) ) {
doError(request, response, theMap, "outcomes.invalid", "lti_message_type="+lti_message_type, null);
return;
}
String releaseName = pitch.getProperty(LTIService.LTI_SENDNAME);
String releaseEmail = pitch.getProperty(LTIService.LTI_SENDEMAILADDR);
String assignment = pitch.getProperty("assignment");
String allowOutcomes = ServerConfigurationService.getString(
SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED, SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED_DEFAULT);
if ( ! "true".equals(allowOutcomes) ) allowOutcomes = null;
String maintainRole = site.getMaintainRole();
SakaiBLTIUtil.pushAdvisor();
boolean success = false;
try {
List<Map<String,String>> lm = new ArrayList<Map<String,String>>();
Set<Member> members = site.getMembers();
for (Member member : members ) {
Map<String,String> mm = new TreeMap<String,String>();
Role role = member.getRole();
String ims_user_id = member.getUserId();
mm.put("/user_id",ims_user_id);
String ims_role = "Learner";
if ( maintainRole != null && maintainRole.equals(role.getId())) ims_role = "Instructor";
mm.put("/role",ims_role);
User user = null;
if ( "true".equals(allowOutcomes) && assignment != null ) {
user = UserDirectoryService.getUser(ims_user_id);
String placement_secret = pitch.getProperty(LTIService.LTI_PLACEMENTSECRET);
String result_sourcedid = SakaiBLTIUtil.getSourceDID(user, placement_id, placement_secret);
if ( result_sourcedid != null ) mm.put("/lis_result_sourcedid",result_sourcedid);
}
if ( "on".equals(releaseName) || "on".equals(releaseEmail) ) {
if ( user == null ) user = UserDirectoryService.getUser(ims_user_id);
if ( "on".equals(releaseName) ) {
mm.put("/person_name_given",user.getFirstName());
mm.put("/person_name_family",user.getLastName());
mm.put("/person_name_full",user.getDisplayName());
}
if ( "on".equals(releaseEmail) ) {
mm.put("/person_contact_email_primary",user.getEmail());
mm.put("/person_sourcedid",user.getEid());
}
}
lm.add(mm);
}
theMap.put("/message_response/members/member", lm);
success = true;
} catch (Exception e) {
doError(request, response, theMap, "memberships.fail", "", e);
} finally {
SakaiBLTIUtil.popAdvisor();
}
if ( ! success ) return;
theMap.put("/message_response/statusinfo/codemajor", "Success");
theMap.put("/message_response/statusinfo/severity", "Status");
theMap.put("/message_response/statusinfo/codeminor", "fullsuccess");
String theXml = XMLMap.getXML(theMap, true);
PrintWriter out = response.getWriter();
out.println(theXml);
}
/* IMS POX XML versions of this service */
public void doErrorXML(HttpServletRequest request,HttpServletResponse response,
IMSPOXRequest pox, String s, String message, Exception e)
throws java.io.IOException
{
if (e != null) {
M_log.error(e.getLocalizedMessage(), e);
}
String msg = rb.getString(s) + ": " + message;
M_log.info(msg);
response.setContentType("application/xml");
PrintWriter out = response.getWriter();
String output = null;
if ( pox == null ) {
output = IMSPOXRequest.getFatalResponse(msg);
} else {
String body = null;
String operation = pox.getOperation();
if ( operation != null ) {
body = "<"+operation.replace("Request", "Response")+"/>";
}
output = pox.getResponseFailure(msg, null, body);
}
out.println(output);
M_log.debug(output);
System.out.println("doErrorXML="+output);
}
@SuppressWarnings("unchecked")
protected void doPostJSON(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String ipAddress = request.getRemoteAddr();
M_log.warn("LTI JSON Services not implemented IP=" + ipAddress);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
@SuppressWarnings("unchecked")
protected void doPostXml(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String ipAddress = request.getRemoteAddr();
M_log.debug("LTI POX Service request from IP=" + ipAddress);
String allowOutcomes = ServerConfigurationService.getString(
SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED, SakaiBLTIUtil.BASICLTI_OUTCOMES_ENABLED_DEFAULT);
if ( ! "true".equals(allowOutcomes) ) allowOutcomes = null;
String allowLori = ServerConfigurationService.getString(
SakaiBLTIUtil.BASICLTI_LORI_ENABLED, SakaiBLTIUtil.BASICLTI_LORI_ENABLED_DEFAULT);
if ( ! "true".equals(allowLori) ) allowLori = null;
if (allowOutcomes == null && allowLori == null ) {
M_log.warn("LTI Services are disabled IP=" + ipAddress);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
IMSPOXRequest pox = new IMSPOXRequest(request);
if ( ! pox.valid ) {
doErrorXML(request, response, pox, "pox.invalid", pox.errorMessage, null);
return;
}
//check lti_message_type
String lti_message_type = pox.getOperation();
String sourcedid = null;
String message_type = null;
if ( M_log.isDebugEnabled() ) M_log.debug("POST\n"+XMLMap.prettyPrint(pox.postBody));
Map<String,String> bodyMap = pox.getBodyMap();
if ( ( "replaceResultRequest".equals(lti_message_type) || "readResultRequest".equals(lti_message_type) ||
"deleteResultRequest".equals(lti_message_type) ) && allowOutcomes != null ) {
sourcedid = bodyMap.get("/resultRecord/sourcedGUID/sourcedId");
message_type = "basicoutcome";
} else if ( "getCourseStructureRequest".equals(lti_message_type) ) {
sourcedid = bodyMap.get("/params/sourcedGUID/sourcedId");
message_type = "getstructure";
} else if ( "addCourseResourcesRequest".equals(lti_message_type) ) {
sourcedid = bodyMap.get("/params/sourcedGUID/sourcedId");
message_type = "addstructure";
} else {
String output = pox.getResponseUnsupported("Not supported "+lti_message_type);
response.setContentType("application/xml");
PrintWriter out = response.getWriter();
out.println(output);
return;
}
// No point continuing without a sourcedid
if(BasicLTIUtil.isBlank(sourcedid)) {
doErrorXML(request, response, pox, "outcomes.missing", "sourcedid", null);
return;
}
// Truncate this to the maximum length to insure no cruft at the end
if ( sourcedid.length() > 2048) sourcedid = sourcedid.substring(0,2048);
// Attempt to parse the sourcedid, any failure is fatal
String placement_id = null;
String signature = null;
String user_id = null;
try {
int pos = sourcedid.indexOf(":::");
if ( pos > 0 ) {
signature = sourcedid.substring(0, pos);
String dec2 = sourcedid.substring(pos+3);
pos = dec2.indexOf(":::");
user_id = dec2.substring(0,pos);
placement_id = dec2.substring(pos+3);
}
} catch (Exception e) {
// Log some detail for ourselves
M_log.warn("Unable to decrypt result_sourcedid IP=" + ipAddress + " Error=" + e.getMessage(),e);
signature = null;
placement_id = null;
user_id = null;
}
// Send a more generic message back to the caller
if ( placement_id == null || user_id == null ) {
doErrorXML(request, response, pox, "outcomes.sourcedid", "missing user_id or placement_id", null);
return;
}
M_log.debug("signature="+signature);
M_log.debug("user_id="+user_id);
M_log.debug("placement_id="+placement_id);
Properties pitch = SakaiBLTIUtil.getPropertiesFromPlacement(placement_id, ltiService);
if ( pitch == null ) {
M_log.debug("Error retrieving result_sourcedid information");
doErrorXML(request, response, pox, "outcomes.sourcedid", "sourcedid", null);
return;
}
String siteId = pitch.getProperty(LTIService.LTI_SITE_ID);
Site site = null;
try {
site = SiteService.getSite(siteId);
} catch (Exception e) {
M_log.debug("Error retrieving result_sourcedid site: "+e.getLocalizedMessage(), e);
}
// Send a more generic message back to the caller
if ( site == null ) {
doErrorXML(request, response, pox, "outcomes.sourcedid", "sourcedid", null);
return;
}
// Check the message signature using OAuth
String oauth_consumer_key = pox.getOAuthConsumerKey();
String oauth_secret = pitch.getProperty(LTIService.LTI_SECRET);
M_log.debug("oauth_secret: "+oauth_secret);
oauth_secret = SakaiBLTIUtil.decryptSecret(oauth_secret);
M_log.debug("oauth_secret (decrypted): "+oauth_secret);
String URL = SakaiBLTIUtil.getOurServletPath(request);
pox.validateRequest(oauth_consumer_key, oauth_secret, request, URL);
if ( ! pox.valid ) {
if (pox.base_string != null) {
M_log.warn(pox.base_string);
}
doErrorXML(request, response, pox, "outcome.no.validate", oauth_consumer_key, null);
return;
}
// Check the signature of the sourcedid to make sure it was not altered
String placement_secret = pitch.getProperty(LTIService.LTI_PLACEMENTSECRET);
// Send a generic message back to the caller
if ( placement_secret ==null ) {
M_log.debug("placement_secret is null");
doErrorXML(request, response, pox, "outcomes.sourcedid", "sourcedid", null);
return;
}
String pre_hash = placement_secret + ":::" + user_id + ":::" + placement_id;
String received_signature = ShaUtil.sha256Hash(pre_hash);
M_log.debug("Received signature="+signature+" received="+received_signature);
boolean matched = signature.equals(received_signature);
String old_placement_secret = pitch.getProperty(LTIService.LTI_OLDPLACEMENTSECRET);
if ( old_placement_secret != null && ! matched ) {
pre_hash = placement_secret + ":::" + user_id + ":::" + placement_id;
received_signature = ShaUtil.sha256Hash(pre_hash);
M_log.debug("Received signature II="+signature+" received="+received_signature);
matched = signature.equals(received_signature);
}
// Send a message back to the caller
if ( ! matched ) {
doErrorXML(request, response, pox, "outcomes.sourcedid", "sourcedid", null);
return;
}
String placementLori = pitch.getProperty("allowlori");
if ( allowOutcomes != null && "basicoutcome".equals(message_type) ) {
processOutcomeXml(request, response, lti_message_type, site, siteId, pitch, user_id, pox);
} else if ( allowLori != null && "on".equals(placementLori) && "getstructure".equals(message_type) ) {
processCourseStructureXml(request, response, lti_message_type, siteId, pox);
} else if ( allowLori != null && "on".equals(placementLori) && "addstructure".equals(message_type) ) {
processAddResourceXML(request, response, lti_message_type, siteId, pox);
} else {
response.setContentType("application/xml");
PrintWriter writer = response.getWriter();
String desc = "Message received and validated operation="+pox.getOperation();
String output = pox.getResponseUnsupported(desc);
writer.println(output);
}
}
protected void processCourseStructureXml(HttpServletRequest request, HttpServletResponse response,
String lti_message_type, String siteId, IMSPOXRequest pox)
throws java.io.IOException
{
// userId is irrelevant as this is server to server
Map<String,String> bodyMap = pox.getBodyMap();
String context_id = bodyMap.get("/params/courseId");
if ( context_id == null || ! context_id.equals(siteId) ) {
doErrorXML(request, response, pox, "outcomes.sourcedid", "sourcedid", null);
M_log.warn("mis-match courseId="+context_id+" siteId="+siteId);
return;
}
// First make sure that we have Lessons in the site
SitePage lessonsPage = null;
ToolConfiguration lessonsConfig = null;
try {
Site site = SiteService.getSite(siteId);
for (SitePage page : (List<SitePage>)site.getPages()) {
for(ToolConfiguration tool : (List<ToolConfiguration>) page.getTools()) {
String tid = tool.getToolId();
if ( "sakai.lessonbuildertool".equals(tid) ) {
lessonsPage = page;
lessonsConfig = tool;
break;
}
}
}
} catch (IdUnusedException ex) {
doErrorXML(request, response, pox, "outcomes.notools", "sourcedid", null);
M_log.warn("Could not scan site for Lessons tool.");
return;
}
if ( lessonsConfig == null ) {
M_log.warn("Could not find sakai.lessonbulder in site="+siteId);
doErrorXML(request, response, pox, "outcomes.nolessons", "sourcedid", null);
return;
}
// Now lets find the structure within Lessons
List<Long> structureList = new ArrayList<Long>();
List<SimplePageItem> sitePages = LessonsFacade.findItemsInSite(context_id);
List<Map<String,Object>> structureMap = iteratePagesXML(sitePages,structureList,0);
if ( structureMap.size() < 1 ) {
Map<String,Object> cMap = new TreeMap<String,Object>();
cMap.put("/folderId","0");
cMap.put("/title",lessonsPage.getTitle());
cMap.put("/description",lessonsPage.getTitle());
cMap.put("/type","folder");
structureMap.add(cMap);
}
Map<String,Object> theMap = new TreeMap<String,Object>();
theMap.put("/getCourseStructureResponse/resources/resource",structureMap);
String theXml = XMLMap.getXMLFragment(theMap, true);
String output = pox.getResponseSuccess("processCourseStructureXml", theXml);
PrintWriter out = response.getWriter();
out.println(output);
M_log.debug(output);
return;
}
protected void processAddResourceXML(HttpServletRequest request, HttpServletResponse response,
String lti_message_type, String siteId, IMSPOXRequest pox)
throws java.io.IOException
{
// userId is irrelevant because this is server to server
Map<String,String> bodyMap = pox.getBodyMap();
String context_id = bodyMap.get("/params/courseId");
if ( context_id == null || ! context_id.equals(siteId) ) {
doErrorXML(request, response, pox, "outcomes.sourcedid", "sourcedid", null);
M_log.warn("mis-match courseId="+context_id+" siteId="+siteId);
return;
}
String folder_id = bodyMap.get("/params/folderId");
Long folderId = null;
try { folderId = new Long(folder_id); }
catch (Exception e) { folderId = null; }
List<Long> structureList = new ArrayList<Long>();
List<SimplePageItem> sitePages = LessonsFacade.findItemsInSite(context_id);
SimplePageItem thePage = LessonsFacade.findFolder(sitePages, folderId, structureList, 1);
// Something wrong, add on the first page
if ( thePage == null ) {
M_log.debug("Inserting at top...");
for (SimplePageItem i : sitePages) {
if (i.getType() != SimplePageItem.PAGE) continue;
// System.out.println("item="+i.getName()+"id="+i.getId()+" sakaiId="+i.getSakaiId());
thePage = i;
break;
}
}
// No pages in Lessons yet...
// If we can find the Lessons tool, lets add its first page.
if ( thePage == null ) {
M_log.debug("Creating top page...");
SitePage lessonsPage = null;
ToolConfiguration lessonsConfig = null;
try {
Site site = SiteService.getSite(siteId);
for (SitePage page : (List<SitePage>)site.getPages()) {
for(ToolConfiguration tool : (List<ToolConfiguration>) page.getTools()) {
String tid = tool.getToolId();
if ( "sakai.lessonbuildertool".equals(tid) ) {
lessonsPage = page;
lessonsConfig = tool;
break;
}
}
}
} catch (IdUnusedException ex) {
M_log.warn("Could not load site.");
}
if ( lessonsConfig == null ) {
M_log.warn("Could not find sakai.lessonbulder in site="+siteId);
} else {
String title = lessonsPage.getTitle();
String toolId = lessonsConfig.getPageId();
thePage = LessonsFacade.addFirstPage(siteId, toolId, title);
}
}
if ( thePage == null ) {
doErrorXML(request, response, pox, "lessons.page.notfound",
"Unable to find page in structure at "+folderId, null);
return;
}
Element bodyElement = pox.bodyElement;
// System.out.println(XMLMap.nodeToString(bodyElement));
// System.out.println(XMLMap.nodeToString(bodyElement, true));
NodeList nl = null;
try {
Object result = LESSONS_RESOURCES_EXPR.evaluate(bodyElement, XPathConstants.NODESET);
nl = (NodeList) result;
// System.out.println("result = "+result+" count="+nl.getLength());
} catch(Exception e) {
e.printStackTrace();
nl = null;
}
if ( nl == null || nl.getLength() < 1 ) {
doErrorXML(request, response, pox, "lessons.page.noresources",
"No resources to add", null);
return;
}
Long pageNum = Long.valueOf(thePage.getSakaiId());
List<SimplePageItem> items = LessonsFacade.findItemsOnPage(pageNum);
int seq = items.size() + 1;
List<Map<String,String>> resultList = new ArrayList<Map<String,String>>();
recursivelyAddResourcesXML(context_id, thePage, nl, seq, resultList);
// One success means overall status is a success
boolean success = false;
for ( Map<String,String> result : resultList ) {
if ( "success".equals(result.get("/status")) ) success = true;
}
Map<String,Object> theMap = new TreeMap<String,Object>();
theMap.put("/addCourseResourcesResponse/resources/resource",resultList);
String theXml = XMLMap.getXMLFragment(theMap, true);
response.setContentType("application/xml");
String output = null;
if ( success ) {
output = pox.getResponseSuccess("Items Added",theXml);
} else {
output = pox.getResponseFailure("Items were not added", null);
}
PrintWriter out = response.getWriter();
out.println(output);
M_log.debug(output);
}
protected void recursivelyAddResourcesXML(String siteId, SimplePageItem thePage, NodeList nl,
int startPos, List<Map<String,String>> resultList)
{
for(int i=0, cnt=nl.getLength(); i<cnt; i++)
{
Node node = nl.item(i);
if ( node.getNodeType() != Node.ELEMENT_NODE ) continue;
M_log.debug("Node="+node.getNodeName());
if ( ! "resource".equals(node.getNodeName()) ) {
continue;
}
String typeStr = null;
try {
typeStr = (String) LESSONS_TYPE_EXPR.evaluate(node);
} catch (Exception e) {
typeStr = null;
}
String titleStr = null;
try {
titleStr = (String) LESSONS_TITLE_EXPR.evaluate(node);
} catch (Exception e) {
titleStr = null;
}
String tempId = null;
try {
tempId = (String) LESSONS_TEMPID_EXPR.evaluate(node);
} catch (Exception e) {
tempId = null;
}
if ( "folder".equals(typeStr) ) {
SimplePageItem subPageItem = LessonsFacade.addLessonsFolder(thePage, titleStr, startPos);
if ( tempId != null ) {
Map<String,String> result = new TreeMap<String,String> ();
result.put("/tempId",tempId);
result.put("/id", subPageItem.getSakaiId());
resultList.add(result);
}
startPos++;
NodeList childNodes = null;
try {
Object result = LESSONS_FOLDER_EXPR.evaluate(node, XPathConstants.NODESET);
childNodes = (NodeList) result;
M_log.debug("children of the folder = "+result+" count="+childNodes.getLength());
} catch(Exception e) {
e.printStackTrace();
nl = null;
}
M_log.debug("===== DOWN THE RABIT HOLE ==========");
recursivelyAddResourcesXML(siteId, subPageItem, childNodes, 1, resultList);
continue;
}
if ( ! "lti".equals(typeStr) ) {
M_log.warn("No support for type:"+typeStr);
continue;
}
String launchUrl = null;
try {
launchUrl = (String) LESSONS_URL_EXPR.evaluate(node);
} catch (Exception e) {
launchUrl = null;
}
String launchParams = null;
try {
launchParams = (String) LESSONS_CUSTOM_EXPR.evaluate(node);
} catch (Exception e) {
launchParams = null;
}
if ( titleStr == null || launchUrl == null || launchParams == null ) {
M_log.warn("Missing required value type, name, url, launch, parms");
continue;
}
M_log.debug("type="+typeStr+" name="+titleStr+" launchUrl="+launchUrl+" lanchParams="+launchParams);
Map<String,String> result = new TreeMap<String,String> ();
result.put("/tempId",tempId);
// Time to add the launch tool
String sakaiId = null;
try {
sakaiId = LessonsFacade.doImportTool(siteId, launchUrl, titleStr, null, launchParams);
if ( sakaiId == null ) {
result.put("/status", "failure");
result.put("/description","doImportTool failed");
M_log.warn("Unable to add LTI Placement "+titleStr);
} else {
result.put("/status", "success");
result.put("/description","doImportTool success");
result.put("/id", sakaiId);
}
} catch (Exception e) {
sakaiId = null;
e.printStackTrace();
result.put("/status", "failure");
result.put("/description", e.getMessage());
}
resultList.add(result);
if ( sakaiId == null ) continue;
LessonsFacade.addLessonsLaunch(thePage, sakaiId, titleStr, startPos);
}
}
protected List<Map<String,Object>> iteratePagesXML(List<SimplePageItem> sitePages,
List<Long> structureList, int depth)
{
List<Map<String,Object>> structureMap = new ArrayList<Map<String,Object>>();
if ( depth > 10 ) return null;
for (SimplePageItem i : sitePages) {
if ( structureList.size() > 50 ) return structureMap;
// System.out.println("d="+depth+" o="+structureList.size()+" Page ="+i.getSakaiId()+" title="+i.getName());
if (i.getType() != SimplePageItem.PAGE) continue;
Long pageNum = Long.valueOf(i.getSakaiId());
String title = i.getName();
if ( structureList.size() == 50 ) title = " ... ";
structureList.add(i.getId());
Map<String,Object> cMap = new TreeMap<String,Object>();
cMap.put("/folderId",i.getSakaiId());
cMap.put("/title",title);
cMap.put("/description",title);
cMap.put("/type","folder");
List<SimplePageItem> items = LessonsFacade.findItemsOnPage(pageNum);
// System.out.println("Items="+items);
List<Map<String,Object>> subMap = iteratePagesXML(items, structureList, depth+1);
if (subMap != null && subMap.size() > 0 ) {
cMap.put("/resources/resource",subMap);
}
structureMap.add(cMap);
}
return structureMap;
}
protected void processOutcomeXml(HttpServletRequest request, HttpServletResponse response,
String lti_message_type,
Site site, String siteId, Properties pitch,
String user_id, IMSPOXRequest pox)
throws java.io.IOException
{
// Make sure the user exists in the site
boolean userExistsInSite = false;
try {
Member member = site.getMember(user_id);
if(member != null ) userExistsInSite = true;
} catch (Exception e) {
M_log.warn(e.getLocalizedMessage() + " siteId="+siteId, e);
doErrorXML(request, response, pox, "outcome.site.membership", "", e);
return;
}
// Make sure the placement is configured to receive grades
String assignment = pitch.getProperty("assignment");
M_log.debug("ASSN="+assignment);
if ( assignment == null ) {
doErrorXML(request, response, pox, "outcome.no.assignment", "", null);
return;
}
// Look up the assignment so we can find the max points
GradebookService g = (GradebookService) ComponentManager
.get("org.sakaiproject.service.gradebook.GradebookService");
SakaiBLTIUtil.pushAdvisor();
Assignment assignmentObject = getOrMakeAssignment(assignment, siteId, g);
SakaiBLTIUtil.popAdvisor();
if ( assignmentObject == null ) {
doErrorXML(request, response, pox, "outcome.no.assignment", "", null);
return;
}
// Things look good - time to process the grade
boolean isRead = BasicLTIUtil.equals(lti_message_type, "readResultRequest");
boolean isDelete = BasicLTIUtil.equals(lti_message_type, "deleteResultRequest");
Map<String,String> bodyMap = pox.getBodyMap();
String result_resultscore_textstring = bodyMap.get("/resultRecord/result/resultScore/textString");
String sourced_id = bodyMap.get("/resultRecord/result/sourcedId");
// System.out.println("grade="+result_resultscore_textstring);
if(BasicLTIUtil.isBlank(result_resultscore_textstring) && ! isRead && ! isDelete ) {
doErrorXML(request, response, pox, "outcomes.missing", "result_resultscore_textstring", null);
return;
}
// Lets return an XML Response
Map<String,Object> theMap = new TreeMap<String,Object>();
// We don't need to retrieve the assignments and check if it
// is a valid column because if the column is wrong, we
// will get an exception below
// Lets store or retrieve the grade using the securityadvisor
Session sess = SessionManager.getCurrentSession();
String theGrade = null;
SakaiBLTIUtil.pushAdvisor();
boolean success = false;
String message = null;
try {
// Indicate "who" is setting 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);
Double dGrade;
if ( isRead ) {
theGrade = g.getAssignmentScoreString(siteId, assignment, user_id);
String sGrade = "";
if ( theGrade != null && theGrade.length() > 0 ) {
dGrade = new Double(theGrade);
dGrade = dGrade / assignmentObject.getPoints();
sGrade = dGrade.toString();
}
theMap.put("/readResultResponse/result/sourcedId", sourced_id);
theMap.put("/readResultResponse/result/resultScore/textString", sGrade);
theMap.put("/readResultResponse/result/resultScore/language", "en");
message = "Result read";
} else if ( isDelete ) {
// It would be nice to empty it out but we can't
g.setAssignmentScore(siteId, assignment, user_id, null, "External Outcome");
M_log.info("Delete Score site=" + siteId + " assignment="+ assignment + " user_id=" + user_id);
theMap.put("/deleteResultResponse", "");
message = "Result deleted";
} else {
dGrade = new Double(result_resultscore_textstring);
if ( dGrade < 0.0 || dGrade > 1.0 ) {
throw new Exception("Grade out of range");
}
dGrade = dGrade * assignmentObject.getPoints();
g.setAssignmentScore(siteId, assignment, user_id, dGrade, "External Outcome");
M_log.info("Stored Score=" + siteId + " assignment="+ assignment + " user_id=" + user_id + " score="+ result_resultscore_textstring);
theMap.put("/replaceResultResponse", "");
message = "Result replaced";
}
success = true;
} catch (Exception e) {
doErrorXML(request, response, pox, "outcome.grade.fail", e.getMessage()+" siteId="+siteId, e);
} finally {
sess.invalidate(); // Make sure to leave no traces
SakaiBLTIUtil.popAdvisor();
}
if ( !success ) return;
String output = null;
String theXml = "";
if ( theMap.size() > 0 ) theXml = XMLMap.getXMLFragment(theMap, true);
output = pox.getResponseSuccess(message, theXml);
response.setContentType("application/xml");
PrintWriter out = response.getWriter();
out.println(output);
}
public Assignment getOrMakeAssignment(String assignment, String siteId, GradebookService g )
{
Assignment assignmentObject = null;
try {
List gradebookAssignments = g.getAssignments(siteId);
for (Iterator i=gradebookAssignments.iterator(); i.hasNext();) {
Assignment gAssignment = (Assignment) i.next();
if ( gAssignment.isExternallyMaintained() ) continue;
if ( assignment.equals(gAssignment.getName()) ) {
assignmentObject = gAssignment;
break;
}
}
} catch (Exception e) {
assignmentObject = null; // Just to make double sure
}
// Attempt to add assignment to grade book
if ( assignmentObject == null && g.isGradebookDefined(siteId) ) {
try {
assignmentObject = new Assignment();
assignmentObject.setPoints(Double.valueOf(100));
assignmentObject.setExternallyMaintained(false);
assignmentObject.setName(assignment);
assignmentObject.setReleased(true);
assignmentObject.setUngraded(false);
g.addAssignment(siteId, assignmentObject);
M_log.info("Added assignment: "+assignment);
}
catch (ConflictingAssignmentNameException e) {
M_log.warn("ConflictingAssignmentNameException while adding assignment" + e.getMessage());
assignmentObject = null; // Just to make double sure
}
catch (Exception e) {
M_log.warn("GradebookNotFoundException (may be because GradeBook has not yet been added to the Site) " + e.getMessage());
M_log.warn(this + ":addGradeItem " + e.getMessage());
}
}
return assignmentObject;
}
public void destroy() {
}
}