From 1c5db9f273ec7bcd5188ce3db97129ff979f89e1 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Thu, 11 Sep 2014 08:17:25 +0200
Subject: [PATCH] OO-1190: import and export metadata of qti items in question
 pool

---
 .../java/org/olat/core/util/PathUtils.java    | 105 ++++++
 .../ims/qti/qpool/QTIExportProcessor.java     | 121 ++-----
 .../ims/qti/qpool/QTIImportProcessor.java     | 188 +++++++++--
 .../org/olat/ims/qti/qpool/QTIMetadata.java   | 306 ++++++++++++++++++
 .../qti/qpool/QTIQPoolServiceProvider.java    |  13 +-
 .../qpool/manager/TaxonomyLevelDAO.java       |  24 ++
 .../ims/qti/qpool/QTIExportProcessorTest.java |   8 +-
 .../ims/qti/qpool/QTIImportProcessorTest.java | 136 +++++++-
 .../org/olat/ims/qti/qpool/multiple_items.zip | Bin 0 -> 14107 bytes
 .../olat/ims/qti/qpool/qitem_metadatas.zip    | Bin 0 -> 1758 bytes
 10 files changed, 761 insertions(+), 140 deletions(-)
 create mode 100644 src/main/java/org/olat/core/util/PathUtils.java
 create mode 100644 src/main/java/org/olat/ims/qti/qpool/QTIMetadata.java
 create mode 100644 src/test/java/org/olat/ims/qti/qpool/multiple_items.zip
 create mode 100644 src/test/java/org/olat/ims/qti/qpool/qitem_metadatas.zip

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 00000000000..c49169774e3
--- /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 edf45415ba5..68e013cf7bf 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 57e9250aeae..ecdd8967f01 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 00000000000..75a886f79cc
--- /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 aae2920f58f..62cab9fbac5 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 b1f0bef9c7c..17a31c996ee 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 c34cd93ded8..b49f5c16fef 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 95352b76fad..3d7dc08d5da 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
GIT binary patch
literal 14107
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
T?pT6x`>MY^&<auA{q;Wp4^wJ4

literal 0
HcmV?d00001

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
GIT binary patch
literal 1758
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
O8w9<t>+etP58c1`So<je

literal 0
HcmV?d00001

-- 
GitLab