/** * <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.qpool; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.commons.io.IOUtils; import org.cyberneko.html.parsers.SAXParser; import org.dom4j.Attribute; import org.dom4j.CDATA; import org.dom4j.Document; import org.dom4j.DocumentFactory; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.Node; import org.dom4j.io.OutputFormat; import org.dom4j.io.XMLWriter; 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.ZipUtil; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.VFSLeaf; import org.olat.core.util.vfs.VFSManager; import org.olat.core.util.xml.XMLParser; import org.olat.ims.qti.QTIConstants; import org.olat.ims.qti.editor.QTIEditHelper; import org.olat.ims.resources.IMSEntityResolver; import org.olat.modules.qpool.QuestionItemFull; import org.olat.modules.qpool.manager.QPoolFileStorage; import org.xml.sax.InputSource; /** * * Initial date: 11.03.2013<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class QTIExportProcessor { private static final OLog log = Tracing.createLoggerFor(QTIExportProcessor.class); private final QPoolFileStorage qpoolFileStorage; public QTIExportProcessor(QPoolFileStorage qpoolFileStorage) { this.qpoolFileStorage = qpoolFileStorage; } public void process(QuestionItemFull fullItem, ZipOutputStream zout, Set<String> names) { String dir = fullItem.getDirectory(); VFSContainer container = qpoolFileStorage.getContainer(dir); String rootDir = "qitem_" + fullItem.getKey(); List<VFSItem> items = container.getItems(); addMetadata(fullItem, rootDir, zout); for(VFSItem item:items) { ZipUtil.addToZip(item, rootDir, zout); } } private void addMetadata(QuestionItemFull fullItem, String dir, ZipOutputStream zout) { try { Document document = DocumentHelper.createDocument(); Element qtimetadata = document.addElement("qtimetadata"); QTIMetadataConverter enricher = new QTIMetadataConverter(qtimetadata); enricher.toXml(fullItem); zout.putNextEntry(new ZipEntry(dir + "/" + "qitem_" + fullItem.getKey() + "_metadata.xml")); OutputFormat format = OutputFormat.createPrettyPrint(); XMLWriter writer = new XMLWriter(zout, format); writer.write(document); } catch (IOException e) { log.error("", e); } } /** * <li>List all items * <li>Rewrite path * <li>Assemble qti.xml * <li>Write files at new path * @param fullItems * @param zout */ public void assembleTest(List<QuestionItemFull> fullItems, ZipOutputStream zout) { ItemsAndMaterials itemAndMaterials = new ItemsAndMaterials(); for(QuestionItemFull fullItem:fullItems) { collectMaterials(fullItem, itemAndMaterials); } try { byte[] buffer = new byte[FileUtils.BSIZE]; //write qti.xml Element sectionEl = createSectionBasedAssessment("Assessment"); for(Element itemEl:itemAndMaterials.getItemEls()) { //generate new ident per item String ident = getAttributeValue(itemEl, "ident"); String exportIdent = QTIEditHelper.generateNewIdent(ident); itemEl.addAttribute("ident", exportIdent); sectionEl.add(itemEl); } zout.putNextEntry(new ZipEntry("qti.xml")); XMLWriter xw = new XMLWriter(zout, new OutputFormat(" ", true)); xw.write(sectionEl.getDocument()); zout.closeEntry(); //write materials for(ItemMaterial material:itemAndMaterials.getMaterials()) { String exportPath = material.getExportUri(); zout.putNextEntry(new ZipEntry(exportPath)); InputStream in = material.getLeaf().getInputStream(); int c; while ((c = in.read(buffer, 0, buffer.length)) != -1) { zout.write(buffer, 0, c); } IOUtils.closeQuietly(in); zout.closeEntry(); } } catch (IOException e) { log.error("", e); } } public Element exportToQTIEditor(QuestionItemFull fullItem, VFSContainer editorContainer) { ItemsAndMaterials itemAndMaterials = new ItemsAndMaterials(); collectMaterials(fullItem, itemAndMaterials); if(itemAndMaterials.getItemEls().isEmpty()) { return null;//nothing found } Element itemEl = itemAndMaterials.getItemEls().get(0); //write materials for(ItemMaterial material:itemAndMaterials.getMaterials()) { String exportPath = material.getExportUri(); VFSLeaf leaf = editorContainer.createChildLeaf(exportPath); VFSManager.copyContent(material.getLeaf(), leaf); } return itemEl; } protected void collectMaterials(QuestionItemFull fullItem, ItemsAndMaterials materials) { String dir = fullItem.getDirectory(); String rootFilename = fullItem.getRootFilename(); VFSContainer container = qpoolFileStorage.getContainer(dir); VFSItem rootItem = container.resolve(rootFilename); if(rootItem instanceof VFSLeaf) { VFSLeaf rootLeaf = (VFSLeaf)rootItem; Element el = (Element)readItemXml(rootLeaf).clone(); Element itemEl = (Element)el.clone(); //enrichScore(itemEl); enrichWithMetadata(fullItem, itemEl); collectResources(itemEl, container, materials); materials.addItemEl(itemEl); } } private String getAttributeValue(Element el, String attrName) { if(el == null) return null; Attribute attr = el.attribute(attrName); return (attr == null) ? null : attr.getStringValue(); } private void collectResources(Element el, VFSContainer container, ItemsAndMaterials materials) { collectResourcesInMatText(el, container, materials); collectResourcesInMatMedias(el, container, materials); } /** * Collect the file and rewrite the * @param el * @param container * @param materials * @param paths */ private void collectResourcesInMatText(Element el, VFSContainer container, ItemsAndMaterials materials) { //mattext @SuppressWarnings("unchecked") List<Element> mattextList = el.selectNodes(".//mattext"); for(Element mat:mattextList) { Attribute texttypeAttr = mat.attribute("texttype"); String texttype = texttypeAttr.getValue(); if("text/html".equals(texttype)) { @SuppressWarnings("unchecked") List<Node> childElList = new ArrayList<Node>(mat.content()); for(Node childEl:childElList) { mat.remove(childEl); } for(Node childEl:childElList) { if(Node.CDATA_SECTION_NODE == childEl.getNodeType()) { CDATA data = (CDATA)childEl; boolean changed = false; String text = data.getText(); List<String> materialPaths = findMaterialInMatText(text); for(String materialPath:materialPaths) { VFSItem matVfsItem = container.resolve(materialPath); if(matVfsItem instanceof VFSLeaf) { String exportUri = generateExportPath(materials.getPaths(), matVfsItem); materials.addMaterial(new ItemMaterial((VFSLeaf)matVfsItem, exportUri)); text = text.replaceAll(materialPath, exportUri); changed = true; } } if(changed) { mat.addCDATA(text); } else { mat.add(childEl); } } else { mat.add(childEl); } } } } } @SuppressWarnings("unchecked") private void collectResourcesInMatMedias(Element el, VFSContainer container, ItemsAndMaterials materials) { //matimage uri List<Element> matList = new ArrayList<Element>(); matList.addAll(el.selectNodes(".//matimage")); matList.addAll(el.selectNodes(".//mataudio")); matList.addAll(el.selectNodes(".//matvideo")); for(Element mat:matList) { Attribute uriAttr = mat.attribute("uri"); String uri = uriAttr.getValue(); VFSItem matVfsItem = container.resolve(uri); if(matVfsItem instanceof VFSLeaf) { String exportUri = generateExportPath(materials.getPaths(), matVfsItem); ItemMaterial iMat = new ItemMaterial((VFSLeaf)matVfsItem, exportUri); materials.addMaterial(iMat); mat.addAttribute("uri", exportUri); } } } private String generateExportPath(Set<String> paths, VFSItem leaf) { String filename = leaf.getName(); for(int count=0; paths.contains(filename) && count < 999 ; ) { filename = FileUtils.appendNumberAtTheEndOfFilename(filename, count++); } paths.add(filename); return "media/" + filename; } /** * Parse the content and collect the images source * @param content * @param materialPath */ private List<String> findMaterialInMatText(String content) { try { SAXParser parser = new SAXParser(); QTI12HtmlHandler contentHandler = new QTI12HtmlHandler(); parser.setContentHandler(contentHandler); parser.parse(new InputSource(new StringReader(content))); return contentHandler.getMaterialPath(); } catch (Exception e) { log.error("", e); return Collections.emptyList(); } } private Element createSectionBasedAssessment(String title) { DocumentFactory df = DocumentFactory.getInstance(); Document doc = df.createDocument(); doc.addDocType(QTIConstants.XML_DOCUMENT_ROOT, null, QTIConstants.XML_DOCUMENT_DTD); /* <questestinterop> <assessment ident="frentix_9_87230240084930" title="SR Test"> */ Element questestinterop = doc.addElement(QTIConstants.XML_DOCUMENT_ROOT); Element assessment = questestinterop.addElement("assessment"); assessment.addAttribute("ident", CodeHelper.getGlobalForeverUniqueID()); assessment.addAttribute("title", title); //metadata /* <qtimetadata> <qtimetadatafield> <fieldlabel>qmd_assessmenttype</fieldlabel> <fieldentry>Assessment</fieldentry> </qtimetadatafield> </qtimetadata> */ Element qtimetadata = assessment.addElement("qtimetadata"); addMetadataField("qmd_assessmenttype", "Assessment", qtimetadata); //section /* <section ident="frentix_9_87230240084931" title="Section"> <selection_ordering> <selection/> <order order_type="Sequential"/> </selection_ordering> */ Element section = assessment.addElement("section"); section.addAttribute("ident", CodeHelper.getGlobalForeverUniqueID()); section.addAttribute("title", "Section"); Element selectionOrdering = section.addElement("selection_ordering"); selectionOrdering.addElement("selection"); Element order = selectionOrdering.addElement("order"); order.addAttribute("order_type", "Sequential"); return section; } private void addMetadataField(String label, String entry, Element qtimetadata) { if(entry != null) { Element qtimetadatafield = qtimetadata.addElement("qtimetadatafield"); qtimetadatafield.addElement("fieldlabel").setText(label); qtimetadatafield.addElement("fieldentry").setText(entry); } } private Element readItemXml(VFSLeaf leaf) { Document doc = null; try { InputStream is = leaf.getInputStream(); XMLParser xmlParser = new XMLParser(new IMSEntityResolver()); doc = xmlParser.parse(is, false); Element item = (Element)doc.selectSingleNode("questestinterop/item"); is.close(); return item; } catch (Exception e) { log.error("", e); return null; } } /** * OpenOLAT QTI 1.2 runtime need it: <resprocessing> <outcomes> <decvar varname="SCORE" vartype="Decimal" defaultval="0" minvalue="0.0" maxvalue="1.0" cutvalue="1.0"/> </outcomes> * @param fullItem * @param item */ protected void enrichScoreDontUseIt(Element item) { @SuppressWarnings("unchecked") List<Element> sv = item.selectNodes("./resprocessing/outcomes/decvar[@varname='SCORE']"); // the QTIv1.2 system relies on the SCORE variable of items if (sv.isEmpty()) { //create resprocessing if needed Element resprocessing; if(item.selectNodes("./resprocessing").isEmpty()) { resprocessing = item.addElement("resprocessing"); } else { resprocessing = (Element)item.selectNodes("./resprocessing").get(0); } //create outcomes if needed Element outcomes; if(resprocessing.selectNodes("./outcomes").isEmpty()) { outcomes = resprocessing.addElement("outcomes"); } else { outcomes = (Element)resprocessing.selectNodes("./outcomes").get(0); } //create decvar if needed Element decvar = outcomes.addElement("decvar"); decvar.addAttribute("varname", "SCORE"); decvar.addAttribute("vartype", "Decimal"); decvar.addAttribute("defaultval", "0"); decvar.addAttribute("minvalue", "0.0"); decvar.addAttribute("maxvalue", "1.0"); decvar.addAttribute("cutvalue", "1.0"); } } private void enrichWithMetadata(QuestionItemFull fullItem, Element item) { Element qtimetadata = (Element)item.selectSingleNode("./itemmetadata/qtimetadata"); if(qtimetadata != null) { QTIMetadataConverter enricher = new QTIMetadataConverter(qtimetadata); enricher.toXml(fullItem); } } private static final class ItemsAndMaterials { private final Set<String> paths = new HashSet<String>(); private final List<Element> itemEls = new ArrayList<Element>(); private final List<ItemMaterial> materials = new ArrayList<ItemMaterial>(); public Set<String> getPaths() { return paths; } public List<Element> getItemEls() { return itemEls; } public void addItemEl(Element el) { itemEls.add(el); } public List<ItemMaterial> getMaterials() { return materials; } public void addMaterial(ItemMaterial material) { materials.add(material); } } private static final class ItemMaterial { private final VFSLeaf leaf; private final String exportUri; public ItemMaterial(VFSLeaf leaf, String exportUri) { this.leaf = leaf; this.exportUri = exportUri; } public VFSLeaf getLeaf() { return leaf; } public String getExportUri() { return exportUri; } } }