diff --git a/src/main/java/org/olat/core/util/PathUtils.java b/src/main/java/org/olat/core/util/PathUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c49169774e39594770724a615e383e89e87555ba --- /dev/null +++ b/src/main/java/org/olat/core/util/PathUtils.java @@ -0,0 +1,105 @@ +/** + * <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.core.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * + * Initial date: 08.05.2014<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class PathUtils { + + public static Path visit(File file, String filename, FileVisitor<Path> visitor) + throws IOException { + if(!StringHelper.containsNonWhitespace(filename)) { + filename = file.getName(); + } + + Path fPath = null; + if(file.isDirectory()) { + fPath = file.toPath(); + } else if(filename != null && filename.toLowerCase().endsWith(".zip")) { + fPath = FileSystems.newFileSystem(file.toPath(), null).getPath("/"); + } else { + fPath = file.toPath(); + } + if(fPath != null) { + Files.walkFileTree(fPath, visitor); + } + return fPath; + } + + public static class YesMatcher implements PathMatcher { + @Override + public boolean matches(Path path) { + return true; + } + } + + public static class CopyVisitor extends SimpleFileVisitor<Path> { + + private final Path source; + private final Path destDir; + private final PathMatcher filter; + + public CopyVisitor(Path source, Path destDir, PathMatcher filter) { + this.source = source; + this.destDir = destDir; + this.filter = filter; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Path relativeFile = source.relativize(file); + final Path destFile = Paths.get(destDir.toString(), relativeFile.toString()); + if(filter.matches(file)) { + Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + Path relativeDir = source.relativize(dir); + final Path dirToCreate = Paths.get(destDir.toString(), relativeDir.toString()); + if(Files.notExists(dirToCreate)){ + Files.createDirectory(dirToCreate); + } + return FileVisitResult.CONTINUE; + } + } + +} diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIExportProcessor.java b/src/main/java/org/olat/ims/qti/qpool/QTIExportProcessor.java index edf45415ba590f4e81b5c04a7d92fdf191fc0ad4..68e013cf7bfacffccf140875191999185440cb45 100644 --- a/src/main/java/org/olat/ims/qti/qpool/QTIExportProcessor.java +++ b/src/main/java/org/olat/ims/qti/qpool/QTIExportProcessor.java @@ -36,6 +36,7 @@ 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; @@ -83,10 +84,26 @@ public class QTIExportProcessor { String rootDir = "qitem_" + fullItem.getKey(); List<VFSItem> items = container.getItems(); for(VFSItem item:items) { + addMetadata(fullItem, rootDir, zout); ZipUtil.addToZip(item, rootDir, zout); } } + private void addMetadata(QuestionItemFull fullItem, String dir, ZipOutputStream zout) { + try { + Document document = DocumentHelper.createDocument(); + Element qtimetadata = document.addElement("qtimetadata"); + QTIMetadata enricher = new QTIMetadata(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 @@ -294,7 +311,7 @@ public class QTIExportProcessor { //metadata /* <qtimetadata> - <qtimetadatafield> + <qtimetadatafield> <fieldlabel>qmd_assessmenttype</fieldlabel> <fieldentry>Assessment</fieldentry> </qtimetadatafield> @@ -320,6 +337,14 @@ public class QTIExportProcessor { 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 { @@ -379,100 +404,10 @@ public class QTIExportProcessor { private void enrichWithMetadata(QuestionItemFull fullItem, Element item) { Element qtimetadata = (Element)item.selectSingleNode("./itemmetadata/qtimetadata"); - String path = fullItem.getTaxonomicPath(); + QTIMetadata enricher = new QTIMetadata(qtimetadata); + enricher.toXml(fullItem); } - private void addMetadataField(String label, String entry, Element qtimetadata) { - Element qtimetadatafield = qtimetadata.addElement("qtimetadatafield"); - qtimetadatafield.addElement("fieldlabel").setText(label); - qtimetadatafield.addElement("fieldentry").setText(entry); - } - - /* - * - * <itemmetadata> - <qtimetadata> - <qtimetadatafield> - <fieldlabel>qmd_levelofdifficulty</fieldlabel> - <fieldentry>basic</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>qmd_topic</fieldlabel> - <fieldentry>qtiv1p2test</fieldentry> - </qtimetadatafield> - </qtimetadata> - </itemmetadata> - - <qtimetadata> - <vocabulary uri="imsqtiv1p2_metadata.txt" vocab_type="text/plain"/> - <qtimetadatafield> - <fieldlabel>qmd_weighting</fieldlabel> - <fieldentry>2</fieldentry> - </qtimetadatafield> - ... - </qtimetadata> - - - - http://qtimigration.googlecode.com/svn-history/r29/trunk/pyslet/unittests/data_imsqtiv1p2p1/input/ - - -<qtimetadatafield> - <fieldlabel>name</fieldlabel> - <fieldentry>Metadata New-Style</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>marks</fieldlabel> - <fieldentry>50.0</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>syllabusarea</fieldlabel> - <fieldentry>Migration</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>author</fieldlabel> - <fieldentry>Steve Author</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>creator</fieldlabel> - <fieldentry>Steve Creator</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>owner</fieldlabel> - <fieldentry>Steve Owner</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>item type</fieldlabel> - <fieldentry>MCQ</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>status</fieldlabel> - <fieldentry>Experimental</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>qmd_levelofdifficulty</fieldlabel> - <fieldentry>Professional Development</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>qmd_toolvendor</fieldlabel> - <fieldentry>Steve Lay</fieldentry> - </qtimetadatafield> - <qtimetadatafield> - <fieldlabel>description</fieldlabel> - <fieldentry>General Description Extension</fieldentry> - </qtimetadatafield> - - - <itemmetadata> - <qmd_itemtype>MCQ</qmd_itemtype> - <qmd_levelofdifficulty>Professional Development</qmd_levelofdifficulty> - <qmd_maximumscore>50.0</qmd_maximumscore> - <qmd_status>Experimental</qmd_status> - <qmd_toolvendor>Steve Lay</qmd_toolvendor> - <qmd_topic>Migration</qmd_topic> - </itemmetadata> - */ - private static final class HTMLHandler extends DefaultHandler { private final List<String> materialPath = new ArrayList<String>(); diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java b/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java index 57e9250aeaeac7a767a16c891d827134eaaac246..ecdd8967f01c998d36d5514f1bb7bddaa2070da2 100644 --- a/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java +++ b/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java @@ -27,7 +27,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; +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.Collections; import java.util.List; import java.util.Locale; import java.util.zip.ZipEntry; @@ -40,13 +47,18 @@ import org.dom4j.Document; import org.dom4j.DocumentFactory; import org.dom4j.Element; import org.dom4j.io.OutputFormat; +import org.dom4j.io.SAXReader; import org.dom4j.io.XMLWriter; +import org.olat.core.commons.persistence.DB; import org.olat.core.id.Identity; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.FileUtils; +import org.olat.core.util.PathUtils.CopyVisitor; +import org.olat.core.util.PathUtils.YesMatcher; import org.olat.core.util.StringHelper; import org.olat.core.util.ZipUtil; +import org.olat.core.util.vfs.LocalImpl; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSLeaf; import org.olat.core.util.xml.XMLParser; @@ -56,10 +68,11 @@ import org.olat.ims.qti.editor.beecom.parser.ItemParser; import org.olat.ims.resources.IMSEntityResolver; import org.olat.modules.qpool.QuestionItem; import org.olat.modules.qpool.QuestionType; -import org.olat.modules.qpool.manager.QPoolFileStorage; import org.olat.modules.qpool.manager.QEducationalContextDAO; import org.olat.modules.qpool.manager.QItemTypeDAO; +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.QuestionItemImpl; @@ -86,20 +99,25 @@ class QTIImportProcessor { private final String importedFilename; private final File importedFile; + private final DB dbInstance; private final QItemTypeDAO qItemTypeDao; private final QPoolFileStorage qpoolFileStorage; private final QuestionItemDAO questionItemDao; + private final TaxonomyLevelDAO taxonomyLevelDao; private final QEducationalContextDAO qEduContextDao; public QTIImportProcessor(Identity owner, Locale defaultLocale, QuestionItemDAO questionItemDao, - QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao, QPoolFileStorage qpoolFileStorage) { - this(owner, defaultLocale, null, null, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao, + TaxonomyLevelDAO taxonomyLevelDao, QPoolFileStorage qpoolFileStorage, DB dbInstance) { + this(owner, defaultLocale, null, null, questionItemDao, qItemTypeDao, qEduContextDao, + taxonomyLevelDao, qpoolFileStorage, dbInstance); } public QTIImportProcessor(Identity owner, Locale defaultLocale, String importedFilename, File importedFile, QuestionItemDAO questionItemDao, QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao, - QPoolFileStorage qpoolFileStorage) { + TaxonomyLevelDAO taxonomyLevelDao, QPoolFileStorage qpoolFileStorage, DB dbInstance) { this.owner = owner; + this.dbInstance = dbInstance; this.defaultLocale = defaultLocale; this.importedFilename = importedFilename; this.importedFile = importedFile; @@ -107,20 +125,18 @@ class QTIImportProcessor { this.questionItemDao = questionItemDao; this.qEduContextDao = qEduContextDao; this.qpoolFileStorage = qpoolFileStorage; + this.taxonomyLevelDao = taxonomyLevelDao; } public List<QuestionItem> process() { List<QuestionItem> qItems = new ArrayList<QuestionItem>(); try { - DocInfos docInfos = getDocInfos(); - if(docInfos != null && docInfos.doc != null) { - List<ItemInfos> itemInfos = getItemList(docInfos); - for(ItemInfos itemInfo:itemInfos) { - QuestionItemImpl qItem = processItem(docInfos, itemInfo); - if(qItem != null) { - processFiles(qItem, itemInfo); - qItems.add(qItem); - } + List<DocInfos> docInfoList = getDocInfos(); + if(docInfoList != null) { + for(DocInfos docInfos:docInfoList) { + List<QuestionItem> processdItems = process(docInfos); + qItems.addAll(processdItems); + dbInstance.commit(); } } } catch (IOException e) { @@ -128,6 +144,22 @@ class QTIImportProcessor { } return qItems; } + + private List<QuestionItem> process(DocInfos docInfos) { + List<QuestionItem> qItems = new ArrayList<>(); + if(docInfos.doc != null) { + List<ItemInfos> itemInfos = getItemList(docInfos); + for(ItemInfos itemInfo:itemInfos) { + QuestionItemImpl qItem = processItem(docInfos, itemInfo); + if(qItem != null) { + processFiles(qItem, itemInfo, docInfos); + qItem = questionItemDao.merge(qItem); + qItems.add(qItem); + } + } + } + return qItems; + } protected List<ItemInfos> getItemList(DocInfos doc) { Document document = doc.getDocument(); @@ -157,10 +189,11 @@ class QTIImportProcessor { if(itemInfos.isOriginalItem()) { originalFilename = docInfos.filename; } - return processItem(itemEl, comment, originalFilename, null, null); + return processItem(itemEl, comment, originalFilename, null, null, docInfos); } - protected QuestionItemImpl processItem(Element itemEl, String comment, String originalItemFilename, String editor, String editorVersion) { + protected QuestionItemImpl processItem(Element itemEl, String comment, String originalItemFilename, + String editor, String editorVersion, DocInfos docInfos) { //filename String filename; String ident = getAttributeValue(itemEl, "ident"); @@ -181,6 +214,7 @@ class QTIImportProcessor { if(!StringHelper.containsNonWhitespace(title)) { title = importedFilename; } + QuestionItemImpl poolItem = questionItemDao.create(title, QTIConstants.QTI_12_FORMAT, dir, filename); //description poolItem.setDescription(comment); @@ -200,6 +234,9 @@ class QTIImportProcessor { QItemType defType = qItemTypeDao.loadByType(QuestionType.UNKOWN.name()); poolItem.setType(defType); } + if(docInfos != null) { + processSidecarMetadata(poolItem, docInfos); + } questionItemDao.persist(owner, poolItem); return poolItem; } @@ -284,9 +321,9 @@ class QTIImportProcessor { * @param item * @param itemEl */ - protected void processFiles(QuestionItemImpl item, ItemInfos itemInfos) { + protected void processFiles(QuestionItemImpl item, ItemInfos itemInfos, DocInfos docInfos) { if(itemInfos.originalItem) { - processItemFiles(item); + processItemFiles(item, docInfos); } else { //an assessment package processAssessmentFiles(item, itemInfos); @@ -469,12 +506,22 @@ class QTIImportProcessor { * @param item * @param itemInfos */ - protected void processItemFiles(QuestionItemImpl item) { + protected void processItemFiles(QuestionItemImpl item, DocInfos docInfos) { //a package with an item String dir = item.getDirectory(); String rootFilename = item.getRootFilename(); VFSContainer container = qpoolFileStorage.getContainer(dir); - if(importedFilename.toLowerCase().endsWith(".zip")) { + + if(docInfos != null && docInfos.getRoot() != null) { + try { + Path destDir = ((LocalImpl)container).getBasefile().toPath(); + //unzip to container + Path path = docInfos.getRoot(); + Files.walkFileTree(path, new CopyVisitor(path, destDir, new YesMatcher())); + } catch (IOException e) { + log.error("", e); + } + } else if(importedFilename.toLowerCase().endsWith(".zip")) { ZipUtil.unzipStrict(importedFile, container); } else { VFSLeaf endFile = container.createChildLeaf(rootFilename); @@ -494,6 +541,23 @@ class QTIImportProcessor { } } + private boolean processSidecarMetadata(QuestionItemImpl item, DocInfos docInfos) { + try { + Path path = docInfos.root; + Path metadata = path.resolve(path.getFileName().toString() + "_metadata.xml"); + InputStream metadataIn = Files.newInputStream(metadata); + SAXReader reader = new SAXReader(); + Document document = reader.read(metadataIn); + Element rootElement = document.getRootElement(); + QTIMetadata enricher = new QTIMetadata(rootElement, qItemTypeDao, taxonomyLevelDao, qEduContextDao); + enricher.toQuestion(item); + return true; + } catch (Exception e) { + log.error("", e); + return false; + } + } + private boolean processItemQuestionType(QuestionItemImpl poolItem, String ident, Element itemEl) { boolean openolatFormat = false; @@ -545,12 +609,13 @@ class QTIImportProcessor { return el.getText(); } - protected DocInfos getDocInfos() throws IOException { - DocInfos doc; + protected List<DocInfos> getDocInfos() throws IOException { + List<DocInfos> doc; if(importedFilename.toLowerCase().endsWith(".zip")) { - doc = traverseZip(importedFile); + //doc = traverseZip(importedFile); + doc = traverseZip_nio(importedFile); } else { - doc = traverseFile(importedFile); + doc = Collections.singletonList(traverseFile(importedFile)); } return doc; } @@ -574,32 +639,81 @@ class QTIImportProcessor { } } - private DocInfos traverseZip(File file) throws IOException { + /* + private List<DocInfos> traverseZip(File file) throws IOException { InputStream in = new FileInputStream(file); ZipInputStream zis = new ZipInputStream(in); + List<DocInfos> docInfos = new ArrayList<>(); ZipEntry entry; try { while ((entry = zis.getNextEntry()) != null) { String name = entry.getName(); if(name != null && name.toLowerCase().endsWith(".xml")) { - Document doc = readXml(zis); + Document doc = readXml(new ShieldInputStream(zis)); if(doc != null) { DocInfos d = new DocInfos(); d.doc = doc; d.filename = name; - return d; + docInfos.add(d); } } } - return null; } catch(Exception e) { log.error("", e); - return null; } finally { IOUtils.closeQuietly(zis); IOUtils.closeQuietly(in); } + return docInfos; + } + */ + + private List<DocInfos> traverseZip_nio(File file) throws IOException { + List<DocInfos> docInfos = new ArrayList<>(); + + Path fPath = FileSystems.newFileSystem(file.toPath(), null).getPath("/"); + if(fPath != null) { + DocInfosVisitor visitor = new DocInfosVisitor(); + Files.walkFileTree(fPath, visitor); + + List<Path> xmlFiles = visitor.getXmlFiles(); + for(Path xmlFile:xmlFiles) { + InputStream in = Files.newInputStream(xmlFile); + + Document doc = readXml(in); + if(doc != null) { + DocInfos d = new DocInfos(); + d.setDocument(doc); + d.setRoot(xmlFile.getParent()); + d.setFilename(xmlFile.getFileName().toString()); + docInfos.add(d); + } + + } + } + + + return docInfos; + } + + public static class DocInfosVisitor extends SimpleFileVisitor<Path> { + + private final List<Path> xmlFiles = new ArrayList<>(); + + public List<Path> getXmlFiles() { + return xmlFiles; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + String name = file.getFileName().toString(); + if(name != null && name.toLowerCase().endsWith(".xml")) { + xmlFiles.add(file); + } + return FileVisitResult.CONTINUE; + } } private Document readXml(InputStream in) { @@ -643,6 +757,8 @@ class QTIImportProcessor { public static class DocInfos { private Document doc; private String filename; + private Path root; + private Path metadata; private String qtiComment; public String getFilename() { @@ -661,6 +777,22 @@ class QTIImportProcessor { this.doc = doc; } + public Path getMetadata() { + return metadata; + } + + public void setMetadata(Path metadata) { + this.metadata = metadata; + } + + public Path getRoot() { + return root; + } + + public void setRoot(Path root) { + this.root = root; + } + public String getQtiComment() { return qtiComment; } diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIMetadata.java b/src/main/java/org/olat/ims/qti/qpool/QTIMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..75a886f79ccc740f1b279f68bdaf630c39fc4ff5 --- /dev/null +++ b/src/main/java/org/olat/ims/qti/qpool/QTIMetadata.java @@ -0,0 +1,306 @@ +/** + * <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.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.dom4j.Element; +import org.olat.core.util.StringHelper; +import org.olat.modules.qpool.QuestionItemFull; +import org.olat.modules.qpool.QuestionStatus; +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.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; + +/** + * + * Initial date: 10.09.2014<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +class QTIMetadata { + + private Element qtimetadata; + + private QItemTypeDAO itemTypeDao; + private TaxonomyLevelDAO taxonomyLevelDao; + private QEducationalContextDAO educationalContextDao; + + QTIMetadata(Element qtimetadata) { + this.qtimetadata = qtimetadata; + } + + QTIMetadata(Element qtimetadata, QItemTypeDAO itemTypeDao, + TaxonomyLevelDAO taxonomyLevelDao, QEducationalContextDAO educationalContextDao) { + this.qtimetadata = qtimetadata; + this.itemTypeDao = itemTypeDao; + this.taxonomyLevelDao = taxonomyLevelDao; + this.educationalContextDao = educationalContextDao; + } + + private QItemType toType(String itemType) { + QItemType type = itemTypeDao.loadByType(itemType); + if(type == null) { + type = itemTypeDao.create(itemType, true); + } + return type; + } + + private QLicense toLicense(String str) { + return null; + } + + private TaxonomyLevel toTaxonomy(String str) { + String[] path = str.split("/"); + List<String> cleanedPath = new ArrayList<>(path.length); + for(String segment:path) { + if(StringHelper.containsNonWhitespace(segment)) { + cleanedPath.add(segment); + } + } + + TaxonomyLevel lowerLevel = null; + if(path != null && path.length > 0) { + for(String field :cleanedPath) { + TaxonomyLevel level = taxonomyLevelDao.loadLevelBy(lowerLevel, field); + if(level == null) { + level = taxonomyLevelDao.createAndPersist(lowerLevel, field); + } + lowerLevel = level; + } + } + return lowerLevel; + } + + private QEducationalContext toEducationalContext(String txt) { + QEducationalContext context = educationalContextDao.loadByLevel(txt); + if(context == null) { + context = educationalContextDao.create(txt, true); + } + return context; + } + + protected void toQuestion(QuestionItemImpl fullItem) { + String addInfos = this.getMetadataEntry("additional_informations"); + if(StringHelper.containsNonWhitespace(addInfos)) { + fullItem.setAdditionalInformations(addInfos); + } + String assessmentType = getMetadataEntry("oo_assessment_type"); + if(StringHelper.containsNonWhitespace(assessmentType)) { + fullItem.setAssessmentType(assessmentType); + } + String coverage = getMetadataEntry("coverage"); + if(StringHelper.containsNonWhitespace(coverage)) { + fullItem.setCoverage(coverage); + } + String description = getMetadataEntry("description"); + if(description != null) { + fullItem.setDescription(description); + } + String differentiation = getMetadataEntry("oo_differentiation"); + if(StringHelper.containsNonWhitespace(differentiation)) { + fullItem.setDifferentiation(toBigDecimal(differentiation)); + } + String difficulty = getMetadataEntry("qmd_levelofdifficulty"); + if(StringHelper.containsNonWhitespace(difficulty)) { + fullItem.setDifficulty(toBigDecimal(difficulty)); + } + String vendor = getMetadataEntry("qmd_toolvendor"); + if(vendor != null) { + fullItem.setEditor(vendor); + } + String editorVersion = getMetadataEntry("oo_toolvendor_version"); + if(StringHelper.containsNonWhitespace(editorVersion)) { + fullItem.setEditorVersion(editorVersion); + } + String learningTime = getMetadataEntry("oo_education_learning_time"); + if(StringHelper.containsNonWhitespace(learningTime)) { + fullItem.setEducationalLearningTime(learningTime); + } + String format = getMetadataEntry("format"); + if(StringHelper.containsNonWhitespace(format)) { + fullItem.setFormat(format); + } + String identifier = getMetadataEntry("oo_identifier"); + if(StringHelper.containsNonWhitespace(identifier)) { + fullItem.setMasterIdentifier(identifier); + } + String itemType = getMetadataEntry("type"); + if(StringHelper.containsNonWhitespace(itemType)) { + fullItem.setType(toType(itemType)); + } + String version = getMetadataEntry("version"); + if(StringHelper.containsNonWhitespace(version)) { + fullItem.setItemVersion(version); + } + String keywords = getMetadataEntry("keywords"); + if(StringHelper.containsNonWhitespace(keywords)) { + fullItem.setKeywords(keywords); + } + String language = getMetadataEntry("language"); + if(StringHelper.containsNonWhitespace(language)) { + fullItem.setLanguage(language); + } + String numOfAnswers = getMetadataEntry("oo_num_of_answer_alternatives"); + if(StringHelper.containsNonWhitespace(numOfAnswers)) { + fullItem.setNumOfAnswerAlternatives(toInt(numOfAnswers)); + } + String status = getMetadataEntry("status"); + if(StringHelper.containsNonWhitespace(status) && validStatus(status)) { + fullItem.setStatus(status); + } + String stdDevDifficulty = getMetadataEntry("oo_std_dev_difficulty"); + if(StringHelper.containsNonWhitespace(stdDevDifficulty)) { + fullItem.setStdevDifficulty(toBigDecimal(stdDevDifficulty)); + } + String title = getMetadataEntry("title"); + if(StringHelper.containsNonWhitespace(title)) { + fullItem.setTitle(title); + } + String license = getMetadataEntry("license"); + if(StringHelper.containsNonWhitespace(license)) { + fullItem.setLicense(toLicense(license)); + } + String taxonomy = getMetadataEntry("oo_taxonomy"); + if(StringHelper.containsNonWhitespace(taxonomy)) { + fullItem.setTaxonomyLevel(toTaxonomy(taxonomy)); + } + String educationalContext = getMetadataEntry("oo_educational_context"); + if(StringHelper.containsNonWhitespace(educationalContext)) { + fullItem.setEducationalContext(toEducationalContext(educationalContext)); + } + } + + protected void toXml(QuestionItemFull fullItem) { + addMetadataField("additional_informations", fullItem.getAdditionalInformations(), qtimetadata); + addMetadataField("oo_assessment_type", fullItem.getAssessmentType(), qtimetadata); + addMetadataField("coverage", fullItem.getCoverage(), qtimetadata); + addMetadataField("description", fullItem.getDescription(), qtimetadata); + addMetadataField("oo_differentiation", fullItem.getDifferentiation(), qtimetadata); + addMetadataField("qmd_levelofdifficulty", fullItem.getDifficulty(), qtimetadata); + addMetadataField("qmd_toolvendor", fullItem.getEditor(), qtimetadata); + addMetadataField("oo_toolvendor_version", fullItem.getEditorVersion(), qtimetadata); + addMetadataField("oo_educational_context", fullItem.getEducationalContext(), qtimetadata); + addMetadataField("oo_education_learning_time", fullItem.getEducationalLearningTime(), qtimetadata); + addMetadataField("format", fullItem.getFormat(), qtimetadata); + addMetadataField("oo_identifier", fullItem.getIdentifier(), qtimetadata); + addMetadataField("type", fullItem.getItemType(), qtimetadata); + addMetadataField("version", fullItem.getItemVersion(), qtimetadata); + addMetadataField("keywords", fullItem.getKeywords(), qtimetadata); + addMetadataField("language", fullItem.getLanguage(), qtimetadata); + addMetadataField("license", fullItem.getLicense(), qtimetadata); + addMetadataField("oo_master", fullItem.getMasterIdentifier(), qtimetadata); + addMetadataField("oo_num_of_answer_alternatives", fullItem.getNumOfAnswerAlternatives(), qtimetadata); + addMetadataField("status", fullItem.getQuestionStatus(), qtimetadata); + addMetadataField("oo_std_dev_difficulty", fullItem.getStdevDifficulty(), qtimetadata); + addMetadataField("oo_taxonomy", fullItem.getTaxonomicPath(), qtimetadata); + //fullItem.getTaxonomicLevel(); + addMetadataField("title", fullItem.getTitle(), qtimetadata); + addMetadataField("oo_usage", fullItem.getUsage(), qtimetadata); + } + + private void addMetadataField(String label, int entry, Element qtimetadata) { + if(entry >= 0) { + addMetadataField(label, Integer.toString(entry), qtimetadata); + } + } + + private void addMetadataField(String label, QLicense entry, Element qtimetadata) { + if(entry != null) { + addMetadataField(label, entry.getLicenseText(), qtimetadata); + } + } + + private void addMetadataField(String label, QEducationalContext entry, Element qtimetadata) { + if(entry != null) { + addMetadataField(label, entry.getLevel(), qtimetadata); + } + } + + private void addMetadataField(String label, QuestionStatus entry, Element qtimetadata) { + if(entry != null) { + addMetadataField(label, entry.name(), qtimetadata); + } + } + + private void addMetadataField(String label, BigDecimal entry, Element qtimetadata) { + if(entry != null) { + addMetadataField(label, entry.toPlainString(), qtimetadata); + } + } + + 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 String getMetadataEntry(String label) { + String entry = null; + + @SuppressWarnings("unchecked") + List<Element> qtimetadatafields = qtimetadata.elements("qtimetadatafield"); + for(Element qtimetadatafield:qtimetadatafields) { + Element fieldlabel = qtimetadatafield.element("fieldlabel"); + if(fieldlabel != null && label.equals(fieldlabel.getText())) { + Element fieldentry = qtimetadatafield.element("fieldentry"); + if(fieldentry != null) { + entry = fieldentry.getText(); + } + } + } + + return entry; + } + + private BigDecimal toBigDecimal(String str) { + try { + return new BigDecimal(str); + } catch (Exception e) { + return null; + } + } + + private int toInt(String str) { + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + return 0; + } + } + + private boolean validStatus(String str) { + try { + QuestionStatus.valueOf(str); + return true; + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIQPoolServiceProvider.java b/src/main/java/org/olat/ims/qti/qpool/QTIQPoolServiceProvider.java index aae2920f58f1c104aa2d76537e29ef612d62b339..62cab9fbac5964d996e6efd6cfaebd54accd80cd 100644 --- a/src/main/java/org/olat/ims/qti/qpool/QTIQPoolServiceProvider.java +++ b/src/main/java/org/olat/ims/qti/qpool/QTIQPoolServiceProvider.java @@ -71,6 +71,7 @@ import org.olat.modules.qpool.manager.QEducationalContextDAO; import org.olat.modules.qpool.manager.QItemTypeDAO; 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.DefaultExportFormat; import org.olat.modules.qpool.model.QuestionItemImpl; import org.olat.repository.RepositoryEntry; @@ -103,6 +104,8 @@ public class QTIQPoolServiceProvider implements QPoolSPI { private QuestionItemDAO questionItemDao; @Autowired private QEducationalContextDAO qEduContextDao; + @Autowired + private TaxonomyLevelDAO taxonomyLevelDao; private static final List<ExportFormatOptions> formats = new ArrayList<ExportFormatOptions>(2); static { @@ -185,7 +188,7 @@ public class QTIQPoolServiceProvider implements QPoolSPI { @Override public List<QuestionItem> importItems(Identity owner, Locale defaultLocale, String filename, File file) { QTIImportProcessor processor = new QTIImportProcessor(owner, defaultLocale, filename, file, - questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); return processor.process(); } @@ -215,11 +218,11 @@ public class QTIQPoolServiceProvider implements QPoolSPI { item.setTitle(title); QTIImportProcessor processor = new QTIImportProcessor(owner, defaultLocale, - questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); Document doc = QTIEditHelper.itemToXml(item); Element itemEl = (Element)doc.selectSingleNode("questestinterop/item"); - QuestionItemImpl qitem = processor.processItem(itemEl, "", null, "OpenOLAT", Settings.getVersion()); + QuestionItemImpl qitem = processor.processItem(itemEl, "", null, "OpenOLAT", Settings.getVersion(), null); //save to file System VFSContainer baseDir = qpoolFileStorage.getContainer(qitem.getDirectory()); VFSLeaf leaf = baseDir.createChildLeaf(qitem.getRootFilename()); @@ -229,7 +232,7 @@ public class QTIQPoolServiceProvider implements QPoolSPI { public void importBeecomItem(Identity owner, Item item, VFSContainer sourceDir, Locale defaultLocale) { QTIImportProcessor processor = new QTIImportProcessor(owner, defaultLocale, - questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); String editor = null; String editorVersion = null; @@ -240,7 +243,7 @@ public class QTIQPoolServiceProvider implements QPoolSPI { Document doc = QTIEditHelper.itemToXml(item); Element itemEl = (Element)doc.selectSingleNode("questestinterop/item"); - QuestionItemImpl qitem = processor.processItem(itemEl, "", null, editor, editorVersion); + QuestionItemImpl qitem = processor.processItem(itemEl, "", null, editor, editorVersion, null); //save to file System VFSContainer baseDir = qpoolFileStorage.getContainer(qitem.getDirectory()); VFSLeaf leaf = baseDir.createChildLeaf(qitem.getRootFilename()); diff --git a/src/main/java/org/olat/modules/qpool/manager/TaxonomyLevelDAO.java b/src/main/java/org/olat/modules/qpool/manager/TaxonomyLevelDAO.java index b1f0bef9c7ca37703b832aaf5d89798843410202..17a31c996eeb3ff3be0f55358821b332b7358fb7 100644 --- a/src/main/java/org/olat/modules/qpool/manager/TaxonomyLevelDAO.java +++ b/src/main/java/org/olat/modules/qpool/manager/TaxonomyLevelDAO.java @@ -22,6 +22,8 @@ package org.olat.modules.qpool.manager; import java.util.Date; import java.util.List; +import javax.persistence.TypedQuery; + import org.olat.core.commons.persistence.DB; import org.olat.modules.qpool.TaxonomyLevel; import org.olat.modules.qpool.model.TaxonomyLevelImpl; @@ -126,6 +128,28 @@ public class TaxonomyLevelDAO { .setParameter("path", path + "%") .getResultList(); } + + public TaxonomyLevel loadLevelBy(TaxonomyLevel parent, String field) { + TypedQuery<TaxonomyLevel> query; + if(parent == null) { + String q = "select f from qtaxonomylevel f where f.field=:field and f.parentField is null"; + query = dbInstance.getCurrentEntityManager() + .createQuery(q, TaxonomyLevel.class); + + } else { + String q = "select f from qtaxonomylevel f where f.field=:field and f.parentField=:parent"; + query = dbInstance.getCurrentEntityManager() + .createQuery(q, TaxonomyLevel.class) + .setParameter("parent", parent); + } + List<TaxonomyLevel> fields = query + .setParameter("field", field) + .getResultList(); + if(fields.isEmpty()) { + return null; + } + return fields.get(0); + } public TaxonomyLevel loadLevelById(Long key) { diff --git a/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java b/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java index c34cd93ded8f4ba321154a52041b51f23798a3b4..b49f5c16fefa91dd033d43b9b353c60cd996577e 100644 --- a/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java +++ b/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java @@ -40,10 +40,11 @@ import org.olat.core.commons.persistence.DB; import org.olat.core.id.Identity; import org.olat.modules.qpool.QuestionItem; import org.olat.modules.qpool.QuestionItemFull; -import org.olat.modules.qpool.manager.QPoolFileStorage; import org.olat.modules.qpool.manager.QEducationalContextDAO; import org.olat.modules.qpool.manager.QItemTypeDAO; +import org.olat.modules.qpool.manager.QPoolFileStorage; import org.olat.modules.qpool.manager.QuestionItemDAO; +import org.olat.modules.qpool.manager.TaxonomyLevelDAO; import org.olat.test.JunitTestHelper; import org.olat.test.OlatTestCase; import org.springframework.beans.factory.annotation.Autowired; @@ -67,6 +68,8 @@ public class QTIExportProcessorTest extends OlatTestCase { @Autowired private QuestionItemDAO questionItemDao; @Autowired + private TaxonomyLevelDAO taxonomyLevelDao; + @Autowired private QEducationalContextDAO qEduContextDao; @Before @@ -83,7 +86,8 @@ public class QTIExportProcessorTest extends OlatTestCase { URL itemUrl = QTIExportProcessorTest.class.getResource("mchc_asmimr_106.zip"); Assert.assertNotNull(itemUrl); File itemFile = new File(itemUrl.toURI()); - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); List<QuestionItem> items = proc.process(); Assert.assertNotNull(items); dbInstance.commitAndCloseSession(); diff --git a/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java b/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java index 95352b76fadb48698477f3f0ac16fe3866d57ba6..3d7dc08d5da7869a08ffc97a37cad7c5588b2583 100644 --- a/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java +++ b/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java @@ -22,6 +22,7 @@ package org.olat.ims.qti.qpool; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.math.BigDecimal; import java.net.URISyntaxException; import java.net.URL; import java.util.List; @@ -53,6 +54,7 @@ import org.olat.modules.qpool.manager.QEducationalContextDAO; import org.olat.modules.qpool.manager.QItemTypeDAO; 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.QuestionItemImpl; @@ -79,6 +81,8 @@ public class QTIImportProcessorTest extends OlatTestCase { @Autowired private QuestionItemDAO questionItemDao; @Autowired + private TaxonomyLevelDAO taxonomyLevelDao; + @Autowired private QEducationalContextDAO qEduContextDao; @Before @@ -100,8 +104,13 @@ public class QTIImportProcessorTest extends OlatTestCase { File itemFile = new File(itemUrl.toURI()); //get the document informations - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); - DocInfos docInfos = proc.getDocInfos(); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); + List<DocInfos> docInfoList = proc.getDocInfos(); + Assert.assertNotNull(docInfoList); + Assert.assertEquals(1, docInfoList.size()); + + DocInfos docInfos = docInfoList.get(0); Assert.assertNotNull(docInfos); Assert.assertNotNull(docInfos.getFilename()); Assert.assertNotNull(docInfos.getDocument()); @@ -116,7 +125,7 @@ public class QTIImportProcessorTest extends OlatTestCase { QuestionItemImpl item = proc.processItem(docInfos, itemInfos.get(0)); Assert.assertNotNull(item); dbInstance.commitAndCloseSession(); - proc.processFiles(item, itemInfos.get(0)); + proc.processFiles(item, itemInfos.get(0), null); //reload and check what is saved QuestionItemFull reloadItem = questionItemDao.loadById(item.getKey()); @@ -150,7 +159,8 @@ public class QTIImportProcessorTest extends OlatTestCase { File itemFile = new File(itemUrl.toURI()); //get the document informations - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); List<QuestionItem> items = proc.process(); Assert.assertNotNull(items); Assert.assertEquals(1, items.size()); @@ -183,8 +193,13 @@ public class QTIImportProcessorTest extends OlatTestCase { File testFile = new File(testUrl.toURI()); //get the document informations - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, testFile.getName(), testFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); - DocInfos docInfos = proc.getDocInfos(); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, testFile.getName(), testFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); + List<DocInfos> docInfoList = proc.getDocInfos(); + Assert.assertNotNull(docInfoList); + Assert.assertEquals(1, docInfoList.size()); + + DocInfos docInfos = docInfoList.get(0); Assert.assertNotNull(docInfos); Assert.assertNotNull(docInfos.getFilename()); Assert.assertNotNull(docInfos.getDocument()); @@ -203,7 +218,8 @@ public class QTIImportProcessorTest extends OlatTestCase { File itemFile = new File(itemUrl.toURI()); //get the document informations - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); List<QuestionItem> items = proc.process(); Assert.assertNotNull(items); Assert.assertEquals(4, items.size()); @@ -265,7 +281,8 @@ public class QTIImportProcessorTest extends OlatTestCase { File itemFile = new File(itemUrl.toURI()); //get the document informations - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); List<QuestionItem> items = proc.process(); Assert.assertNotNull(items); Assert.assertEquals(2, items.size()); @@ -311,7 +328,8 @@ public class QTIImportProcessorTest extends OlatTestCase { File itemFile = new File(itemUrl.toURI()); //get the document informations - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); List<QuestionItem> items = proc.process(); Assert.assertNotNull(items); Assert.assertEquals(3, items.size()); @@ -357,6 +375,51 @@ public class QTIImportProcessorTest extends OlatTestCase { } } + @Test + public void testImport_QTI12_multipleItems() throws IOException, URISyntaxException { + URL itemsUrl = QTIImportProcessorTest.class.getResource("multiple_items.zip"); + Assert.assertNotNull(itemsUrl); + File itemFile = new File(itemsUrl.toURI()); + + //get the document informations + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); + List<QuestionItem> items = proc.process(); + Assert.assertNotNull(items); + Assert.assertEquals(2, items.size()); + dbInstance.commitAndCloseSession(); + + //check the files + for(QuestionItem item:items) { + QuestionItemFull itemFull = (QuestionItemFull)item; + String dir = itemFull.getDirectory(); + String file = itemFull.getRootFilename(); + VFSContainer itemContainer = qpoolFileStorage.getContainer(dir); + Assert.assertNotNull(itemContainer); + VFSItem itemLeaf = itemContainer.resolve(file); + Assert.assertNotNull(itemLeaf); + Assert.assertTrue(itemLeaf instanceof VFSLeaf); + + //try to parse it + InputStream is = ((VFSLeaf)itemLeaf).getInputStream(); + XMLParser xmlParser = new XMLParser(new IMSEntityResolver()); + Document doc = xmlParser.parse(is, false); + Node itemNode = doc.selectSingleNode("questestinterop/item"); + Assert.assertNotNull(itemNode); + + //check the attachments + if("Export (blue)".equals(itemFull.getTitle())) { + Assert.assertTrue(exists(itemFull, "media/blue.png")); + Assert.assertFalse(exists(itemFull, "media/purple.png")); + } else if("Export (purple)".equals(itemFull.getTitle())) { + Assert.assertFalse(exists(itemFull, "media/blue.png")); + Assert.assertTrue(exists(itemFull, "media/purple.png")); + } else { + Assert.fail(); + } + } + } + @Test public void testImport_QTI12_metadata() throws IOException, URISyntaxException { URL itemUrl = QTIImportProcessorTest.class.getResource("mchc_i_001.xml"); @@ -364,7 +427,8 @@ public class QTIImportProcessorTest extends OlatTestCase { File itemFile = new File(itemUrl.toURI()); //get the document informations - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); List<QuestionItem> items = proc.process(); Assert.assertNotNull(items); Assert.assertEquals(1, items.size()); @@ -381,6 +445,49 @@ public class QTIImportProcessorTest extends OlatTestCase { Assert.assertEquals("QTITools", item.getEditor()); } + @Test + public void testImport_QTI12_sidecarMetadata() throws IOException, URISyntaxException { + URL itemUrl = QTIImportProcessorTest.class.getResource("qitem_metadatas.zip"); + Assert.assertNotNull(itemUrl); + File itemFile = new File(itemUrl.toURI()); + + //get the document informations + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); + List<QuestionItem> items = proc.process(); + Assert.assertNotNull(items); + Assert.assertEquals(1, items.size()); + dbInstance.commitAndCloseSession(); + + //reload and check metadata + QuestionItem item = questionItemDao.loadById(items.get(0).getKey()); + Assert.assertEquals("Une information en plus", item.getAdditionalInformations()); + Assert.assertEquals("formative", item.getAssessmentType()); + Assert.assertEquals("large", item.getCoverage()); + Assert.assertEquals(0, new BigDecimal("-0.1").compareTo(item.getDifferentiation())); + Assert.assertEquals(0, new BigDecimal("0.45").compareTo(item.getDifficulty())); + Assert.assertEquals("OpenOLAT", item.getEditor()); + Assert.assertEquals("9.4", item.getEditorVersion()); + QEducationalContext level = item.getEducationalContext(); + Assert.assertNotNull(level); + Assert.assertEquals("University", level.getLevel()); + Assert.assertEquals("P5DT4H3M2S", item.getEducationalLearningTime()); + Assert.assertEquals("IMS QTI 1.2", item.getFormat()); + Assert.assertEquals("6bae65ac-f333-40ba-bdd0-13b54d016d59", item.getMasterIdentifier()); + Assert.assertFalse("6bae65ac-f333-40ba-bdd0-13b54d016d59".equals(item.getIdentifier())); + Assert.assertEquals("sc", item.getItemType()); + Assert.assertEquals("1.01", item.getItemVersion()); + Assert.assertEquals("question export import Pluton", item.getKeywords()); + Assert.assertEquals("de", item.getLanguage()); + Assert.assertEquals(1, item.getNumOfAnswerAlternatives()); + Assert.assertNotNull(item.getQuestionStatus()); + Assert.assertEquals("review", item.getQuestionStatus().name()); + Assert.assertEquals(0, new BigDecimal("0.56").compareTo(item.getStdevDifficulty())); + Assert.assertEquals("/Physique/Astronomie/Astrophysique", item.getTaxonomicPath()); + Assert.assertEquals("Une question sur Pluton", item.getTitle()); + Assert.assertEquals(0, item.getUsage()); + } + @Test public void testImport_QTI12_film() throws IOException, URISyntaxException { URL itemUrl = QTIImportProcessorTest.class.getResource("sc_with_film.xml"); @@ -388,11 +495,16 @@ public class QTIImportProcessorTest extends OlatTestCase { File itemFile = new File(itemUrl.toURI()); //get the document informations - QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, questionItemDao, qItemTypeDao, qEduContextDao, qpoolFileStorage); + QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile, + questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance); List<QuestionItem> items = proc.process(); Assert.assertNotNull(items); - DocInfos docInfos = proc.getDocInfos(); + List<DocInfos> docInfoList = proc.getDocInfos(); + Assert.assertNotNull(docInfoList); + Assert.assertEquals(1, docInfoList.size()); + + DocInfos docInfos = docInfoList.get(0); List<ItemInfos> itemInfos = proc.getItemList(docInfos); Assert.assertNotNull(itemInfos); Assert.assertEquals(1, itemInfos.size()); diff --git a/src/test/java/org/olat/ims/qti/qpool/multiple_items.zip b/src/test/java/org/olat/ims/qti/qpool/multiple_items.zip new file mode 100644 index 0000000000000000000000000000000000000000..6e23a54a87e790b09586467c3323bb78c6e6b7b0 Binary files /dev/null and b/src/test/java/org/olat/ims/qti/qpool/multiple_items.zip differ diff --git a/src/test/java/org/olat/ims/qti/qpool/qitem_metadatas.zip b/src/test/java/org/olat/ims/qti/qpool/qitem_metadatas.zip new file mode 100644 index 0000000000000000000000000000000000000000..aa7abd90c694eea11c1dd5e5218462ea4ef99cb5 Binary files /dev/null and b/src/test/java/org/olat/ims/qti/qpool/qitem_metadatas.zip differ