/** * <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.model.xml; import java.util.ArrayDeque; import java.util.Deque; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.olat.core.util.StringHelper; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.ext.DefaultHandler2; /** * It converts Onyx final to qtiWorks. It fix:<br> * <ul> * <li>imsmanifest: type from "imsqti_assessment_xmlv2p1" to "imsqti_test_xmlv2p1"</li> * <li>assessmentTest: surround rubricBlock's text only content with <p></li> * <li>assesementItem: surround itemBody's text only with <p></li> * <li>assesementItem: strip html code from <prompt> * </ul> * * * Initial date: 25.06.2015<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class Onyx38ToQtiWorksHandler extends DefaultHandler2 { private static final String VERSION_MARKER = "Version "; private final XMLStreamWriter xtw; private String version; private int pLevel = -1; private int liLevel = -1; private int itemBodySubLevel = -1; private Deque<String> skipTags = new ArrayDeque<String>(); private boolean envelopP = false; private boolean rubricBlock = false; private StringBuilder rubricCharacterBuffer; private boolean prompt = false; public Onyx38ToQtiWorksHandler(XMLStreamWriter xtw) { this.xtw = xtw; } @Override public void startDocument() throws SAXException { try{ xtw.writeStartDocument("utf-8", "1.0"); } catch (XMLStreamException e) { throw new SAXException(e); } } @Override public void comment(char[] ch, int start, int length) throws SAXException { try{ String comment = new String(ch, start, length); if(comment != null && comment.contains("Onyx Editor")) { int versionIndex = comment.indexOf(VERSION_MARKER); if(versionIndex > 0) { int offset = VERSION_MARKER.length(); version = comment.substring(versionIndex + offset, comment.indexOf(' ', versionIndex + offset)); } } xtw.writeComment(comment); } catch (XMLStreamException e) { throw new SAXException(e); } } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { try { if(prompt || checkIfNeedToSkip(qName, attributes)) { return; } if(rubricBlock) { String characters = rubricCharacterBuffer.toString().trim(); if(characters.length() > 0) { xtw.writeStartElement("p"); xtw.writeCharacters(characters); xtw.writeEndElement(); } rubricBlock = false; rubricCharacterBuffer = null; } if(itemBodySubLevel == 0 && envelopP && isBlock(qName)) { xtw.writeEndElement(); envelopP = false; } if("p".equals(qName)) { pLevel++; if(pLevel == 0) { writeStartElement(qName, attributes); } } else if("li".equals(qName)) { liLevel++; if(liLevel == 0) { writeStartElement(qName, attributes); } } else if("assessmentItem".equals(qName) || "assessmentTest".equals(qName)) { writeAssessmentElement(qName, attributes); } else if("mapTolResponse".equals(qName)) { writeMapTo1ResponseElement(attributes); }else { if(itemBodySubLevel == 0 && !envelopP && !isBlock(qName)) { xtw.writeStartElement("p"); envelopP = true; } if("label".equals(qName)) { //convert label and font which are not part of QTI 2.1 standard to span writeStartElement("span", null); } else { writeStartElement(qName, attributes); } } if("itemBody".equals(qName)) { itemBodySubLevel = 0; } else if("rubricBlock".equals(qName)) { rubricBlock = true; rubricCharacterBuffer = new StringBuilder(); } else if("prompt".equals(qName)) { prompt = true; } else if(itemBodySubLevel >= 0) { itemBodySubLevel++; } } catch (XMLStreamException e) { throw new SAXException(e); } } private void writeStartElement(String qName, Attributes attributes) throws XMLStreamException { xtw.writeStartElement(qName); if(attributes != null) { if("imscp:resource".equals(qName)) { convertImsCPResource(attributes); } else { convertAttributes(attributes); } } } private boolean checkIfNeedToSkip(String qName, Attributes attributes) { if("font".equals(qName)) { skipTags.push(qName); return true; } else if("a".equals(qName) && attributes.getValue("name") != null) { skipTags.push(qName); return true; } else if("span".equals(qName) && itemBodySubLevel == 0) { skipTags.push(qName); return true; } return false; } private void convertImsCPResource(Attributes attributes) throws XMLStreamException { int numOfAttributes = attributes.getLength(); for(int i=0;i<numOfAttributes; i++) { String attrQName = attributes.getQName(i); String attrValue = attributes.getValue(i); if("type".equals(attrQName) && "imsqti_assessment_xmlv2p1".equals(attrValue)) { xtw.writeAttribute(attrQName, "imsqti_test_xmlv2p1"); } else { xtw.writeAttribute(attrQName, attrValue); } } } private void writeAssessmentElement(String qName, Attributes attributes) throws XMLStreamException { xtw.writeStartElement(qName); int numOfAttributes = attributes.getLength(); for(int i=0;i<numOfAttributes; i++) { String attrQName = attributes.getQName(i); String attrValue = attributes.getValue(i); xtw.writeAttribute(attrQName, attrValue); } xtw.writeAttribute("toolName", "Onyx Editor"); if(StringHelper.containsNonWhitespace(version)) { xtw.writeAttribute("toolVersion", version); } } private void writeMapTo1ResponseElement(Attributes attributes) throws XMLStreamException { xtw.writeStartElement("mapResponse"); int numOfAttributes = attributes.getLength(); for(int i=0;i<numOfAttributes; i++) { String attrQName = attributes.getQName(i); if(!"tolerance".equals(attrQName) && !"toleranceMode".equals(attrQName) && !"xmlns".equals(attrQName)) { String attrValue = attributes.getValue(i); xtw.writeAttribute(attrQName, attrValue); } } } private void convertAttributes(Attributes attributes) throws XMLStreamException { int numOfAttributes = attributes.getLength(); for(int i=0;i<numOfAttributes; i++) { String attrQName = attributes.getQName(i); String attrValue = attributes.getValue(i); if("align".equals(attrQName)) {//TODO target?? //ignore align } else if("xmlns".equals(attrQName) && !StringHelper.containsNonWhitespace(attrValue)) { //ignore empty schema } else { xtw.writeAttribute(attrQName, attrValue); } } } @Override public void characters(char[] ch, int start, int length) throws SAXException { try { if(itemBodySubLevel == 0) { if(!envelopP && isCharacterRelevant(ch, start, length)) { xtw.writeStartElement("p"); int diff = trimStart(ch, start, length); start += diff; length -= diff; envelopP = true; } xtw.writeCharacters(ch, start, length); } else if(rubricBlock) { rubricCharacterBuffer.append(ch, start, length); } else { xtw.writeCharacters(ch, start, length); } } catch (XMLStreamException e) { throw new SAXException(e); } } private int trimStart(char[] chArray, int start, int length) { int end = start + length; for(int i=start; i<end; i++) { char ch = chArray[i]; if(ch != '\n' && ch != '\r' && ch != '\t' && ch != ' ') { return start - i; } } return 0; } private boolean isCharacterRelevant(char[] chArray, int start, int length) { int end = start + length; for(int i=start; i<end; i++) { char ch = chArray[i]; if(ch != '\n' && ch != '\r' && ch != '\t' && ch != ' ') { return true; } } return false; } @Override public void endElement(String uri, String localName, String qName) throws SAXException { try { if(skipTags.size() > 0 && skipTags.peek().equals(qName)) { skipTags.pop(); return; } if(rubricBlock) { String characters = rubricCharacterBuffer.toString().trim(); if(characters.length() > 0) { xtw.writeStartElement("p"); xtw.writeCharacters(characters); xtw.writeEndElement(); } rubricBlock = false; rubricCharacterBuffer = null; } else if(prompt) { if(!"prompt".equals(qName)) { return;//only print characters } else { prompt = false; } } else if(itemBodySubLevel >= 0) { itemBodySubLevel--; } if("itemBody".equals(qName)) { if(envelopP) { xtw.writeEndElement();//p envelopP = false; } xtw.writeEndElement();//itemBody itemBodySubLevel = -1; } else if("p".equals(qName)) { if(pLevel == 0) { xtw.writeEndElement(); } pLevel--; } else if("li".equals(qName)) { if(liLevel == 0) { xtw.writeEndElement(); } liLevel--; } else { xtw.writeEndElement(); } } catch (XMLStreamException e) { throw new SAXException(e); } } @Override public void endDocument() throws SAXException { try { xtw.writeEndDocument(); xtw.flush(); xtw.close(); } catch (XMLStreamException e) { throw new SAXException(e); } } /** * The list of block elements allowed in itemBody.<br> * * "http://www.imsglobal.org/xsd/imsqti_v2p1":rubricBlock, * "http://www.imsglobal.org/xsd/imsqti_v2p1":positionObjectStage, * "http://www.imsglobal.org/xsd/imsqti_v2p1":customInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":drawingInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":gapMatchInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":matchInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":graphicGapMatchInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":hotspotInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":graphicOrderInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":selectPointInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":graphicAssociateInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":sliderInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":choiceInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":mediaInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":hottextInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":orderInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":extendedTextInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":uploadInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":associateInteraction, * "http://www.imsglobal.org/xsd/imsqti_v2p1":feedbackBlock, * "http://www.imsglobal.org/xsd/imsqti_v2p1":templateBlock, * "http://www.imsglobal.org/xsd/imsqti_v2p1":infoControl, * "http://www.w3.org/1998athathML":math, * "http://www.w3.org/2001/XInclude":include, * "http://www.imsglobal.org/xsd/imsqti_v2p1":pre, * "http://www.imsglobal.org/xsd/imsqti_v2p1":h1, "http://www.imsglobal.org/xsd/imsqti_v2p1":h2, "http://www.imsglobal.org/xsd/imsqti_v2p1":h3, "http://www.imsglobal.org/xsd/imsqti_v2p1":h4, "http://www.imsglobal.org/xsd/imsqti_v2p1":h5, "http://www.imsglobal.org/xsd/imsqti_v2p1":h6, * "http://www.imsglobal.org/xsd/imsqti_v2p1":p, * "http://www.imsglobal.org/xsd/imsqti_v2p1":address, * "http://www.imsglobal.org/xsd/imsqti_v2p1":dl, * "http://www.imsglobal.org/xsd/imsqti_v2p1":ol, * "http://www.imsglobal.org/xsd/imsqti_v2p1":hr, * "http://www.imsglobal.org/xsd/imsqti_v2p1":ul, * "http://www.imsglobal.org/xsd/imsqti_v2p1":blockquote, * "http://www.imsglobal.org/xsd/imsqti_v2p1":table, * "http://www.imsglobal.org/xsd/imsqti_v2p1":div * * @param qName * @return */ private boolean isBlock(String qName) { switch(qName) { case "p": case "div": case "positionObjectStage": case "customInteraction": case "drawingInteraction": case "gapMatchInteraction": case "matchInteraction": case "graphicGapMatchInteraction": case "hotspotInteraction": case "selectPointInteraction": case "graphicAssociateInteraction": case "sliderInteraction": case "choiceInteraction": case "mediaInteraction": case "hottextInteraction": case "orderInteraction": case "extendedTextInteraction": case "uploadInteraction": case "associateInteraction": case "feedbackBlock": case "templateBlock": case "infoControl": case "math": case "pre": case "h1": case "h2": case "h3": case "h4": case "h5": case "h6": case "address": case "dl": case "ol": case "hr": case "ul": case "blockquote": case "table": case "rubricBlock": return true; default: return false; } } }