Package org.olat.ims.qti.editor

Source Code of org.olat.ims.qti.editor.QTIEditorMainController

/**
* 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.
* <p>
*/

package org.olat.ims.qti.editor;

import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.olat.basesecurity.ManagerFactory;
import org.olat.basesecurity.SecurityGroup;
import org.olat.core.commons.fullWebApp.LayoutMain3ColsController;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.form.Form;
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.Panel;
import org.olat.core.gui.components.tabbedpane.TabbedPane;
import org.olat.core.gui.components.tree.MenuTree;
import org.olat.core.gui.components.tree.SelectionTree;
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.VetoableCloseController;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.controller.MainLayoutBasicController;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.gui.control.generic.dialog.DialogController;
import org.olat.core.gui.control.generic.modal.DialogBoxController;
import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
import org.olat.core.gui.control.generic.tool.ToolController;
import org.olat.core.gui.control.generic.tool.ToolFactory;
import org.olat.core.id.Identity;
import org.olat.core.id.User;
import org.olat.core.id.UserConstants;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.Tracing;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.Formatter;
import org.olat.core.util.StringHelper;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.coordinate.LockResult;
import org.olat.core.util.mail.ContactList;
import org.olat.core.util.mail.ContactMessage;
import org.olat.core.util.memento.Memento;
import org.olat.core.util.nodes.INode;
import org.olat.core.util.tree.TreeVisitor;
import org.olat.core.util.tree.Visitor;
import org.olat.core.util.xml.XStreamHelper;
import org.olat.course.CourseFactory;
import org.olat.course.ICourse;
import org.olat.course.nodes.CourseNode;
import org.olat.course.nodes.iq.IQEditController;
import org.olat.course.tree.TreePosition;
import org.olat.fileresource.types.FileResource;
import org.olat.ims.qti.QTIChangeLogMessage;
import org.olat.ims.qti.QTIResult;
import org.olat.ims.qti.QTIResultManager;
import org.olat.ims.qti.editor.QTIEditHelper;
import org.olat.ims.qti.editor.beecom.objects.ChoiceQuestion;
import org.olat.ims.qti.editor.beecom.objects.Item;
import org.olat.ims.qti.editor.beecom.objects.QTIDocument;
import org.olat.ims.qti.editor.beecom.objects.QTIObject;
import org.olat.ims.qti.editor.beecom.objects.Question;
import org.olat.ims.qti.editor.beecom.objects.Response;
import org.olat.ims.qti.editor.beecom.objects.Section;
import org.olat.ims.qti.editor.beecom.parser.ItemParser;
import org.olat.ims.qti.editor.tree.AssessmentNode;
import org.olat.ims.qti.editor.tree.GenericQtiNode;
import org.olat.ims.qti.editor.tree.InsertItemTreeModel;
import org.olat.ims.qti.editor.tree.ItemNode;
import org.olat.ims.qti.editor.tree.QTIEditorTreeModel;
import org.olat.ims.qti.editor.tree.SectionNode;
import org.olat.ims.qti.process.AssessmentInstance;
import org.olat.ims.qti.process.QTIEditorResolver;
import org.olat.modules.co.ContactFormController;
import org.olat.modules.iq.IQDisplayController;
import org.olat.modules.iq.IQManager;
import org.olat.modules.iq.IQPreviewSecurityCallback;
import org.olat.repository.RepositoryEntry;
import org.olat.repository.RepositoryManager;
import org.olat.resource.references.ReferenceImpl;

/**
* Description: <br>
* QTIEditorMainController is started from within the repository. A persistent
* lock is set to prevent more than one user working on the same document, even
* if the current working author has no active session. If the document is
* already linked to a node in a course, it is opened for corrections only. This
* restricted editing function prohibits structural changes which would
* interfere with already existing results.
* <p>
* Furthermore, if a document is loaded into the editor, it is not available for
* linking in a course. Therefore, a document in the editor can always be saved
* back safely to the repository. But it must be locked that users starting the
* document from an already referencing building block wait until the edited
* document is committed completly to the repository.
* <P>
* Initial Date: Oct 21, 2004 <br>
*
* @author mike
*/
public class QTIEditorMainController extends MainLayoutBasicController implements VetoableCloseController {
  /*
   * Toolbox Commands
   */
  private static final String CMD_TOOLS_CLOSE_EDITOR = "cmd.close";
  private static final String CMD_TOOLS_PREVIEW = "cmd.preview";
  private static final String CMD_TOOLS_CHANGE_MOVE = "cmd.move";
  private static final String CMD_TOOLS_CHANGE_COPY = "cmd.copy";
  private static final String CMD_TOOLS_CHANGE_DELETE = "cmd.delete";
  private static final String CMD_TOOLS_ADD_PREFIX = "cmd.add.";
  private static final String CMD_TOOLS_ADD_FREETEXT = CMD_TOOLS_ADD_PREFIX + "essay";
  private static final String CMD_TOOLS_ADD_FIB = CMD_TOOLS_ADD_PREFIX + "fib";
  private static final String CMD_TOOLS_ADD_MULTIPLECHOICE = CMD_TOOLS_ADD_PREFIX + "mc";
  private static final String CMD_TOOLS_ADD_SINGLECHOICE = CMD_TOOLS_ADD_PREFIX + "sc";
  private static final String CMD_TOOLS_ADD_KPRIM = CMD_TOOLS_ADD_PREFIX + "kprim";
  private static final String CMD_TOOLS_ADD_SECTION = CMD_TOOLS_ADD_PREFIX + "section";

  private static final String CMD_EXIT_SAVE = "exit.save";
  private static final String CMD_EXIT_DISCARD = "exit.discard";
  private static final String CMD_EXIT_CANCEL = "exit.cancel";
 
