/** * <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.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.stream.StreamResult; import org.cyberneko.html.parsers.SAXParser; import org.olat.core.gui.render.StringOutput; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.core.util.filter.FilterFactory; import org.w3c.dom.Attr; import org.w3c.dom.Document; 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.JqtiExtensionManager; import uk.ac.ed.ph.jqtiplus.exception.QtiModelException; import uk.ac.ed.ph.jqtiplus.node.AbstractNode; import uk.ac.ed.ph.jqtiplus.node.LoadingContext; import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; import uk.ac.ed.ph.jqtiplus.node.content.basic.FlowStatic; import uk.ac.ed.ph.jqtiplus.node.content.basic.InlineStatic; import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; import uk.ac.ed.ph.jqtiplus.xmlutils.SimpleDomBuilderHandler; /** * Do the ugly job to convert the Tiny MCE HTML code to the object model * of QTI Works * * Initial date: 10.12.2015<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class AssessmentHtmlBuilder { private static final OLog log = Tracing.createLoggerFor(AssessmentHtmlBuilder.class); private final QtiSerializer qtiSerializer; public AssessmentHtmlBuilder() { JqtiExtensionManager jqtiExtensionManager = new JqtiExtensionManager(); qtiSerializer = new QtiSerializer(jqtiExtensionManager); } public AssessmentHtmlBuilder(QtiSerializer qtiSerializer) { this.qtiSerializer = qtiSerializer; } public boolean containsSomething(String html) { return StringHelper.containsNonWhitespace(FilterFactory.getHtmlTagsFilter().filter(html)); } public String flowStaticString(List<? extends FlowStatic> statics) { StringOutput sb = new StringOutput(); if(statics != null && statics.size() > 0) { for(FlowStatic flowStatic:statics) { qtiSerializer.serializeJqtiObject(flowStatic, new StreamResult(sb)); } } return cleanUpNamespaces(sb); } public String blocksString(List<? extends Block> statics) { StringOutput sb = new StringOutput(); if(statics != null && statics.size() > 0) { for(Block flowStatic:statics) { qtiSerializer.serializeJqtiObject(flowStatic, new StreamResult(sb)); } } return cleanUpNamespaces(sb); } public String inlineStaticString(List<? extends InlineStatic> statics) { StringOutput sb = new StringOutput(); if(statics != null && statics.size() > 0) { for(InlineStatic inlineStatic:statics) { qtiSerializer.serializeJqtiObject(inlineStatic, new StreamResult(sb)); } } return cleanUpNamespaces(sb); } private String cleanUpNamespaces(StringOutput sb) { String content = sb.toString(); content = content.replace(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", ""); content = content.replace("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", ""); content = content.replace("\n xmlns=\"http://www.imsglobal.org/xsd/imsqti_v2p1\"", ""); content = content.replace("xmlns=\"http://www.imsglobal.org/xsd/imsqti_v2p1\"", ""); content = content.replace("\n xsi:schemaLocation=\"http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/imsqti_v2p1.xsd\"", ""); content = content.replace("xsi:schemaLocation=\"http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/imsqti_v2p1.xsd\"", ""); return content.trim(); } public void appendHtml(AbstractNode parent, String htmlFragment) { if(StringHelper.containsNonWhitespace(htmlFragment)) { htmlFragment = htmlFragment.trim(); //tinymce bad habits if(StringHelper.isHtml(htmlFragment)) { if(htmlFragment.startsWith("<p> ")) { htmlFragment = htmlFragment.replace("<p> ", "<p>"); } } else { htmlFragment = "<p>" + htmlFragment + "</p>"; } //wrap around <html> to have a root element for neko Document document = filter("<html>" + htmlFragment + "</html>"); Element docElement = document.getDocumentElement(); cleanUpNamespaces(docElement); parent.getNodeGroups().load(docElement, new HTMLLoadingContext()); } } private void cleanUpNamespaces(Element element) { Attr xsiattr = element.getAttributeNode("xmlns:xsi"); if(xsiattr != null && "http://www.w3.org/2001/XMLSchema-instance".equals(xsiattr.getValue())) { element.removeAttribute("xmlns:xsi"); } Attr attr = element.getAttributeNode("xmlns"); if(attr != null && "http://www.imsglobal.org/xsd/imsqti_v2p1".equals(attr.getValue())) { element.removeAttribute("xmlns"); } for(Node child=element.getFirstChild(); child != null; child = child.getNextSibling()) { if(child instanceof Element) { cleanUpNamespaces((Element)child); } } } private Document filter(String content) { try { SAXParser parser = new SAXParser(); parser.setProperty("http://cyberneko.org/html/properties/names/elems", "lower"); parser.setFeature("http://cyberneko.org/html/features/balance-tags/document-fragment", true); parser.setProperty("http://cyberneko.org/html/properties/default-encoding", "UTF-8"); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.newDocument(); HtmlToDomBuilderHandler contentHandler = new HtmlToDomBuilderHandler(document); parser.setContentHandler(contentHandler); parser.parse(new InputSource(new ByteArrayInputStream(content.getBytes()))); return document; } catch (SAXException e) { log.error("", e); return null; } catch (IOException e) { log.error("", e); return null; } catch (Exception e) { log.error("", e); return null; } } /** * Convert:<br> * <ul> * <li>textentryinteraction -> camel cased textEntryInteraction</li> * </ul> * * Initial date: 26.02.2016<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ private static class HtmlToDomBuilderHandler extends SimpleDomBuilderHandler { private boolean video = false; private StringBuilder scriptBuffer = new StringBuilder(); public HtmlToDomBuilderHandler(Document document) { super(document); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { if(video) return; if("textentryinteraction".equals(localName)) { localName = qName = "textEntryInteraction"; AttributesImpl attributesCleaned = new AttributesImpl(""); for(int i=0; i<attributes.getLength(); i++) { String name = attributes.getLocalName(i); if(!"openolattype".equalsIgnoreCase(name) && !"data-qti-solution".equalsIgnoreCase(name) && !"data-qti-solution-empty".equalsIgnoreCase(name)) { String value = attributes.getValue(i); attributesCleaned.addAttribute(name, value); } } attributes = new AttributesDelegate(attributesCleaned); } else if("span".equals(localName)) { String cssClass = attributes.getValue("class"); if(cssClass != null && "olatFlashMovieViewer".equals(cssClass)) { video = true; return; } } super.startElement(uri, localName, qName, attributes); } @Override public void characters(char[] ch, int start, int length) { if(video) { scriptBuffer.append(new String(ch, start, length)); } else { super.characters(ch, start, length); } } @Override public void endElement(String uri, String localName, String qName) { if(video) { if("span".equals(localName)) { String content = scriptBuffer.toString(); String startScript = "BPlayer.insertPlayer("; int start = content.indexOf(startScript); if(start >= 0) { int end = content.indexOf(")", start); String parameters = content.substring(start + startScript.length(), end); translateToObject(uri, parameters); } video = false; } return; } if("textentryinteraction".equals(localName)) { localName = qName = "textEntryInteraction"; } super.endElement(uri, localName, qName); } private void translateToObject(String uri, String parameters) { String[] array = parameters.split(","); String data = array[0].replace("\"", ""); String id = array[1].replace("\"", ""); String width = array[2]; String height = array[3]; String type = array[6].replace("\"", ""); String ooData = parameters.replace("\"", "'"); AttributesImpl attributes = new AttributesImpl(uri); attributes.addAttribute("data", data); attributes.addAttribute("id", id); attributes.addAttribute("class", "olatFlashMovieViewer"); attributes.addAttribute("width", width); attributes.addAttribute("height", height); attributes.addAttribute("type", type); attributes.addAttribute("data-oo-movie", ooData); super.startElement(uri, "object", "object", attributes); super.endElement(uri, "object", "object"); } } private static class AttributeImpl { private String name; private String value; public AttributeImpl(String name, String value) { this.name = name; this.value = value; } public String getName() { return name; } public String getValue() { return value; } } /** * This implementation is specifically coded for the SimpleDomBuilderHandler * and only for this handler. It implements only the needed methods by this * handler implementation. * * Initial date: 07.09.2016<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ private static class AttributesImpl implements Attributes { private String attributesUri; private List<AttributeImpl> attributes = new ArrayList<>(); public AttributesImpl(String uri) { this.attributesUri = uri; } public void addAttribute(String name, String value) { attributes.add(new AttributeImpl(name, value)); } @Override public int getLength() { return attributes.size(); } @Override public String getURI(int index) { return attributesUri; } @Override public String getLocalName(int index) { return attributes.get(index).getName(); } @Override public String getQName(int index) { return attributes.get(index).getName(); } @Override public String getType(int index) { return null; } @Override public String getValue(int index) { return attributes.get(index).getValue(); } @Override public int getIndex(String uri, String localName) { return 0; } @Override public int getIndex(String qName) { return 0; } @Override public String getType(String uri, String localName) { return null; } @Override public String getType(String qName) { return null; } @Override public String getValue(String uri, String localName) { return null; } @Override public String getValue(String qName) { return null; } } /** * Convert:<br> * <ul> * <li>responseidentifier -> camel cased responseIdentifier</li> * <li>and other attributes of textEntryInteraction</li> * </ul> * * Initial date: 26.02.2016<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ private static class AttributesDelegate implements Attributes { private final Attributes attributes; public AttributesDelegate(Attributes attributes) { this.attributes = attributes; } @Override public int getLength() { return attributes.getLength(); } @Override public String getURI(int index) { return attributes.getURI(index); } @Override public String getLocalName(int index) { String localName = attributes.getLocalName(index); return translateAttributeName(localName); } @Override public String getQName(int index) { String qName = attributes.getQName(index); return translateAttributeName(qName); } private final String translateAttributeName(String attrName) { if(attrName != null) { switch(attrName) { case "responseidentifier": return "responseIdentifier"; case "placeholdertext": return "placeholderText"; case "expectedlength": return "expectedLength"; case "patternmask": return "patternMask"; default: return attrName; } } return attrName; } @Override public String getType(int index) { return attributes.getType(index); } @Override public String getValue(int index) { return attributes.getValue(index); } @Override public int getIndex(String uri, String localName) { return attributes.getIndex(uri, localName); } @Override public int getIndex(String qName) { return attributes.getIndex(qName.toLowerCase()); } @Override public String getType(String uri, String localName) { return attributes.getType(uri, localName); } @Override public String getType(String qName) { return attributes.getType(qName); } @Override public String getValue(String uri, String localName) { return attributes.getValue(uri, localName); } @Override public String getValue(String qName) { return attributes.getValue(qName); } } private static final class HTMLLoadingContext implements LoadingContext { @Override public JqtiExtensionManager getJqtiExtensionManager() { return null; } @Override public void modelBuildingError(QtiModelException exception, Node badNode) { // } } }