diff --git a/src/main/java/org/olat/course/nodes/scorm/ScormRunController.java b/src/main/java/org/olat/course/nodes/scorm/ScormRunController.java index 25854553a31dbec32005740c0187707cf0ea005e..a1843642f5cfd57984055a24bc627f6e038df627 100644 --- a/src/main/java/org/olat/course/nodes/scorm/ScormRunController.java +++ b/src/main/java/org/olat/course/nodes/scorm/ScormRunController.java @@ -369,8 +369,8 @@ public class ScormRunController extends BasicController implements ScormAPICallb @Override public void lmsFinish(String olatSahsId, Properties scoreProp, Properties lessonStatusProp) { + doStartPage(null); if (config.getBooleanSafe(ScormEditController.CONFIG_CLOSE_ON_FINISH, false)) { - doStartPage(null); scormDispC.close(); } } diff --git a/src/main/java/org/olat/modules/scorm/OLATApiAdapter.java b/src/main/java/org/olat/modules/scorm/OLATApiAdapter.java index 33a027186d4e7362727e98217c00b79af6684c52..d5c752529e857f83d0070dbbae01e303c90e1ebf 100644 --- a/src/main/java/org/olat/modules/scorm/OLATApiAdapter.java +++ b/src/main/java/org/olat/modules/scorm/OLATApiAdapter.java @@ -88,8 +88,8 @@ public class OLATApiAdapter implements ch.ethz.pfplms.scorm.api.ApiAdapterInterf private Properties scoresProp; // keys: sahsId; values = raw score of an sco private Properties lessonStatusProp; - private final String SCORE_IDENT = "cmi.core.score.raw"; - private final String LESSON_STATUS_IDENT = "cmi.core.lesson_status"; + private static final String SCORE_IDENT = "cmi.core.score.raw"; + private static final String LESSON_STATUS_IDENT = "cmi.core.lesson_status"; private File scorePropsFile; private File lessonStatusPropsFile; @@ -162,7 +162,7 @@ public class OLATApiAdapter implements ch.ethz.pfplms.scorm.api.ApiAdapterInterf private final void say (String s) { - log.debug("core: "+s); + log.debug("core: {}", s); } diff --git a/src/main/java/org/olat/modules/scorm/ScormAPIMapper.java b/src/main/java/org/olat/modules/scorm/ScormAPIMapper.java index 3ef2ae794d6111734dececeae1cdf6572fe9ab48..8834528cce60edf4f3597dc77aca45304824dfd7 100644 --- a/src/main/java/org/olat/modules/scorm/ScormAPIMapper.java +++ b/src/main/java/org/olat/modules/scorm/ScormAPIMapper.java @@ -36,12 +36,16 @@ import java.util.Properties; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.Logger; +import org.json.JSONArray; +import org.json.JSONObject; import org.olat.basesecurity.BaseSecurity; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.modules.bc.FolderConfig; import org.olat.core.dispatcher.mapper.Mapper; import org.olat.core.gui.media.MediaResource; +import org.olat.core.gui.media.ServletUtil; import org.olat.core.gui.media.StringMediaResource; import org.olat.core.id.Identity; import org.olat.core.id.IdentityEnvironment; @@ -322,43 +326,90 @@ public class ScormAPIMapper implements Mapper, ScormAPICallback, Serializable { String apiCallParamTwo = request.getParameter("apiCallParamTwo"); if(log.isDebugEnabled()) { - log.debug("scorm api request by user:"+ identity.getName() +": " + apiCall + "('" + apiCallParamOne + "' , '" + apiCallParamTwo + "')"); + log.debug("scorm api request by user: {}: {} ('{}' , '{}')", identity.getName(), apiCall, apiCallParamOne, apiCallParamTwo); } - StringMediaResource smr = new StringMediaResource(); - smr.setContentType("text/html"); - smr.setEncoding("utf-8"); - if (apiCall != null && apiCall.equals("initcall")) { //used for Mozilla / firefox only to get more time for fireing the onunload stuff triggered by overwriting the content. - smr.setData("<html><body></body></html>"); - return smr; + log.info("Init call"); + return createInitResource(request); } - String returnValue = ""; if (apiCall != null) { - if (apiCall.equals(LMS_INITIALIZE)) { - returnValue = scormAdapter.LMSInitialize(apiCallParamOne); - } else if (apiCall.equals(LMS_GETVALUE)) { - returnValue = scormAdapter.LMSGetValue(apiCallParamOne); - } else if (apiCall.equals(LMS_SETVALUE)) { - returnValue = scormAdapter.LMSSetValue(apiCallParamOne, apiCallParamTwo); - } else if (apiCall.equals(LMS_COMMIT)) { - returnValue = scormAdapter.LMSCommit(apiCallParamOne); - } else if (apiCall.equals(LMS_FINISH)) { - returnValue = scormAdapter.LMSFinish(apiCallParamOne); - } else if (apiCall.equals(LMS_GETLASTERROR)) { - returnValue = scormAdapter.LMSGetLastError(); - } else if (apiCall.equals(LMS_GETDIAGNOSTIC)) { - returnValue = scormAdapter.LMSGetDiagnostic(apiCallParamOne); - } else if (apiCall.equals(LMS_GETERRORSTRING)) { - returnValue = scormAdapter.LMSGetErrorString(apiCallParamOne); + String returnValue = apiCall(apiCall, apiCallParamOne, apiCallParamTwo); + return createResource(returnValue, request); + } else if(relPath.contains("batch")) { + try { + String batch = IOUtils.toString(request.getReader()); + JSONArray batchArray = new JSONArray(batch); + for(int i=0; i<batchArray.length(); i++) { + JSONObject obj = batchArray.getJSONObject(i); + apiCall = obj.getString("apiCall"); + apiCallParamOne = obj.getString("param1"); + apiCallParamTwo = obj.getString("param2"); + apiCall(apiCall, apiCallParamOne, apiCallParamTwo); + } + } catch (IOException e) { + log.error("", e); } - smr.setData("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body><p>" - + returnValue + "</p></body></html>"); - return smr; } - smr.setData(""); + return createResource("", request); + } + + private String apiCall(String apiCall, String apiCallParamOne, String apiCallParamTwo) { + String returnValue = ""; + if (apiCall.equals(LMS_INITIALIZE)) { + returnValue = scormAdapter.LMSInitialize(apiCallParamOne); + } else if (apiCall.equals(LMS_GETVALUE)) { + returnValue = scormAdapter.LMSGetValue(apiCallParamOne); + } else if (apiCall.equals(LMS_SETVALUE)) { + returnValue = scormAdapter.LMSSetValue(apiCallParamOne, apiCallParamTwo); + } else if (apiCall.equals(LMS_COMMIT)) { + returnValue = scormAdapter.LMSCommit(apiCallParamOne); + } else if (apiCall.equals(LMS_FINISH)) { + returnValue = scormAdapter.LMSFinish(apiCallParamOne); + } else if (apiCall.equals(LMS_GETLASTERROR)) { + returnValue = scormAdapter.LMSGetLastError(); + } else if (apiCall.equals(LMS_GETDIAGNOSTIC)) { + returnValue = scormAdapter.LMSGetDiagnostic(apiCallParamOne); + } else if (apiCall.equals(LMS_GETERRORSTRING)) { + returnValue = scormAdapter.LMSGetErrorString(apiCallParamOne); + } + return returnValue; + } + + private MediaResource createInitResource(HttpServletRequest request) { + MediaResource resource; + boolean acceptJson = ServletUtil.acceptJson(request); + if(acceptJson && request == null) { + resource = createHTMLResource(""); + } else { + resource = createHTMLResource("<html><body></body></html>"); + } + return resource; + } + + private MediaResource createResource(String returnValue, HttpServletRequest request) { + MediaResource resource; + boolean acceptJson = ServletUtil.acceptJson(request); + if(acceptJson && request == null) { + resource = createHTMLResource(""); + + } else if(StringHelper.containsNonWhitespace(returnValue)) { + String data = "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"></head><body><p>" + returnValue + "</p></body></html>"; + resource = createHTMLResource(data); + } else { + resource = createHTMLResource(""); + } + return resource; + } + + + private StringMediaResource createHTMLResource(String data) { + StringMediaResource smr = new StringMediaResource(); + smr.setContentType("text/html"); + smr.setEncoding("utf-8"); + smr.setData(data); return smr; } } \ No newline at end of file diff --git a/src/main/java/org/olat/modules/scorm/ScormAPIandDisplayController.java b/src/main/java/org/olat/modules/scorm/ScormAPIandDisplayController.java index 8a5b183d4099aa79810c3268713f8ec09621e592..1c0b47ebfec3efd5d6e42a6e33df345fa1f7598c 100644 --- a/src/main/java/org/olat/modules/scorm/ScormAPIandDisplayController.java +++ b/src/main/java/org/olat/modules/scorm/ScormAPIandDisplayController.java @@ -29,6 +29,7 @@ import java.io.File; import java.io.IOException; import java.util.Iterator; import java.util.Map; +import java.util.Properties; import org.olat.core.commons.fullWebApp.LayoutMain3ColsBackController; import org.olat.core.commons.fullWebApp.LayoutMain3ColsController; @@ -73,7 +74,7 @@ import org.springframework.beans.factory.annotation.Autowired; * the sco api calls to the scorm RTE backend. It provides also an navigation to * navigate in the tree with "pre" "next" buttons. */ -public class ScormAPIandDisplayController extends MainLayoutBasicController implements ConfigurationChangedListener { +public class ScormAPIandDisplayController extends MainLayoutBasicController implements ConfigurationChangedListener, ScormAPICallback { protected static final String LMS_INITIALIZE = "LMSInitialize"; protected static final String LMS_GETVALUE = "LMSGetValue"; @@ -136,6 +137,7 @@ public class ScormAPIandDisplayController extends MainLayoutBasicController impl try { scormAdapter = new OLATApiAdapter(); scormAdapter.addAPIListener(apiCallback); + scormAdapter.addAPIListener(this); String fullname = UserManager.getInstance().getUserDisplayName(getIdentity()); String scormResourceIdStr = scormResourceId == null ? null : scormResourceId.toString(); scormAdapter.init(cpRoot, scormResourceIdStr, courseIdNodeId, FolderConfig.getCanonicalRoot(), username, fullname, lesson_mode, credit_mode, hashCode()); @@ -322,6 +324,16 @@ public class ScormAPIandDisplayController extends MainLayoutBasicController impl } } + @Override + public void lmsCommit(String olatSahsId, Properties scoScores, Properties scoLessonStatus) { + // + } + + @Override + public void lmsFinish(String olatSahsId, Properties scoProps, Properties scoLessonStatus) { + myContent.setDirty(false); + } + @Override protected void event(UserRequest ureq, Controller source, Event event) { if(source == columnLayoutCtr) { diff --git a/src/main/java/org/olat/modules/scorm/_content/display.html b/src/main/java/org/olat/modules/scorm/_content/display.html index 813ffd77fe6deb379cdfc4b6f6c31ce3eac84c81..aac190e729de829a9bae99176b60c4a3a9103ccb 100644 --- a/src/main/java/org/olat/modules/scorm/_content/display.html +++ b/src/main/java/org/olat/modules/scorm/_content/display.html @@ -10,7 +10,11 @@ ## method to send a ping to the framework to draw itself after finish function pingAfterFinish() { window.suppressOlatOnUnloadOnce = true; - $r.javaScriptCommand('ping'); + try { + $r.javaScriptCommand('ping'); + } catch(e) { + if(window.console) console.log(e); + } } </script> <script> diff --git a/src/main/webapp/static/js/openolat/scormApiAdapter.js b/src/main/webapp/static/js/openolat/scormApiAdapter.js index 3ae1ef35622a9bafc306758e3d7628eeebd5a947..cd528a20daf3eeea56c28e5f682b490d22e5fa45 100644 --- a/src/main/webapp/static/js/openolat/scormApiAdapter.js +++ b/src/main/webapp/static/js/openolat/scormApiAdapter.js @@ -16,137 +16,43 @@ var API = window; * right now only used for moz, as ie has some problems with state handler * room for improvement by makeing it into one function that works for both */ -function scormApiRequest(remoteUrl) -{ - this.remoteOLATurl = remoteUrl; - this.isMozilla = false; - this.httpReq=false; - - this.reqCount = 0; - - this.sendSyncSingle = asSendSyncSingle; - if (window.XMLHttpRequest) - { - this.httpReq = new XMLHttpRequest(); - this.isMozilla = true; - if (debug) dump("func:scormApiRequest :is Mozilla\n"); - } - // code for IE - else if (window.ActiveXObject) - { - try { - this.httpReq = new ActiveXObject("Msxml2.XMLHTTP"); - if (debug) dump("func:scormApiRequest :is new IE \n"); - } catch (e) { - try { - this.httpReq = new ActiveXObject("Microsoft.XMLHTTP"); - if (debug) dump("func:scormApiRequest :is old IE \n"); - } catch (E) { - this.httpReq = false; - } - } - } -} - -function asSendSyncSingle(apiCall, param1, param2) { - // Sync - wait until data arrives - //IE does not like this function, what's it for? - //httpReq.multipart = false; - - // Mozilla/Firefox bug 246518 workaround - // a new XMLHttpRequest object if needed - try { - this.httpReq.onload = showReq; - this.httpReq.onreadystatechange = showReq; - this.httpReq.open('POST', this.remoteOLATurl, false ); - } catch (e) { - this.httpReq = new XMLHttpRequest(); - this.httpReq.onload = showReq; - this.httpReq.onreadystatechange = showReq; - this.httpReq.open('POST', this.remoteOLATurl, false ); - } - - this.httpReq.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8'); - this.httpReq.send('apiCall='+ apiCall + '&apiCallParamOne='+ param1 + '&apiCallParamTwo='+ encodeURIComponent(param2)); - if(this.isMozilla) - { - if (debug) dump("func:asSencSingle: post successfull, calling showReq\n"); - - //add event listener geht auch! ev. besser? - //httpReq.addEventListener("load", showReq, false) - //this.httpReq.onload = showReq; - //this.httpReq.onreadystatechange = showReq; - } else { - if (debug) dump("func:asSencSingle: post successfull by this.httpReq, calling showReq\n"); - //this.httpReq.onload = showReq; - //this.httpReq.onreadystatechange = showReq; - } - //httpReq.send(null); -} - - -function showReq( event ) { - if (debug) dump("func:showReq: inside func.: event type: " +event.type+"\n"); - // ready state 4 means document loaded - //if ( event.target.readyState == 4 ) - if ( this.readyState == 4 ) - { - bMsg = document.getElementById( 'apiReturnHandler' ); - //bMsg.innerHTML = event.target.responseText; - bMsg.innerHTML = this.responseText; - bMsg = null; - if (debug) dump("func:showReq: "+this.responseText+"\n"); - } else { if (debug) dump( 'an error occured! Wrong readyState: ' + event.target.readyState +"\n"); } +function scormApiRequest(remoteUrl) { + this.remoteOLATurl = remoteUrl; + this.reqCount = 0; } /*************************************/ -// global flag -var isIE = false; - -// global request and XML document objects -var scormRTEresponse; // retrieve XML document (reusable generic function); -function loadHTMLDoc(url,apiCall, param1, param2) { - var req; - // inner callback method to handle onreadystatechange event of req object - var processReqChange = function (event){ +function loadHTMLDoc(url, async, apiCall, param1, param2) { + var scormResponse; + var req = new XMLHttpRequest(); + req.onreadystatechange = function (event) { // only if req shows "loaded" if (req.readyState == 4) { // only if "OK" if (req.status == 200) { - rteResponseText = req.responseText; - scormRTEresponse = rteResponseText.substring(rteResponseText.indexOf("<p>")+3,rteResponseText.indexOf("</p>")); - if (debug) dump(scormRTEresponse); - } else { if (debug) dump("There was a problem retrieving the XMLHttpRequest data:\n"+ req.statusText+"\n"); } + scormResponse = loadHTMLDocExtractResponse(req.responseText); + if (debug) dump(scormResponse); + } else if (debug) { + dump("There was a problem retrieving the XMLHttpRequest data:\n"+ req.statusText+"\n"); + } } }; - // branch for native XMLHttpRequest object - if (window.XMLHttpRequest) { - req = new XMLHttpRequest(); - req.onreadystatechange = processReqChange; - //req.open("GET", url+'?apiCall='+ apiCall + '&apiCallParamOne='+ param1 + '&apiCallParamTwo='+ param2, false); - req.open("POST", url, false) - req.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8'); - req.send('apiCall='+ apiCall + '&apiCallParamOne='+ param1 + '&apiCallParamTwo='+ encodeURIComponent(param2)); - if (debug) dump('apiCall='+ apiCall + '&apiCallParamOne='+ param1 + '&apiCallParamTwo='+ encodeURIComponent(param2)); - // branch for IE/Windows ActiveX version - } else if (window.ActiveXObject) { - isIE = true; - req = new ActiveXObject("Microsoft.XMLHTTP"); - if (req) { - req.onreadystatechange = processReqChange; - //req.open("GET", url+'?apiCall='+ apiCall + '&apiCallParamOne='+ param1 + '&apiCallParamTwo='+ param2 + '&rnd='+increment(), false); - //req.send(); - req.open("POST", url, false); - req.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8'); - req.send('apiCall='+ apiCall + '&apiCallParamOne='+ param1 + '&apiCallParamTwo='+ encodeURIComponent(param2)); - } - } - // Help GC - req = null; + req.open("POST", url, async); + req.setRequestHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8'); + req.send('apiCall='+ apiCall + '&apiCallParamOne='+ param1 + '&apiCallParamTwo='+ encodeURIComponent(param2)); + if (debug) dump('apiCall='+ apiCall + '&apiCallParamOne='+ param1 + '&apiCallParamTwo='+ encodeURIComponent(param2)); + return scormResponse; +} + +function loadHTMLDocExtractResponse(responseText) { + if("<html><body></body></html>" == responseText) { + return "";// initial call + } + return responseText.substring(responseText.indexOf("<p>") + 3, responseText.indexOf("</p>")); } /***************************************************************** @@ -164,20 +70,15 @@ function increment(){ * over a synchronous XmlHttpRequest. * Code uses different ways for moz and ie. *******************************************************************/ -function passApiCall(apiCall, param1, param2){ - if(window.ActiveXObject || (navigator.userAgent.indexOf("Safari") != -1)){ - loadHTMLDoc(olatCommandUri,apiCall,param1,param2); - return scormRTEresponse; - }else if(window.XMLHttpRequest && (navigator.userAgent.indexOf("Mozilla") != -1)){ - jsHttpRequest.sendSyncSingle(apiCall,param1, param2); - var responseHTML = document.getElementById( 'apiReturnHandler' ).innerHTML; - return responseHTML.substring(responseHTML.indexOf("<p>")+3,responseHTML.indexOf("</p>")); - }else{ - if (debug) dump("Browser does not support the needed XmlHttpRequest\n"); + +function passApiCall(apiCall, param1, param2) { + try { + return loadHTMLDoc(olatCommandUri, false, apiCall, param1, param2); + } catch(e) { + if(window.console) console.log(e); } } - /** * triggers the onunload stuff often used in scorm content to finish an sco by * opening the iframe document for writing (IE) or replacing the content doc (Mozilla). @@ -185,34 +86,27 @@ function passApiCall(apiCall, param1, param2){ function olatonunload(){ if (debug) dump("func:olatonunload: is called\n"); if(window.frameId && document.getElementById(frameId) && this.frames[frameId]){ - if(window.ActiveXObject){ - //on IE by opening the document, the onunload event gets triggered - var iframeDoc = this.frames[frameId].document; - iframeDoc.open(); - iframeDoc.write("<html><body></body></html>"); - iframeDoc.close(); - iframeDoc = null; - } else { - // Mozilla and others - var iframeDoc = document.getElementById(frameId).contentDocument; - iframeDoc.location.replace("about:blank"); - iframeDoc = null; - //delay(200); - //if((new Date().getTime() - lastRequest) > 60000) alert("Current SCO will be finished before initializing next SCO or terminating! Press OK to proceed."); - - var delayReq = new XMLHttpRequest(); - //"false" waits until the result arrived; - delayReq.open('GET', olatCommandUri, false ); - delayReq.send(null); - } + // Mozilla and others + var iframeDoc = document.getElementById(frameId).contentDocument; + iframeDoc.location.replace("about:blank"); + iframeDoc = null; + + var delayReq = new XMLHttpRequest(); + //"false" waits until the result arrived; + delayReq.open('GET', olatCommandUri, false ); + delayReq.send(null); } return true; } -function delay(gap){ - var then,now; then=new Date().getTime(); - now=then; - while((now-then)<gap) - {now=new Date().getTime();} + +var openolatScormUnloadQueue = new Array(); + +function queueCall(apiCall, param1, param2) { + var entry = new Object(); + entry.apiCall = apiCall; + entry.param1 = param1; + entry.param2 = param2; + openolatScormUnloadQueue.push(entry); } /****************************************************************** @@ -224,7 +118,14 @@ function LMSInitialize (s) { return passApiCall('LMSInitialize',s,''); } function LMSFinish (s) { - var finishedResult = passApiCall('LMSFinish',s,''); + var finishedResult = passApiCall('LMSFinish', s, ''); + if(typeof val === "undefined") { + // Communication problem, try to send all queued data as beacon + queueCall('LMSFinish', s, ''); + var data = JSON.stringify(openolatScormUnloadQueue); + openolatScormUnloadQueue = new Array(); + navigator.sendBeacon(olatCommandUri + "/batch/data/", data); + } // Immediately close module, ping OpenOlat main window to take over control setTimeout(function(){ try { @@ -236,20 +137,38 @@ function LMSFinish (s) { return finishedResult; } function LMSSetValue (l, r) { - return passApiCall('LMSSetValue',l,r); + var val = passApiCall('LMSSetValue',l,r); + if(typeof val === "undefined") { + queueCall('LMSSetValue', l, r); + val = r; + } + return val; } function LMSGetValue (s) { return passApiCall('LMSGetValue',s,''); } function LMSGetLastError () { - return passApiCall('LMSGetLastError','',''); + var val = passApiCall('LMSGetLastError','',''); + if(typeof val === "undefined") { + val = "0"; + } + return val; } function LMSGetErrorString (s) { - return passApiCall('LMSGetErrorString',s,''); + var val = passApiCall('LMSGetErrorString',s,''); + if(typeof val === "undefined") { + val = "No Error"; + } + return val; } function LMSGetDiagnostic (s) { return passApiCall('LMSGetDiagnostic',s,''); } function LMSCommit (s) { - return passApiCall('LMSCommit',s,''); + var val = passApiCall('LMSCommit',s,''); + if(typeof val === "undefined") { + queueCall('LMSCommit', s, ''); + val = r; + } + return val; } \ No newline at end of file