From 8fe93b41c3ea4ce34e65bfd8c85e8eaebbf9a6d9 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Thu, 25 Sep 2014 17:39:46 +0200
Subject: [PATCH] OO-1209: import more metadatas, sanity check for numbers

---
 .../ims/qti/qpool/QTIImportProcessor.java     |  54 ++++-
 .../ims/qti/qpool/QTIMetadataConverter.java   |  25 ++-
 .../qti/qpool/QTIQPoolServiceProvider.java    |   9 +-
 .../CSVToQuestionConverter.java               | 203 +++++++++++++++---
 .../qti/questionimport/ItemAndMetadata.java   |  83 +++++++
 .../ims/qti/questionimport/ItemsPackage.java  |  19 +-
 .../OverviewQuestionController.java           |   1 +
 .../modules/qpool/manager/QLicenseDAO.java    |  15 ++
 .../ims/qti/qpool/QTIExportProcessorTest.java |   5 +-
 .../ims/qti/qpool/QTIImportProcessorTest.java |  23 +-
 .../CSVToQuestionConverterTest.java           |  40 ++++
 .../question_import_fib_en_metadata.txt       |  22 ++
 12 files changed, 439 insertions(+), 60 deletions(-)
 create mode 100644 src/test/java/org/olat/ims/qti/questionimport/question_import_fib_en_metadata.txt

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 9e990d1c1aa..4f9e934a59a 100644
--- a/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java
+++ b/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java
@@ -74,11 +74,13 @@ import org.olat.modules.qpool.QuestionType;
 import org.olat.modules.qpool.TaxonomyLevel;
 import org.olat.modules.qpool.manager.QEducationalContextDAO;
 import org.olat.modules.qpool.manager.QItemTypeDAO;
+import org.olat.modules.qpool.manager.QLicenseDAO;
 import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.manager.QuestionItemDAO;
 import org.olat.modules.qpool.manager.TaxonomyLevelDAO;
 import org.olat.modules.qpool.model.QEducationalContext;
 import org.olat.modules.qpool.model.QItemType;
+import org.olat.modules.qpool.model.QLicense;
 import org.olat.modules.qpool.model.QuestionItemImpl;
 import org.xml.sax.Attributes;
 import org.xml.sax.InputSource;
