/**
* <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.resultexport;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.apache.velocity.VelocityContext;
import org.dom4j.Document;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.gui.render.velocity.VelocityHelper;
import org.olat.core.gui.translator.PackageTranslator;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.UserConstants;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.FileUtils;
import org.olat.core.util.StringHelper;
import org.olat.core.util.WebappHelper;
import org.olat.course.nodes.QTICourseNode;
import org.olat.course.run.environment.CourseEnvironment;
import org.olat.ims.qti.QTIResultManager;
import org.olat.ims.qti.QTIResultSet;
import org.olat.ims.qti.process.AssessmentInstance;
import org.olat.ims.qti.process.FilePersister;
import org.olat.ims.qti.render.LocalizedXSLTransformer;
public class QTI12ResultsExportMediaResource implements MediaResource {
private static final OLog log = Tracing.createLoggerFor(QTI12ResultsExportMediaResource.class);
private static final String DATA = "userdata/";
private static final String SEP = File.separator;
private static final SimpleDateFormat assessmentDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final SimpleDateFormat displayDateFormat = new SimpleDateFormat("HH:mm:ss");
static {
displayDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
}
private VelocityHelper velocityHelper;
private List<Identity> identities;
private final QTIResultManager qtiResultManager;
private QTICourseNode courseNode;
private CourseEnvironment courseEnv;
private UserRequest ureq;
private Locale locale;
private String title, exportFolderName;
private Translator translator;
public QTI12ResultsExportMediaResource(CourseEnvironment courseEnv, UserRequest ureq,
List<Identity> identities, QTICourseNode courseNode) {
this.courseNode = courseNode;
this.courseEnv = courseEnv;
this.ureq = ureq;
this.locale = null;
this.title = "qti12export";
this.identities = identities;
this.velocityHelper = VelocityHelper.getInstance();
translator = new PackageTranslator(QTI12ResultsExportMediaResource.class.getPackage().getName(), ureq.getLocale());
this.exportFolderName = translator.translate("export.folder.name");
qtiResultManager = QTIResultManager.getInstance();
}
public QTI12ResultsExportMediaResource(CourseEnvironment courseEnv, Locale locale, List<Identity> identities,
QTICourseNode courseNode) {
this.courseNode = courseNode;
this.courseEnv = courseEnv;
this.locale = locale;
this.title = "qti12export";
this.identities = identities;
this.velocityHelper = VelocityHelper.getInstance();
translator = new PackageTranslator(QTI12ResultsExportMediaResource.class.getPackage().getName(), locale);
this.exportFolderName = translator.translate("export.folder.name");
qtiResultManager = QTIResultManager.getInstance();
}
@Override
public boolean acceptRanges() {
return false;
}
@Override
public String getContentType() {
return "application/zip";
}
@Override
public Long getSize() {
return null;
}
@Override
public InputStream getInputStream() {
return null;
}
@Override
public Long getLastModified() {
return null;
}
@Override
public void prepare(HttpServletResponse hres) {
String label = StringHelper.transformDisplayNameToFileSystemName(title);
if (label != null && !label.toLowerCase().endsWith(".zip")) {
label += ".zip";
}
String urlEncodedLabel = StringHelper.urlEncodeUTF8(label);
hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + urlEncodedLabel);
hres.setHeader("Content-Description", urlEncodedLabel);
try {
ZipOutputStream zout = new ZipOutputStream(hres.getOutputStream());
zout.setLevel(9);
List<AssessedMember> assessedMembers = createAssessedMembersDetail(zout);
//convert velocity template to zip entry
String usersHTML = createMemberListingHTML(assessedMembers);
convertToZipEntry(zout, exportFolderName + "/index.html", usersHTML);
//Copy resource files or file trees to export file tree
File sasstheme = new File(WebappHelper.getContextRealPath("/static/offline/qti"));
fsToZip(zout, sasstheme.toPath(), exportFolderName + "/css/offline/qti/");
File fontawesome = new File(WebappHelper.getContextRealPath("/static/font-awesome"));
fsToZip(zout, fontawesome.toPath(), exportFolderName + "/css/font-awesome/");
zout.close();
} catch (Exception e) {
log.error("Unknown error while assessment result resource export", e);
}
}
private List<ResultDetail> createResultDetail (Identity identity, ZipOutputStream zout, String idDir) throws IOException {
Long resourceId = courseEnv.getCourseResourceableId();
String resourceDetail = courseNode.getIdent();
Long resid = courseNode.getReferencedRepositoryEntry().getKey();
List<QTIResultSet> resultSets = qtiResultManager.getResultSets(resourceId, resourceDetail, resid, identity);
List<ResultDetail> assessments = new ArrayList<ResultDetail>();
for (QTIResultSet qtiResultSet : resultSets) {
Long assessmentID = qtiResultSet.getAssessmentID();
String idPath = idDir + translator.translate("table.user.attempt") + (resultSets.indexOf(qtiResultSet)+1) + SEP;
createZipDirectory(zout, idPath);
String linkToHTML = createHTMLfromQTIResultSet(idPath, idDir, zout, identity, qtiResultSet);
// content of result table
ResultDetail resultDetail = new ResultDetail(createLink(String.valueOf(assessmentID), linkToHTML, true),
assessmentDateFormat.format(qtiResultSet.getCreationDate()),
displayDateFormat.format(new Date(qtiResultSet.getDuration())),
qtiResultSet.getScore(), createPassedIcons(qtiResultSet.getIsPassed()), linkToHTML);
assessments.add(resultDetail);
}
return assessments;
}
private List<AssessedMember> createAssessedMembersDetail (ZipOutputStream zout) throws IOException {
List<AssessedMember> assessedMembers = new ArrayList<AssessedMember>();
for (Identity identity : identities) {
String idDir = exportFolderName + "/" + DATA + identity.getName();
idDir = idDir.endsWith(SEP) ? idDir : idDir + SEP;
createZipDirectory(zout, idDir);
//content of single assessed member
String userName = identity.getName();
String firstName = identity.getUser().getProperty(UserConstants.FIRSTNAME, null);
String lastName = identity.getUser().getProperty(UserConstants.LASTNAME, null);
String memberEmail = identity.getUser().getProperty(UserConstants.EMAIL, null);
AssessedMember assessedMember = new AssessedMember (userName, lastName, firstName, memberEmail, null);
List<ResultDetail> assessments = createResultDetail(identity, zout, idDir);
String oneUserHTML = createResultListingHTML(assessments, assessedMember);
convertToZipEntry(zout, exportFolderName + "/" + DATA + identity.getName() + "/index.html", oneUserHTML);
String linkToUser = idDir.replace(exportFolderName + "/", "") + "index.html";
//content of assessed members table
AssessedMember member = new AssessedMember();
member.setUsername(createLink(identity.getName(), linkToUser, false));
member.setLastname(createLink(identity.getUser().getProperty(UserConstants.LASTNAME, null),linkToUser,false));
member.setFirstname(createLink(identity.getUser().getProperty(UserConstants.FIRSTNAME, null),linkToUser,false));
member.setTries(String.valueOf(assessments.size()));
assessedMembers.add(member);
}
return assessedMembers;
}
/**
* Adds the result export to existing zip stream.
*
* @throws Exception
*/
public void exportTestResults(ZipOutputStream zout) throws IOException {
List<AssessedMember> assessedMembers = createAssessedMembersDetail(zout);
//convert velocity template to zip entry
String usersHTML = createMemberListingHTML(assessedMembers);
convertToZipEntry(zout, exportFolderName + "/index.html", usersHTML);
//Copy resource files or file trees to export file tree
File sasstheme = new File(WebappHelper.getContextRealPath("/static/offline/qti"));
fsToZip(zout, sasstheme.toPath(), exportFolderName + "/css/offline/qti/");
File fontawesome = new File(WebappHelper.getContextRealPath("/static/font-awesome"));
fsToZip(zout, fontawesome.toPath(), exportFolderName + "/css/font-awesome/");
}
private String createLink(String name, String href, boolean userview){
String targetLink = userview ? "_blank" : "_self";
return "<a href='" + href + "' target='" + targetLink + "' class='userLink'>" + name + "</a>";
}
private String createPassedIcons(boolean passed) {
String icon = passed ? "<i class='o_icon o_passed o_icon_passed text-success'></i>"
: "<i class='o_icon o_failed o_icon_failed text-danger'></i>";
return icon;
}
private String createResultHTML (String results){
VelocityContext ctx = new VelocityContext();
ctx.put("results", results);
ctx.put("rootTitle", translator.translate("table.grading"));
String template = FileUtils.load(QTI12ResultsExportMediaResource.class
.getResourceAsStream("_content/qti12results.html"), "utf-8");
return velocityHelper.evaluateVTL(template, ctx);
}
private String createResultListingHTML (List<ResultDetail> assessments, AssessedMember assessedMember){
// now put values to velocityContext
VelocityContext ctx = new VelocityContext();
ctx.put("t", translator);
ctx.put("title", translator.translate("table.overview"));
ctx.put("return", translator.translate("button.return"));
ctx.put("assessments", assessments);
ctx.put("assessedMember", assessedMember);
if (assessments.size() > 0) ctx.put("hasResults", Boolean.TRUE);
String template = FileUtils.load(QTI12ResultsExportMediaResource.class
.getResourceAsStream("_content/qtiListing.html"), "utf-8");
return velocityHelper.evaluateVTL(template, ctx);
}
private String createMemberListingHTML(List<AssessedMember> assessedMembers) {
Collections.sort(assessedMembers, new Comparator<AssessedMember>() {
@Override
public int compare(AssessedMember o1, AssessedMember o2) {
return o1.getUsername().compareTo(o2.getUsername());
}
});
// now put values to velocityContext
VelocityContext ctx = new VelocityContext();
ctx.put("t", translator);
ctx.put("rootTitle", translator.translate("table.overview"));
ctx.put("assessedMembers", assessedMembers);
String template = FileUtils.load(QTI12ResultsExportMediaResource.class
.getResourceAsStream("_content/qtiUserlisting.html"), "utf-8");
return velocityHelper.evaluateVTL(template, ctx);
}
private File retrieveXML (Identity subj, long aiid){
String RES_REPORTING = "resreporting";
String type = AssessmentInstance.QMD_ENTRY_TYPE_ASSESS;
File fUserdataRoot = new File(WebappHelper.getUserDataRoot());
String path = RES_REPORTING + SEP + subj.getName() + SEP + type + SEP + aiid + ".xml";
File fDoc = new File(fUserdataRoot, path);
return fDoc;
}
private String createHTMLfromQTIResultSet(String idPath, String idDir, ZipOutputStream zout,
Identity assessedIdentity, QTIResultSet resultSet) throws IOException {
Document doc = FilePersister.retreiveResultsReporting(assessedIdentity,
AssessmentInstance.QMD_ENTRY_TYPE_ASSESS, resultSet.getAssessmentID());
if (doc == null) {
return "null";
}
File resourceXML = retrieveXML(assessedIdentity, resultSet.getAssessmentID());
String resultsHTML = LocalizedXSLTransformer.getInstance(locale != null ? locale : ureq.getLocale()).renderResults(doc);
resultsHTML = createResultHTML(resultsHTML);
String html = idPath + resultSet.getAssessmentID() + ".html";
String xml = html.replace(".html", ".xml");
convertToZipEntry(zout, html, resultsHTML);
convertToZipEntry(zout, xml, resourceXML);
return idPath.replace(idDir, "") + resultSet.getAssessmentID() + ".html";
}
private void fsToZip(ZipOutputStream zout, final Path sourceFolder, final String targetPath) throws IOException {
Files.walkFileTree(sourceFolder, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
zout.putNextEntry(new ZipEntry(targetPath + sourceFolder.relativize(file).toString()));
Files.copy(file, zout);
zout.closeEntry();
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
zout.putNextEntry(new ZipEntry(targetPath + sourceFolder.relativize(dir).toString() + "/"));
zout.closeEntry();
return FileVisitResult.CONTINUE;
}
});
}
private void convertToZipEntry(ZipOutputStream zout, String link, File file) throws IOException {
zout.putNextEntry(new ZipEntry(link));
try (InputStream in = new FileInputStream(file)) {
FileUtils.copy(in, zout);
} catch (Exception e) {
log.error("Error during copy of resource export", e);
} finally {
zout.closeEntry();
}
}
private void convertToZipEntry(ZipOutputStream zout, String link, String content) throws IOException {
zout.putNextEntry(new ZipEntry(link));
try (InputStream in = new ByteArrayInputStream(content.getBytes())) {
FileUtils.copy(in, zout);
} catch (Exception e) {
log.error("Error during copy of resource export", e);
} finally {
zout.closeEntry();
}
}
private void createZipDirectory (ZipOutputStream zout, String dir) throws IOException{
dir = dir.endsWith(SEP) ? dir : dir + SEP;
zout.putNextEntry(new ZipEntry(dir));
zout.closeEntry();
}
@Override
public void release() {
}
}