/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.ims.qti21.manager.openxml; import static org.olat.ims.qti21.model.xml.QtiNodesExtractor.extractIdentifiersFromCorrectResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.net.URI; import java.util.ArrayList; import java.util.Collections; 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.cyberneko.html.parsers.SAXParser; 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.openxml.OpenXMLGraphic; 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.AssessmentTestBuilder; import org.olat.ims.qti21.model.xml.QtiNodesExtractor; 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 org.xml.sax.InputSource; import org.xml.sax.SAXException; import uk.ac.ed.ph.jqtiplus.attribute.Attribute; import uk.ac.ed.ph.jqtiplus.node.QtiNode; 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.expression.operator.Shape; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse; import uk.ac.ed.ph.jqtiplus.node.item.ModalFeedback; 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.HottextInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.MatchInteraction; 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.interaction.choice.SimpleAssociableChoice; import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleMatchSet; import uk.ac.ed.ph.jqtiplus.node.item.interaction.content.Hottext; import uk.ac.ed.ph.jqtiplus.node.item.interaction.graphic.HotspotChoice; import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.MapEntry; import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; import uk.ac.ed.ph.jqtiplus.node.shared.FieldValue; 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.utils.QueryUtils; import uk.ac.ed.ph.jqtiplus.value.BaseType; import uk.ac.ed.ph.jqtiplus.value.Cardinality; import uk.ac.ed.ph.jqtiplus.value.DirectedPairValue; import uk.ac.ed.ph.jqtiplus.value.SingleValue; /** * * Initial date: 04.07.2016<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ 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(); URI itemUri = resolvedAssessmentTest.getSystemIdByItemRefMap().get(itemRef); renderAssessmentItem(assessmentItem, new File(itemUri), document, withResponses, translator, htmlBuilder); 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 static void renderAssessmentItem(AssessmentItem item, File itemFile, OpenXMLDocument document, boolean withResponses, Translator translator, AssessmentHtmlBuilder htmlBuilder) { 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; case upload: typeDescription = translator.translate("form.upload"); break; case drawing: typeDescription = translator.translate("form.drawing"); break; case match: typeDescription = translator.translate("form.match"); break; default: typeDescription = null; break; } Double maxScore = QtiNodesExtractor.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()); List<Block> itemBodyBlocks = item.getItemBody().getBlocks(); String html = htmlBuilder.blocksString(itemBodyBlocks); document.appendHtmlText(html, true, new QTI21AndHTMLToOpenXMLHandler(document, item, itemFile, withResponses, htmlBuilder, translator)); if(withResponses && (type == QTI21QuestionType.essay || type == QTI21QuestionType.upload || type == QTI21QuestionType.drawing)) { renderCorrectSolutionForWord(item, document, translator, htmlBuilder); } } private static void renderCorrectSolutionForWord(AssessmentItem item, OpenXMLDocument document, Translator translator, AssessmentHtmlBuilder htmlBuilder) { List<ModalFeedback> feedbacks = item.getModalFeedbacks(); if(feedbacks != null && feedbacks.size() > 0) { for(ModalFeedback feedback:feedbacks) { if(feedback.getOutcomeIdentifier() != null && QTI21Constants.CORRECT_SOLUTION_IDENTIFIER.equals(feedback.getOutcomeIdentifier())) { Attribute<?> title = feedback.getAttributes().get("title"); String feedbackTitle = null; if(title != null && title.getValue() != null) { feedbackTitle = title.getValue().toString(); } if(!StringHelper.containsNonWhitespace(feedbackTitle)) { feedbackTitle = translator.translate("correct.solution"); } document.appendHeading2(feedbackTitle, null); String html = htmlBuilder.flowStaticString(feedback.getFlowStatics()); document.appendHtmlText(html, true); } } } } private static class QTI21AndHTMLToOpenXMLHandler extends HTMLToOpenXMLHandler { private final File itemFile; private final AssessmentItem assessmentItem; private final boolean withResponses; private final AssessmentHtmlBuilder htmlBuilder; private final Translator translator; private String simpleChoiceIdentifier; private String responseIdentifier; private boolean renderElement = true; public QTI21AndHTMLToOpenXMLHandler(OpenXMLDocument document, AssessmentItem assessmentItem, File itemFile, boolean withResponses, AssessmentHtmlBuilder htmlBuilder, Translator translator) { super(document); this.itemFile = itemFile; this.withResponses = withResponses; this.assessmentItem = assessmentItem; this.htmlBuilder = htmlBuilder; this.translator = translator; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { String tag = localName.toLowerCase(); switch(tag) { case "choiceinteraction": responseIdentifier = attributes.getValue("responseidentifier"); break; case "simplechoice": if(currentTable == null) { startTable(); } currentTable.addRowEl(); currentTable.addCellEl(factory.createTableCell("E9EAF2", 4560, Unit.pct), 1); simpleChoiceIdentifier = attributes.getValue("identifier"); break; case "textentryinteraction": startTextEntryInteraction(tag, attributes); break; case "extendedtextinteraction": startExtendedTextInteraction(attributes); break; case "hotspotinteraction": startHotspotInteraction(attributes); break; case "inlinechoiceinteraction": case "hottextinteraction": break; case "hottext": renderElement = false; startHottext(attributes); break; case "matchinteraction": renderElement = false; Interaction interaction = getInteractionByResponseIdentifier(attributes); if(interaction instanceof MatchInteraction) { MatchInteraction matchInteraction = (MatchInteraction)interaction; QTI21QuestionType type = QTI21QuestionType.getTypeOfMatch(assessmentItem, matchInteraction); if(type == QTI21QuestionType.kprim) { startKPrim(matchInteraction); } else { startMatch(matchInteraction); } } break; case "gapmatchinteraction": break;//TODO case "selectpointinteraction": startSelectPointInteraction(attributes); break; case "graphicassociateinteraction": startGraphicAssociateInteraction(attributes); break; case "graphicorderinteraction": startGraphicOrderInteraction(attributes); break; case "graphicgapmatchinteraction": case "associateinteraction": break;//TODO case "uploadinteraction": break; case "positionobjectinteraction": startPositionObjectInteraction(attributes); break; case "sliderinteraction": break;//TODO case "drawinginteraction": startDrawingInteraction(attributes); break; case "simplematchset": case "simpleassociablechoice": //do nothing break; default: { if(renderElement) { super.startElement(uri, localName, qName, attributes); } } } } @Override public void characters(char[] ch, int start, int length) { if(renderElement) { super.characters(ch, start, length); } } @Override public void endElement(String uri, String localName, String qName) { String tag = localName.toLowerCase(); switch(tag) { case "choiceinteraction": endTable(); break; case "simplechoice": endSimpleChoice(); break; case "textentryinteraction": //auto closing tag case "extendedtextinteraction": //auto closing tag case "hotspotinteraction": //all work done during start break; case "hottextinteraction": case "hottext": //all work done during start renderElement = true; break; case "matchinteraction": renderElement = true; break; case "simplematchset": case "simpleassociablechoice": //do nothing break; default: { if(renderElement) { super.endElement(uri, localName, qName); } } } } private void endSimpleChoice() { 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(); } private void startHottext(Attributes attributes) { Hottext hottext = getHottextByIdentifier(attributes); if(hottext != null) { HottextInteraction interaction = null; for(QtiNode parentNode=hottext.getParent(); parentNode.getParent() != null; parentNode = parentNode.getParent()) { if(parentNode instanceof HottextInteraction) { interaction = (HottextInteraction)parentNode; break; } } if(interaction != null) { boolean checked = false; if(withResponses) { List<Identifier> correctAnswers = CorrectResponsesUtil .getCorrectIdentifierResponses(assessmentItem, interaction.getResponseIdentifier()); checked = correctAnswers.contains(hottext.getIdentifier()); } flushText(); Element paragraphEl = getCurrentParagraph(false); Node responseEl = factory.createCheckbox(checked, false); Element runEl = factory.createRunEl(Collections.singletonList(responseEl)); paragraphEl.appendChild(runEl); String html = htmlBuilder.inlineStaticString(hottext.getInlineStatics()); appendHtmlText(html, paragraphEl); } } } private Hottext getHottextByIdentifier(Attributes attributes) { String identifier = attributes.getValue("identifier"); if(StringHelper.containsNonWhitespace(identifier)) { Identifier rIdentifier = Identifier.assumedLegal(identifier); List<Hottext> hottexts = QueryUtils.search(Hottext.class, assessmentItem.getItemBody()); for(Hottext hottext:hottexts) { if(rIdentifier.equals(hottext.getIdentifier())) { return hottext; } } } return null; } 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; Object object = hotspotInteraction.getObject(); if(object != null && StringHelper.containsNonWhitespace(object.getData())) { File backgroundImg = new File(itemFile.getParentFile(), object.getData()); List<Identifier> correctAnswers = new ArrayList<>(); ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(interaction.getResponseIdentifier()); if(responseDeclaration != null) { CorrectResponse correctResponse = responseDeclaration.getCorrectResponse(); if(correctResponse != null) { extractIdentifiersFromCorrectResponse(correctResponse, correctAnswers); } } List<OpenXMLGraphic> elements = new ArrayList<>(); List<HotspotChoice> choices = hotspotInteraction.getHotspotChoices(); for(HotspotChoice choice:choices) { OpenXMLGraphic.Style style = OpenXMLGraphic.Style.accent1; if(withResponses) { boolean correct = correctAnswers.contains(choice.getIdentifier()); if(correct) { style = OpenXMLGraphic.Style.accent3; } } Shape shape = choice.getShape(); if(shape == Shape.CIRCLE || shape == Shape.ELLIPSE) { elements.add(new OpenXMLGraphic(OpenXMLGraphic.Type.circle, style, choice.getCoords())); } else if(shape == Shape.RECT) { elements.add(new OpenXMLGraphic(OpenXMLGraphic.Type.rectangle, style, choice.getCoords())); } } startGraphic(backgroundImg, elements); } } } private void startMatch(MatchInteraction matchInteraction) { SimpleMatchSet questionMatchSetVertical = matchInteraction.getSimpleMatchSets().get(0); SimpleMatchSet questionMatchSetHorizontal = matchInteraction.getSimpleMatchSets().get(1); List<SimpleAssociableChoice> horizontalAssociableChoices = questionMatchSetHorizontal.getSimpleAssociableChoices(); List<SimpleAssociableChoice> verticalAssociableChoices = questionMatchSetVertical.getSimpleAssociableChoices(); // calculate the width of the table () and of its columns int tableWidthDxa = 11294; int tableWidthPct = 4858; int numOfColumns = horizontalAssociableChoices.size() + 1; int columnWidthDxa = tableWidthDxa / numOfColumns; int columnWidthPct = tableWidthPct / numOfColumns; Integer[] columnsWidth = new Integer[numOfColumns]; for(int i=numOfColumns; i-->0; ) { columnsWidth[i] = columnWidthDxa; } startTable(columnsWidth); currentTable.addRowEl(); // white corner Node emptyCell = currentTable.addCellEl(factory.createTableCell(null, columnWidthDxa, Unit.dxa), 1); emptyCell.appendChild(factory.createParagraphEl("")); // horizontal headers for(SimpleAssociableChoice choice:horizontalAssociableChoices) { Element answerCell = currentTable.addCellEl(factory.createTableCell("E9EAF2", columnWidthPct, Unit.pct), 1); appendSimpleAssociableChoice(choice, answerCell); } currentTable.closeRow(); for(SimpleAssociableChoice choice:verticalAssociableChoices) { currentTable.addRowEl(); //answer Element answerCell = currentTable.addCellEl(factory.createTableCell("E9EAF2", columnWidthPct, Unit.pct), 1); appendSimpleAssociableChoice(choice, answerCell) ; //checkbox for(SimpleAssociableChoice horizontalChoice:horizontalAssociableChoices) { boolean correct = isCorrectMatchResponse(choice.getIdentifier(), horizontalChoice.getIdentifier(), matchInteraction); appendMatchCheckBox(correct, columnWidthPct, factory); } currentTable.closeRow(); } endTable(); } private void appendSimpleAssociableChoice(SimpleAssociableChoice choice, Element answerCell) { String html = htmlBuilder.flowStaticString(choice.getFlowStatics()); Element wrapEl = factory.createParagraphEl(); List<Node> nodes = appendHtmlText(html, wrapEl); for(Node node:nodes) { answerCell.appendChild(node); } } public List<Node> appendHtmlText(String html, Element wrapEl) { if(!StringHelper.containsNonWhitespace(html)) { return Collections.emptyList(); } try { SAXParser parser = new SAXParser(); HTMLToOpenXMLHandler handler = new HTMLToOpenXMLHandler(factory, wrapEl, false); parser.setContentHandler(handler); parser.parse(new InputSource(new StringReader(html))); return handler.getContent(); } catch (SAXException | IOException e) { log.error("", e); return Collections.emptyList(); } } private void startKPrim(MatchInteraction matchInteraction) { SimpleMatchSet questionMatchSet = matchInteraction.getSimpleMatchSets().get(0); //open a table with 3 columns startTable(new Integer[]{9062, 1116, 1116}); currentTable.addRowEl(); //draw header with +/- Node emptyCell = currentTable.addCellEl(factory.createTableCell(null, 9062, Unit.dxa), 1); emptyCell.appendChild(factory.createParagraphEl("")); Node plusCell = currentTable.addCellEl(factory.createTableCell(null, 1116, Unit.dxa), 1); plusCell.appendChild(factory.createParagraphEl(translator.translate("kprim.plus"))); Node minusCell = currentTable.addCellEl(factory.createTableCell(null, 1116, Unit.dxa), 1); minusCell.appendChild(factory.createParagraphEl(translator.translate("kprim.minus"))); currentTable.closeRow(); for(SimpleAssociableChoice choice:questionMatchSet.getSimpleAssociableChoices()) { currentTable.addRowEl(); //answer Element answerCell = currentTable.addCellEl(factory.createTableCell("E9EAF2", 4120, Unit.pct), 1); appendSimpleAssociableChoice(choice, answerCell); //checkbox boolean correct = isCorrectKPrimResponse(choice.getIdentifier(), QTI21Constants.CORRECT_IDENTIFIER, matchInteraction); appendMatchCheckBox(correct, 369, factory); boolean wrong = isCorrectKPrimResponse(choice.getIdentifier(), QTI21Constants.WRONG_IDENTIFIER, matchInteraction); appendMatchCheckBox(wrong, 369, factory); currentTable.closeRow(); } endTable(); } private boolean isCorrectKPrimResponse(Identifier choiceIdentifier, Identifier targetIdentifier, MatchInteraction interaction) { if(!withResponses) return false; ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(interaction.getResponseIdentifier()); List<MapEntry> mapEntries = responseDeclaration.getMapping().getMapEntries(); for(MapEntry mapEntry:mapEntries) { SingleValue mapKey = mapEntry.getMapKey(); if(mapKey instanceof DirectedPairValue) { DirectedPairValue pairValue = (DirectedPairValue)mapKey; Identifier source = pairValue.sourceValue(); Identifier destination = pairValue.destValue(); if(source.equals(choiceIdentifier) && destination.equals(targetIdentifier)) { return true; } } } return false; } private boolean isCorrectMatchResponse(Identifier choiceIdentifier, Identifier targetIdentifier, MatchInteraction interaction) { if(!withResponses) return false; ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(interaction.getResponseIdentifier()); if(responseDeclaration.getCorrectResponse() != null && responseDeclaration.getCorrectResponse().getFieldValues().size() > 0) { List<FieldValue> values = responseDeclaration.getCorrectResponse().getFieldValues(); for(FieldValue value:values) { SingleValue sValue = value.getSingleValue(); if(sValue instanceof DirectedPairValue) { DirectedPairValue dpValue = (DirectedPairValue)sValue; Identifier sourceId = dpValue.sourceValue(); Identifier targetId = dpValue.destValue(); if(sourceId.equals(choiceIdentifier) && targetId.equals(targetIdentifier)) { return true; } } } } else if(responseDeclaration.getMapping() != null && responseDeclaration.getMapping().getMapEntries().size() > 0) { List<MapEntry> mapEntries = responseDeclaration.getMapping().getMapEntries(); for(MapEntry mapEntry:mapEntries) { SingleValue mapKey = mapEntry.getMapKey(); if(mapKey instanceof DirectedPairValue) { DirectedPairValue pairValue = (DirectedPairValue)mapKey; Identifier source = pairValue.sourceValue(); Identifier destination = pairValue.destValue(); if(source.equals(choiceIdentifier) && destination.equals(targetIdentifier)) { double val = mapEntry.getMappedValue(); return val > 0.0; } } } } return false; } private void appendMatchCheckBox(boolean checked, int width, OpenXMLDocument document) { Node checkboxCell = currentTable.addCellEl(document.createTableCell(null, width, Unit.pct), 1); Node responseEl = document.createCheckbox(checked); Node wrapEl = document.wrapInParagraph(responseEl); checkboxCell.appendChild(wrapEl); } 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); } } }