From eae00a6d75ddac64bef53bfc3e6fd0625af0694a Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Tue, 7 Jan 2020 19:41:43 +0100
Subject: [PATCH] OO-4438: add correction time to question's metadata

---
 pom.xml                                       |   2 +-
 .../java/org/olat/core/util/FileUtils.java    |  48 +++----
 .../ims/qti/qpool/QTIImportProcessor.java     | 136 ++++++------------
 .../ims/qti/qpool/QTIMetadataConverter.java   |  18 ++-
 .../model/xml/AssessmentItemMetadata.java     |  13 ++
 .../ims/qti21/model/xml/ManifestBuilder.java  |  18 +--
 .../model/xml/ManifestMetadataBuilder.java    |   5 +
 .../ims/qti21/pool/QTI21ImportProcessor.java  |  11 +-
 .../olat/modules/qpool/QuestionItemShort.java |   3 +
 .../qpool/manager/QuestionItemDAO.java        |   3 +
 .../manager/QuestionItemDocumentFactory.java  |   4 +-
 .../olat/modules/qpool/model/ItemWrapper.java |  28 ++--
 .../modules/qpool/model/QuestionItemImpl.java |  13 ++
 .../qpool/ui/AbstractItemListController.java  |   1 +
 .../org/olat/modules/qpool/ui/ItemRow.java    |   5 +
 .../qpool/ui/QuestionItemAuditLogExport.java  |   7 +-
 .../qpool/ui/QuestionItemDataModel.java       |   4 +-
 .../qpool/ui/_i18n/LocalStrings_de.properties |   2 +
 .../qpool/ui/_i18n/LocalStrings_en.properties |   2 +
 .../QuestionMetadataEditController.java       |  15 +-
 .../database/mysql/alter_14_1_x_to_14_2_0.sql |   4 +
 .../database/mysql/setupDatabase.sql          |   1 +
 .../oracle/alter_14_1_x_to_14_2_0.sql         |   2 +
 .../database/oracle/setupDatabase.sql         |   1 +
 .../postgresql/alter_14_1_x_to_14_2_0.sql     |   4 +
 .../database/postgresql/setupDatabase.sql     |   1 +
 .../ims/qti/qpool/QTIExportProcessorTest.java |   7 +-
 .../ims/qti/qpool/QTIImportProcessorTest.java |  59 +++++---
 .../olat/ims/qti/qpool/qitem_metadatas.zip    | Bin 1758 -> 3128 bytes
 .../QTIStatisticsManagerLargeTest.java        |  33 +++--
 .../qpool/manager/QuestionDAOTest.java        |   2 +
 .../org/olat/selenium/QuestionPoolTest.java   |   5 +-
 .../page/qpool/QuestionMetadataPage.java      |  15 +-
 src/test/resources/arquillian.xml             |   2 +-
 34 files changed, 271 insertions(+), 203 deletions(-)

diff --git a/pom.xml b/pom.xml
index 0ccba6f8b19..2cf4175b96f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1826,7 +1826,7 @@
 		<dependency>
 			<groupId>org.openolat.imscp</groupId>
 			<artifactId>manifest</artifactId>
-			<version>1.4.3</version>
+			<version>1.4.4</version>
 		</dependency>
 		<dependency>
 			<groupId>com.rometools</groupId>
diff --git a/src/main/java/org/olat/core/util/FileUtils.java b/src/main/java/org/olat/core/util/FileUtils.java
index 58cba9304f2..414630b439b 100644
--- a/src/main/java/org/olat/core/util/FileUtils.java
+++ b/src/main/java/org/olat/core/util/FileUtils.java
@@ -30,6 +30,7 @@ import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
 import java.io.File;
 import java.io.FileFilter;
 import java.io.FileInputStream;
@@ -50,9 +51,9 @@ import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.AssertException;
 import org.olat.core.logging.OLATRuntimeException;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSItem;
