From 948fbca19778ff4001ad400ae12a5bc140f8b919 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Fri, 1 Jul 2016 08:46:30 +0200
Subject: [PATCH] OO-1593: start work on word export, fix image of alien item
 in preview

---
 .../util/openxml/HTMLToOpenXMLHandler.java    | 116 ++--
 .../core/util/openxml/OpenXMLDocument.java    |  47 +-
 .../util/openxml/OpenXMLDocumentWriter.java   | 144 +++--
 .../olat/core/util/openxml/OpenXMLUtils.java  |   2 +-
 .../qti21/manager/CorrectResponsesUtil.java   |   7 +-
 .../manager/openxml/QTI21WordExport.java      | 531 ++++++++++++++++++
 .../model/xml/AssessmentItemFactory.java      |  16 +
 .../model/xml/AssessmentTestBuilder.java      |   4 +-
 .../FIBAssessmentItemBuilder.java             |   2 +-
 .../ui/AssessmentItemDisplayController.java   |   5 +-
 .../AssessmentItemEditorController.java       |   4 +-
 .../AssessmentItemPreviewController.java      |   5 +-
 .../AssessmentTestComposerController.java     |  19 +-
 .../editor/_i18n/LocalStrings_de.properties   |   1 +
 .../editor/_i18n/LocalStrings_en.properties   |   1 +
 .../editor/_i18n/LocalStrings_fr.properties   |   1 +
 16 files changed, 786 insertions(+), 119 deletions(-)
 create mode 100644 src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java

diff --git a/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java b/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java
index 47782ba9b0e..e9a9316c7d7 100644
--- a/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java
+++ b/src/main/java/org/olat/core/util/openxml/HTMLToOpenXMLHandler.java
@@ -19,6 +19,7 @@
  */
 package org.olat.core.util.openxml;
 
+import java.io.File;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -54,33 +55,41 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 	private StringBuilder textBuffer;
 	private Spacing startSpacing;
 	
-	private final OpenXMLDocument factory;
+	protected final OpenXMLDocument factory;
 	
-	private List<Node> content = new ArrayList<Node>();
-	private Deque<StyleStatus> styleStack = new ArrayDeque<StyleStatus>();
+	protected List<Node> content = new ArrayList<Node>();
+	protected Deque<StyleStatus> styleStack = new ArrayDeque<StyleStatus>();
 	
-	private Table currentTable;
-	private Element currentParagraph;
-	private ListParagraph currentListParagraph;
-	private boolean pNeedNewParagraph = true;
+	protected Table currentTable;
+	protected Element currentParagraph;
+	protected ListParagraph currentListParagraph;
+	protected boolean pNeedNewParagraph = true;
 	