@@ -104,6 +106,7 @@ class QTIImportProcessor {
 	private final File importedFile;
 
 	private final DB dbInstance;
+	private final QLicenseDAO qLicenseDao;
 	private final QItemTypeDAO qItemTypeDao;
 	private final QPoolFileStorage qpoolFileStorage;
 	private final QuestionItemDAO questionItemDao;
@@ -112,19 +115,22 @@ class QTIImportProcessor {
 	
 	public QTIImportProcessor(Identity owner, Locale defaultLocale, QuestionItemDAO questionItemDao,
 			QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao,
-			TaxonomyLevelDAO taxonomyLevelDao, QPoolFileStorage qpoolFileStorage, DB dbInstance) {
+			TaxonomyLevelDAO taxonomyLevelDao, QLicenseDAO qLicenseDao, QPoolFileStorage qpoolFileStorage,
+			DB dbInstance) {
 		this(owner, defaultLocale, null, null, questionItemDao, qItemTypeDao, qEduContextDao,
-				taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 	}
 
 	public QTIImportProcessor(Identity owner, Locale defaultLocale, String importedFilename, File importedFile,
 			QuestionItemDAO questionItemDao, QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao,
-			TaxonomyLevelDAO taxonomyLevelDao, QPoolFileStorage qpoolFileStorage, DB dbInstance) {
+			TaxonomyLevelDAO taxonomyLevelDao, QLicenseDAO qLicenseDao, QPoolFileStorage qpoolFileStorage,
+			DB dbInstance) {
 		this.owner = owner;
 		this.dbInstance = dbInstance;
 		this.defaultLocale = defaultLocale;
 		this.importedFilename = importedFilename;
 		this.importedFile = importedFile;
+		this.qLicenseDao = qLicenseDao;
 		this.qItemTypeDao = qItemTypeDao;
 		this.questionItemDao = questionItemDao;
 		this.qEduContextDao = qEduContextDao;
@@ -285,10 +291,48 @@ class QTIImportProcessor {
 		
 		String taxonomyPath = metadata.getTaxonomyPath();
 		if(StringHelper.containsNonWhitespace(taxonomyPath)) {
-			QTIMetadataConverter converter = new QTIMetadataConverter(null, taxonomyLevelDao, null);
+			QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao);
 			TaxonomyLevel taxonomyLevel = converter.toTaxonomy(taxonomyPath);
 			poolItem.setTaxonomyLevel(taxonomyLevel);
 		}
+		
+		String level = metadata.getLevel();
+		if(StringHelper.containsNonWhitespace(level)) {
+			QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao);
+			QEducationalContext educationalContext = converter.toEducationalContext(level);
+			poolItem.setEducationalContext(educationalContext);
+		}
+		
+		String time = metadata.getTypicalLearningTime();
+		if(StringHelper.containsNonWhitespace(time)) {
+			poolItem.setEducationalLearningTime(time);
+		}
+		
+		String editor = metadata.getEditor();
+		if(StringHelper.containsNonWhitespace(editor)) {
+			poolItem.setEditor(editor);
+		}
+		
+		String editorVersion = metadata.getEditorVersion();
+		if(StringHelper.containsNonWhitespace(editorVersion)) {
+			poolItem.setEditorVersion(editorVersion);
+		}
+		
+		int numOfAnswerAlternatives = metadata.getNumOfAnswerAlternatives();
+		if(numOfAnswerAlternatives > 0) {
+			poolItem.setNumOfAnswerAlternatives(numOfAnswerAlternatives);
+		}
+		
+		poolItem.setDifficulty(metadata.getDifficulty());
+		poolItem.setDifferentiation(metadata.getDifferentiation());
+		poolItem.setStdevDifficulty(metadata.getStdevDifficulty());
+		
+		String license = metadata.getLicense();
+		if(StringHelper.containsNonWhitespace(license)) {
+			QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao);
+			QLicense qLicense = converter.toLicense(license);
+			poolItem.setLicense(qLicense);
+		}
 	}
 	
 	private void processItemMetadata(QuestionItemImpl poolItem, Element itemEl) {
@@ -600,7 +644,7 @@ class QTIImportProcessor {
 				SAXReader reader = new SAXReader();
 		        Document document = reader.read(metadataIn);
 		        Element rootElement = document.getRootElement();
-		        QTIMetadataConverter enricher = new QTIMetadataConverter(rootElement, qItemTypeDao, taxonomyLevelDao, qEduContextDao);
+		        QTIMetadataConverter enricher = new QTIMetadataConverter(rootElement, qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao);
 		        enricher.toQuestion(item);
 			}
 	        return true;
diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java b/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java
index bc37da9151b..1f6e5d6a513 100644
--- a/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java
+++ b/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java
@@ -24,12 +24,14 @@ import java.util.ArrayList;
 import java.util.List;
 
 import org.dom4j.Element;
+import org.jgroups.util.UUID;
 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.QLicenseDAO;
 import org.olat.modules.qpool.manager.TaxonomyLevelDAO;
 import org.olat.modules.qpool.model.QEducationalContext;
 import org.olat.modules.qpool.model.QItemType;
@@ -48,7 +50,8 @@ import org.olat.modules.qpool.model.QuestionItemImpl;
 class QTIMetadataConverter {
 	
 	private Element qtimetadata;
-	
+
+	private QLicenseDAO licenseDao;
 	private QItemTypeDAO itemTypeDao;
 	private TaxonomyLevelDAO taxonomyLevelDao;
 	private QEducationalContextDAO educationalContextDao;
@@ -57,16 +60,18 @@ class QTIMetadataConverter {
 		this.qtimetadata = qtimetadata;
 	}
 	
-	QTIMetadataConverter(Element qtimetadata, QItemTypeDAO itemTypeDao,
+	QTIMetadataConverter(Element qtimetadata, QItemTypeDAO itemTypeDao, QLicenseDAO licenseDao,
 			TaxonomyLevelDAO taxonomyLevelDao, QEducationalContextDAO educationalContextDao) {
 		this.qtimetadata = qtimetadata;
+		this.licenseDao = licenseDao;
 		this.itemTypeDao = itemTypeDao;
 		this.taxonomyLevelDao = taxonomyLevelDao;
 		this.educationalContextDao = educationalContextDao;
 	}
 	
-	QTIMetadataConverter(QItemTypeDAO itemTypeDao,
+	QTIMetadataConverter(QItemTypeDAO itemTypeDao, QLicenseDAO licenseDao,
 			TaxonomyLevelDAO taxonomyLevelDao, QEducationalContextDAO educationalContextDao) {
+		this.licenseDao = licenseDao;
 		this.itemTypeDao = itemTypeDao;
 		this.taxonomyLevelDao = taxonomyLevelDao;
 		this.educationalContextDao = educationalContextDao;
@@ -80,8 +85,16 @@ class QTIMetadataConverter {
 		return type;
 	}
 	
-	private QLicense toLicense(String str) {
-		return null;
+	public QLicense toLicense(String license) {
+		QLicense qLicense = null;
+		if(StringHelper.containsNonWhitespace(license)) {
+			qLicense = licenseDao.searchLicense(license);
+			if(qLicense == null) {
+				String key = "perso-" + UUID.randomUUID().toString();
+				qLicense = licenseDao.create(key, license, false);
+			}
+		}
+		return qLicense;
 	}
 	
 	public TaxonomyLevel toTaxonomy(String str) {
@@ -106,7 +119,7 @@ class QTIMetadataConverter {
 		return lowerLevel;
 	}
 	
-	private QEducationalContext toEducationalContext(String txt) {
+	public QEducationalContext toEducationalContext(String txt) {
 		QEducationalContext context = educationalContextDao.loadByLevel(txt);
 		if(context == null) {
 			context = educationalContextDao.create(txt, true);
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 ecfc6155b1a..88958c53c6a 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.QuestionItemFull;
 import org.olat.modules.qpool.QuestionItemShort;
 import org.olat.modules.qpool.manager.QEducationalContextDAO;
 import org.olat.modules.qpool.manager.QItemTypeDAO;
+import org.olat.modules.qpool.manager.QLicenseDAO;
 import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.manager.QuestionItemDAO;
 import org.olat.modules.qpool.manager.TaxonomyLevelDAO;
@@ -101,6 +102,8 @@ public class QTIQPoolServiceProvider implements QPoolSPI {
 	@Autowired
 	private QPoolFileStorage qpoolFileStorage;
 	@Autowired
+	private QLicenseDAO qLicenseDao;
+	@Autowired
 	private QItemTypeDAO qItemTypeDao;
 	@Autowired
 	private QuestionItemDAO questionItemDao;
@@ -190,7 +193,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, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		return processor.process();
 	}
 	
@@ -220,7 +223,7 @@ public class QTIQPoolServiceProvider implements QPoolSPI {
 		item.setTitle(title);
 		
 		QTIImportProcessor processor = new QTIImportProcessor(owner, defaultLocale,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		
 		Document doc = QTIEditHelper.itemToXml(item);
 		Element itemEl = (Element)doc.selectSingleNode("questestinterop/item");
@@ -234,7 +237,7 @@ public class QTIQPoolServiceProvider implements QPoolSPI {
 	
 	public void importBeecomItem(Identity owner, ItemAndMetadata itemAndMetadata, VFSContainer sourceDir, Locale defaultLocale) {
 		QTIImportProcessor processor = new QTIImportProcessor(owner, defaultLocale,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		
 		String editor = null;
 		String editorVersion = null;
diff --git a/src/main/java/org/olat/ims/qti/questionimport/CSVToQuestionConverter.java b/src/main/java/org/olat/ims/qti/questionimport/CSVToQuestionConverter.java
index 01e6105a21e..a2948dc3436 100644
--- a/src/main/java/org/olat/ims/qti/questionimport/CSVToQuestionConverter.java
+++ b/src/main/java/org/olat/ims/qti/questionimport/CSVToQuestionConverter.java
@@ -19,12 +19,14 @@
  */
 package org.olat.ims.qti.questionimport;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 
 import org.olat.core.gui.translator.Translator;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
+import org.olat.core.util.StringHelper;
 import org.olat.core.util.filter.Filter;
 import org.olat.core.util.filter.FilterFactory;
 import org.olat.ims.qti.editor.QTIEditHelper;
@@ -105,82 +107,217 @@ public class CSVToQuestionConverter {
 			case "keywords": processKeywords(parts); break;
 			case "abdeckung":
 			case "coverage": processCoverage(parts); break;
-			case "level": break;
+			case "level": processLevel(parts); break;
 			case "sprache":
 			case "language": processLanguage(parts); break;
+			case "durchschnittliche bearbeitungszeit":
+			case "typical learning time": processTypicalLearningTime(parts); break;
+			case "itemschwierigkeit":
+			case "difficulty index": processDifficultyIndex(parts); break;
+			case "standardabweichung itemschwierigkeit":
+			case "standard deviation": processStandardDeviation(parts); break;
+			case "trennsch\u00E4rfe":
+			case "discrimination index": processDiscriminationIndex(parts); break;
+			case "anzahl distraktoren":
+			case "distractors": processDistractors(parts); break;
+			case "editor": processEditor(parts); break;
+			case "editor version": processEditorVersion(parts); break;
+			case "lizenz":
+			case "license": processLicense(parts); break;
 			default: processChoice(parts);
 		}
 	}
+
+	private void processLevel(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
+		
+		String level = parts[1];
+		if(StringHelper.containsNonWhitespace(level)) {
+			currentItem.setLevel(level.trim());
+		}
+	}
 	
-	private void processType(String[] parts) {
-		if(currentItem != null) {
-			items.add(currentItem);
+	private void processTypicalLearningTime(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
+		
+		String time = parts[1];
+		if(StringHelper.containsNonWhitespace(time)) {
+			currentItem.setTypicalLearningTime(time.trim());
+		}
+	}
+	
+	private void processLicense(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
+		
+		String license = parts[1];
+		if(StringHelper.containsNonWhitespace(license)) {
+			currentItem.setLicense(license.trim());
+		}
+	}
+	
+	private void processEditor(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
+		
+		String editor = parts[1];
+		if(StringHelper.containsNonWhitespace(editor)) {
+			currentItem.setEditor(editor.trim());
 		}
+	}
+	
+	private void processEditorVersion(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
+		
+		String editorVersion = parts[1];
+		if(StringHelper.containsNonWhitespace(editorVersion)) {
+			currentItem.setEditorVersion(editorVersion.trim());
+		}
+	}
+	
+	private void processDistractors(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
 		
-		String type = parts[1].toLowerCase();
-		switch(type) {
-			case "fib": {
-				currentItem = new ItemAndMetadata(QTIEditHelper.createFIBItem(translator));
-				((FIBQuestion)currentItem.getItem().getQuestion()).getResponses().clear();
-				break;
+		String distractors = parts[1];
+		if(StringHelper.containsNonWhitespace(distractors)) {
+			try {
+				currentItem.setNumOfAnswerAlternatives(Integer.parseInt(distractors.trim()));
+			} catch (NumberFormatException e) {
+				log.warn("", e);
 			}
-			case "mc": {
-				currentItem = new ItemAndMetadata(QTIEditHelper.createMCItem(translator));
-				((ChoiceQuestion)currentItem.getItem().getQuestion()).getResponses().clear();
-				break;
+		}
+	}
+	
+	private void processDiscriminationIndex(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
+		
+		String discriminationIndex = parts[1];
+		if(StringHelper.containsNonWhitespace(discriminationIndex)) {
+			try {
+				currentItem.setDifferentiation(new BigDecimal(discriminationIndex.trim()));
+			} catch (Exception e) {
+				log.warn("", e);
 			}
-			case "sc": {
-				currentItem = new ItemAndMetadata(QTIEditHelper.createSCItem(translator));
-				((ChoiceQuestion)currentItem.getItem().getQuestion()).getResponses().clear();
-				break;
+		}
+	}
+	
+	private void processDifficultyIndex(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
+		
+		String difficulty = parts[1];
+		if(StringHelper.containsNonWhitespace(difficulty)) {
+			try {
+				BigDecimal dif = new BigDecimal(difficulty.trim());
+				if(dif.doubleValue() >= 0.0d && dif.doubleValue() <= 1.0d) {
+					currentItem.setDifficulty(dif);
+				} else {
+					currentItem.setHasError(true);
+				}
+			} catch (Exception e) {
+				log.warn("", e);
 			}
-			default: {
-				log.warn("Question type not supported: " + type);
-				currentItem = null;
+		}
+	}
+	
+	private void processStandardDeviation(String[] parts) {
+		if(currentItem == null || parts.length < 2) return;
+		
+		String stddev = parts[1];
+		if(StringHelper.containsNonWhitespace(stddev)) {
+			try {
+				BigDecimal dev = new BigDecimal(stddev.trim());
+				if(dev.doubleValue() >= 0.0d && dev.doubleValue() <= 1.0d) {
+					currentItem.setStdevDifficulty(dev);
+				} else {
+					currentItem.setHasError(true);
+				}
+			} catch (Exception e) {
+				log.warn("", e);
+			}
+		}
+	}
+	
+	private void processType(String[] parts) {
+		if(currentItem != null) {
+			items.add(currentItem);
+		}
+		
+		if(parts.length > 1) {
+			String type = parts[1].toLowerCase();
+			switch(type) {
+				case "fib": {
+					currentItem = new ItemAndMetadata(QTIEditHelper.createFIBItem(translator));
+					((FIBQuestion)currentItem.getItem().getQuestion()).getResponses().clear();
+					break;
+				}
+				case "mc": {
+					currentItem = new ItemAndMetadata(QTIEditHelper.createMCItem(translator));
+					((ChoiceQuestion)currentItem.getItem().getQuestion()).getResponses().clear();
+					break;
+				}
+				case "sc": {
+					currentItem = new ItemAndMetadata(QTIEditHelper.createSCItem(translator));
+					((ChoiceQuestion)currentItem.getItem().getQuestion()).getResponses().clear();
+					break;
+				}
+				default: {
+					log.warn("Question type not supported: " + type);
+					currentItem = null;
+				}
 			}
 		}
 	}
 	
 	private void processCoverage(String[] parts) {
-		if(currentItem == null) return;
+		if(currentItem == null || parts.length < 2) return;
 		
 		String coverage = parts[1];
-		currentItem.setCoverage(coverage);
+		if(StringHelper.containsNonWhitespace(coverage)) {
+			currentItem.setCoverage(coverage);
+		}
 	}
 	
 	private void processKeywords(String[] parts) {
-		if(currentItem == null) return;
+		if(currentItem == null || parts.length < 2) return;
 		
 		String keywords = parts[1];
-		currentItem.setKeywords(keywords);
+		if(StringHelper.containsNonWhitespace(keywords)) {
+			currentItem.setKeywords(keywords);
+		}
 	}
 	
 	private void processTaxonomyPath(String[] parts) {
-		if(currentItem == null) return;
+		if(currentItem == null || parts.length < 2) return;
 		
 		String taxonomyPath = parts[1];
-		currentItem.setTaxonomyPath(taxonomyPath);
+		if(StringHelper.containsNonWhitespace(taxonomyPath)) {
+			currentItem.setTaxonomyPath(taxonomyPath);
+		}
 	}
 	
 	private void processLanguage(String[] parts) {
-		if(currentItem == null) return;
+		if(currentItem == null || parts.length < 2) return;
 		
 		String language = parts[1];
-		currentItem.setLanguage(language);
+		if(StringHelper.containsNonWhitespace(language)) {
+			currentItem.setLanguage(language);
+		}
 	}
 	
 	private void processTitle(String[] parts) {
-		if(currentItem == null) return;
+		if(currentItem == null || parts.length < 2) return;
 		
 		String title = parts[1];
-		currentItem.setTitle(title);
+		if(StringHelper.containsNonWhitespace(title)) {
+			currentItem.setTitle(title);
+		}
 	}
 	
 	private void processDescription(String[] parts) {
-		if(currentItem == null) return;
+		if(currentItem == null || parts.length < 2) return;
 		
 		String description = parts[1];
-		currentItem.setDescription(description);
+		if(StringHelper.containsNonWhitespace(description)) {
+			currentItem.setDescription(description);
+		}
 	}
 	
 	private void processQuestion(String[] parts) {
diff --git a/src/main/java/org/olat/ims/qti/questionimport/ItemAndMetadata.java b/src/main/java/org/olat/ims/qti/questionimport/ItemAndMetadata.java
index 132232bc2b1..a4877fe420f 100644
--- a/src/main/java/org/olat/ims/qti/questionimport/ItemAndMetadata.java
+++ b/src/main/java/org/olat/ims/qti/questionimport/ItemAndMetadata.java
@@ -19,6 +19,8 @@
  */
 package org.olat.ims.qti.questionimport;
 
+import java.math.BigDecimal;
+
 import org.olat.ims.qti.editor.beecom.objects.Item;
 
 /**
@@ -35,6 +37,15 @@ public class ItemAndMetadata {
 	private String taxonomyPath;
 	private String keywords;
 	private String coverage;
+	private String level;
+	private String typicalLearningTime;
+	private String license;
+	private String editor;
+	private String editorVersion;
+	private int numOfAnswerAlternatives;
+	private BigDecimal difficulty;
+	private BigDecimal differentiation;
+	private BigDecimal stdevDifficulty;
 
 	private boolean hasError;
 	
@@ -46,6 +57,78 @@ public class ItemAndMetadata {
 		return item;
 	}
 	
+	public BigDecimal getDifficulty() {
+		return difficulty;
+	}
+
+	public void setDifficulty(BigDecimal difficulty) {
+		this.difficulty = difficulty;
+	}
+
+	public BigDecimal getStdevDifficulty() {
+		return stdevDifficulty;
+	}
+
+	public void setStdevDifficulty(BigDecimal stdevDifficulty) {
+		this.stdevDifficulty = stdevDifficulty;
+	}
+
+	public BigDecimal getDifferentiation() {
+		return differentiation;
+	}
+
+	public void setDifferentiation(BigDecimal differentiation) {
+		this.differentiation = differentiation;
+	}
+
+	public int getNumOfAnswerAlternatives() {
+		return numOfAnswerAlternatives;
+	}
+
+	public void setNumOfAnswerAlternatives(int numOfAnswerAlternatives) {
+		this.numOfAnswerAlternatives = numOfAnswerAlternatives;
+	}
+
+	public String getEditor() {
+		return editor;
+	}
+
+	public void setEditor(String editor) {
+		this.editor = editor;
+	}
+
+	public String getEditorVersion() {
+		return editorVersion;
+	}
+
+	public void setEditorVersion(String editorVersion) {
+		this.editorVersion = editorVersion;
+	}
+
+	public String getLicense() {
+		return license;
+	}
+
+	public void setLicense(String license) {
+		this.license = license;
+	}
+
+	public String getLevel() {
+		return level;
+	}
+
+	public void setLevel(String level) {
+		this.level = level;
+	}
+
+	public String getTypicalLearningTime() {
+		return typicalLearningTime;
+	}
+
+	public void setTypicalLearningTime(String typicalLearningTime) {
+		this.typicalLearningTime = typicalLearningTime;
+	}
+
 	public String getCoverage() {
 		return coverage;
 	}
diff --git a/src/main/java/org/olat/ims/qti/questionimport/ItemsPackage.java b/src/main/java/org/olat/ims/qti/questionimport/ItemsPackage.java
index b5e474c8950..9fdbab5e8f3 100644
--- a/src/main/java/org/olat/ims/qti/questionimport/ItemsPackage.java
+++ b/src/main/java/org/olat/ims/qti/questionimport/ItemsPackage.java
@@ -19,6 +19,7 @@
  */
 package org.olat.ims.qti.questionimport;
 
+import java.lang.reflect.Method;
 import java.util.List;
 
 /**
@@ -39,6 +40,20 @@ public class ItemsPackage {
 		this.items = items;
 	}
 	
-	
-
+	public boolean hasField(String field) {
+		boolean found = false;
+		try {
+			Method method = ItemAndMetadata.class.getDeclaredMethod(field);
+			for(ItemAndMetadata item:items) {
+				Object value = method.invoke(item);
+				if(value != null) {
+					found = true;
+					break;
+				}
+			}
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return found;
+	}
 }
diff --git a/src/main/java/org/olat/ims/qti/questionimport/OverviewQuestionController.java b/src/main/java/org/olat/ims/qti/questionimport/OverviewQuestionController.java
index e7085823746..9f523c57349 100644
--- a/src/main/java/org/olat/ims/qti/questionimport/OverviewQuestionController.java
+++ b/src/main/java/org/olat/ims/qti/questionimport/OverviewQuestionController.java
@@ -69,6 +69,7 @@ public class OverviewQuestionController extends StepFormBasicController {
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.type.i18n(), Cols.type.ordinal()));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.title.i18n(), Cols.title.ordinal()));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.points.i18n(), Cols.points.ordinal()));
+		
 		ItemsTableDataModel model = new ItemsTableDataModel(importedItems.getItems(), columnsModel);
 		uifactory.addTableElement(getWindowControl(), "overviewTable", model, formLayout);
 	}
diff --git a/src/main/java/org/olat/modules/qpool/manager/QLicenseDAO.java b/src/main/java/org/olat/modules/qpool/manager/QLicenseDAO.java
index 5f76604c5cc..de89752a2c6 100644
--- a/src/main/java/org/olat/modules/qpool/manager/QLicenseDAO.java
+++ b/src/main/java/org/olat/modules/qpool/manager/QLicenseDAO.java
@@ -99,6 +99,21 @@ public class QLicenseDAO implements ApplicationListener<ContextRefreshedEvent> {
 		return licenses.get(0);
 	}
 	
+	public QLicense searchLicense(String license) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("select license from qlicense license")
+		  .append(" where license.licenseKey=:license or license.licenseText=:license");
+		
+		List<QLicense> licenses = dbInstance.getCurrentEntityManager()
+				.createQuery(sb.toString(), QLicense.class)
+				.setParameter("license", license)
+				.getResultList();
+		if(licenses.isEmpty()) {
+			return null;
+		}
+		return licenses.get(0);
+	}
+	
 	
 	public boolean delete(QLicense license) {
 		QLicense reloadLicense = loadById(license.getKey());
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 b49f5c16fef..7b2414e0e3b 100644
--- a/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java
+++ b/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java
@@ -42,6 +42,7 @@ import org.olat.modules.qpool.QuestionItem;
 import org.olat.modules.qpool.QuestionItemFull;
 import org.olat.modules.qpool.manager.QEducationalContextDAO;
 import org.olat.modules.qpool.manager.QItemTypeDAO;
+import org.olat.modules.qpool.manager.QLicenseDAO;
 import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.manager.QuestionItemDAO;
 import org.olat.modules.qpool.manager.TaxonomyLevelDAO;
@@ -66,6 +67,8 @@ public class QTIExportProcessorTest extends OlatTestCase {
 	@Autowired
 	private QItemTypeDAO qItemTypeDao;
 	@Autowired
+	private QLicenseDAO qLicenseDao;
+	@Autowired
 	private QuestionItemDAO questionItemDao;
 	@Autowired
 	private TaxonomyLevelDAO taxonomyLevelDao;
@@ -87,7 +90,7 @@ public class QTIExportProcessorTest extends OlatTestCase {
 		Assert.assertNotNull(itemUrl);
 		File itemFile = new File(itemUrl.toURI());
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, 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 a31e8c28f83..7bb58d0ffa7 100644
--- a/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java
+++ b/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java
@@ -52,6 +52,7 @@ import org.olat.modules.qpool.QuestionStatus;
 import org.olat.modules.qpool.QuestionType;
 import org.olat.modules.qpool.manager.QEducationalContextDAO;
 import org.olat.modules.qpool.manager.QItemTypeDAO;
+import org.olat.modules.qpool.manager.QLicenseDAO;
 import org.olat.modules.qpool.manager.QPoolFileStorage;
 import org.olat.modules.qpool.manager.QuestionItemDAO;
 import org.olat.modules.qpool.manager.TaxonomyLevelDAO;
@@ -79,6 +80,8 @@ public class QTIImportProcessorTest extends OlatTestCase {
 	@Autowired
 	private QItemTypeDAO qItemTypeDao;
 	@Autowired
+	private QLicenseDAO qLicenseDao;
+	@Autowired
 	private QuestionItemDAO questionItemDao;
 	@Autowired
 	private TaxonomyLevelDAO taxonomyLevelDao;
@@ -105,7 +108,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao,qpoolFileStorage, dbInstance);
 		List<DocInfos> docInfoList = proc.getDocInfos();
 		Assert.assertNotNull(docInfoList);
 		Assert.assertEquals(1, docInfoList.size());
@@ -160,7 +163,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<QuestionItem> items = proc.process();
 		Assert.assertNotNull(items);
 		Assert.assertEquals(1, items.size());
@@ -194,7 +197,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, testFile.getName(), testFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<DocInfos> docInfoList = proc.getDocInfos();
 		Assert.assertNotNull(docInfoList);
 		Assert.assertEquals(1, docInfoList.size());
@@ -219,7 +222,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<QuestionItem> items = proc.process();
 		Assert.assertNotNull(items);
 		Assert.assertEquals(4, items.size());
@@ -282,7 +285,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<QuestionItem> items = proc.process();
 		Assert.assertNotNull(items);
 		Assert.assertEquals(2, items.size());
@@ -329,7 +332,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<QuestionItem> items = proc.process();
 		Assert.assertNotNull(items);
 		Assert.assertEquals(3, items.size());
@@ -383,7 +386,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<QuestionItem> items = proc.process();
 		Assert.assertNotNull(items);
 		Assert.assertEquals(2, items.size());
@@ -428,7 +431,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<QuestionItem> items = proc.process();
 		Assert.assertNotNull(items);
 		Assert.assertEquals(1, items.size());
@@ -453,7 +456,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<QuestionItem> items = proc.process();
 		Assert.assertNotNull(items);
 		Assert.assertEquals(1, items.size());
@@ -496,7 +499,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		
 		//get the document informations
 		QTIImportProcessor proc = new QTIImportProcessor(owner, Locale.ENGLISH, itemFile.getName(), itemFile,
-				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qpoolFileStorage, dbInstance);
+				questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance);
 		List<QuestionItem> items = proc.process();
 		Assert.assertNotNull(items);
 		
diff --git a/src/test/java/org/olat/ims/qti/questionimport/CSVToQuestionConverterTest.java b/src/test/java/org/olat/ims/qti/questionimport/CSVToQuestionConverterTest.java
index 9a09213daa9..68ddbe2d438 100644
--- a/src/test/java/org/olat/ims/qti/questionimport/CSVToQuestionConverterTest.java
+++ b/src/test/java/org/olat/ims/qti/questionimport/CSVToQuestionConverterTest.java
@@ -153,4 +153,44 @@ public class CSVToQuestionConverterTest {
 		Assert.assertEquals(2, ((FIBResponse)responses.get(3)).getMaxLength());
 		Assert.assertEquals(2, ((FIBResponse)responses.get(5)).getMaxLength());
 	}
+	
+	@Test
+	public void importFillInBlanck_en_metadata() throws IOException, URISyntaxException {
+		URL importTxtUrl = CSVToQuestionConverterTest.class.getResource("question_import_fib_en_metadata.txt");
+		Assert.assertNotNull(importTxtUrl);
+		File importTxt = new File(importTxtUrl.toURI());
+		String input = FileUtils.readFileToString(importTxt);
+		
+		Translator translator = new KeyTranslator(Locale.ENGLISH);
+		CSVToQuestionConverter converter = new CSVToQuestionConverter(translator);
+		converter.parse(input);
+		
+		List<ItemAndMetadata> items = converter.getItems();
+		Assert.assertNotNull(items);
+		Assert.assertEquals(1, items.size());
+		
+		ItemAndMetadata importedItem = items.get(0);
+		Item item = importedItem.getItem();
+		Assert.assertNotNull(item);
+		Assert.assertEquals(Question.TYPE_FIB, item.getQuestion().getType());
+		Assert.assertTrue(item.getQuestion() instanceof FIBQuestion);
+
+		FIBQuestion question = (FIBQuestion)item.getQuestion();
+		List<Response> responses = question.getResponses();
+		Assert.assertNotNull(responses);
+		Assert.assertEquals(2, responses.size());
+		//check java type
+		for(Response response:responses) {
+			Assert.assertTrue(response instanceof FIBResponse);
+		}
+		
+		//check type
+		Assert.assertEquals(FIBResponse.TYPE_CONTENT, ((FIBResponse)responses.get(0)).getType());
+		Assert.assertEquals(FIBResponse.TYPE_BLANK, ((FIBResponse)responses.get(1)).getType());
+		//check size
+		Assert.assertEquals(20, ((FIBResponse)responses.get(1)).getSize());
+		//check max length
+		Assert.assertEquals(50, ((FIBResponse)responses.get(1)).getMaxLength());
+	}
+		
 }
\ No newline at end of file
diff --git a/src/test/java/org/olat/ims/qti/questionimport/question_import_fib_en_metadata.txt b/src/test/java/org/olat/ims/qti/questionimport/question_import_fib_en_metadata.txt
new file mode 100644
index 00000000000..4e9aeca95ba
--- /dev/null
+++ b/src/test/java/org/olat/ims/qti/questionimport/question_import_fib_en_metadata.txt
@@ -0,0 +1,22 @@
+Keyword / Punkte	Value	Extra
+		
+Type	FIB	
+Title	Englisch: Grundwortschatz: ban	
+Description	Eine lange Beschreibung	
+Keywords	Englisch Wortschatz	
+Coverage		
+Language	de	
+Subject	/Sprache/English/Wortschatz	
+License	CC by-nc-nd	
+Level	Primarschule	
+Typical learning time		
+Difficulty index	0.55	
+Standard deviation	0.6	
+Discrimination index	-0.33	
+Distractors	1	
+Editor	OpenOLAT	
+Editor Version	9.4.2	
+Points	1	
+Text	ban	
+1	verbannen	20,50
+		
\ No newline at end of file
-- 
GitLab