package fi.otavanopisto.muikku.plugins.dnm.unembed;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Lock;
import javax.ejb.Singleton;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import fi.otavanopisto.muikku.controller.PluginSettingsController;
import fi.otavanopisto.muikku.plugins.dnm.parser.DeusNexInternalException;
import fi.otavanopisto.muikku.plugins.dnm.parser.DeusNexXmlUtils;
import fi.otavanopisto.muikku.plugins.material.HtmlMaterialController;
import fi.otavanopisto.muikku.plugins.material.model.HtmlMaterial;
import fi.otavanopisto.muikku.plugins.material.model.Material;
import fi.otavanopisto.muikku.plugins.workspace.WorkspaceMaterialContainsAnswersExeption;
import fi.otavanopisto.muikku.plugins.workspace.WorkspaceMaterialController;
import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceFolder;
import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceMaterial;
import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceMaterialAssignmentType;
import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceMaterialCorrectAnswersDisplay;
import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceNode;
@Singleton
public class MaterialUnEmbedder {
@Inject
private WorkspaceMaterialController workspaceMaterialController;
@Inject
private HtmlMaterialController htmlMaterialController;
@Inject
private PluginSettingsController pluginSettingsController;
@Inject
private Logger logger;
@Lock
public void unembedWorkspaceMaterials(WorkspaceNode parentNode) throws DeusNexInternalException {
try {
loadHtmlMaterialPieces();
for (WorkspaceNode node : workspaceMaterialController.listWorkspaceNodesByParent(parentNode)) {
unembedWorkspaceNode(parentNode, node);
}
saveHtmlMaterialPieces();
} catch (XPathExpressionException | SAXException | IOException | ParserConfigurationException | TransformerException
| WorkspaceMaterialContainsAnswersExeption e) {
throw new DeusNexInternalException(e.getClass().getName() + ": " + e.getMessage());
}
}
private void loadHtmlMaterialPieces() throws JsonParseException, JsonMappingException, IOException {
if (pluginSettingsController.getPluginSettingKey("deus-nex-machina", "unembed-html-material-pieces") != null) {
htmlMaterialPieces = new HashMap<Long, List<Long>>();
} else {
String jsonHtmlMaterialPieces = pluginSettingsController.getPluginSetting("deus-nex-machina", "unembed-html-material-pieces");
ObjectMapper objectMapper = new ObjectMapper();
htmlMaterialPieces = objectMapper.readValue(jsonHtmlMaterialPieces, new TypeReference<Map<Long, List<Long>>>() {
});
}
}
private void saveHtmlMaterialPieces() throws JsonGenerationException, JsonMappingException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
String jsonHtmlMaterialPieces = objectMapper.writeValueAsString(htmlMaterialPieces);
pluginSettingsController.setPluginSetting("deus-nex-machina", "unembed-html-material-pieces", jsonHtmlMaterialPieces);
}
private void unembedWorkspaceNode(WorkspaceNode parent, WorkspaceNode workspaceNode) throws XPathExpressionException, SAXException,
IOException, ParserConfigurationException, DeusNexInternalException, TransformerException, WorkspaceMaterialContainsAnswersExeption {
if (workspaceNode instanceof WorkspaceFolder) {
for (WorkspaceNode childNode : workspaceMaterialController.listWorkspaceNodesByParent(workspaceNode)) {
unembedWorkspaceNode(workspaceNode, childNode);
}
} else if (workspaceNode instanceof WorkspaceMaterial) {
WorkspaceMaterial workspaceMaterial = (WorkspaceMaterial) workspaceNode;
Material material = workspaceMaterialController.getMaterialForWorkspaceMaterial(workspaceMaterial);
if (material instanceof HtmlMaterial) {
HtmlMaterial htmlMaterial = (HtmlMaterial) material;
unembedHtmlMaterial(parent, workspaceMaterial, htmlMaterial);
}
}
}
private void unembedHtmlMaterial(WorkspaceNode parent, WorkspaceMaterial workspaceMaterial, HtmlMaterial htmlMaterial)
throws DeusNexInternalException, IOException {
String html = htmlMaterial.getHtml();
if (StringUtils.isNotBlank(html)) {
InputStream is = null;
logger.info("Unembedding html material " + htmlMaterial.getId());
try {
is = new ByteArrayInputStream(html.getBytes("UTF-8"));
DocumentBuilder builder = DeusNexXmlUtils.createDocumentBuilder();
Document document = builder.parse(is);
if (document != null) {
NodeList iframes = DeusNexXmlUtils.findNodesByXPath(document.getDocumentElement(), "//iframe[@data-type='embedded-document']");
if (iframes.getLength() > 0) {
List<Document> splittedHtmlDocument = splitHtmlDocument(document);
for (int i = 0; i < splittedHtmlDocument.size(); i++) {
List<Long> pieceList = new ArrayList<Long>();
htmlMaterialPieces.put(htmlMaterial.getId(), pieceList);
HashMap<Long, WorkspaceMaterialAssignmentType> assignmentTypes = new HashMap<Long, WorkspaceMaterialAssignmentType>();
Document documentPiece = splittedHtmlDocument.get(i);
List<HtmlMaterial> pieceHtmlMaterials;
if (isEmbedPiece(documentPiece)) {
WorkspaceMaterialAssignmentType assignmentType = embeddedHtmlMaterialAssignmentType(documentPiece);
pieceHtmlMaterials = new ArrayList<HtmlMaterial>();
long embeddedHtmlMaterialId = embeddedHtmlMaterialId(documentPiece);
if (htmlMaterialPieces.containsKey(embeddedHtmlMaterialId)) {
for (Long htmlMaterialId : htmlMaterialPieces.get(embeddedHtmlMaterialId)) {
logger.info("Existing html material " + htmlMaterialId + " embedded in " + htmlMaterial.getId());
HtmlMaterial pieceHtmlMaterial = htmlMaterialController.findHtmlMaterialById(htmlMaterialId);
pieceHtmlMaterials.add(pieceHtmlMaterial);
pieceList.add(pieceHtmlMaterial.getId());
assignmentTypes.put(pieceHtmlMaterial.getId(), assignmentType);
}
}
else {
HtmlMaterial pieceHtmlMaterial = htmlMaterialController.findHtmlMaterialById(embeddedHtmlMaterialId);
logger.info("Existing html material " + embeddedHtmlMaterialId + " embedded in " + htmlMaterial.getId());
pieceHtmlMaterials.add(pieceHtmlMaterial);
pieceList.add(pieceHtmlMaterial.getId());
assignmentTypes.put(pieceHtmlMaterial.getId(), assignmentType);
}
}
else {
String license = null;
HtmlMaterial pieceHtmlMaterial = htmlMaterialController.createHtmlMaterial(htmlMaterial.getTitle() + " (" + i + ")",
DeusNexXmlUtils.serializeElement(documentPiece.getDocumentElement(), true, false, "xml"), "text/html; editor=CKEditor",
0l, license);
logger.info("New html material piece " + pieceHtmlMaterial.getId() + " split from " + htmlMaterial.getId());
pieceHtmlMaterials = new ArrayList<HtmlMaterial>();
pieceHtmlMaterials.add(pieceHtmlMaterial);
pieceList.add(pieceHtmlMaterial.getId());
}
for (HtmlMaterial pieceHtmlMaterial : pieceHtmlMaterials) {
WorkspaceNode newNode = workspaceMaterialController.createWorkspaceMaterial(parent, pieceHtmlMaterial,
assignmentTypes.get(pieceHtmlMaterial.getId()), WorkspaceMaterialCorrectAnswersDisplay.ALWAYS);
workspaceMaterialController.moveAbove(newNode, workspaceMaterial);
}
}
workspaceMaterialController.deleteWorkspaceMaterial(workspaceMaterial, true);
htmlMaterialController.deleteHtmlMaterial(htmlMaterial);
}
else {
logger.info("Html material " + htmlMaterial.getId() + " has no embeds");
}
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Html material " + htmlMaterial.getId() + " unembed fail", e);
throw new DeusNexInternalException("MaterialUnEmbedder:unembedHtmlMaterial", e);
} finally {
if (is != null) {
is.close();
}
}
}
}
private boolean isEmbedPiece(Document documentPiece) throws XPathExpressionException {
Node iframe = DeusNexXmlUtils.findNodeByXPath(documentPiece.getDocumentElement(), "body/iframe[@data-type='embedded-document']");
return iframe != null;
}
private long embeddedHtmlMaterialId(Document documentPiece) throws XPathExpressionException {
Node iframe = DeusNexXmlUtils.findNodeByXPath(documentPiece.getDocumentElement(), "body/iframe[@data-type='embedded-document']");
return Long.parseLong(iframe.getAttributes().getNamedItem("data-material-id").getNodeValue(), 10);
}
private WorkspaceMaterialAssignmentType embeddedHtmlMaterialAssignmentType(Document documentPiece) throws XPathExpressionException {
Node iframe = DeusNexXmlUtils.findNodeByXPath(documentPiece.getDocumentElement(), "body/iframe[@data-type='embedded-document']");
Node assignmentTypeNode = iframe.getAttributes().getNamedItem("data-assignment-type");
String assignmentType = assignmentTypeNode == null ? null : assignmentTypeNode.getNodeValue();
if ("EXERCISE".equals(assignmentType))
return WorkspaceMaterialAssignmentType.EXERCISE;
if ("EVALUATED".equals(assignmentType))
return WorkspaceMaterialAssignmentType.EVALUATED;
return null;
}
private List<Document> splitHtmlDocument(Document document) throws XPathExpressionException, DeusNexInternalException {
while (embedIframesInNonTopLevelElement(document)) {
bubbleUpEmbedIframes(document);
}
List<Document> documentPieces = new ArrayList<Document>();
NodeList pieceNodes = DeusNexXmlUtils.findNodesByXPath(document.getDocumentElement(), "body/*");
List<Node> paragraphs = new ArrayList<Node>();
boolean isEmbeddedDocument = false;
for (int i = 0; i < pieceNodes.getLength(); i++) {
isEmbeddedDocument = false;
Node pieceNode = pieceNodes.item(i);
if (pieceNode instanceof Element) {
Element element = (Element) pieceNode;
if ("iframe".equals(element.getTagName())) {
String type = element.getAttribute("data-type");
isEmbeddedDocument = "embedded-document".equals(type);
}
}
if (isEmbeddedDocument) {
// text content
if (!paragraphs.isEmpty()) {
documentPieces.add(createDocument(paragraphs));
paragraphs.clear();
}
// embedded document
paragraphs.add(pieceNode);
documentPieces.add(createDocument(paragraphs));
paragraphs.clear();
} else {
paragraphs.add(pieceNode);
}
}
// text content
if (!paragraphs.isEmpty()) {
documentPieces.add(createDocument(paragraphs));
paragraphs.clear();
}
return documentPieces;
}
private Document createDocument(List<Node> paragraphs) throws DeusNexInternalException {
DocumentBuilder documentBuilder = DeusNexXmlUtils.createDocumentBuilder();
Document documentPiece = documentBuilder.newDocument();
Node html = documentPiece.createElement("html");
documentPiece.appendChild(html);
Node body = documentPiece.createElement("body");
html.appendChild(body);
for (Node paragraph : paragraphs) {
Node adoptedPieceNode = documentPiece.adoptNode(paragraph);
body.appendChild(adoptedPieceNode);
}
return documentPiece;
}
private boolean embedIframesInNonTopLevelElement(Document document) throws XPathExpressionException {
NodeList iframes = DeusNexXmlUtils.findNodesByXPath(document.getDocumentElement(), "body/*//iframe[@data-type='embedded-document']");
if (iframes.getLength() != 0) {
logger.info(iframes.getLength() + " iframes in non-top-level element");
}
return iframes.getLength() > 0;
}
private void bubbleUpEmbedIframes(Document document) throws XPathExpressionException {
NodeList iframes = DeusNexXmlUtils.findNodesByXPath(document.getDocumentElement(), "body/*//iframe[@data-type='embedded-document']");
for (int i = 0; i < iframes.getLength(); i++) {
Node iframe = iframes.item(i);
bubbleUp(iframe);
}
if (iframes.getLength() != 0) {
logger.info("bubbled up " + iframes.getLength() + " iframes");
}
}
private void bubbleUp(Node node) {
Node parent = node.getParentNode();
Node parentsParent = parent.getParentNode();
Node newParent = node.getOwnerDocument().createElement(parent.getNodeName());
while (node.getNextSibling() != null) {
Node sibling = node.getNextSibling();
newParent.appendChild(sibling);
}
parentsParent.insertBefore(newParent, parent.getNextSibling());
parentsParent.insertBefore(node, newParent);
if (!parent.hasChildNodes()) {
parentsParent.removeChild(parent);
}
if (!newParent.hasChildNodes()) {
parentsParent.removeChild(newParent);
}
}
private Map<Long, List<Long>> htmlMaterialPieces;
}