/** * <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.ims.qti.export; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; import javax.servlet.http.HttpServletResponse; import org.olat.core.CoreSpringFactory; import org.olat.core.gui.media.DefaultMediaResource; import org.olat.core.gui.media.MediaResource; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.OLATRuntimeException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.FileUtils; import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.WebappHelper; import org.olat.core.util.coordinate.LockResult; import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.course.assessment.model.AssessmentNodeData; import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.IQSELFCourseNode; import org.olat.course.nodes.IQSURVCourseNode; import org.olat.course.nodes.IQTESTCourseNode; import org.olat.course.nodes.iq.IQEditController; import org.olat.fileresource.types.ImsQTI21Resource; import org.olat.ims.qti.QTIResult; import org.olat.ims.qti.QTIResultManager; import org.olat.ims.qti.QTIResultSet; import org.olat.ims.qti.editor.beecom.parser.ItemParser; import org.olat.ims.qti.export.helper.QTIItemObject; import org.olat.ims.qti.export.helper.QTIObjectTreeBuilder; import org.olat.ims.qti21.manager.archive.QTI21ArchiveFormat; import org.olat.ims.qti21.model.QTI21StatisticSearchParams; import org.olat.repository.RepositoryEntry; import org.olat.repository.handlers.RepositoryHandler; import org.olat.repository.handlers.RepositoryHandlerFactory; import org.olat.resource.OLATResource; import de.bps.onyx.plugin.OnyxExportManager; import de.bps.onyx.plugin.OnyxModule; /** * * Initial date: 21.04.2016<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class QTIArchiver { public static final String TEST_USER_PROPERTIES = QTIExportFormatterCSVType1.class.getName(); private static final OLog log = Tracing.createLoggerFor(QTIArchiver.class); private CourseNode courseNode; private AssessmentNodeData data; private final Locale locale; private final Identity identity; private final OLATResourceable courseOres; private boolean participants = true; private boolean allUsers = true; private boolean anonymUsers = true; private Boolean results; private List<QTIItemObject> qtiItemObjectList; private Map<Class<?>, QTIExportItemFormatConfig> qtiItemConfigs; private final QTIResultManager qrm; private final QTIExportManager qem; private final OnyxExportManager onyxExportManager; private Type type; public enum Type { onyx, qti12, qti21 } public QTIArchiver(OLATResourceable courseOres, Identity identity, Locale locale) { this.locale = locale; this.identity = identity; this.courseOres = courseOres; qem = QTIExportManager.getInstance(); qrm = CoreSpringFactory.getImpl(QTIResultManager.class); onyxExportManager = OnyxExportManager.getInstance(); } public CourseNode getCourseNode() { return courseNode; } public boolean isParticipants() { return participants; } public void setParticipants(boolean participants) { this.participants = participants; } public boolean isAllUsers() { return allUsers; } public void setAllUsers(boolean allUsers) { this.allUsers = allUsers; } public boolean isAnonymUsers() { return anonymUsers; } public void setAnonymUsers(boolean anonymUsers) { this.anonymUsers = anonymUsers; } public AssessmentNodeData getData() { return data; } public void setData(AssessmentNodeData data) { this.type = null; this.results = null; this.data = data; ICourse course = CourseFactory.loadCourse(courseOres); courseNode = course.getRunStructure().getNode(data.getIdent()); getQTIItemConfigs(); getType(); } public Type getType() { if(type == null) { if (courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE_QTI) != null) { boolean isOnyx = courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE_QTI).equals(IQEditController.CONFIG_VALUE_QTI2); if(isOnyx) { type = Type.onyx; } } if(type != Type.onyx) { RepositoryEntry testRe = courseNode.getReferencedRepositoryEntry(); if(ImsQTI21Resource.TYPE_NAME.equals(testRe.getOlatResource().getResourceableTypeName())) { type = Type.qti21; } else { type = Type.qti12; } } } return type; } public boolean hasResults() { if(results != null) return results.booleanValue(); if (courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE_QTI) != null) { boolean isOnyx = courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE_QTI).equals(IQEditController.CONFIG_VALUE_QTI2); if(isOnyx) { type = Type.onyx; } } ICourse course = CourseFactory.loadCourse(courseOres); RepositoryEntry testRe = courseNode.getReferencedRepositoryEntry(); boolean success = false; if (type == Type.onyx) { if (courseNode instanceof IQSURVCourseNode) { File courseContainer = course.getCourseEnvironment().getCourseBaseContainer().getBasefile(); File surveyDir = new File(courseContainer, courseNode.getIdent()); success = surveyDir.exists() && surveyDir.listFiles().length > 0; } else { // <OLATBPS-498> List<QTIResultSet> resultSets = qrm.getResultSets(course.getResourceableId(), courseNode.getIdent(), testRe.getKey(), null); File fUserdataRoot = new File(WebappHelper.getUserDataRoot()); for (QTIResultSet rs : resultSets) { String resultXml = onyxExportManager.getResultXml(rs.getIdentity().getName(), courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE).toString(), courseNode.getIdent(), rs.getAssessmentID()); File xml = new File(fUserdataRoot, resultXml); if (xml != null && xml.exists()) { // there is at least one result file success = true; break; } } // </OLATBPS-498> } } else if(ImsQTI21Resource.TYPE_NAME.equals(testRe.getOlatResource().getResourceableTypeName())) { type = Type.qti21; RepositoryEntry courseEntry = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry(); QTI21StatisticSearchParams searchParams = new QTI21StatisticSearchParams(testRe, courseEntry, courseNode.getIdent(), allUsers, anonymUsers); success = new QTI21ArchiveFormat(locale, searchParams).hasResults(); } else { type = Type.qti12; success = qrm.hasResultSets(courseOres.getResourceableId(), courseNode.getIdent(), testRe.getKey()); } results = new Boolean(success); return success; } public List<QTIItemObject> getQtiItemObjectList() { if(qtiItemObjectList == null) { RepositoryEntry testRe = courseNode.getReferencedRepositoryEntry(); qtiItemObjectList = new QTIObjectTreeBuilder().getQTIItemObjectList(testRe.getKey()); } return qtiItemObjectList; } public Map<Class<?>, QTIExportItemFormatConfig> getQTIItemConfigs() { if(qtiItemConfigs == null) { RepositoryEntry testRe = courseNode.getReferencedRepositoryEntry(); if(OnyxModule.isOnyxTest(testRe.getOlatResource())) { qtiItemConfigs = Collections.emptyMap(); } else if(ImsQTI21Resource.TYPE_NAME.equals(testRe.getOlatResource().getResourceableTypeName())) { qtiItemConfigs = Collections.emptyMap(); } else { qtiItemConfigs = getQTIItemConfigs(getQtiItemObjectList()); } } return qtiItemConfigs; } public MediaResource export() throws IOException { switch(type) { case onyx: return exportOnyx(); case qti12: return exportQTI12(); case qti21: return exportQTI21(); default: return null; } } public MediaResource exportQTI21() { ICourse course = CourseFactory.loadCourse(courseOres); RepositoryEntry testRe = courseNode.getReferencedRepositoryEntry(); RepositoryEntry courseEntry = course.getCourseEnvironment().getCourseGroupManager().getCourseEntry(); QTI21StatisticSearchParams searchParams = new QTI21StatisticSearchParams(testRe, courseEntry, courseNode.getIdent(), allUsers, anonymUsers); return new QTI21ArchiveFormat(locale, searchParams).exportCourseElement(); } public MediaResource exportQTI12() throws IOException { RepositoryEntry testRe = courseNode.getReferencedRepositoryEntry(); String sep = "\\t"; // fields separated by String emb = "\""; // fields embedded by String car = "\\r\\n"; // carriage return sep = convert2CtrlChars(sep); car = convert2CtrlChars(car); boolean tagLess = true; QTIExportFormatter formatter = getFormatter(sep, emb, car, tagLess); formatter.setMapWithExportItemConfigs(qtiItemConfigs); return new DefaultMediaResource() { @Override public String getContentType() { return "text/csv"; } @Override public void prepare(HttpServletResponse hres) { try { hres.setCharacterEncoding("UTF-8"); } catch (Exception e) { log.error("", e); } String label = courseNode.getType() + "_" + StringHelper.transformDisplayNameToFileSystemName(courseNode.getShortName()) + "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis())) + ".csv"; String urlEncodedLabel = StringHelper.urlEncodeUTF8(label); hres.setHeader("Content-Disposition","attachment; filename*=UTF-8''" + urlEncodedLabel); hres.setHeader("Content-Description", urlEncodedLabel); try { OutputStream out = hres.getOutputStream(); List<QTIResult> qtiResults = qrm.selectResults(courseOres.getResourceableId(), courseNode.getIdent(), testRe.getKey(), null, 5); qem.exportResults(formatter, qtiResults, qtiItemObjectList, out); } catch (IOException e) { log.error("", e); } } }; } private QTIExportFormatter getFormatter(String se, String em, String ca, boolean tagless){ QTIExportFormatter frmtr = null; if (courseNode instanceof IQTESTCourseNode){ frmtr = new QTIExportFormatterCSVType1(locale, se, em, ca, tagless); } else if (courseNode instanceof IQSELFCourseNode){ frmtr = new QTIExportFormatterCSVType1(locale, se, em, ca, tagless); ((QTIExportFormatterCSVType1)frmtr).setAnonymous(true); } else { // type == 3 frmtr = new QTIExportFormatterCSVType3(locale, null, se, em, ca, tagless); } return frmtr; } private MediaResource exportOnyx() { return new DefaultMediaResource() { @Override public String getContentType() { return "application/zip"; } @Override public void prepare(HttpServletResponse hres) { try { String label = courseNode.getType() + "_" + StringHelper.transformDisplayNameToFileSystemName(courseNode.getShortName()) + "_" + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis())) + ".zip"; String urlEncodedLabel = StringHelper.urlEncodeUTF8(label); hres.setHeader("Content-Disposition","attachment; filename*=UTF-8''" + urlEncodedLabel); hres.setHeader("Content-Description", urlEncodedLabel); OutputStream out = hres.getOutputStream(); ICourse course = CourseFactory.loadCourse(courseOres); if (courseNode.getClass().equals(IQSURVCourseNode.class)) { // it is an onyx survey File surveyPath = new File(course.getCourseEnvironment().getCourseBaseContainer().getBasefile(), courseNode.getIdent()); onyxExportManager.exportResults(surveyPath, courseNode, out); } else { File csvFile = null; RepositoryEntry testRe = courseNode.getReferencedRepositoryEntry(); // <OLATCE-654> if (testRe != null) { List<QTIResultSet> resultSets = qrm.getResultSets(course.getResourceableId(), courseNode.getIdent(), testRe.getKey(), null); OLATResource testResource = testRe.getOlatResource(); RepositoryHandler repoHandler = RepositoryHandlerFactory.getInstance().getRepositoryHandler(testRe); if (testRe != null) { boolean isAlreadyLocked = repoHandler.isLocked(testResource); LockResult lockResult = null; try { lockResult = repoHandler.acquireLock(testResource, identity); if (lockResult == null || (lockResult != null && lockResult.isSuccess() && !isAlreadyLocked)) { MediaResource mr = repoHandler.getAsMediaResource(testResource, false); if (mr != null) { try { File exportDir = new File(WebappHelper.getTmpDir(), UUID.randomUUID().toString()); String filename = onyxExportManager.exportAssessmentResults(resultSets, exportDir, mr, courseNode, false, csvFile); File archive = new File(exportDir, filename); FileUtils.copy(new FileInputStream(archive), out); } catch (FileNotFoundException e) { log.error("", e); } } } else if (lockResult != null && lockResult.isSuccess() && isAlreadyLocked) { lockResult = null; // invalid lock, it was already locked } } finally { if ((lockResult != null && lockResult.isSuccess() && !isAlreadyLocked)) { repoHandler.releaseLock(lockResult); lockResult = null; } } } } // </OLATCE-654> } } catch (IOException e) { log.error("", e); } } }; } public static String convert2CtrlChars(String source) { if (source == null) return null; StringBuilder sb = new StringBuilder(300); int len = source.length(); char[] cs = source.toCharArray(); for (int i = 0; i < len; i++) { char c = cs[i]; switch (c) { case '\\': // check on \\ first if (i < len - 1 && cs[i + 1] == 't') { // we have t as next char sb.append("\t");i++; }else if (i < len - 1 && cs[i + 1] == 'r') { // we have r as next char sb.append("\r");i++; }else if (i < len - 1 && cs[i + 1] == 'n') { // we have n as next char sb.append("\n");i++; } else { sb.append("\\"); } break; default: sb.append(c); } } return sb.toString(); } public static Map<Class<?>, QTIExportItemFormatConfig> getQTIItemConfigs(List<QTIItemObject> itemList){ Map<Class<?>, QTIExportItemFormatConfig> itConfigs = new HashMap<>(); for (Iterator<QTIItemObject> iter = itemList.iterator(); iter.hasNext();) { QTIItemObject item = iter.next(); if (item.getItemIdent().startsWith(ItemParser.ITEM_PREFIX_SCQ)){ if (itConfigs.get(QTIExportSCQItemFormatConfig.class) == null){ QTIExportSCQItemFormatConfig confSCQ = new QTIExportSCQItemFormatConfig(true, false, false, false); itConfigs.put(QTIExportSCQItemFormatConfig.class, confSCQ); } } else if (item.getItemIdent().startsWith(ItemParser.ITEM_PREFIX_MCQ)){ if (itConfigs.get(QTIExportMCQItemFormatConfig.class) == null){ QTIExportMCQItemFormatConfig confMCQ = new QTIExportMCQItemFormatConfig(true, false, false, false); itConfigs.put(QTIExportMCQItemFormatConfig.class, confMCQ ); } } else if (item.getItemIdent().startsWith(ItemParser.ITEM_PREFIX_KPRIM)){ if (itConfigs.get(QTIExportKPRIMItemFormatConfig.class) == null){ QTIExportKPRIMItemFormatConfig confKPRIM = new QTIExportKPRIMItemFormatConfig(true, false, false, false); itConfigs.put(QTIExportKPRIMItemFormatConfig.class, confKPRIM); } } else if (item.getItemIdent().startsWith(ItemParser.ITEM_PREFIX_ESSAY)){ if (itConfigs.get(QTIExportEssayItemFormatConfig.class) == null){ QTIExportEssayItemFormatConfig confEssay = new QTIExportEssayItemFormatConfig(true, false); itConfigs.put(QTIExportEssayItemFormatConfig.class, confEssay); } } else if (item.getItemIdent().startsWith(ItemParser.ITEM_PREFIX_FIB)){ if (itConfigs.get(QTIExportFIBItemFormatConfig.class) == null){ QTIExportFIBItemFormatConfig confFIB = new QTIExportFIBItemFormatConfig(true, false, false); itConfigs.put(QTIExportFIBItemFormatConfig.class, confFIB); } } //if cannot find the type via the ItemParser, look for the QTIItemObject type else if (item.getItemType().equals(QTIItemObject.TYPE.A)){ QTIExportEssayItemFormatConfig confEssay = new QTIExportEssayItemFormatConfig(true, false); itConfigs.put(QTIExportEssayItemFormatConfig.class, confEssay); } else if (item.getItemType().equals(QTIItemObject.TYPE.R)){ QTIExportSCQItemFormatConfig confSCQ = new QTIExportSCQItemFormatConfig(true, false, false, false); itConfigs.put(QTIExportSCQItemFormatConfig.class, confSCQ); } else if (item.getItemType().equals(QTIItemObject.TYPE.C)){ QTIExportMCQItemFormatConfig confMCQ = new QTIExportMCQItemFormatConfig(true, false, false, false); itConfigs.put(QTIExportMCQItemFormatConfig.class, confMCQ ); } else if (item.getItemType().equals(QTIItemObject.TYPE.B)){ QTIExportFIBItemFormatConfig confFIB = new QTIExportFIBItemFormatConfig(true, false, false); itConfigs.put(QTIExportFIBItemFormatConfig.class, confFIB); } else { throw new OLATRuntimeException(null,"Can not resolve QTIItem type", null); } } return itConfigs; } }