  // REVIEW:2008-11-20: patrickb, scalability project issue -> read/write lock in distributed system
  //
  // Problem:
  // - Editor Session holds a copy to work, in case the work copy is "committed" e.g. saved - the qti file(s)
  //   are copied and replaced -> this may lead to "uncommitted" reads of users starting the qti test, during the
  //   very same moment the files are written.
  // - Because qti tests may hold media files and the like the copying and replacing can last surprisingly long.
  //
  // This means saving a test must be an exclusive operation. Reads for test sessions should be concurrent.
  //
  // History of solutions:
  // 1) An OLAT wide lock (object) was used -> possible congestion, delay if many qti tests are started or edited.
  // 2) Read/Write Lock used to grant non-congestion on reading, only OLAT wide write lock - still not optimal
  // 2a) Optimal solution in non-distributed system (singleVM) - ReadWriteLock on specific resource, instead OLAT wide.
  // 3) Scalability Project: how often does it happen compared to how often a test is started only?
  //    => pragmatic solution to protect but not slow down starting of many concurrent readers.
  //    An open and active editor session for a specific test -> possible writes!! => no reads
  //    An open but not active editor session for a specific test -> no possible writes to expect. => allow reads
  //    No open
  // ----|start editor session=>copy files for working copy | work on copy |     close browser                     | restart work on copy   | commit work|---
  // ----|~~~~~~~~~~~~~~~~~~~~~~~~~~~~active session  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|  Sessiontimeout    |~~~~ active sesson ~~~~~~~~~~~~~~~~~|     
  //                                  qti file locked                                             ,no more active         qti file locked   
  //                                                                                              session, qti file
  //                                                                                              free for read
  // Case 1: Starting editor session (acquiring lock) at the same time as starting some qti test session(s) (polling lock only)
  //          => only a problem if user exits and saves as fast as possible and some big qti file. (unlikely)
  // Case 2: Active editor session (acquired lock) and trying to start test
  //          => annoying for users in the course, not best service of the LMS for its authors, but pragmatic. (rare case)
  // Case 3: Closing editor session (releasing lock) while somebody tries to start the test (polling lock only)
  //          => annoying for user, as he just missed it for some milliseconds to be able to start. (very rare case)
  // Case 4: No qti editor session or no active editor session
  //          => benefit of fast starting qti tests, best service for the LMS clients (98.512% of cases)
  //
  // This leads to the solution as follows:
  // - (as it was already the case) A persistent lock for started qti sessions, used to prevent multiple authors "branching" test versions and overwriting changes of the others.
  // - a non persistent GUI lock to signal an active editor session, this can be polled before starting a qti test.
  // - lock out qti readers in the case of an active editor session
  //
  //public static final ReentrantReadWriteLock IS_SAVING_RWL = new ReentrantReadWriteLock();

  private QTIEditorPackage qtiPackage;

  private VelocityContainer main, exitVC, chngMsgFormVC, restrictedEditWarningVC;
  private ToolController mainToolC;
  private MenuTree menuTree;
  private Panel mainPanel, wrapperPanel;
  private LayoutMain3ColsController columnLayoutCtr;

  private QTIDocument qtiDoc;
  private QTIEditorTreeModel menuTreeModel;
  private DialogBoxController deleteDialog;
  private DialogBoxController deleteMediaDialog;
  private IQDisplayController previewController;
  private SelectionTree moveTree, copyTree, insertTree;
  private InsertItemTreeModel insertTreeModel;
  private GenericQtiNode insertObject;
  private LockResult lockEntry;
  private Controller failedMonolog, lockMonolog;
  private boolean restrictedEdit;
  private Map history = null;
  private String startedWithTitle;
  private List referencees;
  private ChangeMessageForm chngMsgFrom;
  private DialogController proceedRestricedEditDialog;
  private ContactMessage changeEmail;
  private ContactFormController cfc;
  private String changeLog = null;
  private CloseableModalController cmc, cmcPrieview, cmcExit;
  private Panel exitPanel;
  private boolean notEditable;
  private LockResult activeSessionLock;
  private Link notEditableButton;
  private Set<String> deletableMediaFiles;
 
  public QTIEditorMainController(List referencees, UserRequest ureq, WindowControl wControl, FileResource fileResource) {
    super(ureq, wControl);

    for(Iterator iter = referencees.iterator(); iter.hasNext(); ) {
      ReferenceImpl ref = (ReferenceImpl)iter.next();
      if ("CourseModule".equals(ref.getSource().getResourceableTypeName())) {
        ICourse course = CourseFactory.loadCourse(ref.getSource().getResourceableId());
        CourseNode courseNode = course.getEditorTreeModel().getCourseNode(ref.getUserdata());
        String repositorySoftKey = (String) courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_REPOSITORY_SOFTKEY);
        Long repKey = RepositoryManager.getInstance().lookupRepositoryEntryBySoftkey(repositorySoftKey, true).getKey();
        List<QTIResult> results = QTIResultManager.getInstance().selectResults(course.getResourceableId(), courseNode.getIdent(), repKey, 1);
        this.restrictedEdit = ((CoordinatorManager.getCoordinator().getLocker().isLocked(course, null)) || (results != null && results.size() > 0)) ? true : false;
      }
      if(restrictedEdit) break;
    }
    if(CoordinatorManager.getCoordinator().getLocker().isLocked(fileResource, null)) {
      this.restrictedEdit = true;
    }
    this.referencees = referencees;
   
    qtiPackage = new QTIEditorPackage(ureq.getIdentity(), fileResource, getTranslator());

