/** * <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.FileOutputStream; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; 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 java.util.concurrent.atomic.DoubleAdder; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; 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.ZipUtil; import org.olat.core.util.io.ShieldOutputStream; import org.olat.ims.qti21.QTI21Service; import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.model.xml.AssessmentTestBuilder; import org.olat.ims.qti21.model.xml.AssessmentTestFactory; import org.olat.ims.qti21.model.xml.ManifestBuilder; import org.olat.ims.qti21.model.xml.ManifestMetadataBuilder; import org.olat.ims.qti21.model.xml.QtiNodesExtractor; import org.olat.ims.qti21.pool.ImportExportHelper.AssessmentItemsAndResources; import org.olat.ims.qti21.pool.ImportExportHelper.ItemMaterial; import org.olat.imscp.xml.manifest.ResourceType; import org.olat.modules.qpool.QuestionItemFull; import org.olat.modules.qpool.manager.QPoolFileStorage; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; /** * * Initial date: 05.02.2016<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class QTI21ExportProcessor { private static final OLog log = Tracing.createLoggerFor(QTI21ExportProcessor.class); private final Locale locale; private final QTI21Service qtiService; private final QPoolFileStorage qpoolFileStorage; public QTI21ExportProcessor(QTI21Service qtiService, QPoolFileStorage qpoolFileStorage, Locale locale) { this.locale = locale; this.qtiService = qtiService; this.qpoolFileStorage = qpoolFileStorage; } public void process(QuestionItemFull qitem, ZipOutputStream zout) { String dir = qitem.getDirectory(); File rootDirectory = qpoolFileStorage.getDirectory(dir); String rootDir = "qitem_" + qitem.getKey(); File imsmanifest = new File(rootDirectory, "imsmanifest.xml"); ManifestBuilder manifestBuilder; if(imsmanifest.exists()) { manifestBuilder = ManifestBuilder.read(imsmanifest); } else { manifestBuilder = new ManifestBuilder(); } File resourceFile = new File(rootDirectory, qitem.getRootFilename()); URI assessmentItemUri = resourceFile.toURI(); ResolvedAssessmentItem resolvedAssessmentItem = qtiService .loadAndResolveAssessmentItemForCopy(assessmentItemUri, rootDirectory); enrichWithMetadata(qitem, resolvedAssessmentItem, manifestBuilder); try { zout.putNextEntry(new ZipEntry(rootDir + "/imsmanifest.xml")); manifestBuilder.write(new ShieldOutputStream(zout)); zout.closeEntry(); } catch (Exception e) { log.error("", e); } try { Files.walkFileTree(rootDirectory.toPath(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String filename = file.getFileName().toString(); if(!"imsmanifest.xml".equals(filename) && !filename.startsWith(".")) { String relPath = rootDirectory.toPath().relativize(file).toString(); ZipUtil.addFileToZip(rootDir + "/" + relPath, file, zout); } return FileVisitResult.CONTINUE; } }); } catch (IOException e) { log.error("", e); } } public ResolvedAssessmentItem exportToQTIEditor(QuestionItemFull fullItem, File editorContainer) throws IOException { AssessmentItemsAndResources itemAndMaterials = new AssessmentItemsAndResources(); collectMaterials(fullItem, itemAndMaterials); if(itemAndMaterials.getAssessmentItems().isEmpty()) { return null;//nothing found } ResolvedAssessmentItem assessmentItem = itemAndMaterials.getAssessmentItems().get(0); //write materials for(ItemMaterial material:itemAndMaterials.getMaterials()) { String exportPath = material.getExportUri(); File originalFile = material.getFile(); File exportFile = new File(editorContainer, exportPath); if(!exportFile.getParentFile().exists()) { exportFile.getParentFile().mkdirs(); } FileUtils.bcopy(originalFile, exportFile, "Copy material QTI 2.1"); } return assessmentItem; } protected void collectMaterials(QuestionItemFull fullItem, AssessmentItemsAndResources materials) { String dir = fullItem.getDirectory(); String rootFilename = fullItem.getRootFilename(); File resourceDirectory = qpoolFileStorage.getDirectory(dir); File itemFile = new File(resourceDirectory, rootFilename); if(itemFile.exists()) { ResolvedAssessmentItem resolvedAssessmentItem = qtiService.loadAndResolveAssessmentItemForCopy(itemFile.toURI(), resourceDirectory); AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); //enrichScore(itemEl); //enrichWithMetadata(fullItem, itemEl); ImportExportHelper.getMaterials(assessmentItem, itemFile, materials); materials.addItemEl(resolvedAssessmentItem); } } public void enrichWithMetadata(QuestionItemFull qitem, ResolvedAssessmentItem resolvedAssessmentItem, ManifestBuilder manifestBuilder) { ResourceType resource = manifestBuilder.getResourceTypeByHref(qitem.getRootFilename()); if(resource == null) { resource = manifestBuilder.appendAssessmentItem(qitem.getRootFilename()); } ManifestMetadataBuilder metadataBuilder = manifestBuilder.getMetadataBuilder(resource, true); enrichWithMetadata(qitem, resolvedAssessmentItem, metadataBuilder); } public void assembleTest(List<QuestionItemFull> fullItems, File directory) { try { QtiSerializer qtiSerializer = qtiService.qtiSerializer(); //imsmanifest ManifestBuilder manifest = ManifestBuilder.createAssessmentTestBuilder(); //assessment test DoubleAdder atomicMaxScore = new DoubleAdder(); AssessmentTest assessmentTest = AssessmentTestFactory.createAssessmentTest("Assessment test from pool", "Section"); String assessmentTestFilename = assessmentTest.getIdentifier() + ".xml"; manifest.appendAssessmentTest(assessmentTestFilename); //make a section AssessmentSection section = assessmentTest.getTestParts().get(0).getAssessmentSections().get(0); //assessment items for(QuestionItemFull qitem:fullItems) { File resourceDirectory = qpoolFileStorage.getDirectory(qitem.getDirectory()); File itemFile = new File(resourceDirectory, qitem.getRootFilename()); ResolvedAssessmentItem resolvedAssessmentItem = qtiService.loadAndResolveAssessmentItemForCopy(itemFile.toURI(), resourceDirectory); AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); assessmentItem.setIdentifier(QTI21QuestionType.generateNewIdentifier(assessmentItem.getIdentifier())); //save the item in its own container String container = qitem.getKey().toString(); File containerDir = new File(directory, container); containerDir.mkdirs(); File newItemFile = new File(containerDir, assessmentItem.getIdentifier() + ".xml"); String newItemFilename = container + "/" + newItemFile.getName(); qtiService.persistAssessmentObject(newItemFile, assessmentItem); AssessmentTestFactory.appendAssessmentItem(section, newItemFilename); manifest.appendAssessmentItem(newItemFilename); ManifestMetadataBuilder metadata = manifest.getResourceBuilderByHref(newItemFilename); enrichWithMetadata(qitem, resolvedAssessmentItem, metadata); Double maxScore = QtiNodesExtractor.extractMaxScore(assessmentItem); if(maxScore != null) { atomicMaxScore.add(maxScore.doubleValue()); } //write materials AssessmentItemsAndResources materials = new AssessmentItemsAndResources(); ImportExportHelper.getMaterials(assessmentItem, itemFile, materials); for(ItemMaterial material:materials.getMaterials()) { String exportPath = material.getExportUri(); File originalFile = material.getFile(); File exportFile = new File(container, exportPath); if(!exportFile.getParentFile().exists()) { exportFile.getParentFile().mkdirs(); } FileUtils.bcopy(originalFile, exportFile, "Copy material QTI 2.1"); } } AssessmentTestBuilder assessmentTestBuilder = new AssessmentTestBuilder(assessmentTest); double sumMaxScore = atomicMaxScore.sum(); if(sumMaxScore > 0.0d) { assessmentTestBuilder.setMaxScore(sumMaxScore); } assessmentTest = assessmentTestBuilder.build(); try(FileOutputStream out = new FileOutputStream(new File(directory, assessmentTestFilename))) { qtiSerializer.serializeJqtiObject(assessmentTest, out); } catch(Exception e) { log.error("", e); } manifest.write(new File(directory, "imsmanifest.xml")); } catch (Exception e) { log.error("", e); } } public void assembleTest(List<QuestionItemFull> fullItems, ZipOutputStream zout) { try { QtiSerializer qtiSerializer = qtiService.qtiSerializer(); //imsmanifest ManifestBuilder manifest = ManifestBuilder.createAssessmentTestBuilder(); //assessment test AssessmentTest assessmentTest = AssessmentTestFactory.createAssessmentTest("Assessment test from pool", "Section"); String assessmentTestFilename = assessmentTest.getIdentifier() + ".xml"; manifest.appendAssessmentTest(assessmentTestFilename); //make a section AssessmentSection section = assessmentTest.getTestParts().get(0).getAssessmentSections().get(0); //assessment items for(QuestionItemFull qitem:fullItems) { File resourceDirectory = qpoolFileStorage.getDirectory(qitem.getDirectory()); File itemFile = new File(resourceDirectory, qitem.getRootFilename()); String itemFilename = itemFile.getName(); String container = qitem.getKey().toString(); String containedFilename = container + "/" + itemFilename; ResolvedAssessmentItem resolvedAssessmentItem = qtiService.loadAndResolveAssessmentItemForCopy(itemFile.toURI(), resourceDirectory); ZipUtil.addFileToZip(containedFilename, itemFile, zout); AssessmentTestFactory.appendAssessmentItem(section, containedFilename); manifest.appendAssessmentItem(containedFilename); ManifestMetadataBuilder metadata = manifest.getResourceBuilderByHref(containedFilename); enrichWithMetadata(qitem, resolvedAssessmentItem, metadata); //write materials try { Files.walkFileTree(resourceDirectory.toPath(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String filename = file.getFileName().toString(); if(!"imsmanifest.xml".equals(filename) && !filename.startsWith(".") && !itemFilename.equals(filename)) { String relPath = resourceDirectory.toPath().relativize(file).toString(); ZipUtil.addFileToZip(container + "/" + relPath, file, zout); } return FileVisitResult.CONTINUE; } }); } catch (IOException e) { log.error("", e); } } zout.putNextEntry(new ZipEntry(assessmentTestFilename)); qtiSerializer.serializeJqtiObject(assessmentTest, new ShieldOutputStream(zout)); zout.closeEntry(); zout.putNextEntry(new ZipEntry("imsmanifest.xml")); manifest.write(new ShieldOutputStream(zout)); zout.closeEntry(); } catch (IOException | URISyntaxException e) { log.error("", e); } } private void enrichWithMetadata(QuestionItemFull qitem, ResolvedAssessmentItem resolvedAssessmentItem, ManifestMetadataBuilder metadata) { String lang = qitem.getLanguage(); if(!StringHelper.containsNonWhitespace(lang)) { lang = locale.getLanguage(); } //general if(StringHelper.containsNonWhitespace(qitem.getTitle())) { metadata.setTitle(qitem.getTitle(), lang); } if(StringHelper.containsNonWhitespace(qitem.getDescription())) { metadata.setDescription(qitem.getDescription(), lang); } if(StringHelper.containsNonWhitespace(qitem.getKeywords())) { //general and classification too metadata.setGeneralKeywords(qitem.getKeywords(), lang); } if(StringHelper.containsNonWhitespace(qitem.getCoverage())) { metadata.setCoverage(qitem.getCoverage(), lang); } //educational if(qitem.getEducationalContext() != null) { String level = qitem.getEducationalContext().getLevel(); metadata.setEducationalContext(level, lang); } if(qitem.getEducationalLearningTime() != null) { String time = qitem.getEducationalLearningTime(); metadata.setEducationalLearningTime(time); } if(qitem.getLanguage() != null) { String language = qitem.getLanguage(); metadata.setLanguage(language, lang); } //classification if(qitem.getTaxonomicPath() != null) { metadata.setClassificationTaxonomy(qitem.getTaxonomicPath(), lang); } //life-cycle if(StringHelper.containsNonWhitespace(qitem.getItemVersion())) { metadata.setLifecycleVersion(qitem.getItemVersion()); } // rights if(qitem.getLicense() != null && StringHelper.containsNonWhitespace(qitem.getLicense().getLicenseText())) { metadata.setLicense(qitem.getLicense().getLicenseText()); } //qti metadata if(StringHelper.containsNonWhitespace(qitem.getEditor()) || StringHelper.containsNonWhitespace(qitem.getEditorVersion())) { metadata.setQtiMetadataTool(qitem.getEditor(), null, qitem.getEditorVersion()); } if(resolvedAssessmentItem != null) { AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); List<Interaction> interactions = assessmentItem.getItemBody().findInteractions(); List<String> interactionNames = new ArrayList<>(interactions.size()); for(Interaction interaction:interactions) { interactionNames.add(interaction.getQtiClassName()); } metadata.setQtiMetadata(interactionNames); } //openolat metadata metadata.setOpenOLATMetadataQuestionType(qitem.getItemType()); if(qitem.getAssessmentType() != null) {//summative, formative, both metadata.setOpenOLATMetadataAssessmentType(qitem.getAssessmentType()); } if(qitem.getDifficulty() != null) { metadata.setOpenOLATMetadataMasterDifficulty(qitem.getDifficulty().doubleValue()); } if(qitem.getDifferentiation() != null) { metadata.setOpenOLATMetadataMasterDiscriminationIndex(qitem.getDifferentiation().doubleValue()); } if(qitem.getNumOfAnswerAlternatives() >= 0) { metadata.setOpenOLATMetadataMasterDistractors(qitem.getNumOfAnswerAlternatives()); } if(qitem.getStdevDifficulty() != null) { metadata.setOpenOLATMetadataMasterStandardDeviation(qitem.getStdevDifficulty().doubleValue()); } if(qitem.getUsage() >= 0) { metadata.setOpenOLATMetadataUsage(qitem.getUsage()); } if(StringHelper.containsNonWhitespace(qitem.getMasterIdentifier())) { metadata.setOpenOLATMetadataMasterIdentifier(qitem.getMasterIdentifier()); } } }