/**
* <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>
* BPS Bildungsportal Sachsen GmbH, http://www.bps-system.de
* <p>
*/
package de.bps.onyx.plugin;
import java.io.File;
import java.io.FileFilter;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.type.Type;
import org.olat.commons.lifecycle.LifeCycleManager;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.DBFactory;
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.CodeHelper;
import org.olat.core.util.FileUtils;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.ZipUtil;
import org.olat.course.CourseFactory;
import org.olat.course.ICourse;
import org.olat.course.nodes.AbstractAccessableCourseNode;
import org.olat.course.nodes.AssessableCourseNode;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.IQSELFCourseNode;
import org.olat.course.nodes.IQTESTCourseNode;
import org.olat.course.nodes.QTICourseNode;
import org.olat.course.nodes.iq.IQEditController;
import org.olat.course.run.scoring.ScoreEvaluation;
import org.olat.course.run.userview.UserCourseEnvironment;
import org.olat.course.run.userview.UserCourseEnvironmentImpl;
import org.olat.ims.qti.QTIResultSet;
import org.olat.repository.RepositoryEntry;
import de.bps.webservices.clients.onyxreporter.OnyxReporterConnector;
import de.bps.webservices.clients.onyxreporter.OnyxReporterException;
/**
* @author Ingmar Kroll
*/
public class OnyxResultManager {
//<ONYX-705>
public static final String PASS = "pass";
public static final String SCORE = "score";
//</ONYX-705>
public static final String SUFFIX_ZIP = ".zip";
public static final String SUFFIX_XML = ".xml";
private static final String RES_REPORTING = "resreporting";
private static final String REPORTER_NOT_FINISHED = "reporter_not_finshed";
public final static long IGNORE_PREVIEW_CASE = -1l;
public static String getResReporting() {
return RES_REPORTING;
}
public static OLog LOGGER = Tracing.createLoggerFor(OnyxResultManager.class);
public static void persistOnyxResults(QTIResultSet qtiResultSet, final String resultfile) {
//if onyx was started from learningressources or bookmark no results are persisted
if (qtiResultSet == null) {
LOGGER.info("persit onyx result: qtiResultSet is null!!!");
return;
}
// Get course and course node
final ICourse course = CourseFactory.loadCourse(qtiResultSet.getOlatResource());
final CourseNode courseNode = course.getRunStructure().getNode(qtiResultSet.getOlatResourceDetail());
Boolean isSurvey = false;
// <OLATBPS-363>
if (!courseNode.getClass().equals(IQTESTCourseNode.class) && !courseNode.getClass().equals(IQSELFCourseNode.class)) {
// </OLATBPS-363>
isSurvey = true;
}
LOGGER.info("persit onyx result: identiyname=" + qtiResultSet.getIdentity().getName() + " nodeident=" + courseNode.getIdent() + " resultfile="
+ resultfile);
String path = null;
if (isSurvey) {
final OlatRootFolderImpl courseRootContainer = course.getCourseEnvironment().getCourseBaseContainer();
path = courseRootContainer.getBasefile() + File.separator + courseNode.getIdent() + File.separator;
} else {
path = WebappHelper.getUserDataRoot() + File.separator + RES_REPORTING + File.separator + qtiResultSet.getIdentity().getName() + File.separator
+ courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE).toString() + File.separator;
}
final File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
final File resultfileF = new File(resultfile);
final File resultfileUnzippedDir = new File(resultfileF.getAbsolutePath().substring(0, resultfileF.getAbsolutePath().length() - 4) + "__unzipped");
if (!resultfileUnzippedDir.exists()) {
resultfileUnzippedDir.mkdir();
}
ZipUtil.unzip(resultfileF, resultfileUnzippedDir);
final File[] results = resultfileUnzippedDir.listFiles(new FileFilter() {
@Override
public boolean accept(java.io.File result) {
return result.getName().toLowerCase().startsWith("result");
}
});
if (results == null || results.length < 1) {
throw new UnsupportedOperationException("Onyx result zip does not contain exactly 1 result file");
}
File file_s = null;
for (File result : results) {
final String name = result.getName();
LOGGER.debug("Found file: " + name);
String suffix = SUFFIX_XML;
if (name != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
suffix = name.substring(i);
}
LOGGER.debug("Using suffix: " + suffix);
}
//add onyx session id (assessment id in qtiresultset table) to identify the different test attempts
final String prefix = getResultsFilenamePrefix(path, courseNode, qtiResultSet.getAssessmentID());
final File file = new File(prefix + suffix);
if (SUFFIX_ZIP.equals(suffix)) {
// the result file to use with result set
file_s = file;
} else if (file_s == null) {
// the xml file to use with result set
// only take XML instead of ZIP file, if no ZIP file found already
file_s = file;
}
//result.copyTo(file_s);
FileUtils.copyFileToFile(result, file, false);
result.delete();
}
resultfileUnzippedDir.delete();
//if this is a onyx survey we are done here
if (isSurvey) {
return;
} else {
// before asking onyxReporter for resultsets, save the QTIResultSet
// with the flag "reporterFinsished = false"
qtiResultSet = (QTIResultSet) DBFactory.getInstance().loadObject(qtiResultSet);
qtiResultSet.setLastModified(new Date());
try {
DBFactory.getInstance().updateObject(qtiResultSet);
} catch (Exception e) {
LOGGER.error("Unable to initialy save the QTIResultSet after finishing Onyx Test.", e);
}
// create an identenv with no roles, no attributes, no locale
IdentityEnvironment ienv = new IdentityEnvironment();
ienv.setIdentity(qtiResultSet.getIdentity());
UserCourseEnvironment userCourseEnvironment = new UserCourseEnvironmentImpl(ienv, course.getCourseEnvironment());
boolean reporterFinished = false;
try {
reporterFinished = OnyxResultManager.performOnyxReport(qtiResultSet);
} catch (Exception e) {
LOGGER.error("unable to to finish ReporterTask", e);
}
qtiResultSet = (QTIResultSet) DBFactory.getInstance().loadObject(qtiResultSet);
QTICourseNode node = (QTICourseNode) (courseNode instanceof QTICourseNode ? courseNode : null);
if (reporterFinished && !qtiResultSet.getSuspended() && node != null) {
boolean bestResultConfigured = false;
ScoreEvaluation sc = OnyxModule.getUserScoreEvaluationFromQtiResult(userCourseEnvironment.getCourseEnvironment().getCourseResourceableId(), node,
bestResultConfigured, qtiResultSet.getIdentity());
if(node instanceof AssessableCourseNode){
((AssessableCourseNode) node).updateUserScoreEvaluation(sc, userCourseEnvironment, qtiResultSet.getIdentity(), false);
}
} else {
LOGGER.info("Won't update ScoreEvaluation for user for resultKey: " + qtiResultSet.getKey() + " assessmentId: " + qtiResultSet.getAssessmentID()
+ "; reporterFinished: " + reporterFinished + "; suspended: " + qtiResultSet.getSuspended());
}
qtiResultSet = null;
DBFactory.getInstance().commitAndCloseSession();
}
}
public static QTIResultSet createQTIResultSet(Identity identity, CourseNode node, Long olatResourceId, Long assessmentId) {
QTIResultSet qtiResultSet = new QTIResultSet();
qtiResultSet.setAssessmentID(assessmentId);
qtiResultSet.setOlatResource(olatResourceId);
qtiResultSet.setOlatResourceDetail(node.getIdent());
qtiResultSet.setRepositoryRef(node.getReferencedRepositoryEntry().getKey().longValue());
qtiResultSet.setIdentity(identity);
qtiResultSet.setQtiType(1);
qtiResultSet.setLastModified(new Date());
DBFactory.getInstance().saveObject(qtiResultSet);
DBFactory.getInstance().commitAndCloseSession();
qtiResultSet = (QTIResultSet) DBFactory.getInstance().loadObject(qtiResultSet, true);
return qtiResultSet;
}
public static String getUniqueIdForShowOnly(Identity identity, RepositoryEntry entry) {
final String uId = String.valueOf(CodeHelper.getGlobalForeverUniqueID().hashCode());
QTIResultSet qtiResultSet = new QTIResultSet();
qtiResultSet.setAssessmentID(Long.valueOf(uId));
qtiResultSet.setOlatResource(IGNORE_PREVIEW_CASE);
qtiResultSet.setOlatResourceDetail(uId);
qtiResultSet.setRepositoryRef(entry.getKey());
qtiResultSet.setIdentity(identity);
qtiResultSet.setQtiType(1);
qtiResultSet.setLastModified(new Date());
DBFactory.getInstance().saveObject(qtiResultSet);
DBFactory.getInstance().commitAndCloseSession();
qtiResultSet = (QTIResultSet) DBFactory.getInstance().loadObject(qtiResultSet, true);
return uId;
}
public static QTIResultSet getResultSet(final long uniqueId) {
final List<Long> liste = getResultSetByAssassmentId(uniqueId);
QTIResultSet qtiResultSet = null;
if (liste != null && liste.size() > 0) {
Long key = liste.get(0);
qtiResultSet = DBFactory.getInstance().loadObject(QTIResultSet.class, key);
DBFactory.getInstance().intermediateCommit();
}
return qtiResultSet;
}
public static Boolean isLastTestTry(QTIResultSet testTry) {
Boolean isLast = true;
String query = "select rset.key from org.olat.ims.qti.QTIResultSet rset where rset.identity=? and rset.olatResourceDetail=? and rset.creationDate >= ?";
@SuppressWarnings("unchecked")
List<Long> results = DBFactory.getInstance().find(query,
new Object[] { testTry.getIdentity().getKey(), testTry.getOlatResourceDetail(), testTry.getCreationDate() },
new Type[] { StandardBasicTypes.LONG, StandardBasicTypes.STRING, StandardBasicTypes.DATE });
for (Long result : results) {
if (!(testTry.getKey().equals(result)) && testTry.getKey() < result) {
isLast = false;
break;
}
}
return isLast;
}
public static QTIResultSet getLastSuspendedQTIResultSet(Identity identity, CourseNode node) {
List<Long> suspendedResults = getSuspendedQTIResultSet(identity, node);
QTIResultSet lastResultSet = null;
for (Long resultSet : suspendedResults) {
QTIResultSet res = (DBFactory.getInstance().loadObject(QTIResultSet.class, resultSet));
if (lastResultSet != null) {
if (lastResultSet.getCreationDate().before(res.getCreationDate())) {
lastResultSet = res;
}
} else {
lastResultSet = res;
}
}
return lastResultSet;
}
private static List<Long> getSuspendedQTIResultSet(Identity identity, CourseNode node) {
String query = "select rset.key from org.olat.ims.qti.QTIResultSet rset where rset.suspended = ? and rset.identity=? and rset.olatResourceDetail=?";
List<Long> results = DBFactory.getInstance().find(query, new Object[] { Boolean.TRUE, identity.getKey(), node.getIdent() },
new Type[] { StandardBasicTypes.BOOLEAN, StandardBasicTypes.LONG, StandardBasicTypes.STRING });
DBFactory.getInstance().intermediateCommit();
return results;
}
private static List<Long> getResultSetByAssassmentId(Long assessmentID) {
DB db = DBFactory.getInstance();
db.commitAndCloseSession();
StringBuilder slct = new StringBuilder();
slct.append("select rset.key from ");
slct.append("org.olat.ims.qti.QTIResultSet rset ");
slct.append("where ");
slct.append("rset.assessmentID=? ");
List<Long> results = db.find(slct.toString(), new Object[] { assessmentID }, new Type[] { StandardBasicTypes.LONG });
db.intermediateCommit();
return results;
}
/**
* Ask the Onyx Reporter with a given file and save the results to db.
*
* @param qtiResultSet
* @param file_s
*/
static boolean performOnyxReport(QTIResultSet qtiResultSet) {
boolean reporterFinsished = true;
LOGGER.info("PerfomReport Begin for " + qtiResultSet.getAssessmentID() + " # " + qtiResultSet.getKey());
//Get course and course node
ICourse course = CourseFactory.loadCourse(qtiResultSet.getOlatResource());
CourseNode courseNode = course.getRunStructure().getNode(qtiResultSet.getOlatResourceDetail());
//<OLATCE-1048> SelfTests and Surveys are not AssessableCourseNode --> no assessments saved --> Switched to AbstractAssessableCourseNode
AbstractAccessableCourseNode node = null;
if (courseNode instanceof AbstractAccessableCourseNode) {
node = (AbstractAccessableCourseNode) courseNode;
} else {
LOGGER.warn("Tried to perform an OnyxReport with a non-assessable course node! "
+ (courseNode != null ? (courseNode.getShortName()
+ " Class: " + courseNode.getClass()) : "NULL"));
}
// </OLATCE-1048>
//<ONYX-705>
Map<String, String> results = null;
//</ONYX-705>
String path = WebappHelper.getUserDataRoot() + File.separator + RES_REPORTING + File.separator + qtiResultSet.getIdentity().getName() + File.separator
+ courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE).toString() + File.separator;
File file_s = getResultsFile(path, courseNode, qtiResultSet.getAssessmentID());
if (!file_s.exists()) {
LOGGER.error("performOnyxReport was called but no result.xml exists with path: " + file_s.getAbsolutePath());
reporterFinsished = false;
}
if(reporterFinsished){
try {
//<ONYX-705>
OnyxReporterConnector onyxReporter = new OnyxReporterConnector();
// <OLATBPS-363>
results = onyxReporter.getResults(file_s, node, qtiResultSet.getIdentity());
// </OLATBPS-363>
} catch (OnyxReporterException e) {
//</ONYX-705>
LifeCycleManager.createInstanceFor(qtiResultSet).markTimestampFor(REPORTER_NOT_FINISHED);
reporterFinsished = false;
LOGGER.warn("OnyxReporter was unreachable during get the results. An entry in Lifecyclemanager is done and the report will be finshed with a job.");
}
}
if(reporterFinsished) {
String score = null, passed = null;
//<ONYX-705>
for (String vars : results.keySet()) {
// only testoutcomes "score" and "passed" are stored at olat db
if (SCORE.equalsIgnoreCase(vars)) {
score = results.get(vars);
} else if (PASS.equalsIgnoreCase(vars)) {
passed = results.get(vars);
} else {
LOGGER.debug("TestOutCome "+results.get(vars)+ " is not stored in OLAT DB");
}
}
qtiResultSet = (QTIResultSet) DBFactory.getInstance().loadObject(qtiResultSet);
synchronized (qtiResultSet) {
if (score != null || passed != null) {
Float scoreValue = null;
try {
if (score != null) {
scoreValue = Float.valueOf(score);
qtiResultSet.setScore(scoreValue);
}
//if own cutvalue for passed is configured use this instead of the PASS variable from onyx test.
if (courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_CUTVALUE) != null) {
Float cutValue = ((Float) courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_CUTVALUE));
if (scoreValue >= cutValue) {
passed = "true";
} else {
passed = "false";
}
}
} catch (NumberFormatException nfe) {
LOGGER.error("Unable to parse score: " + score + "to float", nfe);
} catch (ClassCastException cce) {
LOGGER.error("Unable to cast cut-value to float", cce);
}
if (passed != null) {
qtiResultSet.setIsPassed(Boolean.valueOf(passed));
}
}
qtiResultSet.setLastModified(new Date());
DBFactory.getInstance().updateObject(qtiResultSet);
DBFactory.getInstance().commitAndCloseSession();
}
}
LOGGER.info("PerfomReport Finished for " + qtiResultSet.getAssessmentID() + " # " + qtiResultSet.getKey());
return reporterFinsished;
}
/**
* This is called by a nightly job: update all resultsets where the Onyx Reporter has not finished yet. (maybe because the reporter was not available).
*/
public static void updateOnyxResults() {
final List<QTIResultSet> liste = findResultSets();
for (final QTIResultSet qTIResultSet : liste) {
LifeCycleManager lcm = null;
if (qTIResultSet != null) {
lcm = LifeCycleManager.createInstanceFor(qTIResultSet);
}
if (lcm != null && lcm.lookupLifeCycleEntry(REPORTER_NOT_FINISHED) != null) {
if (performOnyxReport(qTIResultSet)) {
lcm.deleteAllEntriesForPersistentObject();
}
}
}
}
public static List<QTIResultSet> findResultSets() {
final DB db = DBFactory.getInstance();
final StringBuilder slct = new StringBuilder();
slct.append("select rset from ");
slct.append("org.olat.ims.qti.QTIResultSet rset ");
return db.find(slct.toString());
}
public static final String getResultsFilenamePrefix(final String path, final CourseNode courseNode, final long assessmentId) {
final String prefix = path + courseNode.getIdent() + "v" + assessmentId;
return prefix;
}
/**
* Retrieves the results file.
*
* @param path
* @param courseNode
* @param assessmentId
* @return Delivers the result.zip, if found, the result.xml otherwise.
* Returns null if not found.
*/
public static final File getResultsFile(final String path, final CourseNode courseNode, final long assessmentId) {
final String prefix = getResultsFilenamePrefix(path, courseNode, assessmentId);
File file = new File(prefix + SUFFIX_ZIP);
if (file.exists()) {
return file;
}
file = new File(prefix + SUFFIX_XML);
if (file.exists()) {
return file;
}
return null;
}
}