/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <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> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. */ package org.olat.modules.cp; import java.io.IOException; import java.util.List; import org.olat.core.CoreSpringFactory; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.htmlsite.HtmlStaticPageComponent; import org.olat.core.gui.components.htmlsite.NewInlineUriEvent; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.components.link.LinkFactory; import org.olat.core.gui.components.tree.MenuTree; import org.olat.core.gui.components.tree.TreeEvent; import org.olat.core.gui.components.tree.TreeNode; import org.olat.core.gui.components.velocity.VelocityContainer; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.BasicController; import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController; import org.olat.core.gui.control.generic.dtabs.Activateable2; import org.olat.core.gui.control.generic.iframe.DeliveryOptions; import org.olat.core.gui.control.generic.iframe.IFrameDisplayController; import org.olat.core.gui.control.generic.iframe.NewIframeUriEvent; import org.olat.core.gui.control.winmgr.JSCommand; import org.olat.core.gui.media.MediaResource; import org.olat.core.gui.media.NotFoundMediaResource; import org.olat.core.id.OLATResourceable; import org.olat.core.id.context.BusinessControlFactory; import org.olat.core.id.context.ContextEntry; import org.olat.core.id.context.StateEntry; import org.olat.core.logging.AssertException; import org.olat.core.logging.OLATRuntimeException; import org.olat.core.logging.activity.CourseLoggingAction; import org.olat.core.logging.activity.LearningResourceLoggingAction; import org.olat.core.logging.activity.OlatResourceableType; import org.olat.core.logging.activity.ThreadLocalUserActivityLogger; import org.olat.core.util.StringHelper; import org.olat.core.util.resource.OresHelper; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.VFSLeaf; import org.olat.core.util.vfs.VFSMediaResource; import org.olat.course.ICourse; import org.olat.search.SearchServiceUIFactory; import org.olat.search.SearchServiceUIFactory.DisplayOption; import org.olat.search.ui.SearchInputController; import org.olat.util.logging.activity.LoggingResourceable; /** * Description:<br> * shows the actual content package with or without a menu * * @author Felix Jost */ public class CPDisplayController extends BasicController implements Activateable2 { private static final String FILE_SUFFIX_HTM = "htm"; private static final String FILE_SUFFIX_XML = "xml"; private VelocityContainer myContent; private MenuTree cpTree; private CPManifestTreeModel ctm; private VFSContainer rootContainer; private String selNodeId; private HtmlStaticPageComponent cpComponent; private IFrameDisplayController cpContentCtr; private SearchInputController searchCtrl; private Link nextLink, previousLink; private Link printLink; private String mapperBaseURL; private CPPrintMapper printMapper; private CPSelectPrintPagesController printController; private CloseableModalController printPopup; /** * @param ureq * @param cpRoot * @param showMenu * @param showNavigation Show the next/previous link * @param activateFirstPage * @param identPrefix In a course, set a unique prefix per node, if someone set 2x the same CPs in the course, the node identifiers * of the CP elements must be different but predictable */ public CPDisplayController(UserRequest ureq, WindowControl wControl, VFSContainer rootContainer, boolean showMenu, boolean showNavigation, boolean activateFirstPage, boolean showPrint, DeliveryOptions deliveryOptions, String initialUri, OLATResourceable ores, String identPrefix, boolean randomizeMapper) { super(ureq, wControl); this.rootContainer = rootContainer; // wrapper velocity container for page content myContent = createVelocityContainer("cpcontent"); // the cp component, added to the velocity if(!ureq.getUserSession().getRoles().isGuestOnly()) { SearchServiceUIFactory searchServiceUIFactory = (SearchServiceUIFactory)CoreSpringFactory.getBean(SearchServiceUIFactory.class); searchCtrl = searchServiceUIFactory.createInputController(ureq, wControl, DisplayOption.BUTTON, null); myContent.put("search_input", searchCtrl.getInitialComponent()); listenTo(searchCtrl); } cpContentCtr = new IFrameDisplayController(ureq, getWindowControl(),rootContainer, null, ores, deliveryOptions, false, randomizeMapper); cpContentCtr.setAllowDownload(true); listenTo(cpContentCtr); myContent.put("cpContent", cpContentCtr.getInitialComponent()); myContent.contextPut("isIframeDelivered", Boolean.TRUE); // even if we do not show the menu, we need to build parse the manifest and // find the first node to display at startup VFSItem mani = rootContainer.resolve("imsmanifest.xml"); if (mani == null || !(mani instanceof VFSLeaf)) { showError("error.manifest.missing"); return; } // initialize tree model in any case try { ctm = new CPManifestTreeModel((VFSLeaf) mani, identPrefix); } catch (IOException e) { showError("error.manifest.corrupted"); return; } if (showMenu) { // the menu is only initialized when needed. cpTree = new MenuTree("cpDisplayTree"); cpTree.setScrollTopOnClick(true); cpTree.setTreeModel(ctm); cpTree.addListener(this); } if(showPrint) { printLink = LinkFactory.createCustomLink("print", "print", null, Link.LINK + Link.NONTRANSLATED, myContent, this); printLink.setCustomDisplayText(""); printLink.setIconLeftCSS("o_icon o_icon-fw o_icon_print o_icon-lg"); printLink.setCustomEnabledLinkCSS("o_print"); printLink.setTitle(translate("print.node")); String themeBaseUri = wControl.getWindowBackOffice().getWindow().getGuiTheme().getBaseURI(); printMapper = new CPPrintMapper(ctm, rootContainer, themeBaseUri); mapperBaseURL = registerMapper(ureq, printMapper); printMapper.setBaseUri(mapperBaseURL); } if(showNavigation && ctm.getRootNode().getChildCount() > 0) { nextLink = LinkFactory.createCustomLink("next", "next", null, Link.LINK + Link.NONTRANSLATED, myContent, this); nextLink.setCustomDisplayText(""); nextLink.setIconLeftCSS("o_icon o_icon-fw o_icon_next o_icon-lg"); nextLink.setCustomEnabledLinkCSS("o_next"); nextLink.setTitle(translate("next")); previousLink = LinkFactory.createCustomLink("previous", "previous", null, Link.LINK + Link.NONTRANSLATED, myContent, this); previousLink.setCustomDisplayText(""); previousLink.setIconLeftCSS("o_icon o_icon-fw o_icon_previous o_icon-lg"); previousLink.setCustomEnabledLinkCSS("o_previous"); previousLink.setTitle(translate("previous")); myContent.put("next", nextLink); myContent.put("previous", previousLink); } LoggingResourceable nodeInfo = null; if (activateFirstPage) { // set content to first accessible child or root node if no children // available TreeNode node = ctm.getRootNode(); if (node == null) throw new OLATRuntimeException(CPDisplayController.class, "root node of content packaging was null, file:" + rootContainer, null); while (node != null && !node.isAccessible()) { if (node.getChildCount() > 0) { node = (TreeNode) node.getChildAt(0); } else node = null; } if (node != null) { // node.isAccessible String nodeUri = (String) node.getUserObject(); if (cpContentCtr != null) cpContentCtr.setCurrentURI(nodeUri); if (cpComponent != null) cpComponent.setCurrentURI(nodeUri); if (showMenu) cpTree.setSelectedNodeId(node.getIdent()); // activate the selected node in the menu (skips the root node that is // empty anyway and saves one user click) selNodeId = node.getIdent(); nodeInfo = LoggingResourceable.wrapCpNode(nodeUri); updateNextPreviousLink(node); if(node.getUserObject() != null) { String identifierRes = (String)node.getUserObject(); OLATResourceable pOres = OresHelper.createOLATResourceableInstanceWithoutCheck("path=" + identifierRes, 0l); addToHistory(ureq, pOres, null); } } } else if (initialUri != null) { // set page if (cpContentCtr != null) cpContentCtr.setCurrentURI(initialUri); if (cpComponent != null) cpComponent.setCurrentURI(initialUri); // update menu TreeNode newNode = ctm.lookupTreeNodeByHref(initialUri); if (newNode != null) { // user clicked on a link which is listed in the // toc if (cpTree != null) { cpTree.setSelectedNodeId(newNode.getIdent()); } else { selNodeId = newNode.getIdent(); } updateNextPreviousLink(newNode); if(newNode.getUserObject() != null) { String identifierRes = (String)newNode.getUserObject(); Long id = Long.parseLong(newNode.getIdent()); OLATResourceable pOres = OresHelper.createOLATResourceableInstanceWithoutCheck("path=" + identifierRes, id); addToHistory(ureq, pOres, null); } } nodeInfo = LoggingResourceable.wrapCpNode(initialUri); } // Note: the ores has a typename of ICourse - see // CPCourseNode.createNodeRunConstructorResult // which has the following line: // OresHelper.createOLATResourceableInstance(ICourse.class, userCourseEnv.getCourseEnvironment().getCourseResourceableId()); // therefore we use OresHelper.calculateTypeName(ICourse.class) here if (ores!=null && nodeInfo!= null && !OresHelper.calculateTypeName(ICourse.class).equals(ores.getResourceableTypeName())) { addLoggingResourceable(LoggingResourceable.wrap(ores, OlatResourceableType.cp)); ThreadLocalUserActivityLogger.log(LearningResourceLoggingAction.LEARNING_RESOURCE_OPEN, getClass(), nodeInfo); } putInitialPanel(myContent); } public void setContentEncoding(String encoding) { if(cpContentCtr != null) { cpContentCtr.setContentEncoding(encoding); } if(printMapper != null) { printMapper.setContentEncoding(encoding); } } public void setJSEncoding(String encoding) { if(cpContentCtr != null) { cpContentCtr.setJSEncoding(encoding); } if(printMapper != null) { printMapper.setJSEncoding(encoding); } } /** * @return The menu component for this content packaging. The Controller must * be initialized properly to use this method */ public Component getMenuComponent() { return cpTree; } /** * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, * org.olat.core.gui.components.Component, * org.olat.core.gui.control.Event) */ @Override public void event(UserRequest ureq, Component source, Event event) { if (source == cpTree) { // FIXME:fj: cleanup between MenuTree.COMMAND_TREENODE_CLICKED and // TreeEvent.dito... if (event.getCommand().equals(MenuTree.COMMAND_TREENODE_CLICKED)) { TreeEvent te = (TreeEvent) event; switchToPage(ureq, te); } } else if (source == cpComponent) { if (event instanceof NewInlineUriEvent) { NewInlineUriEvent nue = (NewInlineUriEvent) event; // adjust the tree selection to the current choice if found selectTreeNode(ureq, nue.getNewUri()); } } else if (source == nextLink) { TreeNode nextUri = (TreeNode)nextLink.getUserObject(); switchToPage(ureq, nextUri); if(cpTree != null) { cpTree.setSelectedNode(nextUri); } fireEvent(ureq, new TreeNodeEvent(nextUri)); } else if (source == previousLink) { TreeNode previousUri = (TreeNode)previousLink.getUserObject(); if(cpTree != null) { cpTree.setSelectedNode(previousUri); } switchToPage(ureq, previousUri); fireEvent(ureq, new TreeNodeEvent(previousUri)); } else if (source == printLink) { selectPagesToPrint(ureq); } } @Override protected void event(UserRequest ureq, Controller source, Event event) { if (source == cpContentCtr) { // a .html click within the contentpackage if (event instanceof NewInlineUriEvent) { NewInlineUriEvent nue = (NewInlineUriEvent) event; // adjust the tree selection to the current choice if found selectTreeNode(ureq, nue.getNewUri()); } else if (event instanceof NewIframeUriEvent) { NewIframeUriEvent nue = (NewIframeUriEvent) event; selectTreeNode(ureq, nue.getNewUri()); }// else ignore (e.g. misplaced olatcmd event (inner olat link found in a // contentpackaging file) } else if (source == printPopup) { removeAsListenerAndDispose(printPopup); removeAsListenerAndDispose(printController); printController = null; printPopup = null; } else if (source == printController) { if(Event.DONE_EVENT == event) { List<String> nodeToPrint = printController.getSelectedNodeIdentifiers(); printPages(nodeToPrint); } printPopup.deactivate(); removeAsListenerAndDispose(printPopup); removeAsListenerAndDispose(printController); printController = null; printPopup = null; } } @Override public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { if(entries == null || entries.isEmpty()) return; Long id = entries.get(0).getOLATResourceable().getResourceableId(); TreeNode newNode = null; if(id != null && id.longValue() > 0l) { newNode = ctm.getNodeById(id.toString()); } if(newNode == null) { String path = BusinessControlFactory.getInstance().getPath(entries.get(0)); newNode = ctm.lookupTreeNodeByHref(path); } if(newNode != null) { selectTreeNode(ureq, newNode); switchToPage(ureq, new TreeEvent(TreeEvent.COMMAND_TREENODES_SELECTED, newNode.getIdent())); } } private void printPages(final List<String> selectedNodeIds) { StringBuilder sb = new StringBuilder(); sb.append("window.open('" + mapperBaseURL + "/print.html', '_print','height=800,left=100,top=100,width=800,toolbar=no,titlebar=0,status=0,menubar=yes,location= no,scrollbars=1');"); printMapper.setSelectedNodeIds(selectedNodeIds); getWindowControl().getWindowBackOffice().sendCommandTo(new JSCommand(sb.toString())); } private void selectPagesToPrint(UserRequest ureq) { removeAsListenerAndDispose(printController); removeAsListenerAndDispose(printPopup); printController = new CPSelectPrintPagesController(ureq, getWindowControl(), ctm); listenTo(printController); printPopup = new CloseableModalController(getWindowControl(), "cancel", printController.getInitialComponent(), true, translate("print.node.list.title")); listenTo(printPopup); printPopup.activate(); } /** * adjust the cp menu tree with the page selected with a link clicked in the content * @param ureq * @param newUri */ public void selectTreeNode(UserRequest ureq, String newUri) { TreeNode newNode = ctm.lookupTreeNodeByHref(newUri); if (newNode == null && newUri.contains("?")) { // remove any url paramters in case it is not an html5 app. E.g. some ELML contents newUri = newUri.substring(0, newUri.indexOf("?")); newNode = ctm.lookupTreeNodeByHref(newUri); } selectTreeNode(ureq, newNode); ThreadLocalUserActivityLogger.log(CourseLoggingAction.CP_GET_FILE, getClass(), LoggingResourceable.wrapCpNode(newUri)); } public void selectTreeNode(UserRequest ureq, TreeNode newNode) { if (newNode != null) { // user clicked on a link which is listed in the // toc if (cpTree != null) { cpTree.setSelectedNodeId(newNode.getIdent()); } else { // for the case the tree is outside this controller (e.g. in the // course), we fire an event with the chosen node) fireEvent(ureq, new TreeNodeEvent(newNode)); } updateNextPreviousLink(newNode); } } private void updateNextPreviousLink(TreeNode currentNode) { if(nextLink != null) { TreeNode nextNode = ctm.getNextNodeWithContent(currentNode); nextLink.setEnabled(nextNode != null); nextLink.setUserObject(nextNode); } if(previousLink != null) { TreeNode previousNode = ctm.getPreviousNodeWithContent(currentNode); previousLink.setEnabled(previousNode != null); previousLink.setUserObject(previousNode); } } /** * @param ureq * @param te */ public void switchToPage(UserRequest ureq, TreeEvent te) { // all treeevents receiced here are event clicked only // if (!te.getCommand().equals(TreeEvent.COMMAND_TREENODE_CLICKED)) throw // new AssertException("error"); // switch to the new page String nodeId = te.getNodeId(); TreeNode tn = ctm.getNodeById(nodeId); if(tn != null) { switchToPage(ureq, tn); } } public void switchToPage(UserRequest ureq, TreeNode tn) { String identifierRes = (String) tn.getUserObject(); OLATResourceable ores = OresHelper.createOLATResourceableInstanceWithoutCheck("path=" + identifierRes, 0l); addToHistory(ureq, ores, null); // security check if (identifierRes.indexOf("../") != -1) throw new AssertException("a non-normalized url encountered in a manifest item:" + identifierRes); // Check if path ends with .html, .htm or .xhtml. We do this by searching for "htm" // and accept positions of this string at length-3 or length-4 // Check also for XML resources that use XSLT for rendering if (identifierRes.toLowerCase().lastIndexOf(FILE_SUFFIX_HTM) >= (identifierRes.length() - 4) || identifierRes.toLowerCase().endsWith(FILE_SUFFIX_XML)) { // display html files inline or in an iframe if (cpContentCtr != null) cpContentCtr.setCurrentURI(identifierRes); if (cpComponent != null) cpComponent.setCurrentURI(identifierRes); } else { // Also display pdf and other files in the iframe if it has been // initialized. Delegates displaying to the browser (and its plugins). if (cpContentCtr != null) { cpContentCtr.setCurrentURI(identifierRes); } else { // if an entry in a manifest points e.g. to a pdf file and the iframe // controller has not been initialized display it non-inline VFSItem currentItem = rootContainer.resolve(identifierRes); MediaResource mr; if (currentItem == null || !(currentItem instanceof VFSLeaf)) mr = new NotFoundMediaResource(identifierRes); else mr = new VFSMediaResource((VFSLeaf) currentItem); ureq.getDispatchResult().setResultingMediaResource(mr); // Prevent 'don't reload' warning cpTree.setDirty(false); } } updateNextPreviousLink(tn); ThreadLocalUserActivityLogger.log(CourseLoggingAction.CP_GET_FILE, getClass(), LoggingResourceable.wrapCpNode(identifierRes)); } /** * * @see org.olat.core.gui.control.DefaultController#doDispose(boolean) */ @Override protected void doDispose() { ThreadLocalUserActivityLogger.log(LearningResourceLoggingAction.LEARNING_RESOURCE_CLOSE, getClass()); cpTree = null; ctm = null; myContent = null; rootContainer = null; cpComponent = null; } /** * @return the treemodel. (for read-only usage) Useful if you would like to * integrate the menu at some other place */ public CPManifestTreeModel getTreeModel() { return ctm; } /** * to use with the option "external menu" only * * @return */ public String getInitialSelectedNodeId() { return selNodeId; } public String getNodeByUri(String uri) { if(StringHelper.containsNonWhitespace(uri)) { TreeNode node = ctm.lookupTreeNodeByHref(uri); if(node != null) { return node.getIdent(); } } return getInitialSelectedNodeId(); } }