/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <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 the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <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>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.modules.scorm;
import static org.olat.modules.scorm.ScormAPIandDisplayController.LMS_COMMIT;
import static org.olat.modules.scorm.ScormAPIandDisplayController.LMS_FINISH;
import static org.olat.modules.scorm.ScormAPIandDisplayController.LMS_GETDIAGNOSTIC;
import static org.olat.modules.scorm.ScormAPIandDisplayController.LMS_GETERRORSTRING;
import static org.olat.modules.scorm.ScormAPIandDisplayController.LMS_GETLASTERROR;
import static org.olat.modules.scorm.ScormAPIandDisplayController.LMS_GETVALUE;
import static org.olat.modules.scorm.ScormAPIandDisplayController.LMS_INITIALIZE;
import static org.olat.modules.scorm.ScormAPIandDisplayController.LMS_SETVALUE;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.Iterator;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
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.StringMediaResource;
import org.olat.core.id.Identity;
import org.olat.core.id.IdentityEnvironment;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.course.CourseFactory;
import org.olat.course.ICourse;
import org.olat.course.nodes.ScormCourseNode;
import org.olat.course.nodes.scorm.ScormEditController;
import org.olat.course.run.scoring.ScoreEvaluation;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.course.run.userview.UserCourseEnvironmentImpl;
import org.olat.modules.ModuleConfiguration;
import org.olat.user.UserManager;
/**
* The mapper for scorm, serializable
*
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*/
public class ScormAPIMapper implements Mapper, ScormAPICallback, Serializable {
private static final long serialVersionUID = -144400398761676983L;
private static final OLog log = Tracing.createLoggerFor(ScormAPIMapper.class);
private transient Identity identity;
private transient OLATApiAdapter scormAdapter;
private transient ScormCourseNode scormNode;
private transient UserCourseEnvironment userCourseEnv;
private Long identityKey;
private String resourceId;
private String courseIdNodeId;
private String lesson_mode;
private String credit_mode;
private boolean isAssessable;
private String assessableType;
private boolean attemptsIncremented;
private Float currentScore;
private Boolean currentPassed;
private File cpRoot;
public ScormAPIMapper() {
//for XStream
}
public ScormAPIMapper(Identity identity, String resourceId, String courseIdNodeId, String assessableType,
File cpRoot, OLATApiAdapter scormAdapter, boolean attemptsIncremented) {
this.scormAdapter = scormAdapter;
this.identity = identity;
this.identityKey = identity.getKey();
this.resourceId = resourceId;
this.courseIdNodeId = courseIdNodeId;
this.isAssessable = StringHelper.containsNonWhitespace(assessableType);
this.assessableType = assessableType;
this.cpRoot = cpRoot;
this.lesson_mode = scormAdapter.getLessonMode();
this.credit_mode = scormAdapter.getCreditMode();
this.scormAdapter.addAPIListener(this);
this.attemptsIncremented = attemptsIncremented;
//setup the current score
currentScore();
}
private void currentScore() {
if(isAssessable) {
checkForLms();
if(scormNode.hasScoreConfigured()) {
currentScore = scormNode.getUserScoreEvaluation(userCourseEnv).getScore();
}
if(scormNode.hasPassedConfigured()) {
currentPassed = scormNode.getUserScoreEvaluation(userCourseEnv).getPassed();
}
}
}
private final void check() {
if(identity == null) {
identity = CoreSpringFactory.getImpl(BaseSecurity.class).loadIdentityByKey(identityKey);
}
if(isAssessable && !StringHelper.containsNonWhitespace(assessableType)) {
assessableType = ScormEditController.CONFIG_ASSESSABLE_TYPE_SCORE;
}
if(scormAdapter == null) {
try {
scormAdapter = new OLATApiAdapter();
String fullname = UserManager.getInstance().getUserDisplayName(identity);
scormAdapter.init(cpRoot, resourceId, courseIdNodeId, FolderConfig.getCanonicalRoot(), identity.getName(), fullname, lesson_mode, credit_mode, hashCode());
scormAdapter.addAPIListener(this);
} catch (IOException e) {
log.error("", e);
}
}
}
private final void checkForLms() {
check();
if(scormNode == null) {
int sep = courseIdNodeId.indexOf('-');
String courseId = courseIdNodeId.substring(0, sep);
String nodeId = courseIdNodeId.substring(sep + 1);
ICourse course = CourseFactory.loadCourse(Long.parseLong(courseId));
scormNode = (ScormCourseNode)course.getRunStructure().getNode(nodeId);
IdentityEnvironment identityEnvironment = new IdentityEnvironment();
identityEnvironment.setIdentity(identity);
userCourseEnv = new UserCourseEnvironmentImpl(identityEnvironment, course.getCourseEnvironment());
}
}
@Override
public void lmsCommit(String olatSahsId, Properties scoreProp, Properties lessonStatusProp) {
if (isAssessable) {
checkForLms();
calculateResults(olatSahsId, scoreProp, lessonStatusProp, false);
}
}
@Override
public void lmsFinish(String olatSahsId, Properties scoreProp, Properties lessonStatusProp) {
if (isAssessable) {
checkForLms();
calculateResults(olatSahsId, scoreProp, lessonStatusProp, true);
}
}
private void calculateResults(String olatSahsId, Properties scoreProp, Properties lessonStatusProp, boolean finish) {
if(ScormEditController.CONFIG_ASSESSABLE_TYPE_PASSED.equals(assessableType)) {
calculatePassed(olatSahsId, lessonStatusProp, finish);
} else {
calculateScorePassed(olatSahsId, scoreProp, finish);
}
}
private void calculatePassed(String olatSahsId, Properties lessonStatusProp, boolean finish) {
int found = 0;
boolean passedScos = true;
for (Iterator<Object> it_status = lessonStatusProp.values().iterator(); it_status.hasNext();) {
String status = (String)it_status.next();
passedScos &= "passed".equals(status);
found++;
}
boolean passed = (found == scormAdapter.getNumOfSCOs()) && passedScos;
// if advanceScore option is set update the score only if it is higher
// <OLATEE-27>
ModuleConfiguration config = scormNode.getModuleConfiguration();
if (config.getBooleanSafe(ScormEditController.CONFIG_ADVANCESCORE, true)) {
if (currentPassed == null || !currentPassed.booleanValue()) {
// </OLATEE-27>
boolean increment = !attemptsIncremented && finish;
ScoreEvaluation sceval = new ScoreEvaluation(new Float(0.0f), Boolean.valueOf(passed));
scormNode.updateUserScoreEvaluation(sceval, userCourseEnv, identity, increment);
if(increment) {
attemptsIncremented = true;
}
} else if (!config.getBooleanSafe(ScormEditController.CONFIG_ATTEMPTSDEPENDONSCORE, false)) {
boolean increment = !attemptsIncremented && finish;
ScoreEvaluation sceval = scormNode.getUserScoreEvaluation(userCourseEnv);
scormNode.updateUserScoreEvaluation(sceval, userCourseEnv, identity, increment);
if(increment) {
attemptsIncremented = true;
}
}
} else {
boolean increment = !attemptsIncremented && finish;
ScoreEvaluation sceval = new ScoreEvaluation(new Float(0.0f), Boolean.valueOf(passed));
scormNode.updateUserScoreEvaluation(sceval, userCourseEnv, identity, false);
if(increment) {
attemptsIncremented = true;
}
}
if (log.isDebug()) {
String msg = "for scorm node:" + scormNode.getIdent() + " (" + scormNode.getShortTitle() + ") a lmsCommit for scoId "
+ olatSahsId + " occured, passed: " + passed
+ ", all lesson status now = " + lessonStatusProp.toString();
log.debug(msg, null);
}
}
private void calculateScorePassed(String olatSahsId, Properties scoProperties, boolean finish) {
// do a sum-of-scores over all sco scores
// <OLATEE-27>
float score = -1f;
// </OLATEE-27>
for (Iterator<Object> it_score = scoProperties.values().iterator(); it_score.hasNext();) {
// <OLATEE-27>
if (score < 0f) {
score = 0f;
}
// </OLATEE-27>
String aScore = (String) it_score.next();
float ascore = Float.parseFloat(aScore);
score += ascore;
}
float cutval = scormNode.getCutValueConfiguration().floatValue();
boolean passed = (score >= cutval);
// if advanceScore option is set update the score only if it is higher
// <OLATEE-27>
ModuleConfiguration config = scormNode.getModuleConfiguration();
if (config.getBooleanSafe(ScormEditController.CONFIG_ADVANCESCORE, true)) {
if (score > (currentScore != null ? currentScore : -1f)) {
// </OLATEE-27>
boolean increment = !attemptsIncremented && finish;
ScoreEvaluation sceval = new ScoreEvaluation(new Float(score), Boolean.valueOf(passed));
scormNode.updateUserScoreEvaluation(sceval, userCourseEnv, identity, increment);
if(increment) {
attemptsIncremented = true;
}
} else if (!config.getBooleanSafe(ScormEditController.CONFIG_ATTEMPTSDEPENDONSCORE, false)) {
boolean increment = !attemptsIncremented && finish;
ScoreEvaluation sceval = scormNode.getUserScoreEvaluation(userCourseEnv);
scormNode.updateUserScoreEvaluation(sceval, userCourseEnv, identity, increment);
if(increment) {
attemptsIncremented = true;
}
}
} else {
// <OLATEE-27>
if (score < 0f) {
score = 0f;
}
// </OLATEE-27>
boolean increment = !attemptsIncremented && finish;
ScoreEvaluation sceval = new ScoreEvaluation(new Float(score), Boolean.valueOf(passed));
scormNode.updateUserScoreEvaluation(sceval, userCourseEnv, identity, false);
if(increment) {
attemptsIncremented = true;
}
}
if (log.isDebug()) {
String msg = "for scorm node:" + scormNode.getIdent() + " (" + scormNode.getShortTitle() + ") a lmsCommit for scoId "
+ olatSahsId + " occured, total sum = " + score + ", cutvalue =" + cutval + ", passed: " + passed
+ ", all scores now = " + scoProperties.toString();
log.debug(msg, null);
}
}
public MediaResource handle(String relPath, HttpServletRequest request) {
check();
String apiCall = request.getParameter("apiCall");
String apiCallParamOne = request.getParameter("apiCallParamOne");
String apiCallParamTwo = request.getParameter("apiCallParamTwo");
if(log.isDebug()) {
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;
}
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);
}
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 smr;
}
}