/** * 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.scorm; import java.io.File; import java.io.IOException; import java.util.Iterator; import java.util.Map; import org.olat.core.commons.fullWebApp.LayoutMain3ColsBackController; import org.olat.core.commons.fullWebApp.LayoutMain3ColsController; import org.olat.core.commons.fullWebApp.LayoutMain3ColsPreviewController; import org.olat.core.commons.modules.bc.FolderConfig; import org.olat.core.dispatcher.mapper.Mapper; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.htmlheader.jscss.JSAndCSSComponent; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.components.link.LinkFactory; import org.olat.core.gui.components.panel.ListPanel; import org.olat.core.gui.components.panel.Panel; import org.olat.core.gui.components.tree.GenericTreeNode; 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.ConfigurationChangedListener; 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.MainLayoutBasicController; import org.olat.core.gui.control.generic.iframe.DeliveryOptions; import org.olat.core.gui.control.generic.iframe.IFrameDisplayController; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.AssertException; import org.olat.core.logging.OLATRuntimeException; import org.olat.core.logging.activity.LearningResourceLoggingAction; import org.olat.core.logging.activity.ThreadLocalUserActivityLogger; import org.olat.core.util.FileUtils; import org.olat.core.util.WebappHelper; import org.olat.core.util.resource.OresHelper; import org.olat.core.util.vfs.LocalFolderImpl; import org.olat.course.CourseModule; import org.olat.user.UserManager; /** * Description:<br> * Controller that handles the display of a single Scorm sco item and delegates * the sco api calls to the scorm RTE backend. It provides also an navigation to * navigate in the tree with "pre" "next" buttons. */ public class ScormAPIandDisplayController extends MainLayoutBasicController implements ConfigurationChangedListener { protected static final String LMS_INITIALIZE = "LMSInitialize"; protected static final String LMS_GETVALUE = "LMSGetValue"; protected static final String LMS_SETVALUE = "LMSSetValue"; protected static final String LMS_FINISH = "LMSFinish"; protected static final String LMS_GETLASTERROR = "LMSGetLastError"; protected static final String LMS_GETERRORSTRING = "LMSGetErrorString"; protected static final String LMS_GETDIAGNOSTIC = "LMSGetDiagnostic"; protected static final String LMS_COMMIT = "LMSCommit"; protected static final String SCORM_CONTENT_FRAME = "scormContentFrame"; private String scorm_lesson_mode; private VelocityContainer myContent; private MenuTree menuTree; private Controller columnLayoutCtr; private ScormCPManifestTreeModel treeModel; private IFrameDisplayController iframectr; private OLATApiAdapter scormAdapter; private String username; private Link nextScoTop, nextScoBottom, previousScoTop, previousScoBottom; private ListPanel scoTopButtons, scoBottomButtons; /** * @param ureq * @param wControl * @param showMenu if true, the ims cp menu is shown * @param apiCallback the callback to where lmssetvalue data is mirrored, or null if no callback is desired * @param cpRoot * @param resourceId * @param courseIdNodeId The course ID and optional the course node ID combined with "-". Example: 77554952047098-77554952047107 * @param lesson_mode add null for the default value or "normal", "browse" or * "review" * @param credit_mode add null for the default value or "credit", "no-credit" * @param attemptsIncremented Is the attempts counter already incremented * @param deliveryOptions This delivery options can override the default from the SCORM module */ ScormAPIandDisplayController(UserRequest ureq, WindowControl wControl, boolean showMenu, ScormAPICallback apiCallback, File cpRoot, Long scormResourceId, String courseIdNodeId, String lesson_mode, String credit_mode, boolean previewMode, String assessableType, boolean activate, boolean fullWindow, boolean attemptsIncremented, DeliveryOptions deliveryOptions) { super(ureq, wControl); // logging-note: the callers of createScormAPIandDisplayController make sure they have the scorm resource added to the ThreadLocalUserActivityLogger ThreadLocalUserActivityLogger.log(LearningResourceLoggingAction.LEARNING_RESOURCE_OPEN, getClass()); this.username = ureq.getIdentity().getName(); if (!lesson_mode.equals(ScormConstants.SCORM_MODE_NORMAL) && !lesson_mode.equals(ScormConstants.SCORM_MODE_REVIEW) && !lesson_mode.equals(ScormConstants.SCORM_MODE_BROWSE)) throw new AssertException( "Wrong parameter for constructor, only 'normal', 'browse' or 'review' are allowed for lesson_mode"); if (!credit_mode.equals("credit") && !credit_mode.equals("no-credit")) throw new AssertException( "Wrong parameter for constructor, only 'credit' or 'no-credit' are allowed for credit_mode"); scorm_lesson_mode = lesson_mode; myContent = createVelocityContainer("display"); JSAndCSSComponent jsAdapter = new JSAndCSSComponent("apiadapter", new String[] {"js/openolat/scormApiAdapter.js"}, null); myContent.put("apiadapter", jsAdapter); // init SCORM adapter try { scormAdapter = new OLATApiAdapter(); scormAdapter.addAPIListener(apiCallback); String fullname = UserManager.getInstance().getUserDisplayName(getIdentity()); String scormResourceIdStr = scormResourceId == null ? null : scormResourceId.toString(); scormAdapter.init(cpRoot, scormResourceIdStr, courseIdNodeId, FolderConfig.getCanonicalRoot(), username, fullname, lesson_mode, credit_mode, hashCode()); } catch (IOException e) { showError("error.manifest.corrupted"); LayoutMain3ColsController ctr = new LayoutMain3ColsController(ureq, getWindowControl(), null, new Panel("empty"), "scorm" + scormResourceId); columnLayoutCtr = ctr; putInitialPanel(columnLayoutCtr.getInitialComponent()); return; } // at this point we know the filelocation for our xstream-sco-score file (FIXME:fj: do better // even if we do not show the menu, we need to build parse the manifest // and find the first node to display at startup File mani = new File(cpRoot, "imsmanifest.xml"); if (!mani.exists()) { throw new OLATRuntimeException( "error.manifest.missing", null, getClass().getName(), "CP " + cpRoot.getAbsolutePath() + " has no imsmanifest", null ); } treeModel = new ScormCPManifestTreeModel(mani, scormAdapter.getScoItemsStatus()); menuTree = new MenuTree("cpDisplayTree"); menuTree.setTreeModel(treeModel); menuTree.addListener(this); OLATResourceable courseOres = null; // load course where this scorm package runs in if (courseIdNodeId != null) { String courseId = courseIdNodeId; int delimiterPos = courseId.indexOf("-"); if (delimiterPos != -1) { // remove course node id from combined course id / node id value courseId = courseId.substring(0, delimiterPos); } courseOres = OresHelper.createOLATResourceableInstance(CourseModule.class, Long.valueOf(courseId)); } ScormPackageConfig packageConfig = ScormMainManager.getInstance().getScormPackageConfig(cpRoot); if((deliveryOptions == null || (deliveryOptions.getInherit() != null && deliveryOptions.getInherit().booleanValue())) && packageConfig != null) { deliveryOptions = packageConfig.getDeliveryOptions(); } iframectr = new IFrameDisplayController(ureq, wControl, new LocalFolderImpl(cpRoot), SCORM_CONTENT_FRAME, courseOres, deliveryOptions, true, previewMode); listenTo(iframectr); myContent.contextPut("frameId", SCORM_CONTENT_FRAME); //pre next navigation links nextScoTop = LinkFactory.createCustomLink("nextScoTop", "nextsco", "", Link.NONTRANSLATED | Link.BUTTON, myContent, this); nextScoTop.setIconLeftCSS("o_icon o_icon_next_page"); previousScoTop = LinkFactory.createCustomLink("previousScoTop", "previoussco", "", Link.NONTRANSLATED | Link.BUTTON, myContent, this); previousScoTop.setIconLeftCSS("o_icon o_icon_previous_page"); nextScoBottom = LinkFactory.createCustomLink("nextScoBottom", "nextsco", "", Link.NONTRANSLATED | Link.BUTTON, myContent, this); nextScoBottom.setIconLeftCSS("o_icon o_icon_next_page"); previousScoBottom = LinkFactory.createCustomLink("previousScoBottom", "previoussco", "", Link.NONTRANSLATED | Link.BUTTON, myContent, this); previousScoBottom.setIconLeftCSS("o_icon o_icon_previous_page"); scoTopButtons = new ListPanel("scoTopButtons", "o_scorm_navigation"); scoTopButtons.addContent(previousScoTop); scoTopButtons.addContent(nextScoTop); scoBottomButtons = new ListPanel("scoBottomButtons", "o_scorm_navigation"); scoBottomButtons.addContent(previousScoBottom); scoBottomButtons.addContent(nextScoBottom); // show the buttons, default. use setter method to change default behaviour myContent.contextPut("showNavButtons", Boolean.TRUE); myContent.put("scoTopButtons", scoTopButtons); myContent.put("scoBottomButtons", scoBottomButtons); // bootId is the item the user left the sco last time or the first one String bootId = scormAdapter.getScormLastAccessedItemId(); // if bootId is -1 all course sco's are completed, we show a message // <OLATCE-289> // if (bootId.equals("-1")) { // iframectr.getInitialComponent().setVisible(false); // showInfo("scorm.course.completed"); // // } else { scormAdapter.launchItem(bootId); TreeNode bootnode = treeModel.getNodeByScormItemId(bootId); iframectr.setCurrentURI((String) bootnode.getUserObject()); menuTree.setSelectedNodeId(bootnode.getIdent()); // } // </OLATCE-239> updateNextPreviousButtons(bootId); myContent.put("contentpackage", iframectr.getInitialComponent()); if (activate) { if (previewMode) { LayoutMain3ColsPreviewController ctr = new LayoutMain3ColsPreviewController(ureq, getWindowControl(), (showMenu ? menuTree : null), myContent, "scorm" + scormResourceId); if(fullWindow) ctr.setAsFullscreen(); columnLayoutCtr = ctr; } else { LayoutMain3ColsBackController ctr = new LayoutMain3ColsBackController(ureq, getWindowControl(), (showMenu ? menuTree : null), myContent, "scorm" + scormResourceId); if(fullWindow) ctr.setAsFullscreen(); columnLayoutCtr = ctr; } } else { LayoutMain3ColsController ctr = new LayoutMain3ColsController(ureq, getWindowControl(), (showMenu ? menuTree : null), myContent, "scorm" + scormResourceId); columnLayoutCtr = ctr; putInitialPanel(columnLayoutCtr.getInitialComponent()); } listenTo(columnLayoutCtr); //scrom API calls get handled by this mapper String scormResourceIdStr = (scormResourceId == null ? null : scormResourceId.toString()); Mapper mapper = new ScormAPIMapper(ureq.getIdentity(), scormResourceIdStr, courseIdNodeId, assessableType, cpRoot, scormAdapter, attemptsIncremented); String scormCallbackUri = registerMapper(ureq, mapper); myContent.contextPut("scormCallbackUri", scormCallbackUri+"/"); } /** * Configuration method to enable/disable the havigation buttons that appear * on the right side above and below the content. Default is set to true. * * @param showNavButtons */ public void showNavButtons(boolean showNavButtons) { myContent.contextPut("showNavButtons", Boolean.valueOf(showNavButtons)); } /** * Configuration method to use an explicit height for the iframe instead of * the default automatic sizeing code. If you don't call this method, OLAT * will try to size the iframe so that no scrollbars appear. In most cases * this works. If it does not work, use this method to set an explicit height. * <br /> * Set 0 to reset to automatic behaviour. * * @param height */ public void setHeightPX(int height) { iframectr.setHeightPX(height); } public void setRawContent(boolean rawContent) { iframectr.setRawContent(rawContent); } public DeliveryOptions getDeliveryOptions() { return iframectr.getDeliveryOptions(); } public void setDeliveryOptions(DeliveryOptions config) { iframectr.setDeliveryOptions(config); } public void setContentEncoding(String encoding) { iframectr.setContentEncoding(encoding); } public void setJSEncoding(String encoding) { iframectr.setJSEncoding(encoding); } //fxdiff FXOLAT-116: SCORM improvements public void close() { if(columnLayoutCtr instanceof LayoutMain3ColsBackController) { ((LayoutMain3ColsBackController)columnLayoutCtr).deactivate(); } } /** * @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) */ public void event(UserRequest ureq, Component source, Event event) { if (source instanceof Link) { switchToNextOrPreviousSco((Link)source); } else if (source == menuTree) { // user clicked a node in the tree navigation TreeEvent te = (TreeEvent) event; switchToPage(te); } else if (source == myContent && event.getCommand().equals("abort")) { // user has wrong browser - abort fireEvent(ureq, Event.FAILED_EVENT); } } @Override //fxdiff FXOLAT-116: SCORM improvements protected void event(UserRequest ureq, Controller source, Event event) { if(source == columnLayoutCtr) { if(event == Event.BACK_EVENT) { fireEvent(ureq, Event.BACK_EVENT); } } super.event(ureq, source, event); } private void switchToNextOrPreviousSco(Link link) { String nextScoId = (String)link.getUserObject(); GenericTreeNode tn = (GenericTreeNode) treeModel.getNodeByScormItemId(nextScoId); menuTree.setSelectedNodeId(tn.getIdent()); iframectr.getInitialComponent().setVisible(true); String identifierRes = (String) tn.getUserObject(); updateNextPreviousButtons(nextScoId); displayMessages(nextScoId, identifierRes); updateMenuTreeIconsAndMessages(); } /** * @param te is an Event fired by clicking a node in a tree */ public void switchToPage(TreeEvent te) { // switch to the new page String nodeId = te.getNodeId(); GenericTreeNode tn = (GenericTreeNode) treeModel.getNodeById(nodeId); if (te.getCommand().equals(MenuTree.COMMAND_TREENODE_EXPANDED)) { iframectr.getInitialComponent().setVisible(false); myContent.setDirty(true);//update the view } else { iframectr.getInitialComponent().setVisible(true); String scormId = String.valueOf(treeModel.lookupScormNodeId(tn)); updateNextPreviousButtons(scormId); displayMessages(scormId, (String) tn.getUserObject()); } updateMenuTreeIconsAndMessages(); } private void displayMessages(String scormId, String identifierRes) { if (scormAdapter.hasItemPrerequisites(scormId)) { iframectr.getInitialComponent().setVisible(false); showInfo("scorm.item.has.preconditions"); return; } scormAdapter.launchItem(scormId); iframectr.setCurrentURI(identifierRes); } @Override public void configurationChanged() { if(columnLayoutCtr instanceof LayoutMain3ColsBackController) { LayoutMain3ColsBackController layoutCtr = (LayoutMain3ColsBackController)columnLayoutCtr; layoutCtr.deactivate(); } else if(columnLayoutCtr instanceof LayoutMain3ColsController) { LayoutMain3ColsController layoutCtr = (LayoutMain3ColsController)columnLayoutCtr; layoutCtr.deactivate(null); } } /** * @see org.olat.core.gui.control.DefaultController#doDispose(boolean) */ protected void doDispose() { cleanUpCollectedScoData(); } /** * in "browse" or "review" mode we dont collect sco data */ private void cleanUpCollectedScoData() { if(scorm_lesson_mode.equals(ScormConstants.SCORM_MODE_BROWSE) || scorm_lesson_mode.equals(ScormConstants.SCORM_MODE_REVIEW)){ StringBuilder path = new StringBuilder(); path.append(WebappHelper.getTmpDir()) .append("/tmp").append(WebappHelper.getInstanceId()).append("scorm/") .append(hashCode()); FileUtils.deleteDirsAndFiles( new File(path.toString()),true, true); } } /** * @return the treemodel. (for read-only usage) Useful if you would like to * integrate the menu at some other place */ public ScormCPManifestTreeModel getTreeModel() { return treeModel; } private void updateNextPreviousButtons(String nextScoId) { Integer nextInt = scormAdapter.getNextSco(nextScoId); Integer preInt = scormAdapter.getPreviousSco(nextScoId); nextScoTop.setUserObject(nextInt.toString()); nextScoBottom.setUserObject(nextInt.toString()); if(nextInt.intValue() != -1 ) { nextScoTop.setVisible(true); nextScoBottom.setVisible(true); } else { nextScoTop.setVisible(false); nextScoBottom.setVisible(false); } previousScoTop.setUserObject(preInt.toString()); previousScoBottom.setUserObject(preInt.toString()); if(preInt.intValue() != -1 ) { previousScoTop.setVisible(true); previousScoBottom.setVisible(true); } else { previousScoTop.setVisible(false); previousScoBottom.setVisible(false); } } public void activate(){ if (columnLayoutCtr instanceof LayoutMain3ColsPreviewController) { LayoutMain3ColsPreviewController ctrl = (LayoutMain3ColsPreviewController) columnLayoutCtr; ctrl.activate(); } else if (columnLayoutCtr instanceof LayoutMain3ColsBackController){ LayoutMain3ColsBackController ctrl = (LayoutMain3ColsBackController)columnLayoutCtr; ctrl.activate(); } } private void updateMenuTreeIconsAndMessages() { menuTree.setDirty(true); Map<String,String> itemsStat = scormAdapter.getScoItemsStatus(); Map<String,GenericTreeNode> idToNode = treeModel.getScormIdToNodeRelation(); for (Iterator<String> it = itemsStat.keySet().iterator(); it.hasNext();) { String itemId = it.next(); GenericTreeNode tn = idToNode.get(itemId); // change icon decorator tn.setIconDecorator1CssClass("o_scorm_" + itemsStat.get(itemId)); } } }