package fi.otavanopisto.muikku.plugins.workspace; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.UUID; import java.util.logging.Logger; import javax.enterprise.event.Event; import javax.inject.Inject; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.xerces.parsers.DOMParser; import org.cyberneko.html.HTMLConfiguration; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; import fi.otavanopisto.muikku.i18n.LocaleController; import fi.otavanopisto.muikku.model.workspace.WorkspaceEntity; import fi.otavanopisto.muikku.plugins.material.HtmlMaterialController; import fi.otavanopisto.muikku.plugins.material.MaterialController; import fi.otavanopisto.muikku.plugins.material.model.HtmlMaterial; import fi.otavanopisto.muikku.plugins.material.model.Material; import fi.otavanopisto.muikku.plugins.material.model.MaterialProducer; import fi.otavanopisto.muikku.plugins.material.model.MaterialViewRestrict; import fi.otavanopisto.muikku.plugins.workspace.dao.WorkspaceFolderDAO; import fi.otavanopisto.muikku.plugins.workspace.dao.WorkspaceMaterialDAO; import fi.otavanopisto.muikku.plugins.workspace.dao.WorkspaceNodeDAO; import fi.otavanopisto.muikku.plugins.workspace.dao.WorkspaceRootFolderDAO; import fi.otavanopisto.muikku.plugins.workspace.events.WorkspaceFolderCreateEvent; import fi.otavanopisto.muikku.plugins.workspace.events.WorkspaceFolderUpdateEvent; import fi.otavanopisto.muikku.plugins.workspace.events.WorkspaceMaterialCreateEvent; import fi.otavanopisto.muikku.plugins.workspace.events.WorkspaceMaterialDeleteEvent; import fi.otavanopisto.muikku.plugins.workspace.events.WorkspaceMaterialUpdateEvent; import fi.otavanopisto.muikku.plugins.workspace.events.WorkspaceRootFolderCreateEvent; import fi.otavanopisto.muikku.plugins.workspace.events.WorkspaceRootFolderUpdateEvent; import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceFolder; import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceFolderType; 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; import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceNodeType; import fi.otavanopisto.muikku.plugins.workspace.model.WorkspaceRootFolder; import fi.otavanopisto.muikku.schooldata.WorkspaceEntityController; import fi.otavanopisto.muikku.session.SessionController; // TODO Should probably be split or renamed WorkspaceNodeController public class WorkspaceMaterialController { @Inject private Logger logger; @Inject private LocaleController localeController; @Inject private SessionController sessionController; @Inject private WorkspaceEntityController workspaceEntityController; @Inject private WorkspaceRootFolderDAO workspaceRootFolderDAO; @Inject private WorkspaceMaterialDAO workspaceMaterialDAO; @Inject private WorkspaceFolderDAO workspaceFolderDAO; @Inject private WorkspaceNodeDAO workspaceNodeDAO; @Inject private Event<WorkspaceRootFolderCreateEvent> workspaceRootFolderCreateEvent; @SuppressWarnings("unused") @Inject private Event<WorkspaceRootFolderUpdateEvent> workspaceRootFolderUpdateEvent; @Inject private Event<WorkspaceFolderCreateEvent> workspaceFolderCreateEvent; @SuppressWarnings("unused") @Inject private Event<WorkspaceFolderUpdateEvent> workspaceFolderUpdateEvent; @Inject private Event<WorkspaceMaterialCreateEvent> workspaceMaterialCreateEvent; @SuppressWarnings("unused") @Inject private Event<WorkspaceMaterialUpdateEvent> workspaceMaterialUpdateEvent; @Inject private Event<WorkspaceMaterialDeleteEvent> workspaceMaterialDeleteEvent; @Inject private MaterialController materialController; @Inject private HtmlMaterialController htmlMaterialController; private static final int FLATTENING_LEVEL = 1; /* WorkspaceNode */ /** * Updates the order numbers of workspace nodes so that <code>workspaceNode</code> appears above <code>referenceNode</code>. * * @param workspaceNode * The workspace node to be moved * @param referenceNode * The workspace node above which <code>workspaceNode</code> is moved * * @return The updated workspace node */ public WorkspaceNode moveAbove(WorkspaceNode workspaceNode, WorkspaceNode referenceNode) { // Order number of the reference node Integer referenceOrderNumber = referenceNode.getOrderNumber() == null ? 0 : referenceNode.getOrderNumber(); // Workspace nodes with order number >= reference order number List<WorkspaceNode> subsequentNodes = workspaceNodeDAO.listByOrderNumberEqualOrGreater(referenceNode); // Sort workspace nodes according to order number sortWorkspaceNodes(subsequentNodes); // node order number = referenceOrderNumber, subsequent nodes = ++referenceOrderNumber workspaceNode = workspaceNodeDAO.updateOrderNumber(workspaceNode, referenceOrderNumber); for (WorkspaceNode subsequentNode : subsequentNodes) { if (!(subsequentNode.getId().equals(workspaceNode.getId()))) { workspaceNodeDAO.updateOrderNumber(subsequentNode, ++referenceOrderNumber); } } return workspaceNode; } /** * Updates the order numbers of workspace nodes so that <code>workspaceNode</code> appears below <code>referenceNode</code>. * * @param workspaceNode * The workspace node to be moved * @param referenceNode * The workspace node below which <code>workspaceNode</code> is moved * * @return The updated workspace node */ public WorkspaceNode moveBelow(WorkspaceNode workspaceNode, WorkspaceNode referenceNode) { // Order number of the reference node Integer referenceOrderNumber = referenceNode.getOrderNumber() == null ? 0 : referenceNode.getOrderNumber(); // Workspace nodes with order number > reference order number List<WorkspaceNode> subsequentNodes = workspaceNodeDAO.listByOrderNumberGreater(referenceNode); // Sort workspace nodes according to order number sortWorkspaceNodes(subsequentNodes); // node order number = referenceOrderNumber + 1, subsequent nodes = ++referenceOrderNumber workspaceNode = workspaceNodeDAO.updateOrderNumber(workspaceNode, ++referenceOrderNumber); for (WorkspaceNode subsequentNode : subsequentNodes) { workspaceNodeDAO.updateOrderNumber(subsequentNode, ++referenceOrderNumber); } return workspaceNode; } public WorkspaceNode findWorkspaceNodeNextSibling(WorkspaceNode referenceNode) { List<WorkspaceNode> nextSiblings = workspaceNodeDAO.listParentByOrderNumberGreaterSortByGreater(referenceNode.getParent(), referenceNode.getOrderNumber(), 0, 1); if (nextSiblings.isEmpty()) { return null; } return nextSiblings.get(0); } /** * Sorts the given list of workspace nodes. * * @param workspaceNodes * The list of workspace nodes to sort */ public void sortWorkspaceNodes(List<WorkspaceNode> workspaceNodes) { Collections.sort(workspaceNodes, new Comparator<WorkspaceNode>() { @Override public int compare(WorkspaceNode o1, WorkspaceNode o2) { int o1OrderNumber = o1.getOrderNumber() == null ? 0 : o1.getOrderNumber(); int o2OrderNumber = o2.getOrderNumber() == null ? 0 : o2.getOrderNumber(); return o1OrderNumber - o2OrderNumber; } }); } public WorkspaceNode findWorkspaceNodeById(Long workspaceMaterialId) { return workspaceNodeDAO.findById(workspaceMaterialId); } public WorkspaceNode findWorkspaceNodeByParentAndUrlName(WorkspaceNode parent, String urlName) { return workspaceNodeDAO.findByParentAndUrlName(parent, urlName); } public WorkspaceNode findWorkspaceNodeByWorkspaceEntityAndPath(WorkspaceEntity workspaceEntity, String path) { String[] pathElements = path.split("/"); WorkspaceNode parent = findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity); for (int i = 0, l = pathElements.length; i < l - 1; i++) { String pathElement = pathElements[i]; parent = findWorkspaceNodeByParentAndUrlName(parent, pathElement); } return findWorkspaceNodeByParentAndUrlName(parent, pathElements[pathElements.length - 1]); } public String getCompletePath(WorkspaceNode workspaceNode) { return String.format("workspace/%s/materials/%s", getWorkspaceNodeWorkspaceUrlName(workspaceNode), workspaceNode.getPath()); } public WorkspaceMaterial findWorkspaceMaterialByRootPath(String path) { if (path.contains("?")) { path = StringUtils.substringBefore(path, "?"); } String[] pathElements = StringUtils.split(path, "/"); if (pathElements.length >= 3 && StringUtils.equals("workspace", pathElements[0]) && StringUtils.equals("materials", pathElements[2])) { String workspaceUrlName = pathElements[1]; WorkspaceEntity workspaceEntity = workspaceEntityController.findWorkspaceByUrlName(workspaceUrlName); if (workspaceEntity != null) { WorkspaceNode workspaceNode = findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity); for (int i = 3; i < pathElements.length; i++) { workspaceNode = findWorkspaceNodeByParentAndUrlName(workspaceNode, pathElements[i]); if (workspaceNode == null) { return null; } } return workspaceNode instanceof WorkspaceMaterial ? (WorkspaceMaterial) workspaceNode : null; } } return null; } public String getWorkspaceNodeWorkspaceUrlName(WorkspaceNode workspaceNode) { WorkspaceNode node = workspaceNode; while (node != null) { if (node instanceof WorkspaceRootFolder) { return ((WorkspaceRootFolder) node).getUrlName(); } node = node.getParent(); } return null; } public List<WorkspaceNode> listWorkspaceNodesByParent(WorkspaceNode parent) { return workspaceNodeDAO.listByParentSortByOrderNumber(parent); } public List<WorkspaceNode> listVisibleWorkspaceNodesByParentSortByOrderNumber(WorkspaceNode parent) { return listVisibleWorkspaceNodesByParentAndFolderTypeSortByOrderNumber(parent, WorkspaceFolderType.DEFAULT); } public List<WorkspaceNode> listWorkspaceNodesByParentAndFolderTypeSortByOrderNumber(WorkspaceNode parent, WorkspaceFolderType folderType) { List<WorkspaceNode> nodes = workspaceNodeDAO.listByParentSortByOrderNumber(parent); // TODO: Do in database for (int i = nodes.size() - 1; i >= 0; i--) { WorkspaceNode node = nodes.get(i); if (node instanceof WorkspaceFolder) { WorkspaceFolder folder = (WorkspaceFolder) node; if (folder.getFolderType() != folderType) { nodes.remove(i); } } } return nodes; } public List<WorkspaceFolder> listWorkspaceFoldersByParentAndFolderTypeSortByOrderNumber(WorkspaceNode parent, WorkspaceFolderType folderType) { List<WorkspaceFolder> nodes = workspaceFolderDAO.listByParentAndFolderType(parent, folderType); // TODO: Do in database for (int i = nodes.size() - 1; i >= 0; i--) { WorkspaceFolder node = nodes.get(i); if (node.getFolderType() != folderType) { nodes.remove(i); } } return nodes; } public List<WorkspaceNode> listVisibleWorkspaceNodesByParentAndFolderTypeSortByOrderNumber(WorkspaceNode parent, WorkspaceFolderType folderType) { List<WorkspaceNode> nodes = workspaceNodeDAO.listByParentAndHiddenSortByOrderNumber(parent, Boolean.FALSE); // TODO: Do in database for (int i = nodes.size() - 1; i >= 0; i--) { WorkspaceNode node = nodes.get(i); if (node instanceof WorkspaceFolder) { WorkspaceFolder folder = (WorkspaceFolder) node; if (folder.getFolderType() != folderType) { nodes.remove(i); } } } return nodes; } public void deleteAllWorkspaceNodes(WorkspaceEntity workspaceEntity) throws WorkspaceMaterialContainsAnswersExeption { WorkspaceRootFolder rootFolder = findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity); deleteAllChildNodes(rootFolder); } private void deleteAllChildNodes(WorkspaceNode node) throws WorkspaceMaterialContainsAnswersExeption { List<WorkspaceNode> childNodes = listWorkspaceNodesByParent(node); for (WorkspaceNode childNode : childNodes) { deleteAllChildNodes(childNode); } switch (node.getType()) { case FRONT_PAGE_FOLDER: case FOLDER: deleteWorkspaceFolder((WorkspaceFolder) node); break; case MATERIAL: deleteWorkspaceMaterial((WorkspaceMaterial) node, true); break; case ROOT_FOLDER: deleteWorkspaceRootFolder((WorkspaceRootFolder) node); break; } } public Material getMaterialForWorkspaceMaterial(WorkspaceMaterial workspaceMaterial) { return workspaceMaterial.getMaterialId() == null ? null : materialController.findMaterialById(workspaceMaterial.getMaterialId()); } public WorkspaceNode cloneWorkspaceNode(WorkspaceNode workspaceNode, WorkspaceNode parent, boolean cloneMaterials) { return cloneWorkspaceNode(workspaceNode, parent, cloneMaterials, false); } private WorkspaceNode cloneWorkspaceNode(WorkspaceNode workspaceNode, WorkspaceNode parent, boolean cloneMaterials, boolean overrideCloneMaterials) { WorkspaceNode newNode; boolean isHtmlMaterial = false; Integer index = workspaceNodeDAO.getMaximumOrderNumber(parent); index = index == null ? 0 : ++index; if (workspaceNode instanceof WorkspaceMaterial) { WorkspaceMaterial workspaceMaterial = (WorkspaceMaterial) workspaceNode; Material material = getMaterialForWorkspaceMaterial(workspaceMaterial); isHtmlMaterial = material instanceof HtmlMaterial; Material clonedMaterial = cloneMaterials && !overrideCloneMaterials ? materialController.cloneMaterial(material) : material; // Implementation of feature #1232 (front and help pages should always be copies) if (isHtmlMaterial && !cloneMaterials) { WorkspaceNode parentNode = workspaceMaterial.getParent(); if (parentNode instanceof WorkspaceFolder) { WorkspaceFolder parentFolder = (WorkspaceFolder) parentNode; if (parentFolder.getFolderType() == WorkspaceFolderType.FRONT_PAGE || parentFolder.getFolderType() == WorkspaceFolderType.HELP_PAGE) { clonedMaterial = materialController.cloneMaterial(material); } } } newNode = createWorkspaceMaterial( parent, clonedMaterial, workspaceMaterial.getTitle(), generateUniqueUrlName(parent, workspaceMaterial.getUrlName()), index, workspaceMaterial.getHidden(), workspaceMaterial.getAssignmentType(), workspaceMaterial.getCorrectAnswers()); } else if (workspaceNode instanceof WorkspaceFolder) { newNode = createWorkspaceFolder( parent, ((WorkspaceFolder) workspaceNode).getTitle(), generateUniqueUrlName(parent, workspaceNode.getUrlName()), index, workspaceNode.getHidden(), ((WorkspaceFolder) workspaceNode).getFolderType(), ((WorkspaceFolder) workspaceNode).getViewRestrict()); } else { throw new IllegalArgumentException("Uncloneable workspace node " + workspaceNode.getClass()); } List<WorkspaceNode> childNodes = workspaceNodeDAO.listByParentSortByOrderNumber(workspaceNode); for (WorkspaceNode childNode : childNodes) { cloneWorkspaceNode(childNode, newNode, cloneMaterials, isHtmlMaterial); } return newNode; } public WorkspaceMaterial revertToOriginMaterial(WorkspaceMaterial workspaceMaterial) { return revertToOriginMaterial(workspaceMaterial, false); } public WorkspaceMaterial revertToOriginMaterial(WorkspaceMaterial workspaceMaterial, boolean updateUrlName) { Material originMaterial = getMaterialForWorkspaceMaterial(workspaceMaterial).getOriginMaterial(); if (originMaterial == null) { throw new IllegalArgumentException("WorkSpaceMaterial has no origin material"); } workspaceMaterialDAO.updateMaterialId(workspaceMaterial, originMaterial.getId()); if (updateUrlName) { String urlName = generateUniqueUrlName(workspaceMaterial.getParent(), workspaceMaterial, originMaterial.getTitle()); if (!workspaceMaterial.getUrlName().equals(urlName)) { workspaceMaterialDAO.updateUrlName(workspaceMaterial, urlName); } } return workspaceMaterial; } /* Workspace material */ public WorkspaceMaterial createWorkspaceMaterial(WorkspaceNode parent, Material material) { return createWorkspaceMaterial(parent, material, null, null); } public WorkspaceMaterial createWorkspaceMaterial(WorkspaceNode parent, Material material, WorkspaceMaterialAssignmentType assignmentType, WorkspaceMaterialCorrectAnswersDisplay correctAnswers) { String urlName = generateUniqueUrlName(parent, material.getTitle()); return createWorkspaceMaterial(parent, material, urlName, assignmentType, correctAnswers); } public WorkspaceMaterial createWorkspaceMaterial(WorkspaceNode parent, Material material, String urlName, WorkspaceMaterialAssignmentType assignmentType, WorkspaceMaterialCorrectAnswersDisplay correctAnswers) { Integer index = workspaceNodeDAO.getMaximumOrderNumber(parent); index = index == null ? 0 : ++index; return createWorkspaceMaterial(parent, material, material.getTitle(), urlName, index, false, assignmentType, correctAnswers); } public WorkspaceMaterial createWorkspaceMaterial(WorkspaceNode parent, Material material, String title, String urlName, Integer index, Boolean hidden, WorkspaceMaterialAssignmentType assignmentType, WorkspaceMaterialCorrectAnswersDisplay correctAnswers) { WorkspaceMaterial workspaceMaterial = workspaceMaterialDAO.create(parent, material.getId(), title, urlName, index, hidden, assignmentType, correctAnswers); workspaceMaterialCreateEvent.fire(new WorkspaceMaterialCreateEvent(workspaceMaterial)); return workspaceMaterial; } public WorkspaceMaterial findWorkspaceMaterialById(Long workspaceMaterialId) { return workspaceMaterialDAO.findById(workspaceMaterialId); } public WorkspaceMaterial findWorkspaceMaterialByParentAndUrlName(WorkspaceNode parent, String urlName) { return workspaceMaterialDAO.findByFolderAndUrlName(parent, urlName); } public WorkspaceMaterial findWorkspaceMaterialByWorkspaceEntityAndPath(WorkspaceEntity workspaceEntity, String path) { WorkspaceNode workspaceNode = findWorkspaceNodeByWorkspaceEntityAndPath(workspaceEntity, path); return workspaceNode instanceof WorkspaceMaterial ? (WorkspaceMaterial) workspaceNode : null; } public List<WorkspaceMaterial> listWorkspaceMaterialsByParent(WorkspaceNode parent) { return workspaceMaterialDAO.listByParent(parent); } public List<WorkspaceMaterial> listWorkspaceMaterialsByMaterial(Material material) { return workspaceMaterialDAO.listByMaterialId(material.getId()); } public WorkspaceMaterial updateWorkspaceMaterialAssignmentType(WorkspaceMaterial workspaceMaterial, WorkspaceMaterialAssignmentType assignmentType) { return workspaceMaterialDAO.updateAssignmentType(workspaceMaterial, assignmentType); } public WorkspaceFolder updateWorkspaceFolder(WorkspaceFolder workspaceFolder, String title, WorkspaceNode parentNode, WorkspaceNode nextSibling, Boolean hidden, MaterialViewRestrict viewRestrict) { if (nextSibling != null && !nextSibling.getParent().getId().equals(parentNode.getId())) { throw new IllegalArgumentException("Next sibling parent is not parent"); } // Parent node & URL name long oldParent = workspaceFolder.getParent() == null ? 0 : workspaceFolder.getParent().getId(); long newParent = parentNode == null ? 0 : parentNode.getId(); if (oldParent != newParent) { // Before changing the parent, make sure the folder's URL name will be unique under it String urlName = generateUniqueUrlName(parentNode, title); // Change the parent workspaceFolder = (WorkspaceFolder) workspaceNodeDAO.updateParent(workspaceFolder, parentNode); // Update URL name if applicable if (!StringUtils.equals(workspaceFolder.getUrlName(), urlName)) { workspaceFolder = (WorkspaceFolder) workspaceNodeDAO.updateUrlName(workspaceFolder, urlName); } } else { // Parent stays the same. Still, make sure title and URL name are in sync String urlName = generateUniqueUrlName(parentNode, workspaceFolder, title); if (!StringUtils.equals(workspaceFolder.getUrlName(), urlName)) { workspaceFolder = (WorkspaceFolder) workspaceNodeDAO.updateUrlName(workspaceFolder, urlName); } } // Next sibling if (nextSibling == null) { Integer orderNumber = workspaceNodeDAO.getMaximumOrderNumber(parentNode); orderNumber = orderNumber == null ? 0 : orderNumber; if (workspaceFolder.getOrderNumber() < orderNumber) { workspaceFolder = (WorkspaceFolder) workspaceNodeDAO.updateOrderNumber(workspaceFolder, ++orderNumber); } } else { workspaceFolder = (WorkspaceFolder) moveAbove(workspaceFolder, nextSibling); } // Hidden workspaceFolder = (WorkspaceFolder) workspaceNodeDAO.updateHidden(workspaceFolder, hidden); // Title String urlName = generateUniqueUrlName(workspaceFolder.getParent(), workspaceFolder, title); workspaceFolder = workspaceFolderDAO.updateFolderName(workspaceFolder, urlName, title); // View restrict workspaceFolder = workspaceFolderDAO.updateViewRestrict(workspaceFolder, viewRestrict); // Return updated folder return workspaceFolder; } public WorkspaceNode updateWorkspaceNode(WorkspaceNode workspaceNode, Long materialId, WorkspaceNode parentNode, WorkspaceNode nextSibling, Boolean hidden, WorkspaceMaterialAssignmentType assignmentType, WorkspaceMaterialCorrectAnswersDisplay correctAnswers, String title) { if (nextSibling != null && !nextSibling.getParent().getId().equals(parentNode.getId())) { throw new IllegalArgumentException("Next sibling parent is not parent"); } // Material id if (workspaceNode instanceof WorkspaceMaterial) { workspaceNode = workspaceMaterialDAO.updateMaterialId((WorkspaceMaterial) workspaceNode, materialId); workspaceNode = workspaceMaterialDAO.updateAssignmentType((WorkspaceMaterial) workspaceNode, assignmentType); workspaceNode = workspaceMaterialDAO.updateCorrectAnswers((WorkspaceMaterial) workspaceNode, correctAnswers); } // Title workspaceNode = workspaceNodeDAO.updateTitle(workspaceNode, title); // Parent node & URL name long oldParent = workspaceNode.getParent() == null ? 0 : workspaceNode.getParent().getId(); long newParent = parentNode == null ? 0 : parentNode.getId(); if (oldParent != newParent) { // Before changing the parent, make sure the node's URL name will be unique under it String urlName = generateUniqueUrlName(parentNode, title); // Change the parent workspaceNode = workspaceNodeDAO.updateParent(workspaceNode, parentNode); // Update URL name workspaceNode = workspaceNodeDAO.updateUrlName(workspaceNode, urlName); } else { // Update URL name String urlName = generateUniqueUrlName(parentNode, workspaceNode, title); workspaceNode = workspaceNodeDAO.updateUrlName(workspaceNode, urlName); } // Next sibling if (nextSibling == null) { Integer orderNumber = workspaceNodeDAO.getMaximumOrderNumber(parentNode); orderNumber = orderNumber == null ? 0 : orderNumber; if (workspaceNode.getOrderNumber() < orderNumber) { workspaceNode = workspaceNodeDAO.updateOrderNumber(workspaceNode, ++orderNumber); } } else { workspaceNode = moveAbove(workspaceNode, nextSibling); } // Hidden workspaceNode = workspaceNodeDAO.updateHidden(workspaceNode, hidden); // Updated node return workspaceNode; } public void showWorkspaceNode(WorkspaceNode workspaceNode) { workspaceNodeDAO.updateHidden(workspaceNode, Boolean.TRUE); } public void hideWorkspaceNode(WorkspaceNode workspaceNode) { workspaceNodeDAO.updateHidden(workspaceNode, Boolean.FALSE); } public void deleteWorkspaceMaterial(WorkspaceMaterial workspaceMaterial, boolean removeAnswers) throws WorkspaceMaterialContainsAnswersExeption { try { workspaceMaterialDeleteEvent.fire(new WorkspaceMaterialDeleteEvent(workspaceMaterial, removeAnswers)); List<WorkspaceNode> childNodes = workspaceNodeDAO.listByParentSortByOrderNumber(workspaceMaterial); for (WorkspaceNode childNode : childNodes) { if (childNode instanceof WorkspaceMaterial) { deleteWorkspaceMaterial((WorkspaceMaterial) childNode, removeAnswers); } else if (childNode instanceof WorkspaceFolder) { deleteWorkspaceFolder((WorkspaceFolder) childNode); } } } catch (Exception e) { Throwable cause = e; while (cause != null) { cause = cause.getCause(); if (cause instanceof WorkspaceMaterialContainsAnswersExeption) { throw (WorkspaceMaterialContainsAnswersExeption) cause; } } throw e; } workspaceMaterialDAO.delete(workspaceMaterial); } /** * Returns the identifier of the workspace entity the given workspace node belongs to. * * @param workspaceNode * The workspace node * * @return The identifier of the workspace entity the given workspace node belongs to */ public Long getWorkspaceEntityId(WorkspaceNode workspaceNode) { WorkspaceNode rootFolder = workspaceNode; while (rootFolder.getParent() != null) { rootFolder = rootFolder.getParent(); } if (!(rootFolder instanceof WorkspaceRootFolder)) { throw new IllegalArgumentException("WorkspaceNode " + workspaceNode.getId() + " has not root folder"); } return ((WorkspaceRootFolder) rootFolder).getWorkspaceEntityId(); } /* Root Folder */ public WorkspaceRootFolder createWorkspaceRootFolder(WorkspaceEntity workspaceEntity) { WorkspaceRootFolder workspaceRootFolder = workspaceRootFolderDAO.create(workspaceEntity); workspaceRootFolderCreateEvent.fire(new WorkspaceRootFolderCreateEvent(workspaceRootFolder)); return workspaceRootFolder; } public WorkspaceRootFolder findWorkspaceRootFolderByWorkspaceEntity(WorkspaceEntity workspaceEntity) { return workspaceRootFolderDAO.findByWorkspaceEntityId(workspaceEntity.getId()); } public WorkspaceRootFolder findWorkspaceRootFolderByWorkspaceNode(WorkspaceNode workspaceNode) { WorkspaceNode node = workspaceNode; while ((node != null) && (!(node instanceof WorkspaceRootFolder))) { node = node.getParent(); } return (WorkspaceRootFolder) node; } public void deleteWorkspaceRootFolder(WorkspaceRootFolder workspaceRootFolder) { workspaceRootFolderDAO.delete(workspaceRootFolder); } /* Folder */ public WorkspaceFolder createWorkspaceFolder(WorkspaceNode parent, String title, String urlName) { Integer index = workspaceNodeDAO.getMaximumOrderNumber(parent); index = index == null ? 0 : ++index; return createWorkspaceFolder(parent, title, urlName, index, Boolean.FALSE, WorkspaceFolderType.DEFAULT, MaterialViewRestrict.NONE); } public WorkspaceFolder createWorkspaceFolder(WorkspaceNode parent, String title, String urlName, Integer index, Boolean hidden, WorkspaceFolderType folderType, MaterialViewRestrict viewRestrict) { WorkspaceFolder workspaceFolder = workspaceFolderDAO.create(parent, title, urlName, index, hidden, folderType, viewRestrict); workspaceFolderCreateEvent.fire(new WorkspaceFolderCreateEvent(workspaceFolder)); return workspaceFolder; } public WorkspaceFolder createWorkspaceFolder(WorkspaceNode parent, String title) { String urlName = generateUniqueUrlName(parent, title); return createWorkspaceFolder(parent, title, urlName); } public WorkspaceMaterial ensureWorkspaceFrontPageExists(WorkspaceEntity workspace) { WorkspaceFolder frontPageFolder = findWorkspaceFrontPageFolder(workspace); if (frontPageFolder == null) { frontPageFolder = createWorkspaceFrontPageFolder(workspace); } WorkspaceMaterial frontPageMaterial = null; List<WorkspaceMaterial> frontPageMaterials = listWorkspaceMaterialsByParent(frontPageFolder); if (frontPageMaterials.isEmpty()) { String title = localeController.getText(sessionController.getLocale(), "plugin.workspace.frontPage.title"); String license = null; HtmlMaterial htmlMaterial = htmlMaterialController.createHtmlMaterial(title, "", "text/html", 0l, license); frontPageMaterial = createWorkspaceMaterial(frontPageFolder, htmlMaterial); } else { frontPageMaterial = frontPageMaterials.get(0); } return frontPageMaterial; } public WorkspaceMaterial ensureWorkspaceHelpPageExists(WorkspaceEntity workspace) { WorkspaceFolder helpPageFolder = findWorkspaceHelpPageFolder(workspace); if (helpPageFolder == null) { helpPageFolder = createWorkspaceHelpPageFolder(workspace); } WorkspaceMaterial helpPageMaterial = null; List<WorkspaceMaterial> helpPageMaterials = listWorkspaceMaterialsByParent(helpPageFolder); if (helpPageMaterials.isEmpty()) { String title = localeController.getText(sessionController.getLocale(), "plugin.workspace.helpPage.title"); String license = null; HtmlMaterial htmlMaterial = htmlMaterialController.createHtmlMaterial(title, "", "text/html", 0l, license); helpPageMaterial = createWorkspaceMaterial(helpPageFolder, htmlMaterial); } else { helpPageMaterial = helpPageMaterials.get(0); } return helpPageMaterial; } public WorkspaceFolder findWorkspaceFolderById(Long workspaceFolderId) { return workspaceFolderDAO.findById(workspaceFolderId); } public void deleteWorkspaceFolder(WorkspaceFolder workspaceFolder) { workspaceFolderDAO.delete(workspaceFolder); } public void updateDefaultMaterial(WorkspaceFolder workspaceFolder, WorkspaceNode defaultMaterial) { workspaceFolderDAO.updateDefaultMaterial(workspaceFolder, defaultMaterial); } /* Utility methods */ public Long getMaterialCountByWorkspaceAndAssignmentType(WorkspaceEntity workspaceEntity, WorkspaceMaterialAssignmentType assignmentType) { List<WorkspaceNode> folders = listVisibleWorkspaceFolders(workspaceEntity); return workspaceMaterialDAO.countByHiddenAndAssignmentTypeAndParents(Boolean.FALSE, assignmentType, folders); } public List<WorkspaceMaterial> listVisibleWorkspaceMaterialsByAssignmentType(WorkspaceEntity workspaceEntity, WorkspaceMaterialAssignmentType assignmentType) { final List<WorkspaceNode> folders = listVisibleWorkspaceFolders(workspaceEntity); List<WorkspaceMaterial> workspaceMaterials = workspaceMaterialDAO.listByHiddenAndAssignmentTypeAndParents(Boolean.FALSE, assignmentType, folders); Collections.sort(workspaceMaterials, new Comparator<WorkspaceMaterial>() { @Override public int compare(WorkspaceMaterial o1, WorkspaceMaterial o2) { int result = o1.getParent().getOrderNumber() - o2.getParent().getOrderNumber(); if (result == 0) { result = o1.getOrderNumber().compareTo(o2.getOrderNumber()); } return result; } }); return workspaceMaterials; } public List<WorkspaceMaterial> listVisibleWorkspaceMaterialsByParentAndAssignmentType(WorkspaceNode parent, WorkspaceEntity workspaceEntity, WorkspaceMaterialAssignmentType assignmentType) { return workspaceMaterialDAO.listByHiddenAndAssignmentTypeAndParents(Boolean.FALSE, assignmentType, Arrays.asList(parent)); } public List<ContentNode> listVisibleEvaluableWorkspaceMaterialsAsContentNodes(WorkspaceEntity workspaceEntity, boolean processHtml) throws WorkspaceMaterialException { List<ContentNode> result = new ArrayList<>(); List<WorkspaceMaterial> workspaceMaterials = listVisibleWorkspaceMaterialsByAssignmentType(workspaceEntity, WorkspaceMaterialAssignmentType.EVALUATED); for (WorkspaceMaterial workspaceMaterial : workspaceMaterials) { result.add(createContentNode(workspaceMaterial, 0, processHtml, false)); } return result; } private List<WorkspaceNode> listVisibleWorkspaceFolders(WorkspaceEntity workspaceEntity) { List<WorkspaceNode> result = new ArrayList<>(); WorkspaceRootFolder rootFolder = findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity); result.add(rootFolder); appendVisibleChildFolders(rootFolder, result); return result; } private void appendVisibleChildFolders(WorkspaceNode parent, List<WorkspaceNode> result) { List<WorkspaceFolder> childFolders = workspaceFolderDAO.listByHiddenAndParentAndFolderType(Boolean.FALSE, parent, WorkspaceFolderType.DEFAULT); result.addAll(childFolders); for (WorkspaceFolder childFolder : childFolders) { appendVisibleChildFolders(childFolder, result); } } public List<FlattenedWorkspaceNode> flattenWorkspaceNodes(List<WorkspaceNode> workspaceNodes, int level, boolean includeHidden) { List<FlattenedWorkspaceNode> result = new ArrayList<>(); for (WorkspaceNode workspaceNode : workspaceNodes) { if (workspaceNode.getType() == WorkspaceNodeType.FOLDER) { WorkspaceFolder workspaceFolder = (WorkspaceFolder) workspaceNode; List<WorkspaceNode> children = includeHidden ? workspaceNodeDAO.listByParentSortByOrderNumber(workspaceFolder) : workspaceNodeDAO.listByParentAndHiddenSortByOrderNumber(workspaceFolder, Boolean.FALSE); result.add(new FlattenedWorkspaceNode(true, workspaceFolder.getTitle(), workspaceFolder, level, workspaceFolder.getParent().getId(), workspaceFolder.getHidden())); result.addAll(flattenWorkspaceNodes(children, level + 1, includeHidden)); } else { result.add(new FlattenedWorkspaceNode(false, null, workspaceNode, level, workspaceNode.getParent().getId(), workspaceNode.getHidden())); } } return result; } public List<ContentNode> listWorkspaceMaterialsAsContentNodes(WorkspaceEntity workspaceEntity, boolean includeHidden) throws WorkspaceMaterialException { List<ContentNode> contentNodes = new ArrayList<>(); WorkspaceRootFolder rootFolder = findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity); List<WorkspaceNode> rootMaterialNodes = includeHidden ? listWorkspaceNodesByParentAndFolderTypeSortByOrderNumber(rootFolder, WorkspaceFolderType.DEFAULT) : listVisibleWorkspaceNodesByParentAndFolderTypeSortByOrderNumber(rootFolder, WorkspaceFolderType.DEFAULT); for (WorkspaceNode rootMaterialNode : rootMaterialNodes) { ContentNode node = createContentNode(rootMaterialNode, 1, true, includeHidden); contentNodes.add(node); } return contentNodes; } public List<ContentNode> listWorkspaceFrontPagesAsContentNodes(WorkspaceEntity workspaceEntity) throws WorkspaceMaterialException { List<ContentNode> contentNodes = new ArrayList<>(); List<WorkspaceMaterial> frontPages = listFrontPages(workspaceEntity); for (WorkspaceNode frontPage : frontPages) { ContentNode node = createContentNode(frontPage); contentNodes.add(node); } return contentNodes; } public List<ContentNode> listWorkspaceHelpPagesAsContentNodes(WorkspaceEntity workspaceEntity) throws WorkspaceMaterialException { List<ContentNode> contentNodes = new ArrayList<>(); List<WorkspaceMaterial> helpPages = listHelpPages(workspaceEntity); for (WorkspaceMaterial helpPage : helpPages) { ContentNode node = createContentNode(helpPage); contentNodes.add(node); } return contentNodes; } public ContentNode createContentNode(WorkspaceNode rootMaterialNode) throws WorkspaceMaterialException { return createContentNode(rootMaterialNode, 1, true, true); } private ContentNode createContentNode(WorkspaceNode rootMaterialNode, int level, boolean processHtml, boolean includeHidden) throws WorkspaceMaterialException { boolean viewRestricted = false; try { switch (rootMaterialNode.getType()) { case FOLDER: WorkspaceFolder workspaceFolder = (WorkspaceFolder) rootMaterialNode; viewRestricted = !sessionController.isLoggedIn() && workspaceFolder.getViewRestrict() == MaterialViewRestrict.LOGGED_IN; ContentNode folderContentNode = new ContentNode( workspaceFolder.getTitle(), "folder", rootMaterialNode.getId(), null, level, null, null, rootMaterialNode.getParent().getId(), rootMaterialNode.getHidden(), null, 0l, 0l, workspaceFolder.getPath(), null, null, workspaceFolder.getViewRestrict(), viewRestricted); List<WorkspaceNode> children = includeHidden ? workspaceNodeDAO.listByParentSortByOrderNumber(workspaceFolder) : workspaceNodeDAO.listByParentAndHiddenSortByOrderNumber(workspaceFolder, Boolean.FALSE); List<FlattenedWorkspaceNode> flattenedChildren; if (level >= FLATTENING_LEVEL) { flattenedChildren = flattenWorkspaceNodes(children, level, includeHidden); } else { flattenedChildren = new ArrayList<>(); for (WorkspaceNode node : children) { flattenedChildren.add(new FlattenedWorkspaceNode(false, null, node, level, node.getParent().getId(), node.getHidden())); } } for (FlattenedWorkspaceNode child : flattenedChildren) { ContentNode contentNode; if (child.isEmptyFolder) { contentNode = new ContentNode(child.emptyFolderTitle, "folder", rootMaterialNode.getId(), null, child.level, null, null, child.parentId, child.hidden, null, 0l, 0l, child.node.getPath(), null, null, MaterialViewRestrict.NONE, false); } else { contentNode = createContentNode(child.node, child.level, processHtml, includeHidden); } folderContentNode.addChild(contentNode); } return folderContentNode; case MATERIAL: DOMParser parser = null; Transformer transformer = null; if (processHtml) { parser = new DOMParser(new HTMLConfiguration()); parser.setProperty("http://cyberneko.org/html/properties/names/elems", "lower"); transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.INDENT, "no"); } WorkspaceMaterial workspaceMaterial = (WorkspaceMaterial) rootMaterialNode; Material material = materialController.findMaterialById(workspaceMaterial.getMaterialId()); Long currentRevision = material instanceof HtmlMaterial ? htmlMaterialController.lastHtmlMaterialRevision((HtmlMaterial) material) : 0l; Long publishedRevision = material instanceof HtmlMaterial ? ((HtmlMaterial) material).getRevisionNumber() : 0l; List<String> producerNames = null; String html; List<MaterialProducer> producers = materialController.listMaterialProducers(material); if ((producers != null) && !producers.isEmpty()) { producerNames = new ArrayList<>(); for (MaterialProducer producer : producers) { producerNames.add(StringUtils.replace(StringEscapeUtils.escapeHtml4(producer.getName()), ",", ",")); } } viewRestricted = !sessionController.isLoggedIn() && material.getViewRestrict() == MaterialViewRestrict.LOGGED_IN; if (!viewRestricted) { html = processHtml ? getMaterialHtml(material, parser, transformer) : null; } else { html = String.format("<p class=\"content-view-restricted-message\">%s</p>", localeController.getText(sessionController.getLocale(), "plugin.workspace.materialViewRestricted")); } return new ContentNode( workspaceMaterial.getTitle(), material.getType(), rootMaterialNode.getId(), material.getId(), level, workspaceMaterial.getAssignmentType(), workspaceMaterial.getCorrectAnswers(), workspaceMaterial.getParent().getId(), workspaceMaterial.getHidden(), html, currentRevision, publishedRevision, workspaceMaterial.getPath(), material.getLicense(), StringUtils.join(producerNames, ','), material.getViewRestrict(), viewRestricted); default: return null; } } catch (SAXNotRecognizedException | SAXNotSupportedException | TransformerConfigurationException e) { throw new WorkspaceMaterialException(e); } } private String getMaterialHtml(Material material, DOMParser parser, Transformer transformer) throws WorkspaceMaterialException { try { if (material instanceof HtmlMaterial) { String html = ((HtmlMaterial) material).getHtml(); if (html == null) { return null; } StringReader htmlReader = new StringReader(html); try { InputSource inputSource = new InputSource(htmlReader); parser.parse(inputSource); Document document = parser.getDocument(); NodeList imgList = document.getElementsByTagName("img"); for (int i = 0, l = imgList.getLength(); i < l; i++) { Element img = (Element) imgList.item(i); String imgClass = img.getAttribute("class"); img.setAttribute("class", imgClass == null ? "lazy" : imgClass + " lazy"); img.setAttribute("data-original", img.getAttribute("src")); img.removeAttribute("src"); } NodeList iframeList = document.getElementsByTagName("iframe"); for (int i = iframeList.getLength() -1 ; i >= 0; i--) { Element iframe = (Element) iframeList.item(i); iframe.setAttribute("data-url", iframe.getAttribute("src")); iframe.removeAttribute("src"); if (iframe.hasAttribute("class")) { iframe.setAttribute("class", iframe.getAttribute("class") + " lazyFrame"); } else { iframe.setAttribute("class", "lazyFrame"); } } StringWriter writer = new StringWriter(); NodeList bodyChildren = (NodeList) XPathFactory.newInstance().newXPath().evaluate("//body/*", document, XPathConstants.NODESET); for (int i = 0, l = bodyChildren.getLength(); i < l; i++) { transformer.transform(new DOMSource(bodyChildren.item(i)), new StreamResult(writer)); } return writer.getBuffer().toString(); } finally { htmlReader.close(); } } return null; } catch (IOException | XPathExpressionException | TransformerException | SAXException e) { throw new WorkspaceMaterialException(e); } } public synchronized String generateUniqueUrlName(String title) { return generateUniqueUrlName(null, null, title); } public synchronized String generateUniqueUrlName(WorkspaceNode parent, String title) { return generateUniqueUrlName(parent, null, title); } public synchronized String generateUniqueUrlName(WorkspaceNode parent, WorkspaceNode targetNode, String title) { String urlName = generateUrlName(title); String fileName = StringUtils.substringBeforeLast(urlName, "."); String extension = StringUtils.substringAfterLast(urlName, "."); int extensionLength = StringUtils.length(extension); boolean isFileName = StringUtils.isAlphanumeric(extension) && extensionLength < StringUtils.length(urlName) - 1; // use urlName as base and uniqueName as final result String uniqueName = urlName; if (parent != null) { // if parent node is given, ensure that the generated url name is unique amongst its child nodes int i = 1; while (true) { // find child node with uniqueName WorkspaceNode workspaceNode = workspaceNodeDAO.findByParentAndUrlName(parent, uniqueName); if (workspaceNode != null) { if (targetNode != null && workspaceNode.getId().equals(targetNode.getId())) { // uniqueName is in use but by the target node itself, so it's okay break; } // uniqueName in use, try again with the next candidate (name, name-2, name-3, etc.) uniqueName = isFileName ? String.format("%s-%d.%s", fileName, ++i, extension) : String.format("%s-%d", urlName, ++i); } else { // Current uniqueName is available break; } } } return uniqueName; } public static String generateUrlName(String title) { // convert to lower-case and replace spaces and slashes with a minus sign String urlName = title == null ? "" : StringUtils.lowerCase(title.replaceAll(" ", "-").replaceAll("/", "-")); // truncate consecutive minus signs into just one while (urlName.indexOf("--") >= 0) { urlName = urlName.replace("--", "-"); } // get rid of accented characters and all special characters other than minus, period, and underscore urlName = StringUtils.stripAccents(urlName).replaceAll("[^a-z0-9\\-\\.\\_]", ""); return StringUtils.isBlank(urlName) ? StringUtils.substringBefore(UUID.randomUUID().toString(), "-") : urlName; } /* Front page */ public WorkspaceFolder createWorkspaceHelpPageFolder(WorkspaceEntity workspaceEntity) { return workspaceFolderDAO.create(findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity), "Ohjeet", "ohjeet", 0, false, WorkspaceFolderType.HELP_PAGE, MaterialViewRestrict.NONE); } public WorkspaceFolder createWorkspaceFrontPageFolder(WorkspaceEntity workspaceEntity) { return workspaceFolderDAO.create(findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity), "Etusivu", "etusivu", 0, false, WorkspaceFolderType.FRONT_PAGE, MaterialViewRestrict.NONE); } private WorkspaceFolder findWorkspaceFrontPageFolder(WorkspaceEntity workspaceEntity) { List<WorkspaceFolder> frontPageFolders = workspaceFolderDAO.listByParentAndFolderType(findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity), WorkspaceFolderType.FRONT_PAGE); if (frontPageFolders.isEmpty()) { return null; } if (frontPageFolders.size() > 1) { logger.warning("Workspace " + workspaceEntity.getId() + " has multiple front page folders"); } return frontPageFolders.get(0); } private WorkspaceFolder findWorkspaceHelpPageFolder(WorkspaceEntity workspaceEntity) { List<WorkspaceFolder> helpPageFolders = workspaceFolderDAO.listByParentAndFolderType(findWorkspaceRootFolderByWorkspaceEntity(workspaceEntity), WorkspaceFolderType.HELP_PAGE); if (helpPageFolders.isEmpty()) { return null; } if (helpPageFolders.size() > 1) { logger.warning("Workspace " + workspaceEntity.getId() + " has multiple help page folders"); } return helpPageFolders.get(0); } private List<WorkspaceMaterial> listFrontPages(WorkspaceEntity workspaceEntity) { WorkspaceFolder frontPageFolder = findWorkspaceFrontPageFolder(workspaceEntity); if (frontPageFolder != null) { return listWorkspaceMaterialsByParent(frontPageFolder); } return Arrays.asList(); } private List<WorkspaceMaterial> listHelpPages(WorkspaceEntity workspaceEntity) { WorkspaceFolder helpPageFolder = findWorkspaceHelpPageFolder(workspaceEntity); if (helpPageFolder != null) { return listWorkspaceMaterialsByParent(helpPageFolder); } return Arrays.asList(); } private static class FlattenedWorkspaceNode { public FlattenedWorkspaceNode(boolean isEmptyFolder, String emptyFolderTitle, WorkspaceNode node, int level, Long parentId, Boolean hidden) { this.isEmptyFolder = isEmptyFolder; this.emptyFolderTitle = emptyFolderTitle; this.node = node; this.level = level; this.parentId = parentId; this.hidden = hidden; } public final boolean isEmptyFolder; public final String emptyFolderTitle; public final WorkspaceNode node; public final int level; public final Long parentId; public final Boolean hidden; } }