/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.modules.scorm;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.olat.core.logging.OLATRuntimeException;
import org.olat.core.logging.Tracing;
import org.olat.core.util.FileUtils;
import org.olat.core.util.StringHelper;
import org.olat.modules.scorm.manager.ScormManager;
import org.olat.modules.scorm.server.beans.LMSDataFormBean;
import org.olat.modules.scorm.server.beans.LMSDataHandler;
import org.olat.modules.scorm.server.beans.LMSResultsBean;
/**
* OLATApiAdapter implements the ApiAdapter Interface from the pfplms code which was initially
* designed for applet use. For the 'Backend' it uses portions of the code developed for the reload
* scorm player.
* see: http://www.scorm.com/scorm-explained/technical-scorm/run-time/run-time-reference/ for an nice overview of the datamodel
*
* @author guido
*/
public class OLATApiAdapter implements ch.ethz.pfplms.scorm.api.ApiAdapterInterface {
private ch.ethz.pfplms.scorm.api.ApiAdapter core;
//private ScormTrackingManager scormTracking;
private Hashtable olatScoCmi = new Hashtable();
private String olatStudentId;
private String olatStudentName;
//was used as reference id like out repo id
//the sco id
private String olatSahsId;
private boolean isLaunched = false;
private boolean isLaunching = false;
private LMSDataHandler odatahandler;
private ScormManager scormManager;
private SettingsHandlerImpl scormSettingsHandler;
private final ScormAPICallback apiCallback;
//
private Properties scoresProp; // keys: sahsId; values = raw score of an sco
private final String SCORE_IDENT = "cmi.core.score.raw";
private File scorePropsFile;
/**
* creates a new API adapter
*/
OLATApiAdapter (ScormAPICallback apiCallback) {
this.apiCallback = apiCallback;
core = new ch.ethz.pfplms.scorm.api.ApiAdapter ();
}
/**
* @param cpRoot
* @param repoId
* @param courseId
* @param userPath
* @param studentId - the olat username
* @param studentName - the students name
* @param isVerbose prints out what is going on inside the scorm RTE
*/
public final void init (File cpRoot, String repoId, String courseId, String storagePath, String studentId, String studentName, String lesson_mode, String credit_mode, int controllerHashCode) {
this.olatStudentId = studentId;
this.olatStudentName = studentName;
say ("cmi.core.student_id=" +olatStudentId);
say ("cmi.core.student_name=" +olatStudentName);
scormSettingsHandler = new SettingsHandlerImpl(cpRoot.getAbsolutePath(), repoId, courseId, storagePath, studentName, studentId, lesson_mode, credit_mode, controllerHashCode);
// get a path for the scores per sco
String savePath = scormSettingsHandler.getFilePath();
scorePropsFile = new File(savePath + "/_olat_score.properties");
scoresProp = new Properties();
if (scorePropsFile.exists()) {
InputStream is = null;
try {
is = new BufferedInputStream(new FileInputStream(scorePropsFile));
scoresProp.load(is);
} catch (IOException e) {
throw new OLATRuntimeException(this.getClass(), "could not load existing scorm-score-properties file: "+scorePropsFile.getAbsolutePath(),e);
}
finally {
if (is != null) FileUtils.closeSafely(is);
}
}
scormManager = new ScormManager(cpRoot.getAbsolutePath(), true, true, true, scormSettingsHandler);
}
private final void say (String s) {
if(Tracing.isDebugEnabled(OLATApiAdapter.class)){
Tracing.logDebug("core: "+s, OLATApiAdapter.class);
}
}
/**
* @param sahs_id
*/
public final void olatLaunchSahs (String sahs_id) {
/*
if (System.currentTimeMillis() < clickTime + 1000) {
say ("Click ignored.");
return;
}
*/
if (isLaunching) {
say ("SCO " +olatSahsId +" is launching.");
return;
}
if (isLaunched && sahs_id.equals(olatSahsId)) {
say ("SAHS " +sahs_id +" is already running.");
return;
}
olatScoCmi.clear();
say ("Launching sahs " +sahs_id);
if (isLaunched) {
say ("SAHS "+olatSahsId +" will be unloaded.");
// IliasLaunchContent (
// "../sahs_presentation.php?cmd=unloadSahs"
// +"&sahs_id=" + olatSahsId
// +"&ref_id=" + olatRefId
// );
} else {
//by launching the sahs the hashtable gets cleard (olatScoCmi)
//and the php script gererates a page with all the data for this sahs and loads it
//by calling API.IliasSetValue by javascript
isLaunching = true;
//clickTime = System.currentTimeMillis();
olatSahsId = sahs_id;
// IliasLaunchContent (
// "../sahs_presentation.php?cmd=launchSahs"
// +"&ref_id=" + olatRefId
// +"&sahs_id=" + olatSahsId
// );
//putting all cmi from the olat storage to the local storage
LMSDataFormBean lmsDataBean = new LMSDataFormBean();
lmsDataBean.setItemID(sahs_id);
lmsDataBean.setLmsAction("get");
odatahandler = new LMSDataHandler(scormManager, lmsDataBean, scormSettingsHandler);
LMSResultsBean lmsBean = odatahandler.getResultsBean();
olatScoCmi.clear();
String[][] strArr = lmsBean.getCmiStrings();
String key = "";
String value = "";
boolean logDebug = Tracing.isDebugEnabled(OLATApiAdapter.class);
if(strArr != null){
for(int i=0;i<strArr.length;i++){
key = strArr[i][0];
value = strArr[i][1];
olatScoCmi.put(key, value);
if(logDebug){
Tracing.logDebug("passing cmi data to api adapter: "+key +": "+ value, OLATApiAdapter.class);
}
}
}
}
}
public final void olatSetValue (String l, String r) {
if (r == null) r = ""; // MSIE bug
say ("OlatSetValue("+l+"="+r+")");
if (l != null) olatScoCmi.put (l, r);
}
public final void olatAbortSahs (String sahs_id) {
if (!olatSahsId.equals (sahs_id)) return;
isLaunching = false;
if (!isLaunched) return;
say ("Warning: sahs " +sahs_id +" did not call LMSFinish()");
olatFinish (false);
core.reset();
}
private final void olatInitialize () {
isLaunching = false;
core.sysPut ("cmi.core.student_id", olatStudentId);
core.sysPut ("cmi.core.student_name", olatStudentName);
core.sysPut (olatScoCmi);
core.transBegin();
isLaunched = true;
}
private final void olatFinish (boolean commit) {
if (!isLaunched) return;
isLaunched = false;
if (commit) olatCommit(false); // Stupid "implicit commit"
}
public final void olatLaunchAsset (String id) {
if (isLaunching) return;
say ("Launching asset: " +id);
if (isLaunched) {
say ("SAHS "+olatSahsId +" will be unloaded.");
// IliasLaunchContent (
// "../sahs_presentation.php?cmd=unloadSahs"
// +"&sahs_id=" + olatSahsId
// );
} else {
//clickTime = System.currentTimeMillis();
olatSahsId = null;
// IliasLaunchContent (
// "../sahs_presentation.php?cmd=launchAsset"
// +"&ref_id=" + olatRefId
// +"&asset_id=" + id
// );
}
}
/**
*
* @param isACommit true, if the call comes from a lmscommit, false if it comes from a lmsfinish
* @return
*/
private final String olatCommit (boolean isACommit) {
if (olatSahsId == null) return "false";
core.transEnd();
//StringBuilder P = new StringBuilder();
Hashtable ins = core.getTransNew ();
Hashtable mod = core.getTransMod ();
core.transBegin();
LMSDataFormBean lmsDataBean = new LMSDataFormBean();
lmsDataBean.setItemID(olatSahsId);
//TODO:gs pass the dataBean for use, and do not get it a second time
lmsDataBean.setNextAction("5");
lmsDataBean.setLmsAction("update");
Map cmiData = new HashMap();
//TODO:gs:c make it possible only to update the changed cmi data.
if (ins.size() > 0){
Set set = ins.keySet();
for(Iterator it = set.iterator();it.hasNext();){
String cmi = (String)it.next();
olatScoCmi.remove(cmi);
olatScoCmi.put(cmi,ins.get(cmi));
}
}
if(mod.size() > 0){
Set set = mod.keySet();
for(Iterator it = set.iterator();it.hasNext();){
String cmi = (String)it.next();
olatScoCmi.remove(cmi);
olatScoCmi.put(cmi,mod.get(cmi));
}
}
cmiData.putAll(olatScoCmi);
//work around for missing cmi's (needed by reload code, but not used in ilias code)
if(cmiData.get("cmi.interactions._count") != null && cmiData.get("cmi.interactions._count") != "0"){
int count = Integer.parseInt((String)cmiData.get("cmi.interactions._count"));
for(int i=0;i<count;i++){
//OLAT-4271: check first if cmi.interactions.n.objectives._count exist before putting a default one
String objectivesCount = (String)cmiData.get("cmi.interactions."+ i +".objectives._count");
if(!StringHelper.containsNonWhitespace(objectivesCount)) {
cmiData.put("cmi.interactions."+ i +".objectives._count","0");
}
}
}
if (isACommit) {
String rawScore = (String) cmiData.get(SCORE_IDENT);
if (rawScore != null && !rawScore.equals("")) {
// we have a score set in this sco.
// persist
// to prevent problems with bad xmlhttprequest timings
synchronized(this) { //o_clusterOK by:fj: instance is spawned by the ScormAPIandDisplayController
scoresProp.put(olatSahsId, rawScore);
OutputStream os = null;
try {
os = new BufferedOutputStream(new FileOutputStream(scorePropsFile));
scoresProp.store(os, null);
} catch (IOException e) {
throw new OLATRuntimeException(this.getClass(), "could not save scorm-properties-file: "+scorePropsFile.getAbsolutePath(), e);
}
finally {
FileUtils.closeSafely(os);
}
// notify
if (apiCallback != null) {
apiCallback.lmsCommit(olatSahsId, scoresProp);
}
}
}
}
lmsDataBean.setDataAsMap(cmiData);
odatahandler = new LMSDataHandler(scormManager, lmsDataBean, scormSettingsHandler);
odatahandler.updateCMIData(olatSahsId);
return "true";
}
/**
* @return a String that points to the last accessed sco itemId
*/
public String getScormLastAccessedItemId(){
LMSDataFormBean lmsDataBean = new LMSDataFormBean();
lmsDataBean.setLmsAction("boot");
odatahandler = new LMSDataHandler(scormManager, lmsDataBean, scormSettingsHandler);
LMSResultsBean lmsBean = odatahandler.getResultsBean();
return lmsBean.getItemID();
}
/**
* @param itemId
* @return true if the item is completed
*/
public boolean isItemCompleted(String itemId){
//TODO:gs make method faster by caching lmsBean, but when to set out of date?
LMSDataFormBean lmsDataBean = new LMSDataFormBean();
lmsDataBean.setItemID(itemId);
lmsDataBean.setLmsAction("get");
odatahandler = new LMSDataHandler(scormManager, lmsDataBean, scormSettingsHandler);
LMSResultsBean lmsBean = odatahandler.getResultsBean();
return lmsBean.getIsItemCompleted().equals("true");
}
/**
* @param itemId
* @return true if item has any not fullfilled preconditions
*/
public boolean hasItemPrerequisites(String itemId) {
//TODO:gs make method faster by caching lmsBean, but when to set out of date?
LMSDataFormBean lmsDataBean = new LMSDataFormBean();
lmsDataBean.setItemID(itemId);
lmsDataBean.setLmsAction("get");
odatahandler = new LMSDataHandler(scormManager, lmsDataBean, scormSettingsHandler);
LMSResultsBean lmsBean = odatahandler.getResultsBean();
return lmsBean.getHasPrerequisites().equals("true");
}
/**
* @return Map containing the recent sco items status
*/
public Map getScoItemsStatus(){
LMSDataFormBean lmsDataBean = new LMSDataFormBean();
lmsDataBean.setLmsAction("boot");
odatahandler = new LMSDataHandler(scormManager, lmsDataBean, scormSettingsHandler);
LMSResultsBean lmsBean = odatahandler.getResultsBean();
String[][] preReqTbl = lmsBean.getPreReqTable();
Map itemsStatus = new HashMap();
//put table into map
for(int i=0; i < preReqTbl.length; i++){
if(preReqTbl[i][1].equals("not attempted")) preReqTbl[i][1] ="not_attempted";
itemsStatus.put(preReqTbl[i][0], preReqTbl[i][1]);
}
return itemsStatus;
}
/**
* @param recentId
* @return the previos Sco itemId
*/
public Integer getPreviousSco(String recentId) {
// TODO:gs make method faster by caching lmsBean, but when to set out of date?
LMSDataFormBean lmsDataBean = new LMSDataFormBean();
lmsDataBean.setItemID(recentId);
lmsDataBean.setLmsAction("get");
odatahandler = new LMSDataHandler(scormManager, lmsDataBean, scormSettingsHandler);
LMSResultsBean lmsBean = odatahandler.getResultsBean();
String[][] pretable = lmsBean.getPreReqTable();
String previousNavScoId = "-1";
for(int i=0; i < pretable.length; i++){
if(pretable[i][0].equals(recentId) && (i != 0 )){
previousNavScoId = pretable[--i][0];
break;
}
}
return new Integer(previousNavScoId);
}
/**
* @param recentId
* @return the next Sco itemId
*/
public Integer getNextSco(String recentId) {
// TODO:gs make method faster by chaching lmsBean, but when to set out of date?
LMSDataFormBean lmsDataBean = new LMSDataFormBean();
lmsDataBean.setItemID(recentId);
lmsDataBean.setLmsAction("get");
odatahandler = new LMSDataHandler(scormManager, lmsDataBean, scormSettingsHandler);
LMSResultsBean lmsBean = odatahandler.getResultsBean();
String[][] pretable = lmsBean.getPreReqTable();
String nextNavScoId = "-1";
for(int i=0; i < pretable.length; i++){
if(pretable[i][0].equals(recentId) && (i != pretable.length-1)){
nextNavScoId = pretable[++i][0];
break;
}
}
return new Integer(nextNavScoId);
}
/****************************************************************************************
* The API functions that an Scorm SCO can call
*
* @see ch.ethz.pfplms.scorm.api.ApiAdapterInterface#LMSInitialize(java.lang.String)
*/
public final String LMSInitialize (String s) {
String rv = core.LMSInitialize(s);
say(" ----------------- ");
say ("LMSInitialize("+s+")="+rv);
if (rv.equals("false")) return rv;
core.reset();
rv = core.LMSInitialize(s);
olatInitialize ();
return rv;
}
/**
* @see ch.ethz.pfplms.scorm.api.ApiAdapterInterface#LMSCommit(java.lang.String)
*/
public final String LMSCommit (String s) {
String rv = core.LMSCommit(s);
if (rv.equals("false")) return rv;
rv = olatCommit(true);
say ("LMSCommit("+s+")="+rv);
return rv;
}
/**
* @see ch.ethz.pfplms.scorm.api.ApiAdapterInterface#LMSFinish(java.lang.String)
*/
public final String LMSFinish (String s) {
String rv = core.LMSFinish(s);
say ("LMSFinish("+s+")="+rv);
say(" ----------------- ");
if (rv.equals("false")) return rv;
olatFinish(true);
core.reset();
return rv;
}
/**
* @see ch.ethz.pfplms.scorm.api.ApiAdapterInterface#LMSGetDiagnostic(java.lang.String)
*/
public final String LMSGetDiagnostic (String e) {
String rv = core.LMSGetDiagnostic (e);
//say ("LMSGetDiagnostic("+e+")="+rv);
return rv;
}
/**
* @see ch.ethz.pfplms.scorm.api.ApiAdapterInterface#LMSGetErrorString(java.lang.String)
*/
public final String LMSGetErrorString (String e) {
String rv = core.LMSGetErrorString (e);
say ("LMSGetErrorString("+e+")="+rv);
return rv;
}
/**
* @see ch.ethz.pfplms.scorm.api.ApiAdapterInterface#LMSGetLastError()
*/
public final String LMSGetLastError () {
String rv = core.LMSGetLastError ();
//say ("LMSLastError()="+rv);
return rv;
}
/**
* @see ch.ethz.pfplms.scorm.api.ApiAdapterInterface#LMSGetValue(java.lang.String)
*/
public final String LMSGetValue (String l) {
String rv = core.LMSGetValue (l);
say ("LMSGetValue("+l+")="+rv);
return rv;
}
/**
* @see ch.ethz.pfplms.scorm.api.ApiAdapterInterface#LMSSetValue(java.lang.String, java.lang.String)
*/
public final String LMSSetValue (String l, String r) {
// BEGIN SABA PUBLISHER conformity hack:
String lowerL = l.toLowerCase();
if (lowerL.endsWith(".text")) {
// undefined SCROM attribute
return "true";
} else if (lowerL.endsWith(".pattern") || lowerL.endsWith(".student_response")) {
// checkbox values have to be in lower case and comma separated!
String [] tmp = r.toLowerCase().trim().split("[\\s,]"); // lower all, trim and split on all non-whitespace
StringBuilder sb = new StringBuilder(); // characters and commas
for (int i = 0; i < tmp.length; ++i) { // for all values
if (tmp[i] != null && tmp[i].length() > 0) // ... which are not empty
sb.append(",").append(tmp[i]); // concat it incl. leading comma
}
r = sb.toString().substring(1); // erase leading comma
}
// END SABA PUBLISHER conformity hack
String rv = core.LMSSetValue (l, r);
say ("LMSSetValue("+l+"="+r+")="+rv);
return rv;
}
}