@@ -269,20 +270,19 @@ public class FileUtils {
 	 * @return true upon success
 	 */
 	public static long getDirSize(File path) {
-		Iterator<File> path_iterator;
-		File current_file;
-		long size;
-
 		File[] f = path.listFiles();
-		if (f == null) { return 0; }
-		path_iterator = (Arrays.asList(f)).iterator();
-		size = 0;
-		while (path_iterator.hasNext()) {
-			current_file = path_iterator.next();
-			if (current_file.isFile()) {
-				size += current_file.length();
+		if (f == null) {
+			return 0;
+		}
+		
+		Iterator<File> pathIterator = (Arrays.asList(f)).iterator();
+		long size = 0l;
+		while (pathIterator.hasNext()) {
+			File currentFile = pathIterator.next();
+			if (currentFile.isFile()) {
+				size += currentFile.length();
 			} else {
-				size += getDirSize(current_file);
+				size += getDirSize(currentFile);
 			}
 		}
 		return size;
@@ -746,30 +746,16 @@ public class FileUtils {
 	}
 	
 	/**
-	 * @param is the inputstream to close, may also be null
+	 * @param cl The closeable to close, may also be null
 	 */
-	public static void closeSafely(InputStream is) {
-		if (is == null) return;
+	public static void closeSafely(Closeable cl) {
+		if (cl == null) return;
 		try {
-			is.close();
+			cl.close();
 		} catch (IOException e) {
 			// nothing to do
 		}
-		
 	}
-	
-	/**
-	 * @param os the outputstream to close, may also be null
-	 */
-	public static void closeSafely(OutputStream os) {
-		if (os == null) return;
-		try {
-			os.close();
-		} catch (IOException e) {
-			// nothing to do
-		}
-		
-	}	
 
 	/**
 	 * Extract file suffix. E.g. 'html' from index.html
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 c45f7c354a0..1365a3cbc6e 100644
--- a/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java
+++ b/src/main/java/org/olat/ims/qti/qpool/QTIImportProcessor.java
@@ -19,7 +19,6 @@
  */
 package org.olat.ims.qti.qpool;
 
-import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.Closeable;
 import java.io.File;
@@ -31,7 +30,6 @@ import java.io.StringReader;
 import java.nio.file.FileSystems;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
@@ -46,6 +44,7 @@ import org.apache.commons.io.IOUtils;
 import org.apache.logging.log4j.Logger;
 import org.dom4j.Attribute;
 import org.dom4j.Document;
+import org.dom4j.DocumentException;
 import org.dom4j.DocumentFactory;
 import org.dom4j.Element;
 import org.dom4j.Node;
@@ -85,7 +84,6 @@ import org.olat.modules.qpool.model.QuestionItemImpl;
 import org.olat.modules.taxonomy.TaxonomyLevel;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
 
 import nu.validator.htmlparser.common.XmlViolationPolicy;
 import nu.validator.htmlparser.sax.HtmlParser;
@@ -145,7 +143,7 @@ class QTIImportProcessor {
 				}
 
 				for(DocInfos docInfos:docInfoList) {
-					IOUtils.closeQuietly(docInfos);
+					FileUtils.closeSafely(docInfos);
 				}
 			}
 		} catch (IOException e) {
@@ -440,7 +438,7 @@ class QTIImportProcessor {
 		
 		//write
 		try {
-			OutputStream os = endFile.getOutputStream(false);;
+			OutputStream os = endFile.getOutputStream(false);
 			XMLWriter xw = new XMLWriter(os, new OutputFormat("  ", true));
 			xw.write(itemDoc.getRootElement());
 			xw.close();
@@ -458,37 +456,32 @@ class QTIImportProcessor {
 	private void processAssessmentMaterials(Element itemEl, VFSContainer container) {
 		List<String> materials = getMaterials(itemEl);
 
-		try {
-			InputStream in = new FileInputStream(importedFile);
-			ZipInputStream zis = new ZipInputStream(in);
+		try(InputStream in = new FileInputStream(importedFile);
+				ZipInputStream zis = new ZipInputStream(in)) {
 
 			ZipEntry entry;
-			try {
-				while ((entry = zis.getNextEntry()) != null) {
-					String name = entry.getName();
-					if(materials.contains(name)) {
-						
-						VFSLeaf leaf = container.createChildLeaf(name);
-						OutputStream out = leaf.getOutputStream(false);
-						BufferedOutputStream bos = new BufferedOutputStream (out);
-						FileUtils.cpio(new BufferedInputStream(zis), bos, "unzip:"+entry.getName());
-						bos.flush();
-						bos.close();
-						out.close();
-					}
+			while ((entry = zis.getNextEntry()) != null) {
+				String name = entry.getName();
+				if(materials.contains(name)) {
+					unzipMaterial(zis, container, name);
 				}
-			} catch(Exception e) {
-				log.error("", e);
-			} finally {
-				IOUtils.closeQuietly(zis);
-				IOUtils.closeQuietly(in);
 			}
 		} catch (IOException e) {
 			log.error("", e);
 		}
 	}
 	
-	@SuppressWarnings("unchecked")
+	private void unzipMaterial(ZipInputStream zis, VFSContainer container, String name) {
+		VFSLeaf leaf = container.createChildLeaf(name);
+		try(OutputStream out = leaf.getOutputStream(false);
+				OutputStream bos = new BufferedOutputStream(out);) {
+			FileUtils.cpio(zis, bos, "unzip:" + name);
+			bos.flush();
+		} catch (IOException e) {
+			log.error("", e);
+		}
+	}
+	
 	protected List<String> getMaterials(Element el) {
 		List<String> materialPath = new ArrayList<>();
 		//mattext
@@ -530,10 +523,6 @@ class QTIImportProcessor {
 			QTI12HtmlHandler contentHandler = new QTI12HtmlHandler(materialPath);
 			parser.setContentHandler(contentHandler);
 			parser.parse(new InputSource(new StringReader(content)));
-		} catch (SAXException e) {
-			log.error("", e);
-		} catch (IOException e) {
-			log.error("", e);
 		} catch (Exception e) {
 			log.error("", e);
 		}
@@ -563,44 +552,41 @@ class QTIImportProcessor {
 			ZipUtil.unzipStrict(importedFile, container);
 		} else {
 			VFSLeaf endFile = container.createChildLeaf(rootFilename);
-			
-			OutputStream out = null;
-			FileInputStream in = null;
-			try {
-				out = endFile.getOutputStream(false);
-				in = new FileInputStream(importedFile);
+			try(OutputStream out = endFile.getOutputStream(false);
+					FileInputStream in = new FileInputStream(importedFile)) {
 				IOUtils.copy(in, out);
 			} catch (IOException e) {
 				log.error("", e);
-			} finally {
-				IOUtils.closeQuietly(out);
-				IOUtils.closeQuietly(in);
 			}
 		}
 	}
 	
 	private boolean processSidecarMetadata(QuestionItemImpl item, DocInfos docInfos) {
-		InputStream metadataIn = null;
 		try {
 			Path path = docInfos.root;
 			if(path != null && path.getFileName() != null) {
 				Path metadata = path.resolve(path.getFileName().toString() + "_metadata.xml");
-				metadataIn = Files.newInputStream(metadata);
-				SAXReader reader = new SAXReader();
-		        Document document = reader.read(metadataIn);
-		        Element rootElement = document.getRootElement();
-		        QTIMetadataConverter enricher = new QTIMetadataConverter(rootElement, qItemTypeDao, qEduContextDao, qpoolService);
-		        enricher.toQuestion(item);
+				Document document = readSidecarMetadata(metadata);
+				if(document != null) {
+			        Element rootElement = document.getRootElement();
+			        QTIMetadataConverter enricher = new QTIMetadataConverter(rootElement, qItemTypeDao, qEduContextDao, qpoolService);
+			        enricher.toQuestion(item);
+				}
 			}
 	        return true;
-		} catch(NoSuchFileException e) {
-			//nothing to do
-			return true;
 		} catch (Exception e) {
 			log.error("", e);
 			return false;
-		} finally {
-			IOUtils.closeQuietly(metadataIn);
+		}
+	}
+	
+	private Document readSidecarMetadata(Path metadata) {
+		try(InputStream metadataIn = Files.newInputStream(metadata)) {
+			SAXReader reader = new SAXReader();
+	        return reader.read(metadataIn);
+		} catch(IOException | DocumentException e) {
+			log.error("", e);
+			return null;
 		}
 	}
 	
@@ -658,7 +644,6 @@ class QTIImportProcessor {
 	protected List<DocInfos> getDocInfos() throws IOException {
 		List<DocInfos> doc;
 		if(importedFilename.toLowerCase().endsWith(".zip")) {
-			//doc = traverseZip(importedFile);
 			doc = traverseZip_nio(importedFile);
 		} else {
 			doc = Collections.singletonList(traverseFile(importedFile));
@@ -666,10 +651,9 @@ class QTIImportProcessor {
 		return doc;
 	}
 	
-	private DocInfos traverseFile(File file) throws IOException {
-		InputStream in = new FileInputStream(file);
+	private DocInfos traverseFile(File file) {
 		try {
-			Document doc = readXml(in);
+			Document doc = readXml(file.toPath());
 			if(doc != null) {
 				DocInfos d = new DocInfos();
 				d.doc = doc;
@@ -680,41 +664,9 @@ class QTIImportProcessor {
 		} catch(Exception e) {
 			log.error("", e);
 			return null;
-		} finally {
-			IOUtils.closeQuietly(in);
 		}
 	}
 	
-	/*
-	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(new ShieldInputStream(zis));
-					if(doc != null) {
-						DocInfos d = new DocInfos();
-						d.doc = doc;
-						d.filename = name;
-						docInfos.add(d);
-					}
-				}
-			}
-		} catch(Exception e) {
-			log.error("", e);
-		} finally {
-			IOUtils.closeQuietly(zis);
-			IOUtils.closeQuietly(in);
-		}
-		return docInfos;
-	}
-	*/
-	
 	private List<DocInfos> traverseZip_nio(File file) throws IOException {
 		List<DocInfos> docInfos = new ArrayList<>();
 		
@@ -725,9 +677,7 @@ class QTIImportProcessor {
 		    
 		    List<Path> xmlFiles = visitor.getXmlFiles();
 		    for(Path xmlFile:xmlFiles) {
-		    	InputStream in = Files.newInputStream(xmlFile);
-		    	
-		    	Document doc = readXml(in);
+		    	Document doc = readXml(xmlFile);
 				if(doc != null) {
 					DocInfos d = new DocInfos();
 					d.setDocument(doc);
@@ -762,9 +712,9 @@ class QTIImportProcessor {
 		}
 	}
 	
-	private Document readXml(InputStream in) {
+	private Document readXml(Path xmlFile) {
 		Document doc = null;
-		try {
+		try(InputStream in = Files.newInputStream(xmlFile)) {
 			XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
 			doc = xmlParser.parse(in, false);
 			return doc;
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 df8de04cf14..8f3e15fe2a8 100644
--- a/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java
+++ b/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java
@@ -130,7 +130,7 @@ public class QTIMetadataConverter {
 		}
 		
 		TaxonomyLevel lowerLevel = null;
-		if(path != null && path.length > 0) {
+		if(path.length > 0) {
 			for(String field :cleanedPath) {
 				List<TaxonomyLevel> levels = qpoolService.getTaxonomyLevelBy(lowerLevel, field);
 				
@@ -248,6 +248,11 @@ public class QTIMetadataConverter {
 		if(StringHelper.containsNonWhitespace(educationalContext)) {
 			fullItem.setEducationalContext(toEducationalContext(educationalContext));
 		}
+		
+		String correctionTime = getMetadataEntry("oo_correction_time");
+		if(StringHelper.containsNonWhitespace(correctionTime) && StringHelper.isLong(correctionTime)) {
+			fullItem.setCorrectionTime(toInteger(correctionTime));
+		}
 	}
 	
 	protected void toXml(QuestionItemFull fullItem) {
@@ -273,10 +278,10 @@ public class QTIMetadataConverter {
 		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_topic", fullItem.getTopic(), qtimetadata);
 		addMetadataField("oo_usage", fullItem.getUsage(), qtimetadata);
+		addMetadataField("oo_correction_time", fullItem.getCorrectionTime(), qtimetadata);
 	}
 	
 	private void addMetadataField(String label, int entry, Element metadata) {
@@ -331,7 +336,6 @@ public class QTIMetadataConverter {
 	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");
@@ -362,6 +366,14 @@ public class QTIMetadataConverter {
 		}
 	}
 	
+	private Integer toInteger(String str) {
+		try {
+			return Integer.parseInt(str);
+		} catch (NumberFormatException e) {
+			return null;
+		}
+	}
+	
 	private boolean validStatus(String str) {
 		try {
 			QuestionStatus.valueOf(str);
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java
index 1d2ca535ee4..b9d8717e048 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java
@@ -53,6 +53,7 @@ public class AssessmentItemMetadata {
 	private BigDecimal difficulty;
 	private BigDecimal differentiation;
 	private BigDecimal stdevDifficulty;
+	private Integer correctionTime;
 
 	private boolean hasError;
 	
@@ -227,6 +228,14 @@ public class AssessmentItemMetadata {
 		this.additionalInformations = additionalInformations;
 	}
 
+	public Integer getCorrectionTime() {
+		return correctionTime;
+	}
+
+	public void setCorrectionTime(Integer correctionTime) {
+		this.correctionTime = correctionTime;
+	}
+
 	public void toBuilder(ManifestMetadataBuilder metadata, Locale locale) {
 		if(getQuestionType() != null) {
 			metadata.setOpenOLATMetadataQuestionType(getQuestionType().getPrefix());
@@ -283,6 +292,7 @@ public class AssessmentItemMetadata {
 		metadata.setOpenOLATMetadataTopic(topic);
 		metadata.setOpenOLATMetadataAssessmentType(assessmentType);
 		metadata.setOpenOLATMetadataAdditionalInformations(additionalInformations);
+		metadata.setOpenOLATMetadataCorrectionTime(correctionTime);
 	}
 	
 	public void fromBuilder(ManifestMetadataBuilder metadata) {
@@ -341,6 +351,9 @@ public class AssessmentItemMetadata {
 			if(openolatMetadata.getAdditionalInformations() != null) {
 				additionalInformations = openolatMetadata.getAdditionalInformations();
 			}
+			if(openolatMetadata.getCorrectionTime() != null) {
+				correctionTime = openolatMetadata.getCorrectionTime();
+			}
 		}
 	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/ManifestBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/ManifestBuilder.java
index 16aee5afb77..4fea39e6353 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/ManifestBuilder.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/ManifestBuilder.java
@@ -20,11 +20,12 @@
 package org.olat.ims.qti21.model.xml;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.UUID;
 
@@ -303,22 +304,15 @@ public class ManifestBuilder {
 	}
 	
 	public static final ManifestBuilder read(File file) {
-		try(InputStream in = new FileInputStream(file )) {
-			ManifestType manifest = (ManifestType)((JAXBElement<?>)context
-					.createUnmarshaller().unmarshal(in)).getValue();
-			return new ManifestBuilder(manifest);
-		} catch (JAXBException | IOException e) {
-			log.error("", e);
-			return null;
-		}
+		return read(file.toPath());
 	}
 	
-	public static final ManifestBuilder read(InputStream in) {
-		try {
+	public static final ManifestBuilder read(Path file) {
+		try(InputStream in = Files.newInputStream(file)) {
 			ManifestType manifest = (ManifestType)((JAXBElement<?>)context
 					.createUnmarshaller().unmarshal(in)).getValue();
 			return new ManifestBuilder(manifest);
-		} catch (JAXBException e) {
+		} catch (JAXBException | IOException e) {
 			log.error("", e);
 			return null;
 		}
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java
index a48fbb2703c..fbd7fd9fe19 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java
@@ -723,6 +723,10 @@ public class ManifestMetadataBuilder {
 		getOpenOLATMetadata(true).setAdditionalInformations(informations);
 	}
 	
+	public void setOpenOLATMetadataCorrectionTime(Integer timeInMinute) {
+		getOpenOLATMetadata(true).setCorrectionTime(timeInMinute);
+	}
+	
 	/**
 	 * Return the qti metadata if it exists or if specified, create
 	 * one and append it to the metadata of the resource.
@@ -912,5 +916,6 @@ public class ManifestMetadataBuilder {
 		setOpenOLATMetadataAssessmentType(item.getAssessmentType());
 		setOpenOLATMetadataTopic(item.getTopic());
 		setOpenOLATMetadataAdditionalInformations(item.getAdditionalInformations());
+		setOpenOLATMetadataCorrectionTime(item.getCorrectionTime());
 	}
 }
diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java b/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
index f872dbe7df9..c857c026064 100644
--- a/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
+++ b/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
@@ -24,7 +24,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.Writer;
 import java.net.URI;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.FileVisitResult;
@@ -42,13 +42,12 @@ import javax.xml.parsers.SAXParserFactory;
 import javax.xml.stream.XMLOutputFactory;
 import javax.xml.stream.XMLStreamWriter;
 
+import org.apache.logging.log4j.Logger;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.id.Identity;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.PathUtils;
 import org.olat.core.util.StringHelper;
-import org.olat.core.util.io.ShieldInputStream;
 import org.olat.fileresource.types.ImsQTI21Resource;
 import org.olat.ims.qti.qpool.QTIMetadataConverter;
 import org.olat.ims.qti21.QTI21Constants;
@@ -127,8 +126,7 @@ public class QTI21ImportProcessor {
 			    
 			    List<Path> imsmanifests = visitor.getImsmanifestFiles();
 			    for(Path imsmanifest:imsmanifests) {
-			    		InputStream in = Files.newInputStream(imsmanifest);
-			    		ManifestBuilder manifestBuilder = ManifestBuilder.read(new ShieldInputStream(in));
+			    		ManifestBuilder manifestBuilder = ManifestBuilder.read(imsmanifest);
 			    		List<ResourceType> resources = manifestBuilder.getResourceList();
 					for(ResourceType resource:resources) {
 						ManifestMetadataBuilder metadataBuilder = manifestBuilder.getMetadataBuilder(resource, true);
@@ -228,7 +226,7 @@ public class QTI21ImportProcessor {
 	
 	private void convertXmlFile(Path inputFile, Path outputFile, QTI21Infos infos) {
 		try(InputStream in = Files.newInputStream(inputFile);
-				Writer out = Files.newBufferedWriter(outputFile, Charset.forName("UTF-8"))) {
+				Writer out = Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8)) {
 			XMLOutputFactory xof = XMLOutputFactory.newInstance();
 	        XMLStreamWriter xtw = xof.createXMLStreamWriter(out);
 
@@ -379,6 +377,7 @@ public class QTI21ImportProcessor {
 		poolItem.setTopic(metadata.getTopic());
 		poolItem.setAssessmentType(metadata.getAssessmentType());
 		poolItem.setAdditionalInformations(metadata.getAdditionalInformations());
+		poolItem.setCorrectionTime(metadata.getCorrectionTime());
 	}
 
 	private void createLicense(QuestionItemImpl poolItem, AssessmentItemMetadata metadata) {
diff --git a/src/main/java/org/olat/modules/qpool/QuestionItemShort.java b/src/main/java/org/olat/modules/qpool/QuestionItemShort.java
index 6dc3d547885..6361e0067dc 100644
--- a/src/main/java/org/olat/modules/qpool/QuestionItemShort.java
+++ b/src/main/java/org/olat/modules/qpool/QuestionItemShort.java
@@ -84,4 +84,7 @@ public interface QuestionItemShort extends OLATResourceable, CreateInfo, Modifie
 	//technics
 	public String getFormat();
 	
+	//management
+	public Integer getCorrectionTime();
+	
 }
diff --git a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java
index 71cab0a872b..5b3d2922c2c 100644
--- a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java
+++ b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java
@@ -153,6 +153,9 @@ public class QuestionItemDAO {
 		copy.setItemVersion(original.getItemVersion());
 		copy.setStatus(QuestionStatus.draft.name());
 		
+		// management
+		copy.setCorrectionTime(original.getCorrectionTime());
+		
 		//technical
 		copy.setEditor(original.getEditor());
 		copy.setEditorVersion(original.getEditorVersion());
diff --git a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDocumentFactory.java b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDocumentFactory.java
index 951961ba9f0..3d227a77779 100644
--- a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDocumentFactory.java
+++ b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDocumentFactory.java
@@ -153,7 +153,7 @@ public class QuestionItemDocumentFactory {
 		ResourceLicense license = licenseService.loadLicense(item);
 		if(license != null && license.getLicenseType() != null) {
 			String licenseKey = String.valueOf(license.getLicenseType().getKey());
-			addTextField(document, QItemDocument.LICENSE_TYPE_FIELD_NAME, licenseKey);
+			addTextField(document, AbstractOlatDocument.LICENSE_TYPE_FIELD_NAME, licenseKey);
 		}
 
 		//technical
@@ -195,7 +195,7 @@ public class QuestionItemDocumentFactory {
 			}
 			TaxonomyLevel taxonomyLevel = item.getTaxonomyLevel();
 			if (taxonomyLevel != null) {
-				String materializedPathKeys = taxonomyLevel.getMaterializedPathKeys().replaceAll("/", "_");
+				String materializedPathKeys = taxonomyLevel.getMaterializedPathKeys().replace("/", "_");
 				TextField field = new TextField(QItemDocument.TAXONOMIC_PATH_FIELD, materializedPathKeys, Field.Store.YES);
 				document.add(field);
 			}
diff --git a/src/main/java/org/olat/modules/qpool/model/ItemWrapper.java b/src/main/java/org/olat/modules/qpool/model/ItemWrapper.java
index 9c76e320ec4..a75173196ae 100644
--- a/src/main/java/org/olat/modules/qpool/model/ItemWrapper.java
+++ b/src/main/java/org/olat/modules/qpool/model/ItemWrapper.java
@@ -57,6 +57,8 @@ public class ItemWrapper implements QuestionItemView {
 	private String educationalContextLevel;
 	private String educationalLearningTime;
 	
+	private Integer correctionTime;
+	
 	private String itemType;
 	private BigDecimal difficulty;
 	private BigDecimal stdevDifficulty;
@@ -247,7 +249,12 @@ public class ItemWrapper implements QuestionItemView {
 	public int getUsage() {
 		return usage;
 	}
-	
+
+	@Override
+	public Integer getCorrectionTime() {
+		return correctionTime;
+	}
+
 	@Override
 	public Date getCreationDate() {
 		return creationDate;
@@ -348,6 +355,8 @@ public class ItemWrapper implements QuestionItemView {
 			itemWrapper.educationalContextLevel = item.getEducationalContextLevel();
 			itemWrapper.educationalLearningTime = item.getEducationalLearningTime();
 			
+			itemWrapper.correctionTime = item.getCorrectionTime();
+			
 			itemWrapper.itemType = item.getItemType();
 			itemWrapper.difficulty = item.getDifficulty();
 			itemWrapper.stdevDifficulty = item.getStdevDifficulty();
@@ -368,7 +377,7 @@ public class ItemWrapper implements QuestionItemView {
 		}
 
 		public ItemWrapperBuilder setAuthor(Number authorCount) {
-			itemWrapper.isAuthor = authorCount == null ? false : authorCount.longValue() > 0;
+			itemWrapper.isAuthor = authorCount != null && authorCount.longValue() > 0;
 			return this;
 		}
 
@@ -378,7 +387,7 @@ public class ItemWrapper implements QuestionItemView {
 		}
 
 		public ItemWrapperBuilder setTeacher(Number teacherCount) {
-			itemWrapper.isTeacher = teacherCount == null ? false : teacherCount.longValue() > 0;
+			itemWrapper.isTeacher = teacherCount != null && teacherCount.longValue() > 0;
 			return this;
 		}
 
@@ -388,12 +397,12 @@ public class ItemWrapper implements QuestionItemView {
 		}
 
 		public ItemWrapperBuilder setManager(Number managerCount) {
-			itemWrapper.isManager = managerCount == null ? false : managerCount.longValue() > 0;
+			itemWrapper.isManager = managerCount != null && managerCount.longValue() > 0;
 			return this;
 		}
 		
 		public ItemWrapperBuilder setRater(Number ratingsCount) {
-			itemWrapper.isRater = ratingsCount == null ? false : ratingsCount.longValue() > 0;
+			itemWrapper.isRater = ratingsCount != null && ratingsCount.longValue() > 0;
 			return this;
 		}
 
@@ -403,7 +412,7 @@ public class ItemWrapper implements QuestionItemView {
 		}
 
 		public ItemWrapperBuilder setEditableInPool(Number editableInPoolCount) {
-			itemWrapper.isEditableInPool = editableInPoolCount == null ? false : editableInPoolCount.longValue() > 0;
+			itemWrapper.isEditableInPool = editableInPoolCount != null && editableInPoolCount.longValue() > 0;
 			return this;
 		}
 
@@ -413,7 +422,7 @@ public class ItemWrapper implements QuestionItemView {
 		}
 
 		public ItemWrapperBuilder setEditableInShare(Number editableInShareCount) {
-			itemWrapper.isEditableInShare = editableInShareCount == null ? false : editableInShareCount.longValue() > 0;
+			itemWrapper.isEditableInShare = editableInShareCount != null && editableInShareCount.longValue() > 0;
 			return this;
 		}
 
@@ -423,7 +432,7 @@ public class ItemWrapper implements QuestionItemView {
 		}
 
 		public ItemWrapperBuilder setMarked(Number markedCount) {
-			itemWrapper.isMarked = markedCount == null ? false : markedCount.longValue() > 0;
+			itemWrapper.isMarked = markedCount != null && markedCount.longValue() > 0;
 			return this;
 		}
 
@@ -438,9 +447,8 @@ public class ItemWrapper implements QuestionItemView {
 		}
 		
 		public ItemWrapper create() {
-			log.debug("Question item wrapped:" + itemWrapper.toString());
+			log.debug("Question item wrapped: {}", itemWrapper);
 			return itemWrapper;
 		}
 	}
-
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/qpool/model/QuestionItemImpl.java b/src/main/java/org/olat/modules/qpool/model/QuestionItemImpl.java
index f7976aeeb9d..94e8d2703c4 100644
--- a/src/main/java/org/olat/modules/qpool/model/QuestionItemImpl.java
+++ b/src/main/java/org/olat/modules/qpool/model/QuestionItemImpl.java
@@ -120,6 +120,10 @@ public class QuestionItemImpl implements QuestionItemFull, Persistable {
 	@Column(name="q_assessment_type", nullable=true, insertable=true, updatable=true)
 	private String assessmentType;
 	
+	//management
+	@Column(name="q_correction_time", nullable=true, insertable=true, updatable=true)
+	private Integer correctionTime;
+	
 	//life cycle
 	@Column(name="q_version", nullable=true, insertable=true, updatable=true)
 	private String itemVersion;
@@ -411,6 +415,15 @@ public class QuestionItemImpl implements QuestionItemFull, Persistable {
 		this.status = status;
 	}
 
+	@Override
+	public Integer getCorrectionTime() {
+		return correctionTime;
+	}
+
+	public void setCorrectionTime(Integer correctionTime) {
+		this.correctionTime = correctionTime;
+	}
+
 	@Override
 	public String getKeywords() {
 		return keywords;
diff --git a/src/main/java/org/olat/modules/qpool/ui/AbstractItemListController.java b/src/main/java/org/olat/modules/qpool/ui/AbstractItemListController.java
index 9aa36b2a0b1..cf3afdf59bb 100644
--- a/src/main/java/org/olat/modules/qpool/ui/AbstractItemListController.java
+++ b/src/main/java/org/olat/modules/qpool/ui/AbstractItemListController.java
@@ -198,6 +198,7 @@ public abstract class AbstractItemListController extends FormBasicController
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, Cols.differentiation));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, Cols.numOfAnswerAlternatives));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, Cols.usage));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, Cols.correctionTime));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.type));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.format));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.rating));
diff --git a/src/main/java/org/olat/modules/qpool/ui/ItemRow.java b/src/main/java/org/olat/modules/qpool/ui/ItemRow.java
index 3db090a14cb..87fb4778c9f 100644
--- a/src/main/java/org/olat/modules/qpool/ui/ItemRow.java
+++ b/src/main/java/org/olat/modules/qpool/ui/ItemRow.java
@@ -212,6 +212,11 @@ public class ItemRow implements QuestionItemView {
 		return delegate.getUsage();
 	}
 	
+	@Override
+	public Integer getCorrectionTime() {
+		return delegate.getCorrectionTime();
+	}
+
 	@Override
 	public Date getCreationDate() {
 		return delegate.getCreationDate();
diff --git a/src/main/java/org/olat/modules/qpool/ui/QuestionItemAuditLogExport.java b/src/main/java/org/olat/modules/qpool/ui/QuestionItemAuditLogExport.java
index 670914e8fac..03221bf71e8 100644
--- a/src/main/java/org/olat/modules/qpool/ui/QuestionItemAuditLogExport.java
+++ b/src/main/java/org/olat/modules/qpool/ui/QuestionItemAuditLogExport.java
@@ -19,7 +19,6 @@
  */
 package org.olat.modules.qpool.ui;
 
-import java.io.IOException;
 import java.io.OutputStream;
 import java.math.BigDecimal;
 import java.util.Collections;
@@ -27,12 +26,12 @@ import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 
+import org.apache.logging.log4j.Logger;
 import org.olat.core.CoreSpringFactory;
 import org.olat.core.commons.services.license.License;
 import org.olat.core.commons.services.license.LicenseModule;
 import org.olat.core.commons.services.license.LicenseService;
 import org.olat.core.gui.translator.Translator;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
@@ -99,8 +98,6 @@ public class QuestionItemAuditLogExport extends OpenXMLWorkbookResource {
 			addSheetSettings(exportSheet);
 			addHeader(exportSheet);
 			addContent(exportSheet, workbook);
-		} catch (IOException e) {
-			log.error("", e);
 		} catch (Exception e) {
 			log.error("", e);
 		}
@@ -139,6 +136,7 @@ public class QuestionItemAuditLogExport extends OpenXMLWorkbookResource {
 		headerRow.addCell(pos++, translator.translate("export.log.header.differentiation"));
 		headerRow.addCell(pos++, translator.translate("export.log.header.numOfAnswerAlternatives"));
 		headerRow.addCell(pos++, translator.translate("export.log.header.usage"));
+		headerRow.addCell(pos++, translator.translate("export.log.header.correctionTime"));
 		headerRow.addCell(pos++, translator.translate("export.log.header.version"));
 		headerRow.addCell(pos++, translator.translate("export.log.header.status"));
 		if (licenseModule.isEnabled(licenseHandler)) {
@@ -183,6 +181,7 @@ public class QuestionItemAuditLogExport extends OpenXMLWorkbookResource {
 				row.addCell(pos++, format(item.getDifferentiation()));
 				row.addCell(pos++, String.valueOf(item.getNumOfAnswerAlternatives()));
 				row.addCell(pos++, String.valueOf(item.getUsage()));
+				row.addCell(pos++, item.getCorrectionTime(), null);
 				row.addCell(pos++, item.getItemVersion());
 				row.addCell(pos++, getTranslatedStatus(item.getQuestionStatus()));
 			} else {
diff --git a/src/main/java/org/olat/modules/qpool/ui/QuestionItemDataModel.java b/src/main/java/org/olat/modules/qpool/ui/QuestionItemDataModel.java
index d35e2c50d24..a6517ec1580 100644
--- a/src/main/java/org/olat/modules/qpool/ui/QuestionItemDataModel.java
+++ b/src/main/java/org/olat/modules/qpool/ui/QuestionItemDataModel.java
@@ -97,6 +97,7 @@ public class QuestionItemDataModel extends DefaultFlexiTableDataSourceModel<Item
 				return item.getNumOfAnswerAlternatives() > 0 ? Integer.toString(item.getNumOfAnswerAlternatives()) : "";
 			case usage:
 				return item.getUsage() > 0 ? Integer.toString(item.getUsage()) : "";
+			case correctionTime: return item.getCorrectionTime();
 			case type: {
 				String type = item.getItemType();
 				if(type == null) {
@@ -149,7 +150,8 @@ public class QuestionItemDataModel extends DefaultFlexiTableDataSourceModel<Item
 		statusLastModified("lifecycle.status.last.modified"),
 		license("rights.license"),
 		editable("editable"),
-		mark("mark");
+		mark("mark"),
+		correctionTime("question.correctionTime");
 		
 		private final String i18nKey;
 	
diff --git a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties
index 93a2c38898d..255f4dcfb9f 100644
--- a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties
@@ -79,6 +79,7 @@ export.log=Log
 export.log.header.additional.informations=$\:general.additional.informations
 export.log.header.assessment.type=$\:question.assessmentType
 export.log.header.context=$\:educational.context
+export.log.header.correctionTime=$\:question.correctionTime
 export.log.header.coverage=$\:general.coverage
 export.log.header.differentiation=$\:question.differentiation
 export.log.header.difficulty=$\:question.difficulty
@@ -231,6 +232,7 @@ question.assessmentType=Testart
 question.assessmentType.both=Both
 question.assessmentType.formative=Formative
 question.assessmentType.summative=Summative
+question.correctionTime=Ben\u00F6tigte manuelle Korrekturszeit (Minuten)
 question.differentiation=Trennsch\u00E4rfe
 question.differentiation.example=Wert zwischen -1.0 und 1.0, zum Beispiel 0.1
 question.difficulty=Itemschwierigkeit
diff --git a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_en.properties
index 1045cc6a1e7..5530355799b 100644
--- a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_en.properties
@@ -79,6 +79,7 @@ export.log=Log
 export.log.header.additional.informations=$\:general.additional.informations
 export.log.header.assessment.type=$\:question.assessmentType
 export.log.header.context=$\:educational.context
+export.log.header.correctionTime=$\:question.correctionTime
 export.log.header.coverage=$\:general.coverage
 export.log.header.differentiation=$\:question.differentiation
 export.log.header.difficulty=$\:question.difficulty
@@ -231,6 +232,7 @@ question.assessmentType=Test type
 question.assessmentType.both=Both
 question.assessmentType.formative=Formative
 question.assessmentType.summative=Summative
+question.correctionTime=Requited manual grading time (minutes)
 question.differentiation=Discrimination index
 question.differentiation.example=Value between -1.0 and 1.0. Example\: 0.1
 question.difficulty=Difficulty index
diff --git a/src/main/java/org/olat/modules/qpool/ui/metadata/QuestionMetadataEditController.java b/src/main/java/org/olat/modules/qpool/ui/metadata/QuestionMetadataEditController.java
index cc6c8b0a1a5..b971e57d03c 100644
--- a/src/main/java/org/olat/modules/qpool/ui/metadata/QuestionMetadataEditController.java
+++ b/src/main/java/org/olat/modules/qpool/ui/metadata/QuestionMetadataEditController.java
@@ -38,6 +38,7 @@ import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.modules.qpool.MetadataSecurityCallback;
 import org.olat.modules.qpool.QPoolService;
@@ -70,6 +71,7 @@ public class QuestionMetadataEditController extends FormBasicController {
 	private TextElement differentiationEl;
 	private TextElement numAnswerAltEl;
 	private TextElement usageEl;
+	private TextElement correctionTimeMinuteElement;
 	private FormLayoutContainer buttonsCont;
 	
 	private QuestionItem item;
@@ -161,7 +163,12 @@ public class QuestionMetadataEditController extends FormBasicController {
 		usageEl = uifactory.addTextElement("question.usage", "question.usage", 24, numUsage, formLayout);
 		usageEl.setElementCssClass("o_sel_usage");
 		usageEl.setDisplaySize(4);
-
+		
+		String correctionTime = item.getCorrectionTime() == null ? "" : item.getCorrectionTime().toString();
+		correctionTimeMinuteElement = uifactory.addTextElement("question.correctionTime", 8, correctionTime, formLayout);
+		correctionTimeMinuteElement.setElementCssClass("o_sel_correction_time");
+		correctionTimeMinuteElement.setDisplaySize(4);
+		
 		buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
 		buttonsCont.setElementCssClass("o_sel_qpool_metadata_buttons");
 		buttonsCont.setRootForm(mainForm);
@@ -205,6 +212,7 @@ public class QuestionMetadataEditController extends FormBasicController {
 		allOk &= validateBigDecimal(differentiationEl, -1.0d, 1.0d, true);
 		allOk &= validateInteger(numAnswerAltEl, 0, Integer.MAX_VALUE, true);
 		allOk &= validateInteger(usageEl, 0, Integer.MAX_VALUE, true);
+		allOk &= validateInteger(correctionTimeMinuteElement, 0, Integer.MAX_VALUE, true);
 		return allOk;
 	}
 	
@@ -242,6 +250,11 @@ public class QuestionMetadataEditController extends FormBasicController {
 			
 			int numUsage = toInt(usageEl.getValue());
 			itemImpl.setUsage(numUsage);
+			
+			if(StringHelper.containsNonWhitespace(correctionTimeMinuteElement.getValue())
+					&& StringHelper.isLong(correctionTimeMinuteElement.getValue())) {
+				itemImpl.setCorrectionTime(Integer.valueOf(correctionTimeMinuteElement.getValue()));
+			}
 
 			item = qpoolService.updateItem(itemImpl);
 			builder.withAfter(item);
diff --git a/src/main/resources/database/mysql/alter_14_1_x_to_14_2_0.sql b/src/main/resources/database/mysql/alter_14_1_x_to_14_2_0.sql
index 544d7b50c7a..6de790bfa5b 100644
--- a/src/main/resources/database/mysql/alter_14_1_x_to_14_2_0.sql
+++ b/src/main/resources/database/mysql/alter_14_1_x_to_14_2_0.sql
@@ -24,3 +24,7 @@ alter table o_noti_sub add column subenabled bit default 1;
 -- index
 create index mark_all_idx on o_mark(resname,resid,creator_id);
 create index idx_eff_stat_course_ident_idx on o_as_eff_statement (fk_identity,course_repo_key);
+
+-- question pool
+alter table o_qp_item add column q_correction_time bigint default null;
+
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index 914261fb98a..7c05dd24ad5 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -1650,6 +1650,7 @@ create table o_qp_item (
    q_differentiation decimal(10,9),
    q_num_of_answers_alt bigint not null default 0,
    q_usage bigint not null default 0,
+   q_correction_time bigint default null,
    q_assessment_type varchar(64),
    q_status varchar(32) not null,
    q_version varchar(50),
diff --git a/src/main/resources/database/oracle/alter_14_1_x_to_14_2_0.sql b/src/main/resources/database/oracle/alter_14_1_x_to_14_2_0.sql
index 8c5828838bb..cd0b42f81b4 100644
--- a/src/main/resources/database/oracle/alter_14_1_x_to_14_2_0.sql
+++ b/src/main/resources/database/oracle/alter_14_1_x_to_14_2_0.sql
@@ -26,3 +26,5 @@ alter table o_noti_sub add subenabled number default 1;
 create index mark_all_idx on o_mark(resname,resid,creator_id);
 create index idx_eff_stat_course_ident_idx on o_as_eff_statement (fk_identity,course_repo_key);
 
+-- question pool
+alter table o_qp_item add q_correction_time number(20) default null;
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index 0860673c063..cdcce51147a 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -2210,6 +2210,7 @@ create table o_qp_item (
    q_differentiation decimal(10,9),
    q_num_of_answers_alt number(20) default 0 not null,
    q_usage number(20) default 0 not null,
+   q_correction_time number(20) default null,
    q_assessment_type varchar2(64 char),
    q_status varchar2(32 char) not null,
    q_version varchar2(50 char),
diff --git a/src/main/resources/database/postgresql/alter_14_1_x_to_14_2_0.sql b/src/main/resources/database/postgresql/alter_14_1_x_to_14_2_0.sql
index 50f3fff6f9b..59e40c347e7 100644
--- a/src/main/resources/database/postgresql/alter_14_1_x_to_14_2_0.sql
+++ b/src/main/resources/database/postgresql/alter_14_1_x_to_14_2_0.sql
@@ -28,3 +28,7 @@ alter table o_noti_sub add column subenabled bool default true;
 -- index
 create index mark_all_idx on o_mark(resname,resid,creator_id);
 create index idx_eff_stat_course_ident_idx on o_as_eff_statement (fk_identity,course_repo_key);
+
+-- question pool
+alter table o_qp_item add column q_correction_time int8 default null;
+
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index e4f9d3225ed..63905caeb91 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -2173,6 +2173,7 @@ create table o_qp_item (
    q_differentiation decimal(10,9),
    q_num_of_answers_alt int8 not null default 0,
    q_usage int8 not null default 0,
+   q_correction_time int8 default null,
    q_assessment_type varchar(64),
    q_status varchar(32) not null,
    q_version varchar(50),
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 60d87d43071..c9ed1afbbb4 100644
--- a/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java
+++ b/src/test/java/org/olat/ims/qti/qpool/QTIExportProcessorTest.java
@@ -31,7 +31,6 @@ import java.util.Locale;
 import java.util.UUID;
 import java.util.zip.ZipOutputStream;
 
-import org.apache.commons.io.IOUtils;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -89,8 +88,8 @@ public class QTIExportProcessorTest extends OlatTestCase {
 		OutputStream out = new ByteArrayOutputStream();
 		ZipOutputStream zout = new ZipOutputStream(out);
 		exportProc.assembleTest(fullItems, zout);
-
-		IOUtils.closeQuietly(zout);
-		IOUtils.closeQuietly(out);
+		
+		zout.close();
+		out.close();
 	}
 }
\ No newline at end of file
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 d4339eae321..3cc94892b75 100644
--- a/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java
+++ b/src/test/java/org/olat/ims/qti/qpool/QTIImportProcessorTest.java
@@ -29,6 +29,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.UUID;
 
+import org.apache.logging.log4j.Logger;
 import org.dom4j.Document;
 import org.dom4j.Element;
 import org.dom4j.Node;
@@ -37,6 +38,7 @@ import org.junit.Before;
 import org.junit.Test;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.id.Identity;
+import org.olat.core.logging.Tracing;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.vfs.VFSContainer;
 import org.olat.core.util.vfs.VFSItem;
@@ -70,6 +72,8 @@ import org.springframework.beans.factory.annotation.Autowired;
  */
 public class QTIImportProcessorTest extends OlatTestCase {
 	
+	private static final Logger log = Tracing.createLoggerFor(QTIImportProcessorTest.class);
+	
 	private static Identity owner;
 	
 	@Autowired
@@ -159,6 +163,8 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		Assert.assertTrue(qtiLeaf instanceof VFSLeaf);
 		Assert.assertTrue(qtiLeaf.exists());
 		Assert.assertEquals(itemFile.length(), ((VFSLeaf)qtiLeaf).getSize());
+		
+		docInfos.close();
 	}
 	
 	@Test
@@ -216,6 +222,8 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		List<ItemInfos> itemElements = proc.getItemList(docInfos);
 		Assert.assertNotNull(itemElements);
 		Assert.assertEquals(4, itemElements.size());
+		
+		docInfos.close();
 	}
 	
 	@Test
@@ -272,10 +280,14 @@ public class QTIImportProcessorTest extends OlatTestCase {
 			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");
+			Node itemNode = null;
+			try(InputStream is = ((VFSLeaf)itemLeaf).getInputStream()) {
+				XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
+				Document doc = xmlParser.parse(is, false);
+				itemNode = doc.selectSingleNode("questestinterop/item");
+			} catch(IOException e) {
+				log.error("", e);
+			}
 			Assert.assertNotNull(itemNode);
 		}
 	}
@@ -305,10 +317,14 @@ public class QTIImportProcessorTest extends OlatTestCase {
 			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");
+			Node itemNode = null;
+			try(InputStream is = ((VFSLeaf)itemLeaf).getInputStream()) {
+				XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
+				Document doc = xmlParser.parse(is, false);
+				itemNode = doc.selectSingleNode("questestinterop/item");
+			} catch(IOException e) {
+				log.error("", e);
+			}
 			Assert.assertNotNull(itemNode);
 			
 		//check the attachments
@@ -351,10 +367,14 @@ public class QTIImportProcessorTest extends OlatTestCase {
 			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");
+			Node itemNode = null;
+			try(InputStream is = ((VFSLeaf)itemLeaf).getInputStream()) {
+				XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
+				Document doc = xmlParser.parse(is, false);
+				itemNode = doc.selectSingleNode("questestinterop/item");
+			} catch(IOException e) {
+				log.error("", e);
+			}
 			Assert.assertNotNull(itemNode);
 			
 			//check the attachments
@@ -404,10 +424,14 @@ public class QTIImportProcessorTest extends OlatTestCase {
 			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");
+			Node itemNode = null;
+			try(InputStream is = ((VFSLeaf)itemLeaf).getInputStream()) {
+				XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
+				Document doc = xmlParser.parse(is, false);
+				itemNode = doc.selectSingleNode("questestinterop/item");
+			} catch(IOException e) {
+				log.error("", e);
+			}
 			Assert.assertNotNull(itemNode);
 			
 			//check the attachments
@@ -487,6 +511,7 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		Assert.assertEquals("/Physique/Astronomie/Astrophysique/", item.getTaxonomicPath());
 		Assert.assertEquals("Une question sur Pluton", item.getTitle());
 		Assert.assertEquals(0, item.getUsage());
+		Assert.assertEquals(Integer.valueOf(2), item.getCorrectionTime());
 	}
 	
 	@Test
@@ -514,6 +539,8 @@ public class QTIImportProcessorTest extends OlatTestCase {
 		Assert.assertNotNull(materials);
 		Assert.assertEquals(1, materials.size());
 		Assert.assertEquals("media/filmH264.mp4", materials.get(0));
+		
+		docInfos.close();
 	}
 	
 	private boolean exists(QuestionItemFull itemFull, String path) {
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
index aa7abd90c694eea11c1dd5e5218462ea4ef99cb5..b5a0be721ccd3137505c2d785830c5be22df46d1 100644
GIT binary patch
literal 3128
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

delta 1618
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

diff --git a/src/test/java/org/olat/ims/qti/statistics/manager/QTIStatisticsManagerLargeTest.java b/src/test/java/org/olat/ims/qti/statistics/manager/QTIStatisticsManagerLargeTest.java
index 6843608d41a..b0431c92b1c 100644
--- a/src/test/java/org/olat/ims/qti/statistics/manager/QTIStatisticsManagerLargeTest.java
+++ b/src/test/java/org/olat/ims/qti/statistics/manager/QTIStatisticsManagerLargeTest.java
@@ -22,6 +22,7 @@ package org.olat.ims.qti.statistics.manager;
 
 import static org.junit.Assert.assertNotNull;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Calendar;
@@ -33,6 +34,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+import org.apache.logging.log4j.Logger;
 import org.dom4j.Document;
 import org.dom4j.Element;
 import org.junit.Assert;
@@ -41,7 +43,6 @@ import org.junit.Test;
 import org.olat.basesecurity.OrganisationService;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.id.Organisation;
-import org.apache.logging.log4j.Logger;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.xml.XMLParser;
 import org.olat.ims.qti.QTIResult;
@@ -328,20 +329,24 @@ public class QTIStatisticsManagerLargeTest extends OlatTestCase {
 	}
 
 	@SuppressWarnings("rawtypes")
-	private void getItemObjectList() {
-		InputStream in = QTIStatisticsManagerLargeTest.class.getResourceAsStream("qti.xml");
-		XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
-		Document doc = xmlParser.parse(in, false);
-		Element root = doc.getRootElement();
-		List items = root.selectNodes("//item");
-		itemObjects = new ArrayList<>();
-		for (Iterator iter = items.iterator(); iter.hasNext();) {
-			Element el_item = (Element) iter.next();
-			if (el_item.selectNodes(".//response_lid").size() > 0) {
-				itemObjects.add(new ItemWithResponseLid(el_item));
-			} else if (el_item.selectNodes(".//response_str").size() > 0) {
-				itemObjects.add(new ItemWithResponseStr(el_item));
+	private void getItemObjectList() throws IOException {
+		try(InputStream in = QTIStatisticsManagerLargeTest.class.getResourceAsStream("qti.xml")) {
+			XMLParser xmlParser = new XMLParser(new IMSEntityResolver());
+			Document doc = xmlParser.parse(in, false);
+			Element root = doc.getRootElement();
+			List items = root.selectNodes("//item");
+			itemObjects = new ArrayList<>();
+			for (Iterator iter = items.iterator(); iter.hasNext();) {
+				Element el_item = (Element) iter.next();
+				if (el_item.selectNodes(".//response_lid").size() > 0) {
+					itemObjects.add(new ItemWithResponseLid(el_item));
+				} else if (el_item.selectNodes(".//response_str").size() > 0) {
+					itemObjects.add(new ItemWithResponseStr(el_item));
+				}
 			}
+		} catch(IOException e) {
+			log.error("", e);
+			throw e;
 		}
 	}
 	
diff --git a/src/test/java/org/olat/modules/qpool/manager/QuestionDAOTest.java b/src/test/java/org/olat/modules/qpool/manager/QuestionDAOTest.java
index c6ec572b6b5..baf4251e78d 100644
--- a/src/test/java/org/olat/modules/qpool/manager/QuestionDAOTest.java
+++ b/src/test/java/org/olat/modules/qpool/manager/QuestionDAOTest.java
@@ -150,6 +150,7 @@ public class QuestionDAOTest extends OlatTestCase {
 		original.setNumOfAnswerAlternatives(4);
 		original.setUsage(5);
 		original.setAssessmentType("formative");
+		original.setCorrectionTime(3);
 		//lifecycle
 		original.setItemVersion("1.0");
 		original.setStatus(QuestionStatus.review.name());
@@ -193,6 +194,7 @@ public class QuestionDAOTest extends OlatTestCase {
 		Assert.assertEquals(original.getNumOfAnswerAlternatives(), clone.getNumOfAnswerAlternatives());
 		Assert.assertEquals(0, clone.getUsage());
 		Assert.assertEquals(original.getAssessmentType(), clone.getAssessmentType());
+		Assert.assertEquals(Integer.valueOf(3), clone.getCorrectionTime());
 		//lifecycle
 		Assert.assertEquals(QuestionStatus.draft.name(), clone.getStatus());
 		Assert.assertNotNull(clone.getQuestionStatusLastModified());
diff --git a/src/test/java/org/olat/selenium/QuestionPoolTest.java b/src/test/java/org/olat/selenium/QuestionPoolTest.java
index d190bff7867..5229f2cbece 100644
--- a/src/test/java/org/olat/selenium/QuestionPoolTest.java
+++ b/src/test/java/org/olat/selenium/QuestionPoolTest.java
@@ -277,7 +277,7 @@ public class QuestionPoolTest extends Deployments {
 			.metadata()
 			.openItemAnalyse()
 			.setLearningTime(1, 5, 3, 35)
-			.setItemAnalyse(0.5d, 0.3d, -0.7d, 2, 3)
+			.setItemAnalyse(0.5d, 0.3d, -0.7d, 2, 3, 5)
 			.saveItemAnalyse();
 		
 		// open quick view
@@ -292,7 +292,8 @@ public class QuestionPoolTest extends Deployments {
 			.assertStandardDeviation(0.3d)
 			.assertDiscriminationIndex(-0.7d)
 			.assertDistractors(2)
-			.assertUsage(3);
+			.assertUsage(3)
+			.assertCorrectionTime(5);
 	}
 
 }
diff --git a/src/test/java/org/olat/selenium/page/qpool/QuestionMetadataPage.java b/src/test/java/org/olat/selenium/page/qpool/QuestionMetadataPage.java
index 48831ae9ef3..77f6aa86fd3 100644
--- a/src/test/java/org/olat/selenium/page/qpool/QuestionMetadataPage.java
+++ b/src/test/java/org/olat/selenium/page/qpool/QuestionMetadataPage.java
@@ -136,11 +136,12 @@ public class QuestionMetadataPage {
 	 * @param standardDeviation Value between 0.0 and 1.0
 	 * @param discriminationIndex Value between -1.0 and 1.0
 	 * @param distractors The number of distractors
-	 * @param usage
+	 * @param usage Number of times this questions is used
+	 * @param correctionTime Time in minutes to correction the question
 	 * @return Itself
 	 */
 	public QuestionMetadataPage setItemAnalyse(Double difficulty, Double standardDeviation,
-			Double discriminationIndex, Integer distractors, Integer usage) {
+			Double discriminationIndex, Integer distractors, Integer usage, Integer correctionTime) {
 		
 		if(difficulty != null) {
 			By difficultyBy = By.cssSelector(".o_sel_qpool_metadata_item_analyse .o_sel_difficulty input[type='text']");
@@ -166,6 +167,10 @@ public class QuestionMetadataPage {
 			usageEl.clear();
 			usageEl.sendKeys(usage.toString());
 		}
+		if(correctionTime != null) {
+			By correctionTimeBy = By.cssSelector(".o_sel_qpool_metadata_item_analyse .o_sel_correction_time input[type='text']");
+			browser.findElement(correctionTimeBy).sendKeys(correctionTime.toString());
+		}
 		return this;
 	}
 	
@@ -261,6 +266,12 @@ public class QuestionMetadataPage {
 		return this;
 	}
 	
+	public QuestionMetadataPage assertCorrectionTime(Integer timeInMinutes) {
+		By correctionTimeBy = By.xpath("//div[contains(@class,'o_sel_correction_time')]//input[@value='" + timeInMinutes + "']");
+		OOGraphene.waitElement(correctionTimeBy, browser);
+		return this;
+	}
+	
 	public QuestionMetadataPage saveGeneralMetadata() {
 		return saveMetadata("o_sel_qpool_metadata_general");
 	}
diff --git a/src/test/resources/arquillian.xml b/src/test/resources/arquillian.xml
index e0a73610f54..d710391bc9b 100644
--- a/src/test/resources/arquillian.xml
+++ b/src/test/resources/arquillian.xml
@@ -25,7 +25,7 @@
 		<property name="dimensions">1024x800</property>
 		<!--
 		<property name="downloadBinaries">no</property>
-		<property name="chromeDriverBinary">target/drone/12b3858a57bfe0a3d450cd194e0992aa/chromedriver</property>
+		<property name="chromeDriverBinary">target/drone/57d2a9629298aa6dc2d759fe09da5d13/chromedriver</property>
 		<property name="firefoxDriverBinary">target/drone/ce03addb1fc8c24900011f90fc80f3c1/geckodriver</property>
 		-->
 		<property name="firefoxUserPreferences">src/test/profile/firefox/prefs.js</property>
-- 
GitLab