/**
* <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.pool;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
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.helpers.DefaultHandler;
/**
*
* Convert the special TinyMCE options for the QTI 1.2 editor (without root
* elements) in a QTI 2.1 compliant HTML code with Block elements as roots.
* For the sake of XML, the handler had a tag <start> as root element
* that the developer need to remove.
*
* Initial date: 28 févr. 2017<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
class QTI12To21HtmlHandler extends DefaultHandler {
private final XMLStreamWriter xtw;
private int subLevel = 0;
private Deque<String> skipTags = new ArrayDeque<String>();
private Map<String,String> materialsMapping = new HashMap<>();
private boolean envelopP = false;
private boolean started = false;
public QTI12To21HtmlHandler(XMLStreamWriter xtw) {
this.xtw = xtw;
}
public Map<String,String> getMaterialsMapping() {
return materialsMapping;
}
@Override
public void startDocument() throws SAXException {
try {
xtw.writeStartElement("start");
} catch (XMLStreamException e) {
throw new SAXException(e);
}
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
try {
qName = qName.toLowerCase();
if("html".equals(qName) || "head".equals(qName)) {
return;
}
if("body".equals(qName)) {
started = true;
return;
}
if(subLevel == 0 && envelopP && isBlock(qName)) {
xtw.writeEndElement();
envelopP = false;
}
if(subLevel == 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.toLowerCase(), attributes);
}
if(subLevel >= 0) {
subLevel++;
}
} catch (XMLStreamException e) {
throw new SAXException(e);
}
}
private void writeStartElement(String qName, Attributes attributes)
throws XMLStreamException {
xtw.writeStartElement(qName);
if(attributes != null) {
convertAttributes(attributes);
}
}
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)) {
//ignore align
} else if("xmlns".equals(attrQName) && !StringHelper.containsNonWhitespace(attrValue)) {
//ignore empty schema
} else if("src".equals(attrQName)) {
if(attrValue.contains(" ")) {
String newValue = attrValue.replace(' ', '_');
materialsMapping.put(attrValue, newValue);
attrValue = newValue;
}
xtw.writeAttribute(attrQName, attrValue);
} else {
xtw.writeAttribute(attrQName, attrValue);
}
}
}
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
if(!started) return;
try {
if(subLevel == 0) {
if(!envelopP && isCharacterRelevant(ch, start, length)) {
xtw.writeStartElement("p");
int diff = trimStart(ch, start, length);
start += diff;
length -= diff;
envelopP = true;
}
if(start < 0) {
start = 0;//Bug neko
}
xtw.writeCharacters(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 {
qName = qName.toLowerCase();
if("html".equals(qName) || "head".equals(qName) || "body".equals(qName)) {
return;
}
if(skipTags.size() > 0 && skipTags.peek().equals(qName)) {
skipTags.pop();
return;
}
if(subLevel >= 0) {
subLevel--;
}
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);
}
}
private boolean isBlock(String qName) {
switch(qName) {
case "p":
case "div":
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": return true;
default: return false;
}
}
}