-	public HTMLToOpenXMLHandler(OpenXMLDocument document, Element paragraph) {
+	public HTMLToOpenXMLHandler(OpenXMLDocument document) {
 		this.factory = document;
+	}
+	
+	public HTMLToOpenXMLHandler(OpenXMLDocument document, Element paragraph) {
+		this(document);
 		this.currentParagraph = paragraph;
 	}
 	
 	public HTMLToOpenXMLHandler(OpenXMLDocument document, Spacing spacing) {
-		this.factory = document;
+		this(document);
 		this.startSpacing = spacing;
 	}
 	
+	public void setInitialParagraph(Element paragraph) {
+		this.currentParagraph = paragraph;
+	}
+	
 	/**
 	 * Flush the text if a new paragraph is created. Trailing text is flushed
 	 * in the previous paragraph.
 	 * @param create
 	 * @return
 	 */
-	private Element getCurrentParagraph(boolean create) {
+	protected Element getCurrentParagraph(boolean create) {
 		if(create || currentParagraph == null) {
 			//flush the text
 			if(textBuffer != null) {
@@ -97,7 +106,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		return currentParagraph;
 	}
 	
-	private Element appendParagraph(Spacing spacing) {
+	protected Element appendParagraph(Spacing spacing) {
 		//flush the text
 		if(textBuffer != null) {
 			flushText();
@@ -110,7 +119,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		return currentParagraph;
 	}
 	
-	private Element getCurrentListParagraph(boolean create) {
+	protected Element getCurrentListParagraph(boolean create) {
 		if(create || currentParagraph == null) {
 			//flush the text
 			if(textBuffer != null) {
@@ -122,14 +131,14 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		return currentParagraph;
 	}
 	
-	private void closeParagraph() {
+	protected void closeParagraph() {
 		flushText();
 		currentParagraph = addContent(currentParagraph);
 		textBuffer = null;
 		latex = false;
 	}
 	
-	private Element addContent(Node element) {
+	protected Element addContent(Node element) {
 		if(element == null) return null;
 		
 		if(currentTable != null) {
@@ -140,7 +149,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		return null;
 	}
 	
-	private void flushText() {
+	protected void flushText() {
 		if(textBuffer == null) return;
 		
 		if(latex) {
@@ -171,7 +180,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 	 * Get or create a run on the current paragraph
 	 * @return
 	 */
-	private Element getCurrentRun() {
+	protected Element getCurrentRun() {
 		Element paragraphEl;
 		if(currentParagraph == null) {
 			Indent indent = getCurrentIndent();
@@ -191,7 +200,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		return (Element)paragraphEl.appendChild(factory.createRunEl(null, runStyle));
 	}
 
-	private Style[] setTextPreferences(String cssStyles) {
+	protected Style[] setTextPreferences(String cssStyles) {
 		if(cssStyles == null) {
 			return setTextPreferences();
 		} else {
@@ -207,19 +216,19 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 	/**
 	 * Create a new run with preferences
 	 */
-	private Style[] setTextPreferences(Style... styles) {
+	protected Style[] setTextPreferences(Style... styles) {
 		Node runPrefs = getRunForTextPreferences();
 		factory.createRunPrefsEl(runPrefs, styles);
 		return styles;
 	}
 	
-	private Style[] unsetTextPreferences(Style... styles) {
+	protected Style[] unsetTextPreferences(Style... styles) {
 		Node runPrefs = getRunForTextPreferences();
 		factory.createRunReversePrefsEl(runPrefs, styles);
 		return styles;
 	}
 	
-	private Node getRunForTextPreferences() {
+	protected Node getRunForTextPreferences() {
 		Element paragraphEl = getCurrentParagraph(false);
 		
 		Node runPrefs = null;
@@ -305,7 +314,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		return null;
 	}
 	
-	private void setImage(String path) {
+	protected void setImage(String path) {
 		Element imgEl = factory.createImageEl(path);
 		if(imgEl != null) {
 			PredefinedStyle style = getCurrentPredefinedStyle();
@@ -314,6 +323,42 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 			paragrapheEl.appendChild(runEl);
 		}
 	}
+	
+	protected void setImage(File file) {
+		Element imgEl = factory.createImageEl(file);
+		if(imgEl != null) {
+			PredefinedStyle style = getCurrentPredefinedStyle();
+			Element runEl = factory.createRunEl(Collections.singletonList(imgEl), style);
+			Element paragrapheEl = getCurrentParagraph(false);
+			paragrapheEl.appendChild(runEl);
+		}
+	}
+	
+	protected void startTable() {
+		closeParagraph();
+		currentTable = new Table();
+	}
+	
+	protected void startCurrentTableRow() {
+		currentTable.addRowEl();
+	}
+	
+	protected void closeCurrentTableRow() {
+		if(currentTable != null) {
+			currentTable.closeRow();
+		}
+		textBuffer = null;
+		latex = false;
+		currentParagraph = null;
+	}
+	
+	protected void endTable() {
+		if(currentTable != null) {
+			content.add(currentTable.getTableEl());
+		}
+		currentTable = null;
+		currentParagraph = null;
+	}
 
 	@Override
 	public void startElement(String uri, String localName, String qName, Attributes attributes) {
@@ -346,10 +391,9 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 			String path = attributes.getValue("src");
 			setImage(path);
 		} else if("table".equalsIgnoreCase(tag)) {
-			closeParagraph();
-			currentTable = new Table();
+			startTable();
 		} else if("tr".equals(tag)) {
-			currentTable.addRowEl();
+			startCurrentTableRow();
 		} else if("td".equals(tag) || "th".equals(tag)) {
 			int colspan = OpenXMLUtils.getSpanAttribute("colspan", attributes);
 			int rowspan = OpenXMLUtils.getSpanAttribute("rowspan", attributes);
@@ -407,21 +451,12 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 			unsetTextPreferences(Style.bold);
 			popStyle(tag);
 		}  else if("table".equals(tag)) {
-			if(currentTable != null) {
-				content.add(currentTable.getTableEl());
-			}
-			currentTable = null;
-			currentParagraph = null;
+			endTable();
 		} else if("td".equals(tag) || "th".equals(tag)) {
 			flushText();
 			currentParagraph = addContent(currentParagraph);
 		} else if("tr".equals(tag)) {
-			if(currentTable != null) {
-				currentTable.closeRow();
-			}
-			textBuffer = null;
-			latex = false;
-			currentParagraph = null;
+			closeCurrentTableRow();
 		} else if("ul".equals(tag) || "ol".equals(tag)) {
 			closeParagraph();
 			currentListParagraph = null;
@@ -444,7 +479,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		}
 	}
 	
-	private static class StyleStatus {
+	public static class StyleStatus {
 		private final String tag;
 		private final Style[] styles;
 		private final boolean quote;
@@ -472,7 +507,7 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 		}
 	}
 	
-	private class Table {
+	public class Table {
 		private final Element tableEl;
 		
 		private int nextCol;
@@ -539,6 +574,13 @@ public class HTMLToOpenXMLHandler extends DefaultHandler {
 			return currentRowEl.appendChild(currentCellEl);
 		}
 		
+		public Node addCellEl(Element cellEl, int colSpan) {
+			nextCol += closeCell(nextCol);
+			currentCellEl = cellEl;
+			nextCol += (colSpan <= 1 ? 1 : colSpan);
+			return currentRowEl.appendChild(currentCellEl);
+		}
+		
 		public int closeCell(int lastIndex) {
 			for(int i=lastIndex+1; i-->0; ) {
 				Span span = rowSpans[i];
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java b/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
index 4c0e44d0b0e..4bb7244b524 100644
--- a/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLDocument.java
@@ -285,6 +285,17 @@ public class OpenXMLDocument {
 		getCursor().appendChild(paragraphEl);
 	}
 	
+	public Element createFillInBlanck(int length) {
+		Element runEl = createRunEl(null);
+		runEl.appendChild(createRunPrefsEl(Style.underline));
+
+		int tabLength = length / 5;
+		for(int i=tabLength; i-->0; ) {
+			runEl.appendChild(document.createElement("w:tab"));
+		}
+		return runEl;
+	}
+	
 /*
 <w:p w:rsidR="00F528BA" w:rsidRPr="00245F75" w:rsidRDefault="00F528BA" w:rsidP="00245F75">
 	<w:pPr>
@@ -296,18 +307,23 @@ public class OpenXMLDocument {
  */
 	public void appendFillInBlanckWholeLine(int rows) {
 		for(int i=rows+1; i-->0; ) {
-			Element paragraphEl = createParagraphEl();
-			Node pargraphPrefs = paragraphEl.appendChild(document.createElement("w:pPr"));
-			Node pargraphBottomPrefs = pargraphPrefs.appendChild(document.createElement("w:pBdr"));
-			Element bottomEl = (Element)pargraphBottomPrefs.appendChild(document.createElement("w:between"));
-			bottomEl.setAttribute("w:val", "single");
-			bottomEl.setAttribute("w:sz", "4");
-			bottomEl.setAttribute("w:space", "1");
-			bottomEl.setAttribute("w:color", "auto");
+			Element paragraphEl = createFillInBlanckWholeLine();
 			getCursor().appendChild(paragraphEl);
 		}
 	}
 	
+	public Element createFillInBlanckWholeLine() {
+		Element paragraphEl = createParagraphEl();
+		Node pargraphPrefs = paragraphEl.appendChild(document.createElement("w:pPr"));
+		Node pargraphBottomPrefs = pargraphPrefs.appendChild(document.createElement("w:pBdr"));
+		Element bottomEl = (Element)pargraphBottomPrefs.appendChild(document.createElement("w:between"));
+		bottomEl.setAttribute("w:val", "single");
+		bottomEl.setAttribute("w:sz", "4");
+		bottomEl.setAttribute("w:space", "1");
+		bottomEl.setAttribute("w:color", "auto");
+		return paragraphEl;
+	}
+	
 	public void appendText(String text, boolean newParagraph, Style... textStyles) {
 		if(!StringHelper.containsNonWhitespace(text)) return;
 		
@@ -412,6 +428,21 @@ public class OpenXMLDocument {
 		}
 	}
 	
+	public void appendHtmlText(String html, boolean newParagraph, HTMLToOpenXMLHandler handler) {
+		if(!StringHelper.containsNonWhitespace(html)) return;
+		try {
+			SAXParser parser = new SAXParser();
+			Element paragraphEl = getParagraphToAppendTo(newParagraph);
+			handler.setInitialParagraph(paragraphEl);
+			parser.setContentHandler(handler);
+			parser.parse(new InputSource(new StringReader(html)));
+		} catch (SAXException e) {
+			log.error("", e);
+		} catch (IOException e) {
+			log.error("", e);
+		}
+	}
+	
 	public Node appendTable(Integer... width) {
 		Element tableEl = createTable(width);
 		return getCursor().appendChild(tableEl);
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java b/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java
index 89a3e00e710..49e1a5efc66 100644
--- a/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLDocumentWriter.java
@@ -28,6 +28,9 @@ import java.util.Collection;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+
 import org.apache.commons.io.IOUtils;
 import org.olat.core.logging.OLog;
 import org.olat.core.logging.Tracing;
@@ -72,32 +75,27 @@ public class OpenXMLDocumentWriter {
 		document.appendPageSettings();
 		
 		//_rels
-		ZipEntry rels = new ZipEntry("_rels/.rels");
-		out.putNextEntry(rels);
+		out.putNextEntry(new ZipEntry("_rels/.rels"));
 		createShadowDocumentRelationships(out);
 		out.closeEntry();
 		
 		//[Content_Types].xml
-		ZipEntry contentType = new ZipEntry("[Content_Types].xml");
-		out.putNextEntry(contentType);
+		out.putNextEntry(new ZipEntry("[Content_Types].xml"));
 		createContentTypes(document, out);
 		out.closeEntry();
 		
 		//docProps/app.xml
-		ZipEntry app = new ZipEntry("docProps/app.xml");
-		out.putNextEntry(app);
+		out.putNextEntry(new ZipEntry("docProps/app.xml"));
 		createDocPropsApp(out);
 		out.closeEntry();
 		
 		//docProps/core.xml
-		ZipEntry core = new ZipEntry("docProps/core.xml");
-		out.putNextEntry(core);
+		out.putNextEntry(new ZipEntry("docProps/core.xml"));
 		createDocPropsCore(out);
 		out.closeEntry();
 		
 		//word/_rels/document.xml.rels
-		ZipEntry docRels = new ZipEntry("word/_rels/document.xml.rels");
-		out.putNextEntry(docRels);
+		out.putNextEntry(new ZipEntry("word/_rels/document.xml.rels"));
 		createDocumentRelationships(out, document);
 		out.closeEntry();
 		
@@ -199,31 +197,39 @@ public class OpenXMLDocumentWriter {
   <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml" />
 </Relationships>
 	 */
-	protected void createDocumentRelationships(OutputStream out, OpenXMLDocument document) {
+	protected void createDocumentRelationships(ZipOutputStream out, OpenXMLDocument document) {
 		try {
+			XMLStreamWriter writer = OpenXMLUtils.createStreamWriter(out);
+			writer.writeStartDocument("UTF-8", "1.0");
+			writer.writeStartElement("Relationships");
+			writer.writeNamespace("", SCHEMA_RELATIONSHIPS);
+			
 			Document doc = OpenXMLUtils.createDocument();
 			Element relationshipsEl = (Element)doc.appendChild(doc.createElement("Relationships"));
 			relationshipsEl.setAttribute("xmlns", SCHEMA_RELATIONSHIPS);
 
 			addRelationship("rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
-					"styles.xml", relationshipsEl, doc);
+					"styles.xml", writer);
 			addRelationship("rId2", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
-					"numbering.xml", relationshipsEl, doc);
+					"numbering.xml", writer);
 			
 			if(document != null) {
 				for(DocReference docRef:document.getImages()) {
 					addRelationship(docRef.getId(), "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
-							"media/" + docRef.getFilename(), relationshipsEl, doc);
+							"media/" + docRef.getFilename(), writer);
 				}
 				
 				for(HeaderReference headerRef:document.getHeaders()) {
 					addRelationship(headerRef.getId(), "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
-							headerRef.getFilename(), relationshipsEl, doc);
+							headerRef.getFilename(), writer);
 				}
 			}
 
-			OpenXMLUtils.writeTo(doc, out, false);
-		} catch (DOMException e) {
+			writer.writeEndElement();// end Relationships
+			writer.writeEndDocument();
+			writer.flush();
+			writer.close();
+		} catch (XMLStreamException e) {
 			log.error("", e);
 		}
 	}
@@ -236,30 +242,36 @@ public class OpenXMLDocumentWriter {
 <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
 </Relationships>
 	*/
-	protected void createShadowDocumentRelationships(OutputStream out) {
+	protected void createShadowDocumentRelationships(ZipOutputStream zout) {
 		try {
-			Document doc = OpenXMLUtils.createDocument();
-			Element relationshipsEl = (Element)doc.appendChild(doc.createElement("Relationships"));
-			relationshipsEl.setAttribute("xmlns", SCHEMA_RELATIONSHIPS);
+			XMLStreamWriter writer = OpenXMLUtils.createStreamWriter(zout);
+			writer.writeStartDocument("UTF-8", "1.0");
+			writer.writeStartElement("Relationships");
+			writer.writeNamespace("", SCHEMA_RELATIONSHIPS);
 
 			addRelationship("rId1", "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
-					"docProps/core.xml", relationshipsEl, doc);
+					"docProps/core.xml", writer);
 			addRelationship("rId2", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
-					"docProps/app.xml", relationshipsEl, doc);
+					"docProps/app.xml", writer);
 			addRelationship("rId3", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
-					"word/document.xml", relationshipsEl, doc);
+					"word/document.xml", writer);
 			
-			OpenXMLUtils.writeTo(doc, out, false);
-		} catch (DOMException e) {
+			writer.writeEndElement();// end Relationships
+			writer.writeEndDocument();
+			writer.flush();
+			writer.close();
+		} catch (XMLStreamException e) {
 			log.error("", e);
 		}
 	}
 	
-	private final void addRelationship(String id, String type, String target, Element propertiesEl, Document doc) {
-		Element relEl = (Element)propertiesEl.appendChild(doc.createElement("Relationship"));
-		relEl.setAttribute("Id", id);
-		relEl.setAttribute("Type", type);
-		relEl.setAttribute("Target", target);
+	private final void addRelationship(String id, String type, String target, XMLStreamWriter writer)
+	throws XMLStreamException {
+		writer.writeStartElement("Relationship");
+		writer.writeAttribute("Id", id);
+		writer.writeAttribute("Type", type);
+		writer.writeAttribute("Target", target);
+		writer.writeEndElement();
 	}
 	
 	/*
@@ -333,41 +345,51 @@ public class OpenXMLDocumentWriter {
 	<Override ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" PartName="/word/styles.xml" />
 </Types>
  */
-	protected void createContentTypes(OpenXMLDocument document, OutputStream out) {
-		Document doc = OpenXMLUtils.createDocument();
-		Element typesEl = (Element)doc.appendChild(doc.createElement("Types"));
-		typesEl.setAttribute("xmlns", SCHEMA_CONTENT_TYPES);
-		//Default
-		createContentTypesDefault("rels", CT_RELATIONSHIP, typesEl, doc);
-		createContentTypesDefault("xml", "application/xml", typesEl, doc);
-		createContentTypesDefault("jpeg", "image/jpeg", typesEl, doc);
-		createContentTypesDefault("jpg", "image/jpeg", typesEl, doc);
-		createContentTypesDefault("png", "image/png", typesEl, doc);
-		createContentTypesDefault("gif", "image/gif", typesEl, doc);
-		//Override
-		createContentTypesOverride("/docProps/app.xml", CT_EXT_PROPERTIES, typesEl, doc);
-		createContentTypesOverride("/docProps/core.xml", CT_CORE_PROPERTIES, typesEl, doc);
-		createContentTypesOverride("/word/document.xml", CT_WORD_DOCUMENT, typesEl, doc);
-		createContentTypesOverride("/word/styles.xml", CT_STYLES, typesEl, doc);
-		createContentTypesOverride("/word/numbering.xml", CT_NUMBERING, typesEl, doc);
-		
-		for(HeaderReference headerRef:document.getHeaders()) {
-			createContentTypesOverride("/word/" + headerRef.getFilename(), CT_HEADER, typesEl, doc);
+	protected void createContentTypes(OpenXMLDocument document, ZipOutputStream out) {
+		try {
+			XMLStreamWriter writer = OpenXMLUtils.createStreamWriter(out);
+			writer.writeStartDocument("UTF-8", "1.0");
+			writer.writeStartElement("Types");
+			writer.writeNamespace("", SCHEMA_CONTENT_TYPES);
+			
+			//Default
+			createContentTypesDefault("rels", CT_RELATIONSHIP, writer);
+			createContentTypesDefault("xml", "application/xml", writer);
+			createContentTypesDefault("jpeg", "image/jpeg", writer);
+			createContentTypesDefault("jpg", "image/jpeg", writer);
+			createContentTypesDefault("png", "image/png", writer);
+			createContentTypesDefault("gif", "image/gif", writer);
+			//Override
+			createContentTypesOverride("/docProps/app.xml", CT_EXT_PROPERTIES, writer);
+			createContentTypesOverride("/docProps/core.xml", CT_CORE_PROPERTIES, writer);
+			createContentTypesOverride("/word/document.xml", CT_WORD_DOCUMENT, writer);
+			createContentTypesOverride("/word/styles.xml", CT_STYLES, writer);
+			createContentTypesOverride("/word/numbering.xml", CT_NUMBERING, writer);
+			
+			for(HeaderReference headerRef:document.getHeaders()) {
+				createContentTypesOverride("/word/" + headerRef.getFilename(), CT_HEADER, writer);
+			}
+			
+			writer.writeEndElement();// end Types
+			writer.flush();
+			writer.close();
+		} catch (XMLStreamException e) {
+			log.error("", e);
 		}
-		OpenXMLUtils.writeTo(doc, out, false);
 	}
 	
-	private final void createContentTypesDefault(String extension, String type, Element typesEl, Document doc) {
-		Element defaultEl = (Element)typesEl.appendChild(doc.createElement("Default"));
-		defaultEl.setAttribute("Extension", extension);
-		defaultEl.setAttribute("ContentType", type);
+	private final void createContentTypesDefault(String extension, String type, XMLStreamWriter writer) throws XMLStreamException {
+		writer.writeStartElement("Default");
+		writer.writeAttribute("Extension", extension);
+		writer.writeAttribute("ContentType", type);
+		writer.writeEndElement();
 	}
 	
-	private final void createContentTypesOverride(String partName, String type, Element typesEl, Document doc) {
-		Element overrideEl = (Element)typesEl.appendChild(doc.createElement("Override"));
-		overrideEl.setAttribute("PartName", partName);
-		overrideEl.setAttribute("ContentType", type);
+	private final void createContentTypesOverride(String partName, String type, XMLStreamWriter writer) throws XMLStreamException {
+		writer.writeStartElement("Override");
+		writer.writeAttribute("PartName", partName);
+		writer.writeAttribute("ContentType", type);
+		writer.writeEndElement();
 	}
-	
 
 }
diff --git a/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java b/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java
index 6971c752705..75b96833c4f 100644
--- a/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java
+++ b/src/main/java/org/olat/core/util/openxml/OpenXMLUtils.java
@@ -136,7 +136,7 @@ public class OpenXMLUtils {
 			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
 			// Turn on validation, and turn on namespaces
 			factory.setValidating(true);
-			factory.setNamespaceAware(true);
+			factory.setNamespaceAware(false);
 			DocumentBuilder builder = factory.newDocumentBuilder();
 			Document doc = builder.newDocument();
 			return doc;
diff --git a/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java b/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java
index e1b690b6ebd..1c8fead8fa1 100644
--- a/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java
+++ b/src/main/java/org/olat/ims/qti21/manager/CorrectResponsesUtil.java
@@ -164,9 +164,12 @@ public class CorrectResponsesUtil {
 	 * @return
 	 */
 	public static final List<Identifier> getCorrectIdentifierResponses(AssessmentItem assessmentItem, Interaction interaction) {
+		return getCorrectIdentifierResponses(assessmentItem, interaction.getResponseIdentifier());
+	}
+	
+	public static final List<Identifier> getCorrectIdentifierResponses(AssessmentItem assessmentItem, Identifier responseIdentifier) {
 		List<Identifier> correctAnswers = new ArrayList<>(5);
-		
-		ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(interaction.getResponseIdentifier());
+		ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(responseIdentifier);
 		if(responseDeclaration != null && responseDeclaration.getCorrectResponse() != null) {
 			CorrectResponse correctResponse = responseDeclaration.getCorrectResponse();
 			if(correctResponse.getCardinality().isOneOf(Cardinality.SINGLE)) {
diff --git a/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java b/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java
new file mode 100644
index 00000000000..a3009c386ca
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/manager/openxml/QTI21WordExport.java
@@ -0,0 +1,531 @@
+package org.olat.ims.qti21.manager.openxml;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.DoubleAdder;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.olat.core.gui.media.MediaResource;
+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.Util;
+import org.olat.core.util.openxml.HTMLToOpenXMLHandler;
+import org.olat.core.util.openxml.OpenXMLDocument;
+import org.olat.core.util.openxml.OpenXMLDocument.Style;
+import org.olat.core.util.openxml.OpenXMLDocument.Unit;
+import org.olat.core.util.openxml.OpenXMLDocumentWriter;
+import org.olat.core.util.vfs.VFSContainer;
+import org.olat.course.assessment.AssessmentHelper;
+import org.olat.ims.qti.editor.beecom.objects.Item;
+import org.olat.ims.qti.export.QTIWordExport;
+import org.olat.ims.qti21.QTI21Constants;
+import org.olat.ims.qti21.manager.CorrectResponsesUtil;
+import org.olat.ims.qti21.model.QTI21QuestionType;
+import org.olat.ims.qti21.model.xml.AssessmentHtmlBuilder;
+import org.olat.ims.qti21.model.xml.AssessmentItemFactory;
+import org.olat.ims.qti21.model.xml.AssessmentTestBuilder;
+import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder;
+import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.NumericalEntry;
+import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntry;
+import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntryAlternative;
+import org.olat.ims.qti21.ui.AssessmentTestDisplayController;
+import org.olat.ims.qti21.ui.editor.AssessmentTestComposerController;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.xml.sax.Attributes;
+
+import uk.ac.ed.ph.jqtiplus.node.content.basic.Block;
+import uk.ac.ed.ph.jqtiplus.node.content.variable.RubricBlock;
+import uk.ac.ed.ph.jqtiplus.node.content.xhtml.object.Object;
+import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.DrawingInteraction;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicAssociateInteraction;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicOrderInteraction;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.HotspotInteraction;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.PositionObjectInteraction;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.SelectPointInteraction;
+import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration;
+import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef;
+import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection;
+import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest;
+import uk.ac.ed.ph.jqtiplus.node.test.SectionPart;
+import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
+import uk.ac.ed.ph.jqtiplus.node.test.outcome.processing.OutcomeCondition;
+import uk.ac.ed.ph.jqtiplus.node.test.outcome.processing.OutcomeRule;
+import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
+import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest;
+import uk.ac.ed.ph.jqtiplus.types.Identifier;
+import uk.ac.ed.ph.jqtiplus.value.BaseType;
+import uk.ac.ed.ph.jqtiplus.value.Cardinality;
+
+public class QTI21WordExport implements MediaResource {
+	
+	private final static OLog log = Tracing.createLoggerFor(QTIWordExport.class);
+	
+	private String encoding;
+	private ResolvedAssessmentTest resolvedAssessmentTest;
+	private VFSContainer mediaContainer;
+	private Locale locale;
+	private final CountDownLatch latch;
+	private final AssessmentHtmlBuilder htmlBuilder;
+	
+	public QTI21WordExport(ResolvedAssessmentTest resolvedAssessmentTest, VFSContainer mediaContainer,
+			Locale locale, String encoding, CountDownLatch latch) {
+		this.encoding = encoding;
+		this.locale = locale;
+		this.resolvedAssessmentTest = resolvedAssessmentTest;
+		this.latch = latch;
+		this.mediaContainer = mediaContainer;
+		htmlBuilder = new AssessmentHtmlBuilder();
+	}
+	
+	@Override
+	public boolean acceptRanges() {
+		return false;
+	}
+	
+	@Override
+	public String getContentType() {
+		return "application/zip";
+	}
+
+	@Override
+	public Long getSize() {
+		return null;
+	}
+
+	@Override
+	public InputStream getInputStream() {
+		return null;
+	}
+
+	@Override
+	public Long getLastModified() {
+		return null;
+	}
+
+	@Override
+	public void release() {
+		//
+	}
+
+	@Override
+	public void prepare(HttpServletResponse hres) {
+		try {
+			hres.setCharacterEncoding(encoding);
+		} catch (Exception e) {
+			log.error("", e);
+		}
+
+		ZipOutputStream zout = null;
+		try {
+			AssessmentTest assessmentTest = resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful();
+			
+			String label = assessmentTest.getTitle();
+			String secureLabel = StringHelper.transformDisplayNameToFileSystemName(label);
+
+			String file = secureLabel + ".zip";
+			hres.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + StringHelper.urlEncodeUTF8(file));			
+			hres.setHeader("Content-Description", StringHelper.urlEncodeUTF8(label));
+			
+			zout = new ZipOutputStream(hres.getOutputStream());
+			zout.setLevel(9);
+
+			ZipEntry test = new ZipEntry(secureLabel + ".docx");
+			zout.putNextEntry(test);
+			exportTest(assessmentTest, label, zout, false);
+			zout.closeEntry();
+			
+			ZipEntry responses = new ZipEntry(secureLabel + "_responses.docx");
+			zout.putNextEntry(responses);
+			exportTest(assessmentTest, label, zout, true);
+			zout.closeEntry();
+		} catch (Exception e) {
+			log.error("", e);
+		} finally {
+			latch.countDown();
+			IOUtils.closeQuietly(zout);
+		}
+	}
+	
+	private void exportTest(AssessmentTest assessmentTest, String header, OutputStream out, boolean withResponses) {
+		ZipOutputStream zout = null;
+		try {
+			OpenXMLDocument document = new OpenXMLDocument();
+			document.setMediaContainer(mediaContainer);
+			document.setDocumentHeader(header);
+			
+			Translator translator = Util.createPackageTranslator(AssessmentTestDisplayController.class, locale,
+					Util.createPackageTranslator(AssessmentTestComposerController.class, locale));
+
+			renderAssessmentTest(assessmentTest, document, translator);
+			
+			for(TestPart testPart:assessmentTest.getChildAbstractParts()) {
+				List<AssessmentSection> assessmentSections = testPart.getAssessmentSections();
+				for(AssessmentSection assessmentSection:assessmentSections) {
+					renderAssessmentSection(assessmentSection, document, withResponses, translator);
+				}
+			}
+
+			zout = new ZipOutputStream(out);
+			zout.setLevel(9);
+			
+			OpenXMLDocumentWriter writer = new OpenXMLDocumentWriter();
+			writer.createDocument(zout, document);
+		} catch (Exception e) {
+			log.error("", e);
+		} finally {
+			if(zout != null) {
+				try {
+					zout.finish();
+				} catch (IOException e) {
+					log.error("", e);
+				}
+			}
+		}
+	}
+
+	public static void renderAlienItem(Item item, OpenXMLDocument document, Translator translator) {
+		String title = item.getTitle();
+		if(!StringHelper.containsNonWhitespace(title)) {
+			title = item.getLabel();
+		}
+		document.appendHeading1(title, null);
+		String notSupported = translator.translate("info.alienitem");
+		document.appendText(notSupported, true, Style.bold);
+	}
+	
+	public void renderAssessmentSection(AssessmentSection assessmentSection, OpenXMLDocument document, boolean withResponses, Translator translator) {
+		String title = assessmentSection.getTitle();
+		document.appendHeading1(title, null);
+		List<RubricBlock> rubricBlocks = assessmentSection.getRubricBlocks();
+		for(RubricBlock rubricBlock:rubricBlocks) {
+			String htmlRubric = htmlBuilder.blocksString(rubricBlock.getBlocks());
+			document.appendHtmlText(htmlRubric, true);
+		}
+		
+		for(SectionPart sectionPart:assessmentSection.getChildAbstractParts()) {
+			if(sectionPart instanceof AssessmentSection) {
+				renderAssessmentSection((AssessmentSection)sectionPart, document, withResponses, translator);
+			} else if(sectionPart instanceof AssessmentItemRef) {
+				AssessmentItemRef itemRef = (AssessmentItemRef)sectionPart;
+				ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
+				AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful();
+				renderAssessmentItem(assessmentItem, itemRef, document, withResponses, translator);
+				document.appendPageBreak();
+			}
+		}
+	}
+	
+	public void renderAssessmentTest(AssessmentTest assessmentTest, OpenXMLDocument document, Translator translator) {
+		String title = assessmentTest.getTitle();
+		document.appendTitle(title);
+		
+		if(assessmentTest.getOutcomeProcessing() != null) {
+			List<OutcomeRule> outcomeRules = assessmentTest.getOutcomeProcessing().getOutcomeRules();
+			for(OutcomeRule outcomeRule:outcomeRules) {
+				// pass rule
+				if(outcomeRule instanceof OutcomeCondition) {
+					OutcomeCondition outcomeCondition = (OutcomeCondition)outcomeRule;
+					boolean findIf = AssessmentTestBuilder.findSetOutcomeValue(outcomeCondition.getOutcomeIf(), QTI21Constants.PASS_IDENTIFIER);
+					boolean findElse = AssessmentTestBuilder.findSetOutcomeValue(outcomeCondition.getOutcomeElse(), QTI21Constants.PASS_IDENTIFIER);
+					if(findIf && findElse) {
+						Double cutValue = AssessmentTestBuilder.extractCutValue(outcomeCondition.getOutcomeIf());
+						String cutValueLabel = translator.translate("cut.value");
+						document.appendText(cutValueLabel + ": " + AssessmentHelper.getRoundedScore(cutValue), true);
+					}
+				}
+			}
+		}
+	}
+	
+	public void renderAssessmentItem(AssessmentItem item, AssessmentItemRef itemRef, OpenXMLDocument document, boolean withResponses, Translator translator) {
+		StringBuilder addText = new StringBuilder();
+		
+		QTI21QuestionType type = QTI21QuestionType.getType(item);
+		String typeDescription = "";
+		switch(type) {
+			case sc: typeDescription = translator.translate("form.choice"); break;
+			case mc: typeDescription = translator.translate("form.choice"); break;
+			case fib: typeDescription = translator.translate("form.fib"); break;
+			case numerical: typeDescription = translator.translate("form.fib"); break;
+			case kprim: typeDescription = translator.translate("form.kprim"); break;
+			case hotspot: typeDescription = translator.translate("form.hotspot"); break;
+			case essay: typeDescription = translator.translate("form.essay"); break;
+			default: typeDescription = null; break;
+		}
+		
+		Double maxScore = AssessmentItemFactory.extractMaxScore(item);
+		
+		if(StringHelper.containsNonWhitespace(typeDescription) || maxScore != null) {
+			if(StringHelper.containsNonWhitespace(typeDescription)) {
+				addText.append("(").append(typeDescription).append(")");
+			}
+			if(maxScore != null) {
+				addText.append(" - ").append(AssessmentHelper.getRoundedScore(maxScore));
+			}
+		}
+		
+		String title = item.getTitle();
+		document.appendHeading1(title, addText.toString());
+		
+		URI itemUri = resolvedAssessmentTest.getSystemIdByItemRefMap().get(itemRef);
+		File itemFile = new File(itemUri);
+		
+		List<Block> itemBodyBlocks = item.getItemBody().getBlocks();
+		String html = htmlBuilder.blocksString(itemBodyBlocks);
+		//System.out.println(html);
+		document.appendHtmlText(html, true, new QTI21AndHTMLToOpenXMLHandler(document, item, itemFile, withResponses));
+	}
+	
+	private static class QTI21AndHTMLToOpenXMLHandler extends HTMLToOpenXMLHandler {
+		
+		private final File itemFile;
+		private final AssessmentItem assessmentItem;
+		private final boolean withResponses;
+		
+		private String simpleChoiceIdentifier;
+		private String responseIdentifier;
+		
+		public QTI21AndHTMLToOpenXMLHandler(OpenXMLDocument document, AssessmentItem assessmentItem, File itemFile, boolean withResponses) {
+			super(document);
+			this.itemFile = itemFile;
+			this.withResponses = withResponses;
+			this.assessmentItem = assessmentItem;
+		}
+		
+		@Override
+		public void startElement(String uri, String localName, String qName, Attributes attributes) {
+			super.startElement(uri, localName, qName, attributes);
+			String tag = localName.toLowerCase();
+			if("choiceinteraction".equals(tag)) {
+				responseIdentifier = attributes.getValue("responseidentifier");
+			} else if("simplechoice".equals(tag)) {
+				if(currentTable == null) {
+					startTable();
+				}
+				currentTable.addRowEl();
+				currentTable.addCellEl(factory.createTableCell("E9EAF2", 4560, Unit.pct), 1);
+				simpleChoiceIdentifier = attributes.getValue("identifier");
+			} else if("textentryinteraction".equals(tag)) {
+				startTextEntryInteraction(tag, attributes);
+			} else if("extendedtextinteraction".equals(tag)) {
+				startExtendedTextInteraction(attributes);
+			} else if("hotspotinteraction".equals(tag)) {
+				startHotspotInteraction(attributes);
+			} else if("inlinechoiceinteraction".equals(tag)) {
+				
+			} else if("hottextinteraction".equals(tag)) {
+				
+			} else if("hottext".equals(tag)) {
+				
+			} else if("matchinteraction".equals(tag)) {
+				//
+			}  else if("gapmatchinteraction".equals(tag)) {
+				
+			} else if("selectpointinteraction".equals(tag)) {
+				startSelectPointInteraction(attributes);
+			} else if("graphicassociateinteraction".equals(tag)) {
+				startGraphicAssociateInteraction(attributes);
+			} else if("graphicorderinteraction".equals(tag)) {
+				startGraphicOrderInteraction(attributes);
+			} else if("graphicgapmatchinteraction".equals(tag)) {
+				// 
+			} else if("associateinteraction".equals(tag)) {
+				//
+			} else if("uploadinteraction".equals(tag)) {
+				//
+			} else if("positionobjectinteraction".equals(tag)) {
+				startPositionObjectInteraction(attributes);
+			} else if("sliderinteraction".equals(tag)) {
+				//
+			} else if("drawinginteraction".equals(tag)) {
+				startDrawingInteraction(attributes);
+			}
+		}
+
+		@Override
+		public void endElement(String uri, String localName, String qName) {
+			String tag = localName.toLowerCase();
+			if("choiceinteraction".equals(tag)) {
+				endTable();
+			} else if("simplechoice".equals(tag)) {
+				Element checkboxCell = factory.createTableCell(null, 369, Unit.pct);
+				Node checkboxNode = currentTable.addCellEl(checkboxCell, 1);
+				
+				boolean checked = false;
+				if(withResponses) {
+					Identifier identifier = Identifier.assumedLegal(simpleChoiceIdentifier);
+					List<Identifier> correctAnswers = CorrectResponsesUtil
+							.getCorrectIdentifierResponses(assessmentItem, Identifier.assumedLegal(responseIdentifier));
+					checked = correctAnswers.contains(identifier);	
+				}
+				
+				Node responseEl = factory.createCheckbox(checked);
+				Node wrapEl = factory.wrapInParagraph(responseEl);
+				checkboxNode.appendChild(wrapEl);
+				closeCurrentTableRow();
+			} else if("textentryinteraction".equals(tag)) {
+				//auto closing tag
+			} else if("extendedtextinteraction".equals(tag)) {
+				//auto closing tag
+			} else if("hotspotinteraction".equals(tag)) {
+				//all work done during start
+			}
+			super.endElement(uri, localName, qName);
+		}
+		
+		private void startDrawingInteraction(Attributes attributes) {
+			Interaction interaction = getInteractionByResponseIdentifier(attributes);
+			if(interaction instanceof DrawingInteraction) {
+				DrawingInteraction drawingInteraction = (DrawingInteraction)interaction;
+				setObject(drawingInteraction.getObject());
+			}
+		}
+		
+		private void startSelectPointInteraction(Attributes attributes) {
+			Interaction interaction = getInteractionByResponseIdentifier(attributes);
+			if(interaction instanceof SelectPointInteraction) {
+				SelectPointInteraction selectPointInteraction = (SelectPointInteraction)interaction;
+				setObject(selectPointInteraction.getObject());
+			}
+		}
+		
+		private void startGraphicAssociateInteraction(Attributes attributes) {
+			Interaction interaction = getInteractionByResponseIdentifier(attributes);
+			if(interaction instanceof GraphicAssociateInteraction) {
+				GraphicAssociateInteraction associateInteraction = (GraphicAssociateInteraction)interaction;
+				setObject(associateInteraction.getObject());
+			}
+		}
+		
+		private void startGraphicOrderInteraction(Attributes attributes) {
+			Interaction interaction = getInteractionByResponseIdentifier(attributes);
+			if(interaction instanceof GraphicOrderInteraction) {
+				GraphicOrderInteraction orderInteraction = (GraphicOrderInteraction)interaction;
+				setObject(orderInteraction.getObject());
+			}
+		}
+		
+		private void startPositionObjectInteraction(Attributes attributes) {
+			Interaction interaction = getInteractionByResponseIdentifier(attributes);
+			if(interaction instanceof PositionObjectInteraction) {
+				PositionObjectInteraction positionObject = (PositionObjectInteraction)interaction;
+				setObject(positionObject.getObject());
+			}
+		}
+		
+		private void startHotspotInteraction(Attributes attributes) {
+			Interaction interaction = getInteractionByResponseIdentifier(attributes);
+			if(interaction instanceof HotspotInteraction) {
+				HotspotInteraction hotspotInteraction = (HotspotInteraction)interaction;
+				setObject(hotspotInteraction.getObject());
+			}
+		}
+		
+		private void setObject(Object object) {
+			if(object != null && StringHelper.containsNonWhitespace(object.getData())) {
+				setImage(new File(itemFile.getParentFile(), object.getData()));
+			}
+		}
+		
+		private Interaction getInteractionByResponseIdentifier(Attributes attributes) {
+			String identifier = attributes.getValue("responseidentifier");
+			if(StringHelper.containsNonWhitespace(identifier)) {
+				Identifier rIdentifier = Identifier.assumedLegal(identifier);
+				List<Interaction> interactions = assessmentItem.getItemBody().findInteractions();
+				for(Interaction interaction:interactions) {
+					if(rIdentifier.equals(interaction.getResponseIdentifier())) {
+						return interaction;
+					}
+				}
+			}
+			return null;
+		}
+		
+		private void startExtendedTextInteraction(Attributes attributes) {
+			closeParagraph();
+
+			int expectedLines = 5;
+			String length = attributes.getValue("expectedlines");
+			try {
+				expectedLines = Integer.parseInt(length) + 1;
+			} catch (NumberFormatException e) {
+				//
+			}
+			
+			for(int i=expectedLines; i-->0; ) {
+				Element paragraphEl = factory.createFillInBlanckWholeLine();
+				currentParagraph = addContent(paragraphEl);
+			}
+		}
+		
+		private void startTextEntryInteraction(String tag, Attributes attributes) {
+			flushText();
+
+			Style[] styles = setTextPreferences(Style.italic);
+			styleStack.add(new StyleStatus(tag, true, styles));
+			pNeedNewParagraph = false;
+			
+			if(withResponses) {
+				String response = "";
+				Identifier responseId = Identifier.assumedLegal(attributes.getValue("responseidentifier"));
+				ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(responseId);
+				if(responseDeclaration != null) {
+					if(responseDeclaration.hasBaseType(BaseType.STRING) && responseDeclaration.hasCardinality(Cardinality.SINGLE)) {
+						TextEntry textEntry = new TextEntry(responseId);
+						FIBAssessmentItemBuilder.extractTextEntrySettingsFromResponseDeclaration(textEntry, responseDeclaration, new AtomicInteger(), new DoubleAdder());
+						
+						StringBuilder sb = new StringBuilder();
+						if(StringHelper.containsNonWhitespace(textEntry.getSolution())) {
+							sb.append(textEntry.getSolution());
+						}
+						if(textEntry.getAlternatives() != null) {
+							for(TextEntryAlternative alt:textEntry.getAlternatives()) {
+								if(StringHelper.containsNonWhitespace(alt.getAlternative())) {
+									if(sb.length() > 0) sb.append(", ");
+									sb.append(alt.getAlternative());
+								}
+							}
+						}
+						response = sb.toString();
+					} else if(responseDeclaration.hasBaseType(BaseType.FLOAT) && responseDeclaration.hasCardinality(Cardinality.SINGLE)) {
+						NumericalEntry numericalEntry = new NumericalEntry(responseId);
+						FIBAssessmentItemBuilder.extractNumericalEntrySettings(assessmentItem, numericalEntry, responseDeclaration, new AtomicInteger(), new DoubleAdder());
+						if(numericalEntry.getSolution() != null) {
+							response = numericalEntry.getSolution().toString();
+						}
+					}
+				}
+				characters(response.toCharArray(), 0, response.length());
+			} else {
+				int expectedLength = 20;
+				String length = attributes.getValue("expectedlength");
+				try {
+					expectedLength = Integer.parseInt(length);
+				} catch (NumberFormatException e) {
+					//
+				}
+				Element blanckEl = factory.createFillInBlanck(expectedLength);
+				getCurrentParagraph(false).appendChild(blanckEl);
+			}
+			
+			flushText();
+			popStyle(tag);
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java
index afcd40ea1af..ad76301de33 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java
@@ -20,6 +20,7 @@
 package org.olat.ims.qti21.model.xml;
 
 import static org.olat.ims.qti21.QTI21Constants.MAXSCORE_CLX_IDENTIFIER;
+import static org.olat.ims.qti21.QTI21Constants.MAXSCORE_IDENTIFIER;
 import static org.olat.ims.qti21.QTI21Constants.MINSCORE_CLX_IDENTIFIER;
 import static org.olat.ims.qti21.QTI21Constants.SCORE_CLX_IDENTIFIER;
 import static org.olat.ims.qti21.QTI21Constants.SCORE_IDENTIFIER;
@@ -1045,4 +1046,19 @@ public class AssessmentItemFactory {
 			}
 		}
 	}
+	
+	public static Double extractMaxScore(AssessmentItem assessmentItem) {
+		Double maxScore = null;
+		OutcomeDeclaration outcomeDeclaration = assessmentItem.getOutcomeDeclaration(MAXSCORE_IDENTIFIER);
+		if(outcomeDeclaration != null) {
+			DefaultValue defaultValue = outcomeDeclaration.getDefaultValue();
+			if(defaultValue != null) {
+				Value maxScoreValue = defaultValue.evaluate();
+				if(maxScoreValue instanceof FloatValue) {
+					maxScore = new Double(((FloatValue)maxScoreValue).doubleValue());
+				}
+			}
+		}
+		return maxScore;
+	}
 }
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentTestBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentTestBuilder.java
index 848d2f6a4a6..dd52049f4d4 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentTestBuilder.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentTestBuilder.java
@@ -102,7 +102,7 @@ public class AssessmentTestBuilder {
 		}
 	}
 	
-	private Double extractCutValue(OutcomeIf outcomeIf) {
+	public static Double extractCutValue(OutcomeIf outcomeIf) {
 		if(outcomeIf != null && outcomeIf.getExpressions().size() > 0) {
 			Expression gte = outcomeIf.getExpressions().get(0);
 			if(gte.getExpressions().size() > 1) {
@@ -118,7 +118,7 @@ public class AssessmentTestBuilder {
 		return null;
 	}
 	
-	private boolean findSetOutcomeValue(OutcomeConditionChild outcomeConditionChild, Identifier identifier) {
+	public static boolean findSetOutcomeValue(OutcomeConditionChild outcomeConditionChild, Identifier identifier) {
 		if(outcomeConditionChild == null
 				|| outcomeConditionChild.getOutcomeRules() == null
 				|| outcomeConditionChild.getOutcomeRules().isEmpty()) return false;
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java
index 595a5b43bd6..def9642a2a4 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java
@@ -323,7 +323,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 	 * @param countAlternatives
 	 * @param mappedScore
 	 */
-	private void extractTextEntrySettingsFromResponseDeclaration(TextEntry textEntry, ResponseDeclaration responseDeclaration,
+	public static void extractTextEntrySettingsFromResponseDeclaration(TextEntry textEntry, ResponseDeclaration responseDeclaration,
 			AtomicInteger countAlternatives, DoubleAdder mappedScore) {
 
 		String solution = null;
diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java
index 86931dfaf11..56c2a025cd5 100644
--- a/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java
@@ -167,11 +167,12 @@ public class AssessmentItemDisplayController extends BasicController implements
 	
 	public AssessmentItemDisplayController(UserRequest ureq, WindowControl wControl,
 			RepositoryEntry testEntry, AssessmentEntry assessmentEntry, boolean authorMode,
-			ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef, File fUnzippedDirRoot,
+			ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef,
+			File fUnzippedDirRoot, File itemFile,
 			AssessmentSessionAuditLogger candidateAuditLogger) {
 		super(ureq, wControl);
 		
-		this.itemFileRef = new File(fUnzippedDirRoot, itemRef.getHref().toString());
+		this.itemFileRef = itemFile;
 		this.fUnzippedDirRoot = fUnzippedDirRoot;
 		this.resolvedAssessmentItem = resolvedAssessmentItem;
 		this.candidateAuditLogger = candidateAuditLogger;
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
index d44188e3064..9d0e81792e9 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
@@ -143,7 +143,7 @@ public class AssessmentItemEditorController extends BasicController {
 		
 		AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(getIdentity(), testEntry, null, testEntry);
 		displayCtrl = new AssessmentItemPreviewController(ureq, getWindowControl(),
-				resolvedAssessmentItem, itemRef, testEntry, assessmentEntry, rootDirectory);
+				resolvedAssessmentItem, itemRef, testEntry, assessmentEntry, rootDirectory, itemFile);
 		listenTo(displayCtrl);
 		displayTabPosition = tabbedPane.addTab(translate("preview"), displayCtrl);
 		
@@ -293,7 +293,7 @@ public class AssessmentItemEditorController extends BasicController {
 				if(testEntry != null) {
 					AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(getIdentity(), testEntry, null, testEntry);
 					displayCtrl = new AssessmentItemPreviewController(ureq, getWindowControl(),
-						resolvedAssessmentItem, itemRef, testEntry, assessmentEntry, rootDirectory);
+						resolvedAssessmentItem, itemRef, testEntry, assessmentEntry, rootDirectory, itemFile);
 				} else {
 					displayCtrl = new AssessmentItemPreviewController(ureq, getWindowControl(), resolvedAssessmentItem, rootDirectory, itemFile);
 				}
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemPreviewController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemPreviewController.java
index fbc32992600..008bbf0de12 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemPreviewController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemPreviewController.java
@@ -84,11 +84,12 @@ public class AssessmentItemPreviewController extends BasicController {
 	public AssessmentItemPreviewController(UserRequest ureq, WindowControl wControl,
 			ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef,
 			RepositoryEntry testEntry, AssessmentEntry assessmentEntry,
-			File rootDirectory) {
+			File rootDirectory, File itemFile) {
 		this(ureq, wControl);
 
 		displayCtrl = new AssessmentItemDisplayController(ureq, getWindowControl(),
-				testEntry, assessmentEntry, true, resolvedAssessmentItem, itemRef, rootDirectory, candidateAuditLogger);
+				testEntry, assessmentEntry, true, resolvedAssessmentItem, itemRef,
+				rootDirectory, itemFile, candidateAuditLogger);
 		listenTo(displayCtrl);
 		mainVC.put("display", displayCtrl.getInitialComponent());
 	}
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
index 78dbedfe53c..6da3e57b7b8 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
@@ -27,6 +27,7 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
@@ -58,6 +59,7 @@ import org.olat.core.gui.control.generic.wizard.Step;
 import org.olat.core.gui.control.generic.wizard.StepRunnerCallback;
 import org.olat.core.gui.control.generic.wizard.StepsMainRunController;
 import org.olat.core.gui.control.generic.wizard.StepsRunContext;
+import org.olat.core.gui.media.MediaResource;
 import org.olat.core.helpers.Settings;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.Util;
@@ -67,6 +69,7 @@ import org.olat.core.util.vfs.VFSContainer;
 import org.olat.fileresource.FileResourceManager;
 import org.olat.ims.qti21.QTI21Constants;
 import org.olat.ims.qti21.QTI21Service;
+import org.olat.ims.qti21.manager.openxml.QTI21WordExport;
 import org.olat.ims.qti21.model.IdentifierGenerator;
 import org.olat.ims.qti21.model.QTI21QuestionType;
 import org.olat.ims.qti21.model.xml.AssessmentItemBuilder;
@@ -131,7 +134,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController
 	private Dropdown exportItemTools, addItemTools, changeItemTools;
 	private Link newTestPartLink, newSectionLink, newSingleChoiceLink, newMultipleChoiceLink, newKPrimLink,
 		newFIBLink, newNumericalLink, newHotspotLink, newEssayLink;
-	private Link importFromPoolLink, importFromTableLink, exportToPoolLink;
+	private Link importFromPoolLink, importFromTableLink, exportToPoolLink, exportToDocxLink;
 	private Link deleteLink, copyLink;
 	private final TooledStackedPanel toolbar;
 	private VelocityContainer mainVC;
@@ -157,6 +160,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController
 	
 	private LockResult lockEntry;
 	private LockResult activeSessionLock;
+	private CountDownLatch exportLatch;
 	
 	@Autowired
 	private QTI21Service qtiService;
@@ -265,6 +269,11 @@ public class AssessmentTestComposerController extends MainLayoutBasicController
 		exportToPoolLink.setIconLeftCSS("o_icon o_icon_table o_icon-fw");
 		exportToPoolLink.setDomReplacementWrapperRequired(false);
 		exportItemTools.addComponent(exportToPoolLink);
+		
+		exportToDocxLink = LinkFactory.createToolLink("export.pool", translate("tools.export.docx"), this, "o_mi_docx_export");
+		exportToDocxLink.setIconLeftCSS("o_icon o_icon_download o_icon-fw");
+		exportToDocxLink.setDomReplacementWrapperRequired(false);
+		exportItemTools.addComponent(exportToDocxLink);
 
 		//changes
 		changeItemTools = new Dropdown("changeTools", "change.elements", false, getTranslator());
@@ -430,6 +439,8 @@ public class AssessmentTestComposerController extends MainLayoutBasicController
 			doImportTable(ureq);
 		} else if(exportToPoolLink == source) {
 			doExportPool();
+		} else if(exportToDocxLink == source) {
+			doExportDocx(ureq);
 		} else if(deleteLink == source) {
 			doConfirmDelete(ureq);
 		} else if(copyLink == source) {
@@ -590,6 +601,12 @@ public class AssessmentTestComposerController extends MainLayoutBasicController
 				.importAssessmentItemRef(getIdentity(), itemRef, assessmentItem, metadata, unzippedDirRoot, getLocale());
 	}
 	
+	private void doExportDocx(UserRequest ureq) {
+		exportLatch = new CountDownLatch(1);
+		MediaResource mr = new QTI21WordExport(resolvedAssessmentTest, unzippedContRoot, getLocale(), "UTF-8", exportLatch);
+		ureq.getDispatchResult().setResultingMediaResource(mr);
+	}
+	
 	private void doImportTable(UserRequest ureq) {
 		removeAsListenerAndDispose(importTableWizard);
 
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
index 7dd6dbf0f38..4e29676eaf4 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
@@ -107,6 +107,7 @@ time.limit.max=Zeitbeschr\u00E4nkung (Minute)
 title.add=$org.olat.ims.qti.editor\:title.add
 tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy
 tools.change.delete=$org.olat.ims.qti.editor\:tools.change.delete
+tools.export.docx=$org.olat.ims.qti.editor\:tools.export.docx
 tools.export.header=$org.olat.ims.qti.editor\:tools.export.header
 tools.export.qpool=$org.olat.ims.qti.editor\:tools.export.qpool
 tools.import.qpool=$org.olat.ims.qti.editor\:tools.import.qpool
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
index b7b5ba8efe9..9df64c3456e 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
@@ -107,6 +107,7 @@ time.limit.max=Time limit (minute)
 title.add=$org.olat.ims.qti.editor\:title.add
 tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy
 tools.change.delete=$org.olat.ims.qti.editor\:tools.change.delete
+tools.export.docx=$org.olat.ims.qti.editor\:tools.export.docx
 tools.export.header=$org.olat.ims.qti.editor\:tools.export.header
 tools.export.qpool=$org.olat.ims.qti.editor\:tools.export.qpool
 tools.import.qpool=$org.olat.ims.qti.editor\:tools.import.qpool
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties
index 27a73370ecc..0348ea71246 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties
@@ -26,6 +26,7 @@ form.score.answer.points=Points
 title.add=$org.olat.ims.qti.editor\:title.add
 tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy
 tools.change.delete=$org.olat.ims.qti.editor\:tools.change.delete
+tools.export.docx=$org.olat.ims.qti.editor\:tools.export.docx
 tools.export.header=$org.olat.ims.qti.editor\:tools.export.header
 tools.export.qpool=$org.olat.ims.qti.editor\:tools.export.qpool
 tools.import.qpool=$org.olat.ims.qti.editor\:tools.import.qpool
-- 
GitLab