    // try to get lock which lives longer then the browser session in case of a closing browser window
    lockEntry = CoordinatorManager.getCoordinator().getLocker().aquirePersistentLock(qtiPackage.getRepresentingResourceable(), ureq.getIdentity(), null);
    if (lockEntry.isSuccess()) {
      // acquired a lock for the duration of the session only
      //fileResource has the RepositoryEntre.getOlatResource within, which is used in qtiPackage
      activeSessionLock = CoordinatorManager.getCoordinator().getLocker().acquireLock(qtiPackage.getRepresentingResourceable(), ureq.getIdentity(), null);
      Long resourceableId = fileResource.getResourceableId();
      //
      qtiDoc = qtiPackage.getQTIDocument();
      if (qtiDoc == null) {
        notEditable = true;       
      } else if (qtiPackage.isResumed()) {
        showInfo("info.resumed", null);
      }
      //
      init(ureq); // initialize the gui
    } else {
      wControl.setWarning( getTranslator().translate("error.lock", new String[] { lockEntry.getOwner().getName(),
        Formatter.formatDatetime(new Date(lockEntry.getLockAquiredTime())) }) );
    }
  }

  /**
   * This constructor may only be used for new or non-referenced QTI files!
   *
   * @param ureq
   * @param wControl
   * @param fileResource
   */
  public QTIEditorMainController(UserRequest ureq, WindowControl wControl, FileResource fileResource) {
    // super(wControl) is called in referenced constructor
    // null as value for the List referencees sets restrictedEdit := false;
    this(null, ureq, wControl, fileResource);
  }

  private void init(UserRequest ureq) {
    main = createVelocityContainer("index");
    JSAndCSSComponent jsAndCss;
    // Add html header js
    jsAndCss = new JSAndCSSComponent("qitjsandcss", this.getClass(), new String[] { "qti.js" }, null, true);
    main.put("qitjsandcss", jsAndCss);
   
    //
    mainPanel = new Panel("p_qti_editor");
    mainPanel.setContent(main);
    //
    if(notEditable) {   
      //test not editable
      VelocityContainer notEditable = createVelocityContainer("notEditable");
      notEditableButton = LinkFactory.createButton("ok", notEditable, this);
      Panel panel = new Panel("notEditable");
      panel.setContent(notEditable);
      columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), null, null, panel, null);
      wrapperPanel = putInitialPanel(columnLayoutCtr.getInitialComponent());
      return;
    }
       
    mainToolC = populateToolC(); // qtiPackage must be loaded previousely
    listenTo(mainToolC);
   
    // initialize the history
    if (qtiPackage.isResumed() && qtiPackage.hasSerializedChangelog()) {
      // there were already changes made -> reload!
      history = qtiPackage.loadChangelog();
    } else {
      // start with a fresh history. Editor is resumed but no changes were made
      // so far.
      history = new HashMap();
    }

    if (restrictedEdit) {
      mainToolC.setEnabled(CMD_TOOLS_ADD_SECTION, false);
      mainToolC.setEnabled(CMD_TOOLS_ADD_SINGLECHOICE, false);
      mainToolC.setEnabled(CMD_TOOLS_ADD_MULTIPLECHOICE, false);

      mainToolC.setEnabled(CMD_TOOLS_ADD_FIB, false);
      if (!qtiPackage.getQTIDocument().isSurvey()) mainToolC.setEnabled(CMD_TOOLS_ADD_KPRIM, false);
      if (qtiPackage.getQTIDocument().isSurvey()) mainToolC.setEnabled(CMD_TOOLS_ADD_FREETEXT, false);
    }
    mainToolC.setEnabled(CMD_TOOLS_CHANGE_DELETE, false);
    mainToolC.setEnabled(CMD_TOOLS_CHANGE_MOVE, false);
    mainToolC.setEnabled(CMD_TOOLS_CHANGE_COPY, false);

    // The menu tree model represents the structure of the qti document.
    // All insert/move operations on the model are propagated to the structure
    // by the node
    menuTreeModel = new QTIEditorTreeModel(qtiPackage);
    menuTree = new MenuTree("QTIDocumentTree");
    menuTree.setTreeModel(menuTreeModel);
    menuTree.setSelectedNodeId(menuTree.getTreeModel().getRootNode().getIdent());
    menuTree.addListener(this);// listen to the tree
    // remember the qtidoc title when we started this editor, to correctly name
    // the history report
    this.startedWithTitle = menuTree.getSelectedNode().getAltText();
    //
    main.put("tabbedPane", menuTreeModel.getQtiRootNode().createEditTabbedPane(ureq, getWindowControl(), getTranslator(), this));
    main.contextPut("qtititle", menuTreeModel.getQtiRootNode().getAltText());
    main.contextPut("isRestrictedEdit", restrictedEdit ? Boolean.TRUE : Boolean.FALSE);
    //
    columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), menuTree, mainToolC.getInitialComponent(), mainPanel, "qtieditor" + qtiPackage.getRepresentingResourceable());
    listenTo(columnLayoutCtr);
    // Add css background
    if (restrictedEdit) {
      columnLayoutCtr.addCssClassToMain("o_editor_qti_correct");
    } else {
      columnLayoutCtr.addCssClassToMain("o_editor_qti");
    }
    wrapperPanel = putInitialPanel(columnLayoutCtr.getInitialComponent());
   
    if (restrictedEdit) {
      restrictedEditWarningVC = createVelocityContainer("restrictedEditDialog");
      proceedRestricedEditDialog = new DialogController(ureq.getLocale(), translate("yes"), translate("no"),translate("qti.restricted.edit.warning")+"<br/><br/>"+createReferenceesMsg(ureq), null, true, null);
      listenTo(proceedRestricedEditDialog);
      restrictedEditWarningVC.put("dialog", proceedRestricedEditDialog.getInitialComponent());
      // we would like to us a modal dialog here, but this does not work! we
      // can't push to stack because the outher workflows pushes us after the
      // controller to the stack. Thus, if we used a modal dialog here the
      // dialog would never show up.
      columnLayoutCtr.setCol3(restrictedEditWarningVC);
      columnLayoutCtr.hideCol1(true);
      columnLayoutCtr.hideCol2(true);
    }
  }


  /**
   * @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 == menuTree) { // catch menu tree clicks
      if (event.getCommand().equals(MenuTree.COMMAND_TREENODE_CLICKED)) {
        GenericQtiNode clickedNode;       
        clickedNode = menuTreeModel.getQtiNode(menuTree.getSelectedNodeId());       
        TabbedPane tabbedPane = clickedNode.createEditTabbedPane(ureq, getWindowControl(), getTranslator(), this);
        if(tabbedPane!=null) {
          main.put("tabbedPane",tabbedPane);
        } else {
          VelocityContainer itemNotEditable = createVelocityContainer("tab_itemAlien");           
          main.put("tabbedPane", itemNotEditable);
          return;         
        }
       
        // enable/disable delete and move
        // if (!restrictedEdit) {
        // only available in full edit mode
        if (clickedNode instanceof AssessmentNode) {
          mainToolC.setEnabled(CMD_TOOLS_CHANGE_DELETE, false);
          mainToolC.setEnabled(CMD_TOOLS_CHANGE_MOVE, false);
          mainToolC.setEnabled(CMD_TOOLS_CHANGE_COPY, false);
        } else {
          mainToolC.setEnabled(CMD_TOOLS_CHANGE_DELETE, true && !restrictedEdit);
          mainToolC.setEnabled(CMD_TOOLS_CHANGE_MOVE, true && !restrictedEdit);
          if (clickedNode instanceof ItemNode) {
            mainToolC.setEnabled(CMD_TOOLS_CHANGE_COPY, true && !restrictedEdit);
          } else {
            mainToolC.setEnabled(CMD_TOOLS_CHANGE_COPY, false);
          }
        }
        // }
      }
    } else if (source == moveTree) { // catch move operations
      cmc.deactivate();
      removeAsListenerAndDispose(cmc);
      cmc = null;
     
      TreeEvent te = (TreeEvent) event;
      if (te.getCommand().equals(TreeEvent.COMMAND_TREENODE_CLICKED)) {
        // user chose a position to insert a new node
        String nodeId = te.getNodeId();
        TreePosition tp = insertTreeModel.getTreePosition(nodeId);
        GenericQtiNode parentTargetNode = (GenericQtiNode) tp.getParentTreeNode();
        int targetPos = tp.getChildpos();
        GenericQtiNode selectedNode = (GenericQtiNode) menuTree.getSelectedNode();
        int selectedPos = selectedNode.getPosition();
        GenericQtiNode parentSelectedNode = (GenericQtiNode) selectedNode.getParent();
        if (parentTargetNode == parentSelectedNode) {
          // if we're on the same subnode
          if (targetPos > selectedNode.getPosition()) {
            // if we're moving after our current position
            targetPos--;
            // decrease insert pos since we're going to be removed from the
            // parent before re-insert
          }
        }
        // insert into menutree (insert on GenericNode do a remove from parent)
        parentTargetNode.insert(selectedNode, targetPos);
        // insert into model (remove from parent needed prior to insert)
        QTIObject subject = parentSelectedNode.removeQTIObjectAt(selectedPos);
        parentTargetNode.insertQTIObjectAt(subject, targetPos);
        qtiPackage.serializeQTIDocument();
        menuTree.setDirty(true); //force rerendering for ajax mode
      }
    } else if (source == copyTree) { // catch copy operations
      cmc.deactivate();
      removeAsListenerAndDispose(cmc);
      cmc = null;
     
      TreeEvent te = (TreeEvent) event;
      if (te.getCommand().equals(TreeEvent.COMMAND_TREENODE_CLICKED)) {
        // user chose a position to insert the node to be copied
        String nodeId = te.getNodeId();
        TreePosition tp = insertTreeModel.getTreePosition(nodeId);
        int targetPos = tp.getChildpos();
        ItemNode selectedNode = (ItemNode) menuTree.getSelectedNode();
        // only items are moveable
        // use XStream instead of ObjectCloner
        // Item qtiItem =
        // (Item)xstream.fromXML(xstream.toXML(selectedNode.getUnderlyingQTIObject()));
        Item toClone = (Item) selectedNode.getUnderlyingQTIObject();
        Item qtiItem = (Item) XStreamHelper.xstreamClone(toClone);
        // copy flow label class too, olat-2791
        Question orgQuestion = toClone.getQuestion();
        if (orgQuestion instanceof ChoiceQuestion) {
          String flowLabelClass = ((ChoiceQuestion)orgQuestion).getFlowLabelClass();
          Question copyQuestion =  qtiItem.getQuestion();
          if (copyQuestion instanceof ChoiceQuestion) {
            ((ChoiceQuestion)copyQuestion).setFlowLabelClass(flowLabelClass);
          } else {
            throw new AssertException("Could not copy flow-label-class, wrong type of copy question , must be 'ChoiceQuestion' but is " +copyQuestion);
          }
        }
        String editorIdentPrefix = "";
        if (qtiItem.getIdent().startsWith(ItemParser.ITEM_PREFIX_SCQ)) editorIdentPrefix = ItemParser.ITEM_PREFIX_SCQ;
        else if (qtiItem.getIdent().startsWith(ItemParser.ITEM_PREFIX_MCQ)) editorIdentPrefix = ItemParser.ITEM_PREFIX_MCQ;
        else if (qtiItem.getIdent().startsWith(ItemParser.ITEM_PREFIX_KPRIM)) editorIdentPrefix = ItemParser.ITEM_PREFIX_KPRIM;
        else if (qtiItem.getIdent().startsWith(ItemParser.ITEM_PREFIX_FIB)) editorIdentPrefix = ItemParser.ITEM_PREFIX_FIB;
        else if (qtiItem.getIdent().startsWith(ItemParser.ITEM_PREFIX_ESSAY)) editorIdentPrefix = ItemParser.ITEM_PREFIX_ESSAY;
        // set new ident... this is all it needs for our engine to recognise it
        // as a new item.
        qtiItem.setIdent(editorIdentPrefix + CodeHelper.getForeverUniqueID());
        // insert into menutree (insert on GenericNode do a remove from parent)
        GenericQtiNode parentTargetNode = (GenericQtiNode) tp.getParentTreeNode();
        GenericQtiNode newNode = new ItemNode(qtiItem, qtiPackage);
        parentTargetNode.insert(newNode, targetPos);
        // insert into model
        parentTargetNode.insertQTIObjectAt(qtiItem, targetPos);
        // activate copied node
        menuTree.setSelectedNodeId(newNode.getIdent());
        event(ureq, menuTree, new Event(MenuTree.COMMAND_TREENODE_CLICKED));
        qtiPackage.serializeQTIDocument();
      }
    } else if (source == insertTree) { // catch insert operations
      cmc.deactivate();
      removeAsListenerAndDispose(cmc);
      cmc = null;
     
      TreeEvent te = (TreeEvent) event;
      if (te.getCommand().equals(TreeEvent.COMMAND_TREENODE_CLICKED)) { // insert
        // new
        // node
        String nodeId = te.getNodeId();
        TreePosition tp = insertTreeModel.getTreePosition(nodeId);
        GenericQtiNode parentTargetNode = (GenericQtiNode) tp.getParentTreeNode();
        // insert into menu tree
        parentTargetNode.insert(insertObject, tp.getChildpos());
        // insert into model
        parentTargetNode.insertQTIObjectAt(insertObject.getUnderlyingQTIObject(), tp.getChildpos());
        // activate inserted node
        menuTree.setSelectedNodeId(insertObject.getIdent());
        event(ureq, menuTree, new Event(MenuTree.COMMAND_TREENODE_CLICKED));
        qtiPackage.serializeQTIDocument();
      }
    } else if (source == exitVC) {
      if (event.getCommand().equals(CMD_EXIT_SAVE)) {
        if (isRestrictedEdit() && history.size() > 0) {
          // changes were recorded
          // start work flow:
          // -sending an e-mail to everybody being a stake holder of this qti
          // resource
          // -email with change message
          // -after sending email successfully -> saveNexit is called.
          chngMsgFormVC = createVelocityContainer("changeMsgForm");
          // FIXME:pb:a Bitte diesen Velocity container entfernen und statt
          // dessen den
          // ContentOnlyController verwenden. Es ist äusserst wichtig dass das
          // Layout nie selber gemacht
          // wird sondern immer die Layout controller verwendet werden, d.h. den
          // ContentOnlyController oder
          // den MenuAndToolController. Dort kann das Tool übrigens auch null
          // sein wenn man nur ein Menü braucht.
          // TODO:pb:a extend ContentOnlyController to work also if menu and
          // tool are null, hence only content is desired
          String userN = ureq.getIdentity().getName();
          String lastN = ureq.getIdentity().getUser().getProperty(UserConstants.LASTNAME, ureq.getLocale());
          String firstN = ureq.getIdentity().getUser().getProperty(UserConstants.FIRSTNAME, ureq.getLocale());
          String changeMsg = "Changed by: " + firstN + " " + lastN + " [" + userN + "]\n";
          changeMsg += createChangeMessage();
          changeEmail.setBodyText(changeMsg);
          chngMsgFormVC.contextPut("chngMsg", changeEmail.getBodyText());
          chngMsgFrom = new ChangeMessageForm("chngMsgForm", getTranslator());
          chngMsgFormVC.put("chngMsgForm", chngMsgFrom);
          chngMsgFrom.addListener(this);
          exitPanel.setContent(chngMsgFormVC);
         
          return;
        } else {
          // remove modal dialog and proceed with exit process
          cmcExit.deactivate();
          removeAsListenerAndDispose(cmcExit);
          cmcExit = null;
          // remove lock, clean tmp dir, fire done event to close editor
          saveAndExit(ureq);
        }
      } else if (event.getCommand().equals(CMD_EXIT_DISCARD)) {
        // remove modal dialog and proceed with exit process
        cmcExit.deactivate();
        removeAsListenerAndDispose(cmcExit);
        cmcExit = null;
        // cleanup, so package does not get resumed
        qtiPackage.cleanupTmpPackageDir();
        // remove lock
        removeLocksAndExit(ureq);
       
      } else if (event.getCommand().equals(CMD_EXIT_CANCEL)) {
        // remove modal dialog and go back to edit mode
        cmcExit.deactivate();
        removeAsListenerAndDispose(cmcExit);
        cmcExit = null;
      }
     
    } else if (source == chngMsgFrom) {
      if (event == Form.EVNT_VALIDATION_OK) {
        // the changemessage is created and user is willing to send it
        String userMsg = chngMsgFrom.getUserMsg();
        changeLog = changeEmail.getBodyText();
        if (StringHelper.containsNonWhitespace(userMsg)) {
          changeEmail.setBodyText(userMsg + "\n" + changeLog);
        }// else nothing was added!
        changeEmail.setSubject("Change log for " + startedWithTitle);
        cfc = new ContactFormController(ureq, getWindowControl(), false, true, false, false, changeEmail);
        listenTo(cfc);
        exitPanel.setContent(cfc.getInitialComponent());
        return;
       
      } else {
        // cancel button was pressed
        // just go back to the editor - remove modal dialog
        cmcExit.deactivate();
        removeAsListenerAndDispose(cmcExit);
        cmcExit = null;
      }
    } else if (source == notEditableButton) {
      fireEvent(ureq, Event.DONE_EVENT); // close editor
    }
  }

  private void removeLocksAndExit(UserRequest ureq) {
    // remove lock
    if (lockEntry.isSuccess()){
      CoordinatorManager.getCoordinator().getLocker().releaseLock(activeSessionLock);
      CoordinatorManager.getCoordinator().getLocker().releasePersistentLock(lockEntry);
    }
    fireEvent(ureq, Event.DONE_EVENT); // close editor
  }

  private void saveAndExit(UserRequest ureq) {
    boolean saveOk = false;
    //
    // acquire write lock
    //IS_SAVING_RWL.writeLock().lock();
    // synchronized(IS_SAVING){
    //try {
      saveOk = qtiPackage.savePackageToRepository();
    //} finally {
    //  IS_SAVING_RWL.writeLock().unlock();
    //}
    // }// release write lock
    if (!saveOk) {
      getWindowControl().setError(translate("error.save"));
      return;
    }
    // cleanup, so package does not get resumed
    qtiPackage.cleanupTmpPackageDir();
    removeLocksAndExit(ureq);
  }

  /**
   * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
   *      org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
   */
  protected void event(UserRequest ureq, Controller source, Event event) {
    if (source == mainToolC) {
      String cmd = event.getCommand();
      if (cmd.equals(CMD_TOOLS_CLOSE_EDITOR)) { // exitVC hook:
        // save package back to repository
        exitVC = createVelocityContainer("exitDialog");
        exitPanel = new Panel("exitPanel");
        exitPanel.setContent(exitVC);
        cmcExit = new CloseableModalController(getWindowControl(), translate("editor.preview.close"), exitPanel);
        cmcExit.activate();
        listenTo(cmcExit);
        return;
       
      } else if (cmd.equals(CMD_TOOLS_PREVIEW)) { // preview
        previewController = IQManager.getInstance().createIQDisplayController(new QTIEditorResolver(qtiPackage),
            qtiPackage.getQTIDocument().isSurvey() ? AssessmentInstance.QMD_ENTRY_TYPE_SURVEY : AssessmentInstance.QMD_ENTRY_TYPE_SELF,
            new IQPreviewSecurityCallback(), ureq, getWindowControl());
        if (previewController.isReady()) {
          // in case previewController was unable to initialize, a message was
          // set by displayController
          // this is the case if no more attempts or security check was
          // unsuccessfull
          previewController.addControllerListener(this);
          cmcPrieview = new CloseableModalController(getWindowControl(), translate("editor.preview.close"),
              previewController.getInitialComponent());
          cmcPrieview.insertHeaderCss();
          cmcPrieview.activate();
          listenTo(cmcPrieview);
         
        } else {
          getWindowControl().setWarning(translate("error.preview"));
        }
      } else if (cmd.equals(CMD_TOOLS_CHANGE_DELETE)) { // prepare delete

        GenericQtiNode clickedNode = menuTreeModel.getQtiNode(menuTree.getSelectedNodeId());
        String msg = "";
        if (clickedNode instanceof SectionNode) {
          if (QTIEditHelper.countSections(qtiPackage.getQTIDocument().getAssessment()) == 1) {
            // At least one section
            getWindowControl().setError(translate("error.atleastonesection"));
            return;
          }
          msg = translate("delete.section", clickedNode.getTitle());
        } else if (clickedNode instanceof ItemNode) {
          if (((SectionNode) clickedNode.getParent()).getChildCount() == 1) {
            // At least one item
            getWindowControl().setError(translate("error.atleastoneitem"));
            return;
          }
          msg = translate("delete.item", clickedNode.getTitle());
        }
        deleteDialog = activateYesNoDialog(ureq, null, msg, deleteDialog);
        deleteDialog.setUserObject(clickedNode);
        return;
      } else if (cmd.equals(CMD_TOOLS_CHANGE_MOVE)) {     
        //cannot move the last item
        GenericQtiNode clickedNode = menuTreeModel.getQtiNode(menuTree.getSelectedNodeId());
        if (clickedNode instanceof ItemNode && ((SectionNode) clickedNode.getParent()).getChildCount() == 1) {       
          getWindowControl().setError(translate("error.move.atleastoneitem"));
          return;
        }
        TreeNode selectedNode = menuTree.getSelectedNode();
        moveTree = new SelectionTree("moveTree", getTranslator());
        moveTree.setFormButtonKey("submit");
        insertTreeModel = new InsertItemTreeModel(menuTreeModel,
            (selectedNode instanceof SectionNode) ? InsertItemTreeModel.INSTANCE_ASSESSMENT : InsertItemTreeModel.INSTANCE_SECTION);
        moveTree.setTreeModel(insertTreeModel);
        moveTree.addListener(this);
        cmc = new CloseableModalController(getWindowControl(),translate("close"), moveTree, true, translate("title.move"));
        cmc.activate();
        listenTo(cmc);
       
      } else if (cmd.equals(CMD_TOOLS_CHANGE_COPY)) {
        copyTree = new SelectionTree("copyTree", getTranslator());
        copyTree.setFormButtonKey("submit");
        insertTreeModel = new InsertItemTreeModel(menuTreeModel, InsertItemTreeModel.INSTANCE_SECTION);
        copyTree.setTreeModel(insertTreeModel);
        copyTree.addListener(this);
        cmc = new CloseableModalController(getWindowControl(), translate("close"), copyTree, true, translate("title.copy"));
        cmc.activate();
        listenTo(cmc);
       
      } else if (cmd.startsWith(CMD_TOOLS_ADD_PREFIX)) { // add new object
        // fetch new object
        if (cmd.equals(CMD_TOOLS_ADD_SECTION)) {
          Section newSection = QTIEditHelper.createSection(getTranslator());
          Item newItem = QTIEditHelper.createSCItem(getTranslator());
          newSection.getItems().add(newItem);
          SectionNode scNode = new SectionNode(newSection, qtiPackage);
          ItemNode itemNode = new ItemNode(newItem, qtiPackage);
          scNode.addChild(itemNode);
          insertObject = scNode;
        } else if (cmd.equals(CMD_TOOLS_ADD_SINGLECHOICE)) insertObject = new ItemNode(QTIEditHelper.createSCItem(getTranslator()), qtiPackage);
        else if (cmd.equals(CMD_TOOLS_ADD_MULTIPLECHOICE)) insertObject = new ItemNode(QTIEditHelper.createMCItem(getTranslator()), qtiPackage);
        else if (cmd.equals(CMD_TOOLS_ADD_KPRIM)) insertObject = new ItemNode(QTIEditHelper.createKPRIMItem(getTranslator()), qtiPackage);
        else if (cmd.equals(CMD_TOOLS_ADD_FIB)) insertObject = new ItemNode(QTIEditHelper.createFIBItem(getTranslator()), qtiPackage);
        else if (cmd.equals(CMD_TOOLS_ADD_FREETEXT)) insertObject = new ItemNode(QTIEditHelper.createEssayItem(getTranslator()), qtiPackage);

        // prepare insert tree
        insertTree = new SelectionTree("insertTree", getTranslator());
        insertTree.setFormButtonKey("submit");
        if (cmd.equals(CMD_TOOLS_ADD_SECTION)) insertTreeModel = new InsertItemTreeModel(menuTreeModel,
            InsertItemTreeModel.INSTANCE_ASSESSMENT);
        else insertTreeModel = new InsertItemTreeModel(menuTreeModel, InsertItemTreeModel.INSTANCE_SECTION);
        insertTree.setTreeModel(insertTreeModel);
        insertTree.addListener(this);
        cmc = new CloseableModalController(getWindowControl(), translate("close"), insertTree, true, translate("title.add") );
        cmc.activate();
        listenTo(cmc);
      }
    } else if (source == deleteDialog) { // event from delete dialog
      if (DialogBoxUIFactory.isYesEvent(event)) { // yes, delete
        GenericQtiNode clickedNode = (GenericQtiNode) deleteDialog.getUserObject();
        //check if any media to delete as well
        if(clickedNode.getUnderlyingQTIObject() instanceof Item) {
          Item selectedItem = (Item)clickedNode.getUnderlyingQTIObject();
          deletableMediaFiles = QTIEditHelper.getDeletableMedia(qtiPackage.getQTIDocument(), selectedItem);
        }
                         
        // remove from underlying model
        ((GenericQtiNode) clickedNode.getParent()).removeQTIObjectAt(clickedNode.getPosition());
                       
        // remove from tree model
        clickedNode.removeFromParent();
        qtiPackage.serializeQTIDocument();
        menuTree.setSelectedNodeId(clickedNode.getParent().getIdent());
        event(ureq, menuTree, new Event(MenuTree.COMMAND_TREENODE_CLICKED));
        //ask user to confirm referenced media removal
        if(deletableMediaFiles!=null && deletableMediaFiles.size()>0) {         
          String msg = translate("delete.item.media", deletableMediaFiles.toString());
          deleteMediaDialog = activateYesNoDialog(ureq, null, msg, deleteMediaDialog);
        }   
      }
      // cleanup controller
      removeAsListenerAndDispose(deleteDialog);
      deleteDialog = null;
   
    } else if (source == deleteMediaDialog) { // event from deleteMediaDialog
      if (DialogBoxUIFactory.isYesEvent(event)) { // yes, delete         
        qtiPackage.removeMediaFiles(deletableMediaFiles);
        deleteMediaDialog = null;
        deletableMediaFiles = null;
      }
    } else if (event instanceof NodeBeforeChangeEvent) {
      NodeBeforeChangeEvent nce = (NodeBeforeChangeEvent) event;
      // active node changed some data
      String activeQtiNodeId = menuTree.getSelectedNodeId();
      GenericQtiNode activeQtiNode = menuTreeModel.getQtiNode(activeQtiNodeId);
      menuTree.setDirty(true); //force rerendering for ajax mode
      /*
       * mementos are only created in restricted mode
       */
      if (isRestrictedEdit()) {
        String key = nce.getSectionIdent() + "/" + nce.getItemIdent() + "/" + nce.getQuestionIdent() + "/" + nce.getResponseIdent();
        if (!history.containsKey(key)) {
          Memento memento = activeQtiNode.createMemento();
          history.put(key, memento);
          qtiPackage.serializeChangelog(history);
        }
      }

      /*
       * generate a Memento, store it for further use
       */
      if (nce.hasNewTitle) {
        // update the treemodel to reflect the change of the underlying qti node
        activeQtiNode.setMenuTitleAndAlt(nce.getNewTitle());
        main.contextPut("qtititle", menuTreeModel.getQtiRootNode().getAltText());
      }
    } else if (source == proceedRestricedEditDialog) {
      // restricted edit warning
      if (event == DialogController.EVENT_FIRSTBUTTON) {
        // remove dialog and continue with real content
        columnLayoutCtr.setCol3(mainPanel);
        columnLayoutCtr.hideCol1(false);
        columnLayoutCtr.hideCol2(false);
        removeAsListenerAndDispose(proceedRestricedEditDialog);
        proceedRestricedEditDialog = null;
      } else {
        // remove lock as user is not interested in restricted edit
        // and quick editor
        removeLocksAndExit(ureq);
      }
     
    } else if (source == cfc) {
      // dispose the content only controller we live in

      // remove modal dialog and cleanup exit process
      // modal dialog must be removed before fire DONE event
      // within the saveAndExit() call, otherwise the wrong
      // gui stack is popped see also OLAT-3056
      cmcExit.deactivate();
      removeAsListenerAndDispose(cmcExit);
      cmcExit = null;
     
      if (event == Event.CANCELLED_EVENT) {
        // nothing to do, back to editor     
      } else {
        QTIChangeLogMessage clm = new QTIChangeLogMessage(changeLog, chngMsgFrom.hasInformLearners());
        qtiPackage.commitChangelog(clm);
        StringBuilder traceMsg = new StringBuilder(chngMsgFrom.hasInformLearners() ? "Visible for ALL \n" : "Visible for GROUP only \n");
        Tracing.logAudit(traceMsg.append(changeLog).toString(), QTIEditorMainController.class);
        // save, remove locks and tmp files
        saveAndExit(ureq);
      }
     
      removeAsListenerAndDispose(cfc);
      cfc = null;
    }
  }

  /**
   * @see org.olat.core.gui.control.DefaultController#doDispose(boolean)
   */
  protected void doDispose() {
    // controlers disposed by BasicController:   
    // release activeSessionLock upon dispose
    if (activeSessionLock!=null && activeSessionLock.isSuccess()){
      CoordinatorManager.getCoordinator().getLocker().releaseLock(activeSessionLock);     
    }
  }

  private ToolController populateToolC() {
    ToolController tc = ToolFactory.createToolController(getWindowControl());
    // tools
    tc.addHeader(translate("tools.tools.header"));
    tc.addLink(CMD_TOOLS_PREVIEW, translate("tools.tools.preview"), CMD_TOOLS_PREVIEW, "b_toolbox_preview");
    tc.addLink(CMD_TOOLS_CLOSE_EDITOR, translate("tools.tools.closeeditor"), null, "b_toolbox_close");
    // if (!restrictedEdit) {
    tc.addHeader(translate("tools.add.header"));
    // adds within the qti document level
    tc.addLink(CMD_TOOLS_ADD_SECTION, translate("tools.add.section"), CMD_TOOLS_ADD_SECTION, "o_mi_qtisection");
    // adds within a section
    tc.addLink(CMD_TOOLS_ADD_SINGLECHOICE, translate("tools.add.singlechoice"), CMD_TOOLS_ADD_SINGLECHOICE, "o_mi_qtisc");
    tc.addLink(CMD_TOOLS_ADD_MULTIPLECHOICE, translate("tools.add.multiplechoice"), CMD_TOOLS_ADD_MULTIPLECHOICE, "o_mi_qtimc");
    if (!qtiPackage.getQTIDocument().isSurvey()) tc.addLink(CMD_TOOLS_ADD_KPRIM, translate("tools.add.kprim"), CMD_TOOLS_ADD_KPRIM,
        "o_mi_qtikprim");
    tc.addLink(CMD_TOOLS_ADD_FIB, translate("tools.add.cloze"), CMD_TOOLS_ADD_FIB, "o_mi_qtifib");
    if (qtiPackage.getQTIDocument().isSurvey()) tc.addLink(CMD_TOOLS_ADD_FREETEXT, translate("tools.add.freetext"),
        CMD_TOOLS_ADD_FREETEXT, "o_mi_qtiessay");
    // change
    tc.addHeader(translate("tools.change.header"));
    // change actions
    tc.addLink(CMD_TOOLS_CHANGE_DELETE, translate("tools.change.delete"), CMD_TOOLS_CHANGE_DELETE, "b_toolbox_delete");
    tc.addLink(CMD_TOOLS_CHANGE_MOVE, translate("tools.change.move"), CMD_TOOLS_CHANGE_MOVE, "b_toolbox_move");
    tc.addLink(CMD_TOOLS_CHANGE_COPY, translate("tools.change.copy"), CMD_TOOLS_CHANGE_COPY, "b_toolbox_copy");
    // }

    return tc;
  }

  /**
   * @see org.olat.core.gui.control.VetoableCloseController#requestForClose()
   */
  public boolean requestForClose() {   
    // enter save/discard dialog if not already in it
    if (cmcExit == null) {
      exitVC = createVelocityContainer("exitDialog");
      exitPanel = new Panel("exitPanel");
      exitPanel.setContent(exitVC);
      cmcExit = new CloseableModalController(getWindowControl(), translate("editor.preview.close"), exitPanel);
      cmcExit.activate();
      listenTo(cmcExit);
    }
    return false;
  }

  /**
   * helper method to create the message about qti resource stakeholders and
   * from where the qti resource is referenced.
   *
   * @return
   */
  private String createReferenceesMsg(UserRequest ureq) {
    /*
     * problems: A tries to reference this test, after test editor has been
     * started
     */
    changeEmail = new ContactMessage(ureq.getIdentity());

    RepositoryManager rm = RepositoryManager.getInstance();
    // the owners of this qtiPkg
    RepositoryEntry myEntry = rm.lookupRepositoryEntry(qtiPackage.getRepresentingResourceable(), false);
    SecurityGroup qtiPkgOwners = myEntry.getOwnerGroup();

    // add qti resource owners as group
    ContactList cl = new ContactList("qtiPkgOwners");
    cl.addAllIdentites(ManagerFactory.getManager().getIdentitiesOfSecurityGroup(qtiPkgOwners));
    changeEmail.addEmailTo(cl);

    StringBuilder result = new StringBuilder();
    result.append(translate("qti.restricted.leading"));
    for (Iterator iter = referencees.iterator(); iter.hasNext();) {
      ReferenceImpl element = (ReferenceImpl) iter.next();
      // FIXME:discuss:possible performance/cache problem
      if ("CourseModule".equals(element.getSource().getResourceableTypeName())) {
        ICourse course = CourseFactory.loadCourse(element.getSource().getResourceableId());

        // the course owners

        RepositoryEntry entry = rm.lookupRepositoryEntry(course, false);
        String courseTitle = course.getCourseTitle();
        SecurityGroup owners = entry.getOwnerGroup();
        List stakeHoldersIds = ManagerFactory.getManager().getIdentitiesOfSecurityGroup(owners);

        // add stakeholders as group
        cl = new ContactList(courseTitle);
        cl.addAllIdentites(stakeHoldersIds);
        changeEmail.addEmailTo(cl);

        StringBuilder stakeHolders = new StringBuilder();
        User user = ((Identity) stakeHoldersIds.get(0)).getUser();
        Locale loc = ureq.getLocale();
        stakeHolders.append(user.getProperty(UserConstants.FIRSTNAME, loc)).append(" ").append(user.getProperty(UserConstants.LASTNAME, loc));
        for (int i = 1; i < stakeHoldersIds.size(); i++) {
          user = ((Identity) stakeHoldersIds.get(i)).getUser();
          stakeHolders.append(", ").append(user.getProperty(UserConstants.FIRSTNAME, loc)).append(" ").append(user.getProperty(UserConstants.LASTNAME, loc));
        }

        CourseNode cn = course.getEditorTreeModel().getCourseNode(element.getUserdata());
        String courseNodeTitle = cn.getShortTitle();
        result.append(translate("qti.restricted.course", courseTitle));
        result.append(translate("qti.restricted.node", courseNodeTitle));
        result.append(translate("qti.restricted.owners", stakeHolders.toString()));
      }
    }
    return result.toString();
  }

  /**
   * helper method to create the change log message
   *
   * @return
   */
  private String createChangeMessage() {

    // FIXME:pb:break down into smaller pieces
    final StringBuilder result = new StringBuilder();
    if (isRestrictedEdit()) {
      Set keys = history.keySet();
      /*
       *
       */
      Visitor v = new Visitor() {
        /*
         * a history key is built as follows
         * sectionkey+"/"+itemkey+"/"+questionkey+"/"+responsekey
         */
        String sectionKey = null;
        String itemkey = null;
        int pos = 0;
        Map itemMap = new HashMap();

        public void visit(INode node) {
          if (node instanceof AssessmentNode) {
            AssessmentNode an = (AssessmentNode) node;
            String key = "null/null/null/null";
            if (history.containsKey(key)) {
              // some assessment top level data changed
              Memento mem = (Memento) history.get(key);
              result.append("---+ Changes in test " + formatVariable(startedWithTitle) + ":");
              result.append(an.createChangeMessage(mem));
            }
          } else if (node instanceof SectionNode) {
            SectionNode sn = (SectionNode) node;
            String tmpKey = ((Section) sn.getUnderlyingQTIObject()).getIdent();
            String key = tmpKey + "/null/null/null";
            if (history.containsKey(key)) {
              // some section only data changed
              Memento mem = (Memento) history.get(key);
              result.append("\n---++ Section " + formatVariable(sn.getAltText()) + " changes:");
              result.append(sn.createChangeMessage(mem));
            }
          } else if (node instanceof ItemNode) {
            ItemNode in = (ItemNode) node;
            SectionNode sn = (SectionNode) in.getParent();
            String parentSectkey = ((Section) ((SectionNode) in.getParent()).getUnderlyingQTIObject()).getIdent();
            Item item = (Item) in.getUnderlyingQTIObject();
            Question question = item.getQuestion();
            String itemKey = item.getIdent();
            String prefixKey = "null/" + itemKey;
            String questionIdent = question != null ? question.getQuestion().getId() : "null";
            String key = prefixKey + "/" + questionIdent + "/null";
            StringBuilder changeMessage = new StringBuilder();
            boolean hasChanges = false;

            if (!itemMap.containsKey(itemKey)) {
              Memento questMem = null;
              Memento respMem = null;
              if (history.containsKey(key)) {
                // question changed!
                questMem = (Memento) history.get(key);
                hasChanges = true;
              }
              // if(!hasChanges){
              // check if a response changed
              // new prefix for responses
              prefixKey += "/null/";
              // list contains org.olat.ims.qti.editor.beecom.objects.Response
              List responses = question != null ? question.getResponses() : null;
              if (responses != null && responses.size() > 0) {
                // check for changes in each response
                for (Iterator iter = responses.iterator(); iter.hasNext();) {
                  Response resp = (Response) iter.next();
                  if (history.containsKey(prefixKey + resp.getIdent())) {
                    // this response changed!
                    Memento tmpMem = (Memento) history.get(prefixKey + resp.getIdent());
                    if (respMem != null) {
                      respMem = respMem.getTimestamp() > tmpMem.getTimestamp() ? tmpMem : respMem;
                    } else {
                      hasChanges = true;
                      respMem = tmpMem;
                    }
                  }
                }
              }
              // }
              // output message
              if (hasChanges) {
                Memento mem = null;
                if (questMem != null && respMem != null) {
                  // use the earlier memento
                  mem = questMem.getTimestamp() > respMem.getTimestamp() ? respMem : questMem;
                } else if (questMem != null) {
                  mem = questMem;
                } else if (respMem != null) {
                  mem = respMem;
                }
                changeMessage.append(in.createChangeMessage(mem));
                itemMap.put(itemKey, itemKey);
                if (!parentSectkey.equals(sectionKey)) {
                  // either this item belongs to a new section or no section
                  // is active
                  result.append("\n---++ Section " + formatVariable(sn.getAltText()) + " changes:");
                  result.append("\n").append(changeMessage);
                  sectionKey = parentSectkey;
                } else {
                  result.append("\n").append(changeMessage);
                }
              }

            }
          }
        }

        private String formatVariable(String var) {
          if (StringHelper.containsNonWhitespace(var)) { return var; }
          return "[no entry]";
        }
      };
      TreeVisitor tv = new TreeVisitor(v, menuTreeModel.getRootNode(), false);
      tv.visitAll();
    }
    /*
     *
     */
    return result.toString();
  }

  /**
   * whether the editor runs in restricted mode or not.
   *
   * @return
   */
  public boolean isRestrictedEdit() {
    return restrictedEdit;
  }

  public boolean isLockedSuccessfully() {
    return lockEntry.isSuccess();
  }

}
TOP

Related Classes of org.olat.ims.qti.editor.QTIEditorMainController

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.