/**
* <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.qti21.pool;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
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.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;
import org.olat.core.id.Identity;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.PathUtils;
import org.olat.core.util.StringHelper;
import org.olat.core.util.io.ShieldInputStream;
import org.olat.fileresource.types.ImsQTI21Resource;
import org.olat.ims.qti.qpool.QTIMetadataConverter;
import org.olat.ims.qti21.QTI21Constants;
import org.olat.ims.qti21.QTI21Service;
import org.olat.ims.qti21.model.IdentifierGenerator;
import org.olat.ims.qti21.model.QTI21QuestionType;
import org.olat.ims.qti21.model.xml.AssessmentItemMetadata;
import org.olat.ims.qti21.model.xml.ManifestBuilder;
import org.olat.ims.qti21.model.xml.ManifestMetadataBuilder;
import org.olat.ims.qti21.model.xml.OnyxToQtiWorksHandler;
import org.olat.imscp.xml.manifest.ResourceType;
import org.olat.modules.qpool.QuestionItem;
import org.olat.modules.qpool.QuestionType;
import org.olat.modules.qpool.TaxonomyLevel;
import org.olat.modules.qpool.manager.QEducationalContextDAO;
import org.olat.modules.qpool.manager.QItemTypeDAO;
import org.olat.modules.qpool.manager.QLicenseDAO;
import org.olat.modules.qpool.manager.QPoolFileStorage;
import org.olat.modules.qpool.manager.QuestionItemDAO;
import org.olat.modules.qpool.manager.TaxonomyLevelDAO;
import org.olat.modules.qpool.model.QEducationalContext;
import org.olat.modules.qpool.model.QItemType;
import org.olat.modules.qpool.model.QLicense;
import org.olat.modules.qpool.model.QuestionItemImpl;
import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
import uk.ac.ed.ph.jqtiplus.reading.AssessmentObjectXmlLoader;
import uk.ac.ed.ph.jqtiplus.reading.QtiXmlReader;
import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
import uk.ac.ed.ph.jqtiplus.xmlutils.locators.FileResourceLocator;
import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator;
/**
*
* Initial date: 05.02.2016<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class QTI21ImportProcessor {
private static final OLog log = Tracing.createLoggerFor(QTI21ImportProcessor.class);
private final Identity owner;
private final Locale defaultLocale;
private final QItemTypeDAO qItemTypeDao;
private final QLicenseDAO qLicenseDao;
private final QTI21Service qtiService;
private final QuestionItemDAO questionItemDao;
private final QPoolFileStorage qpoolFileStorage;
private final TaxonomyLevelDAO taxonomyLevelDao;
private final QEducationalContextDAO qEduContextDao;
public QTI21ImportProcessor(Identity owner, Locale defaultLocale,
QuestionItemDAO questionItemDao, QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao,
TaxonomyLevelDAO taxonomyLevelDao, QLicenseDAO qLicenseDao, QPoolFileStorage qpoolFileStorage, QTI21Service qtiService) {
this.owner = owner;
this.qtiService = qtiService;
this.defaultLocale = defaultLocale;
this.qLicenseDao = qLicenseDao;
this.qItemTypeDao = qItemTypeDao;
this.qEduContextDao = qEduContextDao;
this.questionItemDao = questionItemDao;
this.qpoolFileStorage = qpoolFileStorage;
this.taxonomyLevelDao = taxonomyLevelDao;
}
public List<QuestionItem> process(File file) {
//export zip file
List<QuestionItem> items = new ArrayList<>();
try {
Path fPath = FileSystems.newFileSystem(file.toPath(), null).getPath("/");
if(fPath != null) {
ImsManifestVisitor visitor = new ImsManifestVisitor();
Files.walkFileTree(fPath, visitor);
List<Path> imsmanifests = visitor.getImsmanifestFiles();
for(Path imsmanifest:imsmanifests) {
InputStream in = Files.newInputStream(imsmanifest);
ManifestBuilder manifestBuilder = ManifestBuilder.read(new ShieldInputStream(in));
List<ResourceType> resources = manifestBuilder.getResourceList();
for(ResourceType resource:resources) {
ManifestMetadataBuilder metadataBuilder = manifestBuilder.getMetadataBuilder(resource, true);
QuestionItem qitem = processResource(resource, imsmanifest, metadataBuilder);
if(qitem != null) {
items.add(qitem);
}
}
}
}
} catch (IOException e) {
log.error("", e);
}
return items;
}
private QuestionItem processResource(ResourceType resource, Path imsmanifestPath, ManifestMetadataBuilder metadataBuilder) {
try {
String href = resource.getHref();
Path parentPath = imsmanifestPath.getParent();
Path assessmentItemPath = parentPath.resolve(href);
if(Files.notExists(assessmentItemPath)) {
return null;
}
String dir = qpoolFileStorage.generateDir();
//storage
File itemStorage = qpoolFileStorage.getDirectory(dir);
File outputFile = new File(itemStorage, href);
if(!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdirs();
}
convertXmlFile(assessmentItemPath, outputFile.toPath());
QtiXmlReader qtiXmlReader = new QtiXmlReader(qtiService.jqtiExtensionManager());
ResourceLocator fileResourceLocator = new FileResourceLocator();
ResourceLocator inputResourceLocator =
ImsQTI21Resource.createResolvingResourceLocator(fileResourceLocator);
URI assessmentObjectSystemId = outputFile.toURI();
AssessmentObjectXmlLoader assessmentObjectXmlLoader = new AssessmentObjectXmlLoader(qtiXmlReader, inputResourceLocator);
ResolvedAssessmentItem resolvedAssessmentItem = assessmentObjectXmlLoader.loadAndResolveAssessmentItem(assessmentObjectSystemId);
AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful();
AssessmentItemMetadata metadata = new AssessmentItemMetadata(metadataBuilder);
String editor = null;
String editorVersion = null;
if(StringHelper.containsNonWhitespace(assessmentItem.getToolName())) {
editor = assessmentItem.getToolName();
}
if(StringHelper.containsNonWhitespace(assessmentItem.getToolVersion())) {
editorVersion = assessmentItem.getToolVersion();
}
QuestionItemImpl qitem = processItem(assessmentItem, null, href,
editor, editorVersion, dir, metadata);
//create manifest
ManifestBuilder manifest = ManifestBuilder.createAssessmentItemBuilder();
String itemId = IdentifierGenerator.newAsIdentifier("item").toString();
ResourceType importedResource = manifest.appendAssessmentItem(itemId, href);
ManifestMetadataBuilder importedMetadataBuilder = manifest.getMetadataBuilder(importedResource, true);
importedMetadataBuilder.setMetadata(metadataBuilder.getMetadata());
manifest.write(new File(itemStorage, "imsmanifest.xml"));
//process material
List<String> materials = ImportExportHelper.getMaterials(assessmentItem);
for(String material:materials) {
if(material.indexOf("://") < 0) {// material can be an external URL
Path materialFile = assessmentItemPath.getParent().resolve(material);
PathUtils.copyFileToDir(materialFile, outputFile.getParentFile(), material);
}
}
return qitem;
} catch (Exception e) {
log.error("", e);
return null;
}
}
private void convertXmlFile(Path inputFile, Path outputFile) {
try(InputStream in = Files.newInputStream(inputFile);
Writer out = Files.newBufferedWriter(outputFile, Charset.forName("UTF-8"))) {
XMLOutputFactory xof = XMLOutputFactory.newInstance();
XMLStreamWriter xtw = xof.createXMLStreamWriter(out);
SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
OnyxToQtiWorksHandler myHandler = new OnyxToQtiWorksHandler(xtw, null);
saxParser.setProperty("http://xml.org/sax/properties/lexical-handler", myHandler);
saxParser.parse(in, myHandler);
} catch(Exception e) {
log.error("", e);
}
}
protected QuestionItemImpl processItem(AssessmentItem assessmentItem, String comment, String originalItemFilename,
String editor, String editorVersion, AssessmentItemMetadata metadata) {
String dir = qpoolFileStorage.generateDir();
return processItem(assessmentItem, comment, originalItemFilename, editor, editorVersion, dir, metadata);
}
protected QuestionItemImpl processItem(AssessmentItem assessmentItem, String comment, String originalItemFilename,
String editor, String editorVersion, String dir, AssessmentItemMetadata metadata) {
//filename
String filename;
String ident = assessmentItem.getIdentifier();
if(originalItemFilename != null) {
filename = originalItemFilename;
} else if(StringHelper.containsNonWhitespace(ident)) {
filename = StringHelper.transformDisplayNameToFileSystemName(ident) + ".xml";
} else {
filename = "item.xml";
}
//title
String title = assessmentItem.getTitle();
if(!StringHelper.containsNonWhitespace(title)) {
title = assessmentItem.getLabel();
}
if(!StringHelper.containsNonWhitespace(title)) {
title = ident;
}
QuestionItemImpl poolItem = questionItemDao.create(title, QTI21Constants.QTI_21_FORMAT, dir, filename);
//description
poolItem.setDescription(comment);
//language from default
poolItem.setLanguage(defaultLocale.getLanguage());
//question type first
if(StringHelper.containsNonWhitespace(editor)) {
poolItem.setEditor(editor);
poolItem.setEditorVersion(editorVersion);
}
//if question type not found, can be overridden by the metadatas
processItemMetadata(poolItem, metadata);
if(poolItem.getType() == null) {
QItemType defType = convertType(assessmentItem);
poolItem.setType(defType);
}
/*if(docInfos != null) {
processSidecarMetadata(poolItem, docInfos);
}*/
if(metadata != null) {
//processItemMetadata(poolItem, metadata);
}
questionItemDao.persist(owner, poolItem);
return poolItem;
}
protected QItemType convertType(AssessmentItem assessmentItem) {
QTI21QuestionType qti21Type = QTI21QuestionType.getType(assessmentItem);
switch(qti21Type) {
case sc: return qItemTypeDao.loadByType(QuestionType.SC.name());
case mc: return qItemTypeDao.loadByType(QuestionType.MC.name());
case kprim: return qItemTypeDao.loadByType(QuestionType.KPRIM.name());
case match: return qItemTypeDao.loadByType(QuestionType.MATCH.name());
case fib: return qItemTypeDao.loadByType(QuestionType.FIB.name());
case numerical: return qItemTypeDao.loadByType(QuestionType.NUMERICAL.name());
case hotspot: return qItemTypeDao.loadByType(QuestionType.HOTSPOT.name());
case essay: return qItemTypeDao.loadByType(QuestionType.ESSAY.name());
case upload: return qItemTypeDao.loadByType(QuestionType.UPLOAD.name());
default: return qItemTypeDao.loadByType(QuestionType.UNKOWN.name());
}
}
protected void processItemMetadata(QuestionItemImpl poolItem, AssessmentItemMetadata metadata) {
//non heuristic set of question type
String typeStr = null;
QTI21QuestionType questionType = metadata.getQuestionType();
if(questionType != null && questionType.getPoolQuestionType() != null) {
typeStr = questionType.getPoolQuestionType().name();
}
if(typeStr != null) {
QItemType type = qItemTypeDao.loadByType(typeStr);
if(type != null) {
poolItem.setType(type);
}
}
String coverage = metadata.getCoverage();
if(StringHelper.containsNonWhitespace(coverage)) {
poolItem.setCoverage(coverage);
}
String language = metadata.getLanguage();
if(StringHelper.containsNonWhitespace(language)) {
poolItem.setLanguage(language);
}
String keywords = metadata.getKeywords();
if(StringHelper.containsNonWhitespace(keywords)) {
poolItem.setKeywords(keywords);
}
String taxonomyPath = metadata.getTaxonomyPath();
if(StringHelper.containsNonWhitespace(taxonomyPath)) {
QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao);
TaxonomyLevel taxonomyLevel = converter.toTaxonomy(taxonomyPath);
poolItem.setTaxonomyLevel(taxonomyLevel);
}
String level = metadata.getLevel();
if(StringHelper.containsNonWhitespace(level)) {
QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao);
QEducationalContext educationalContext = converter.toEducationalContext(level);
poolItem.setEducationalContext(educationalContext);
}
String time = metadata.getTypicalLearningTime();
if(StringHelper.containsNonWhitespace(time)) {
poolItem.setEducationalLearningTime(time);
}
String editor = metadata.getEditor();
if(StringHelper.containsNonWhitespace(editor)) {
poolItem.setEditor(editor);
}
String editorVersion = metadata.getEditorVersion();
if(StringHelper.containsNonWhitespace(editorVersion)) {
poolItem.setEditorVersion(editorVersion);
}
int numOfAnswerAlternatives = metadata.getNumOfAnswerAlternatives();
if(numOfAnswerAlternatives > 0) {
poolItem.setNumOfAnswerAlternatives(numOfAnswerAlternatives);
}
poolItem.setDifficulty(metadata.getDifficulty());
poolItem.setDifferentiation(metadata.getDifferentiation());
poolItem.setStdevDifficulty(metadata.getStdevDifficulty());
String license = metadata.getLicense();
if(StringHelper.containsNonWhitespace(license)) {
QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao);
QLicense qLicense = converter.toLicense(license);
poolItem.setLicense(qLicense);
}
}
public static class ImsManifestVisitor extends SimpleFileVisitor<Path> {
private final List<Path> imsmanifestFiles = new ArrayList<>();
public List<Path> getImsmanifestFiles() {
return imsmanifestFiles;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
String name = file.getFileName().toString().toLowerCase();
if(name != null && name.equals("imsmanifest.xml")) {
imsmanifestFiles.add(file);
}
return FileVisitResult.CONTINUE;
}
}
}