Package org.olat.modules.fo

Source Code of org.olat.modules.fo.ForumController$StickyColumnDescriptor

/**
* OLAT - Online Learning and Training<br>
* http://www.olat.orgrmform
* <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.modules.fo;

import java.io.File;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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.apache.commons.lang.StringEscapeUtils;
import org.olat.ControllerFactory;
import org.olat.core.commons.modules.bc.FileUploadController;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.persistence.PersistenceHelper;
import org.olat.core.commons.services.search.ui.SearchServiceUIFactory;
import org.olat.core.commons.services.search.ui.SearchServiceUIFactory.DisplayOption;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
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.table.BooleanColumnDescriptor;
import org.olat.core.gui.components.table.ColumnDescriptor;
import org.olat.core.gui.components.table.CustomCellRenderer;
import org.olat.core.gui.components.table.CustomCssCellRenderer;
import org.olat.core.gui.components.table.CustomRenderColumnDescriptor;
import org.olat.core.gui.components.table.DefaultColumnDescriptor;
import org.olat.core.gui.components.table.DefaultTableDataModel;
import org.olat.core.gui.components.table.GenericObjectArrayTableDataModel;
import org.olat.core.gui.components.table.Table;
import org.olat.core.gui.components.table.TableController;
import org.olat.core.gui.components.table.TableEvent;
import org.olat.core.gui.components.table.TableGuiConfiguration;
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.modal.DialogBoxController;
import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.User;
import org.olat.core.id.UserConstants;
import org.olat.core.id.context.BusinessControl;
import org.olat.core.id.context.BusinessControlFactory;
import org.olat.core.id.context.ContextEntry;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLATSecurityException;
import org.olat.core.logging.activity.ILoggingAction;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.service.ServiceFactory;
import org.olat.core.util.ConsumableBoolean;
import org.olat.core.util.Formatter;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.event.GenericEventListener;
import org.olat.core.util.notifications.ContextualSubscriptionController;
import org.olat.core.util.notifications.NotificationsManager;
import org.olat.core.util.notifications.PublisherData;
import org.olat.core.util.notifications.SubscriptionContext;
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.core.util.vfs.filters.VFSItemExcludePrefixFilter;
import org.olat.modules.fo.archiver.ForumArchiveManager;
import org.olat.modules.fo.archiver.formatters.ForumRTFFormatter;
import org.olat.user.DisplayPortraitController;
import org.olat.util.logging.activity.LoggingResourceable;

/**
* Description: <br>
* CREATE: - new thread (topmessage) -> ask ForumCallback 'mayOpenNewThread' -
* new message -> ask ForumCallback 'mayReplyMessage' <br>
* READ: - everybody may read every message <br>
* UPDATE: - who wrote a message may edit and save his message as long as it has
* no children. - if somebody want to edit a message of somebodyelse -> ask
* ForumCallback 'mayEditMessageAsModerator' <br>
* DELETE: - who wrote a message may delete his message as long as it has no
* children. - if somebody want to delete a message of somebodyelse -> ask
* ForumCallback 'mayDeleteMessageAsModerator' <br>
* <br>
* Notifications: notified when: <br>
* a new thread is opened <br>
* a new reply is given <br>
* a message has been edited <br>
* but not when a message has been deleted <br>
*
* @author Felix Jost
* @author Refactorings: Roman Haag, roman.haag@frentix.com, frentix GmbH
*/
public class ForumController extends BasicController implements GenericEventListener {

  protected static final String TINYMCE_EMPTYLINE_CODE = "<p>&nbsp;</p>"; //is pre/appended to quote message to allow writing inside.

  private static final String CMD_SHOWDETAIL = "showdetail";

  protected static final String GUI_PREFS_THREADVIEW_KEY = "forum.threadview.enabled";

  private ForumCallback focallback;

  Message currentMsg; // current Msg (in detailview), so we know after
  private Formatter f;

  private Collator collator;

  private Panel forumPanel;

  private VelocityContainer vcListTitles;
  private VelocityContainer vcEditMessage;
  private VelocityContainer vcThreadView;
  private VelocityContainer vcFilterView;
  private Link msgCreateButton;
  private Link archiveForumButton;
  private Link archiveThreadButton;
  private Link backLinkListTitles;
  private Link backLinkSearchListTitles;
  private List<Map<String, Object>> currentMessagesMap;
  private Link closeThreadButton;
  private Link openThreadButton;
  private Link hideThreadButton;
  private Link showThreadButton;
  private Link filterForUserButton;
 
  private FileUploadController fuCtr;
  private TableController allThreadTableCtr;
  private TableController singleThreadTableCtr;
  private DisplayPortraitController dpc;

  private GenericObjectArrayTableDataModel attdmodel;
  private ForumMessagesTableDataModel sttdmodel;
  private ForumManager fm;
  private Forum forum;
  private List<Message> msgs;
  private List<Message> threadMsgs;
  private Set<Long> rms; // all keys from messages that the user already read
  private boolean forumChangedEventReceived = false;

  private DialogBoxController yesno, yesNoSplit, archiveFoDiaCtr, archiveThDiaCtr;
  private TableController moveMessageTableCtr;
  List<Message> threadList;
  private CloseableModalController cmcMoveMsg;

  private SubscriptionContext subsContext;
  private ContextualSubscriptionController csc;

  private MessageEditController msgEditCtr;
  private ForumThreadViewModeController viewSwitchCtr;
  private Map<Long, Integer> msgDeepMap;
 
  private boolean searchMode = false;
  private FilterForUserController filterForUserCtr;
 
  private Controller searchController;


  /**
   * @param forum
   * @param focallback
   * @param ureq
   * @param wControl
   */
  ForumController(Forum forum, ForumCallback focallback, UserRequest ureq, WindowControl wControl) {
    super(ureq, wControl);
    this.forum = forum;
    this.focallback = focallback;
    addLoggingResourceable(LoggingResourceable.wrap(forum));
   
    f = Formatter.getInstance(ureq.getLocale());
    fm = ForumManager.getInstance();
       
    msgs = fm.getMessagesByForum(forum);

    collator = Collator.getInstance(ureq.getLocale());
    collator.setStrength(Collator.PRIMARY);

    forumPanel = new Panel("forumPanel");
    forumPanel.addListener(this);

    //create page
    vcListTitles = createVelocityContainer("list_titles");
   
    msgCreateButton = LinkFactory.createButtonSmall("msg.create", vcListTitles, this);
    archiveForumButton = LinkFactory.createButtonSmall("archive.forum", vcListTitles, this);
    filterForUserButton = LinkFactory.createButtonSmall("filter", vcListTitles, this);

    SearchServiceUIFactory searchServiceUIFactory = (SearchServiceUIFactory)ServiceFactory.getService(SearchServiceUIFactory.class);
    searchController = searchServiceUIFactory.createInputController(ureq, wControl, DisplayOption.STANDARD, null);
    listenTo(searchController);
    vcListTitles.put("search_input", searchController.getInitialComponent());
   
    // a list of titles of all messages in all threads
    vcListTitles.contextPut("security", focallback);

    // --- subscription ---
    subsContext = focallback.getSubscriptionContext();
    // if sc is null, then no subscription is desired
    if (subsContext != null) {
      // FIXME fj: implement subscription callback for group forums
      String businessPath = wControl.getBusinessControl().getAsString();
      String data = String.valueOf(forum.getKey());
      PublisherData pdata = new PublisherData(OresHelper.calculateTypeName(Forum.class), data, businessPath);
      csc = new ContextualSubscriptionController(ureq, getWindowControl(), subsContext, pdata);
      vcListTitles.put("subscription", csc.getInitialComponent());
    }

    TableGuiConfiguration tableConfig = new TableGuiConfiguration();
    tableConfig.setCustomCssClass("o_forum");
    tableConfig.setSelectedRowUnselectable(true);
    tableConfig.setDownloadOffered(false);
    tableConfig.setTableEmptyMessage(translate("forum.emtpy"));

    allThreadTableCtr = new TableController(tableConfig, ureq, getWindowControl(), getTranslator(), this)
    allThreadTableCtr.addColumnDescriptor(new CustomRenderColumnDescriptor("table.header.typeimg", 0, null,
        ureq.getLocale(), ColumnDescriptor.ALIGNMENT_LEFT, new MessageIconRenderer()));
    allThreadTableCtr.addColumnDescriptor(new StickyRenderColumnDescriptor("table.thread", 1, CMD_SHOWDETAIL, ureq.getLocale(),
        ColumnDescriptor.ALIGNMENT_LEFT, new StickyThreadCellRenderer()));   
    allThreadTableCtr.addColumnDescriptor(new StickyColumnDescriptor("table.userfriendlyname", 2, null, ureq.getLocale()));
    allThreadTableCtr.addColumnDescriptor(new StickyColumnDescriptor("table.lastModified", 3, null, ureq.getLocale(),
        ColumnDescriptor.ALIGNMENT_CENTER));       
      allThreadTableCtr.addColumnDescriptor(new StickyColumnDescriptor("table.unread", 4, null, ureq.getLocale(),
        ColumnDescriptor.ALIGNMENT_RIGHT));
      allThreadTableCtr.addColumnDescriptor(new StickyColumnDescriptor("table.total", 5, null, ureq.getLocale(),
        ColumnDescriptor.ALIGNMENT_RIGHT));
       
    singleThreadTableCtr = new TableController(tableConfig, ureq, getWindowControl(), getTranslator(), this);
    singleThreadTableCtr.addColumnDescriptor(new ThreadColumnDescriptor("table.title", 0, CMD_SHOWDETAIL));
    singleThreadTableCtr.addColumnDescriptor(new DefaultColumnDescriptor("table.userfriendlyname", 1, null, ureq.getLocale()));
    singleThreadTableCtr.addColumnDescriptor(new DefaultColumnDescriptor("table.modified", 2, null, ureq.getLocale(),
        ColumnDescriptor.ALIGNMENT_CENTER));
    singleThreadTableCtr
        .addColumnDescriptor(new BooleanColumnDescriptor("table.header.state", 3, "", translate("table.row.new")));
   
    rms = getReadSet(ureq.getIdentity()); // here we fetch which messages had   
       
    // been read by the user
    threadList = prepareListTitles(msgs);
   
    //precalulate message deepness
    precalcMessageDeepness(msgs);
   
    // Default view
    forumPanel = putInitialPanel(vcListTitles);
    // jump to either the forum or the folder if the business-launch-path says so.
    BusinessControl bc = getWindowControl().getBusinessControl();
    ContextEntry ce = bc.popLauncherContextEntry();
    if ( ce != null) { // a context path is left for me
      if (isLogDebugEnabled()) logDebug("businesscontrol (for further jumps) would be: ", bc.toString());
      OLATResourceable ores = ce.getOLATResourceable();
      if (isLogDebugEnabled()) logDebug("OLATResourceable= " , ores.toString());
      Long resId = ores.getResourceableId();
      if (resId.longValue() != 0) {
        if (isLogDebugEnabled()) logDebug("messageId=" , ores.getResourceableId().toString());
        currentMsg = fm.findMessage(ores.getResourceableId());
        if (currentMsg != null) {
          showThreadView(ureq, currentMsg);
          scrollToCurrentMessage();         
        } else {
          // message not found, do nothing. Load normal start screen
          logDebug("Invalid messageId=" , ores.getResourceableId().toString());
        }
      } else {
        //FIXME:chg: Should not happen, occurs when course-node are called
        if (isLogDebugEnabled()) logDebug("Invalid messageId=" , ores.getResourceableId().toString());
      }
    }

    // Register for forum events
    CoordinatorManager.getCoordinator().getEventBus().registerFor(this, ureq.getIdentity(), forum);
   
    //filter for user
    vcFilterView = createVelocityContainer("filter_view");
  }

  /**
   * If event received, must update thread overview.
   */
  private boolean checkForumChangedEventReceived() {
    if(forumChangedEventReceived) {
      this.showThreadOverviewView();
      forumChangedEventReceived = false;
      return true;
    }
    return false;
  }
 
  /**
   * @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(checkForumChangedEventReceived()) {
      return;
    }
    String cmd = event.getCommand();
    if (source == msgCreateButton){
      showNewThreadView(ureq);
    } else if (source == archiveForumButton){
      archiveFoDiaCtr = activateYesNoDialog(ureq, null, translate("archive.forum.dialog"), archiveFoDiaCtr);
    } else if (source == filterForUserButton) {
      showFilterForUserView(ureq);
    } else if (source == backLinkListTitles){ // back link list titles
      if(searchMode && filterForUserCtr != null && filterForUserCtr.isShowResults()) {
        forumPanel.setContent(vcFilterView);
      } else {
        searchMode = false;
        showThreadOverviewView();
      }
    } else if (source == backLinkSearchListTitles) {
      if(filterForUserCtr != null && filterForUserCtr.isShowResults()) {
        filterForUserCtr.setShowSearch();
      } else {
        showThreadOverviewView();
      }
    } else if (source == archiveThreadButton){
      archiveThDiaCtr = activateYesNoDialog(ureq, null, translate("archive.thread.dialog"), archiveThDiaCtr);
    } else if (source == closeThreadButton) {
      closeThread(ureq, currentMsg, true);
    } else if (source == openThreadButton) {
      closeThread(ureq, currentMsg, false);
    } else if (source == hideThreadButton) {
      hideThread(ureq, currentMsg, true);
    } else if (source == showThreadButton) {
      hideThread(ureq, currentMsg, false);   
    else if (source == vcThreadView) {
      if (cmd.startsWith("attachment_")) {
        Map<String, Object> messageMap = getMessageMapFromCommand(ureq.getIdentity(), cmd);
        Long messageId = (Long) messageMap.get("id");
        currentMsg = fm.loadMessage(messageId);
        doAttachmentDelivery(ureq, cmd, messageMap);
      }
    } else if (source instanceof Link) {
      // all other commands have the message value map id coded into the
      // the command name. get message from this id
      Link link = (Link) source;
      String command = link.getCommand();
      Map<String, Object> messageMap = getMessageMapFromCommand(ureq.getIdentity(), command);
      Long messageId = (Long) messageMap.get("id");
     
      Message updatedMessage = fm.findMessage(messageId);
      if (updatedMessage!=null) {
        currentMsg = updatedMessage;
        // now dispatch the commands
        if (command.startsWith("qt_")) {
          showReplyView(ureq, true, currentMsg);
        } else if (command.startsWith("rp_")) {
          showReplyView(ureq, false, currentMsg);
        } else if (command.startsWith("dl_")) {
          showDeleteMessageView(ureq);
        } else if (command.startsWith("ed_")) {
          showEditMessageView(ureq);
        else if (command.startsWith("split_")) {
          showSplitThreadView(ureq);
        } else if (command.startsWith("move_")) {
          showMoveMessageView(ureq);
        }
      } else if (currentMsg != null) {
        showInfo("header.cannoteditmessage");
        showThreadOverviewView();
      }     
    }  
  }
 
  /**
   * @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)
   */
  public void event(UserRequest ureq, Controller source, Event event) {
    if(checkForumChangedEventReceived()) {
      return;
    }
    if (source == yesno) {
      if (DialogBoxUIFactory.isYesEvent(event)) { // yes
        doDeleteMessage(ureq);
        if (currentMsg.getThreadtop() == null) {
          showThreadOverviewView(); // was last message in thread
        } else {
          showThreadView(ureq, currentMsg.getThreadtop());
        }
      }
    } else if (source == archiveFoDiaCtr) {
      if (DialogBoxUIFactory.isYesEvent(event)) { // ok
        doArchiveForum(ureq);
        showInfo("archive.forum.successfully");
      }
    } else if (source == archiveThDiaCtr) {
      if (DialogBoxUIFactory.isYesEvent(event)) { // ok
        doArchiveThread(ureq, currentMsg);
        showInfo("archive.thread.successfully");
      }
    } else if (source == singleThreadTableCtr) {
      if (event.getCommand().equals(Table.COMMANDLINK_ROWACTION_CLICKED)) {
        // user has selected a message title from singleThreadTable
        // -> display message details and below all messages with the same
        // topthread_id as the selected one
        TableEvent te = (TableEvent) event;
        String actionid = te.getActionId();
        if (actionid.equals(CMD_SHOWDETAIL)) {
          int rowid = te.getRowId();
          Message m = (Message) sttdmodel.getObjects().get(rowid);
          showThreadView(ureq, m);
          ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_READ, getClass(), LoggingResourceable.wrap(currentMsg));
        }
      }
    } else if (source == allThreadTableCtr) {
      if (event.getCommand().equals(Table.COMMANDLINK_ROWACTION_CLICKED)) {
        TableEvent te = (TableEvent) event;
        String actionid = te.getActionId();
        if (actionid.equals(CMD_SHOWDETAIL)) {
          int rowid = te.getRowId();
          Object[] msgWrapper = (Object[]) attdmodel.getObjects().get(rowid);
          int size = msgWrapper.length;
          Message m = (Message) msgWrapper[size-1];         
          showThreadView(ureq, m);
        }
      }
    } else if (source == yesNoSplit) {
      // the dialogbox is already removed from the gui stack - do not use getWindowControl().pop(); to remove dialogbox
      if (DialogBoxUIFactory.isYesEvent(event)){
        splitThread(ureq);       
      }
    } else if (source == moveMessageTableCtr) {
      TableEvent te = (TableEvent)event;
      Message topMsg = threadList.get(te.getRowId());
      moveMessage(ureq, topMsg);
    }
   
    // events from messageEditor
    else if (source == msgEditCtr){
      //persist changed or new message
      if (event == Event.DONE_EVENT){
        if (msgEditCtr.getLastEditModus().equals(MessageEditController.EDITMODE_NEWTHREAD)){
          // creation done -> save
          doNewThread(ureq);
          msgEditCtr.persistTempUploadedFiles(currentMsg);
        } else if (msgEditCtr.getLastEditModus().equals(MessageEditController.EDITMODE_EDITMSG)){
          // edit done -> save
          Message updatedMessage = fm.findMessage(currentMsg.getKey());
          if(updatedMessage!=null) {
            doEditMessage(ureq);
            //file persisting is done already, as a msg-key was known during edit.
          else {
            showInfo("header.cannoteditmessage");
          }
        } else if (msgEditCtr.getLastEditModus().equals(MessageEditController.EDITMODE_REPLYMSG)){
          // reply done -> save
          Message updatedMessage = fm.findMessage(currentMsg.getKey());
          if(updatedMessage!=null) { 
            doReplyMessage(ureq);
            msgEditCtr.persistTempUploadedFiles(currentMsg);
          } else {
            showInfo("header.cannotsavemessage");
          }
        }
      //show thread view after all kind of operations
      showThreadView(ureq, currentMsg);
     
      //editor was canceled
      } else if (event == Event.CANCELLED_EVENT) {
        // back to 'list all titles' if canceled on new thread
        if (msgEditCtr.getLastEditModus().equals(MessageEditController.EDITMODE_NEWTHREAD)){
          forumPanel.setContent(vcListTitles);
        } else {
          showThreadView(ureq, currentMsg);
        }
      }
     
    } else if (source == viewSwitchCtr){
      if (event == Event.CHANGED_EVENT){
        //viewmode has been switched, so change view:
        showThreadView(ureq, currentMsg);
      }
    } else if (source == filterForUserCtr) {
      if(event instanceof OpenMessageInThreadEvent) {
        OpenMessageInThreadEvent openEvent = (OpenMessageInThreadEvent)event;
        Message selectedMsg = openEvent.getMessage();
        showThreadView(ureq,selectedMsg);
        scrollToCurrentMessage();
      }
    }
  }
 
  /**
   *
   * @see org.olat.core.util.event.GenericEventListener#event(org.olat.core.gui.control.Event)
   */
  public void event(Event event) {
    if(event instanceof ForumChangedEvent) {
      forumChangedEventReceived = true;     
    }   
  }
 
 
  ////////////////////////////////////////
   // Application logic, do sth...
  ////////////////////////////////////////
 
 

  private void doEditMessage(UserRequest ureq) {
    //after editing message
    boolean userIsMsgCreator = ureq.getIdentity().getKey().equals(currentMsg.getCreator().getKey());
    boolean children = fm.hasChildren(currentMsg);

    if (focallback.mayEditMessageAsModerator() || ((userIsMsgCreator) && (children == false))) {

      currentMsg = msgEditCtr.getMessageBackAfterEdit();
      currentMsg.setModifier(ureq.getIdentity());     
     
      fm.updateMessage(currentMsg, null);
      // if notification is enabled -> notify the publisher about news
      if (subsContext != null) {
        NotificationsManager.getInstance().markPublisherNews(subsContext, ureq.getIdentity());
      }

      // do logging
      ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_EDIT, getClass(),
          LoggingResourceable.wrap(currentMsg));

    } else {
      showWarning("may.not.save.msg.as.author");
      forumPanel.setContent(vcEditMessage);
    }
  }

  private void doReplyMessage(UserRequest ureq) {
    //after replying to a message
    Message m = fm.createMessage();
    m = msgEditCtr.getMessageBackAfterEdit();

    fm.replyToMessage(m, ureq.getIdentity(), currentMsg);
    DBFactory.getInstance().intermediateCommit();
    // if notification is enabled -> notify the publisher about news
    if (subsContext != null) {
      NotificationsManager.getInstance().markPublisherNews(subsContext, ureq.getIdentity());
    }
    currentMsg = m;
    markRead(m, ureq.getIdentity());
   
    // do logging
    ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_REPLY_MESSAGE_CREATE, getClass(),
        LoggingResourceable.wrap(currentMsg));
  }

  private void doNewThread(UserRequest ureq) {
    //after creating a thread
    Message m = fm.createMessage();
    m = msgEditCtr.getMessageBackAfterEdit();
   
    if (!focallback.mayOpenNewThread()) throw new OLATSecurityException("not allowed to open new thread in forum " + forum.getKey());
    // open a new thread
    fm.addTopMessage(ureq.getIdentity(), forum, m);
    // if notification is enabled -> notify the publisher about news
    if (subsContext != null) {
      NotificationsManager.getInstance().markPublisherNews(subsContext, ureq.getIdentity());
    }
    currentMsg = m;
    markRead(m, ureq.getIdentity());

    // do logging
    addLoggingResourceable(LoggingResourceable.wrap(m));
    ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_CREATE, getClass());
  }

  private void doAttachmentDelivery(UserRequest ureq, String cmd, Map<String, Object> messageMap) {
    // user selected one attachment from the attachment list
    int pos = Integer.parseInt(cmd.substring(cmd.indexOf("_") + 1, cmd.lastIndexOf("_")));
    // velocity counter starts at 1
    List<VFSItem> attachments = new ArrayList<VFSItem>();
    attachments.addAll((Collection<VFSItem>) messageMap.get("attachments"));
    VFSItem vI = attachments.get(pos - 1);
    VFSLeaf vl = (VFSLeaf) vI;
    ureq.getDispatchResult().setResultingMediaResource(new VFSMediaResource(vl));
  }

  private void doDeleteMessage(UserRequest ureq) {
    boolean children = fm.hasChildren(currentMsg);
    boolean hasParent = currentMsg.getParent() != null;
    boolean userIsMsgCreator = ureq.getIdentity().getKey().equals(currentMsg.getCreator().getKey());
    if (focallback.mayDeleteMessageAsModerator() || (userIsMsgCreator && children == false)) {
      fm.deleteMessageTree(forum.getKey(), currentMsg);
      showInfo("deleteok");
      // do logging
      if(hasParent) {
        ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_DELETE, getClass(),
            LoggingResourceable.wrap(currentMsg));
      } else {
        ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_THREAD_DELETE, getClass(),
            LoggingResourceable.wrap(currentMsg));
      }
    } else {
      showWarning("may.not.delete.msg.as.author");
    }
  }

  private void doArchiveForum(UserRequest ureq) {
    ForumRTFFormatter rtff = new ForumRTFFormatter(getArchiveContainer(ureq), false);
    ForumArchiveManager fam = ForumArchiveManager.getInstance();
    fam.applyFormatter(rtff, forum.getKey().longValue(), focallback);
  }

  private void doArchiveThread(UserRequest ureq, Message currMsg) {
    Message m = currMsg.getThreadtop();
    Long topMessageId = (m == null) ? currMsg.getKey() : m.getKey();

    ForumRTFFormatter rtff = new ForumRTFFormatter(getArchiveContainer(ureq), true);
    ForumArchiveManager fam = ForumArchiveManager.getInstance();
    fam.applyFormatterForOneThread(rtff, forum.getKey().longValue(), topMessageId.longValue());
  }
 
 
 
  ////////////////////////////////////////
   // Presentation
  ////////////////////////////////////////
 
  private void showFilterForUserView(UserRequest ureq) {
    searchMode = true;
    backLinkSearchListTitles = LinkFactory.createCustomLink("backLinkLT", "back", "listalltitles", Link.LINK_BACK, vcFilterView, this);
   
    if(filterForUserCtr != null) {
      removeAsListenerAndDispose(filterForUserCtr);
    }
   
    filterForUserCtr = new FilterForUserController(ureq, getWindowControl(), forum);
    listenTo(filterForUserCtr);
    vcFilterView.put("filterForUser", filterForUserCtr.getInitialComponent());
    forumPanel.setContent(vcFilterView);
  }

  private void showThreadOverviewView() {
    // user has clicked on button 'list all message titles'
    // -> display allThreadTable
    msgs = fm.getMessagesByForum(forum);
    prepareListTitles(msgs);
    forumPanel.setContent(vcListTitles);
    // do logging
    ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_LIST, getClass());
 
 
  private void showNewThreadView(UserRequest ureq) {
    // user has clicked on button 'open new thread'.
    Message m = fm.createMessage();
    msgEditCtr = new MessageEditController(ureq, getWindowControl(), focallback, m, null);
    listenTo(msgEditCtr);

    forumPanel.setContent(msgEditCtr.getInitialComponent());
  }
 
 
  private void showEditMessageView(UserRequest ureq) {
    // user has clicked on button 'edit'
    boolean userIsMsgCreator = ureq.getIdentity().getKey().equals(currentMsg.getCreator().getKey());
    boolean children = fm.hasChildren(currentMsg);
    if (focallback.mayEditMessageAsModerator() || ((userIsMsgCreator) && (children == false))) {
      // user is forum-moderator -> may edit every message on every level
      // or user is author of the current message and it has still no
      // children
      msgEditCtr = new MessageEditController(ureq, getWindowControl(), focallback, currentMsg, null);
      listenTo(msgEditCtr);

      forumPanel.setContent(msgEditCtr.getInitialComponent());
    } else if ((userIsMsgCreator) && (children == true)) {
      // user is author of the current message but it has already at least
      // one child
      showWarning("may.not.save.msg.as.author");
    } else {
      // user isn't author of the current message
      showInfo("may.not.edit.msg");
    }
  }

  private void showDeleteMessageView(UserRequest ureq) {
    // user has clicked on button 'delete'
    // -> display modal dialog 'Do you really want to delete this message?'
    // 'yes': back to allThreadTable, 'no' back to messageDetails
    int numOfChildren = countNumOfChildren(currentMsg, threadMsgs);
    boolean children = fm.hasChildren(currentMsg);
    boolean userIsMsgCreator = ureq.getIdentity().getKey().equals(currentMsg.getCreator().getKey());

    if (focallback.mayDeleteMessageAsModerator()) {
      // user is forum-moderator -> may delete every message on every level
      if (numOfChildren == 0) {
        yesno = activateYesNoDialog(ureq, null, translate("reallydeleteleaf", currentMsg.getTitle()), yesno);
      } else if (numOfChildren == 1) {
        yesno = activateYesNoDialog(ureq, null, translate("reallydeletenode1", currentMsg.getTitle()), yesno);
      } else {
        yesno = activateYesNoDialog(ureq, null, getTranslator().translate("reallydeletenodeN", new String[] { currentMsg.getTitle(), Integer.toString(numOfChildren) }), yesno);
      }
    } else if ((userIsMsgCreator) && (children == false)) {
      // user may delete his own message if it has no children
      yesno = activateYesNoDialog(ureq, null, translate("reallydeleteleaf", currentMsg.getTitle()), yesno);
    } else if ((userIsMsgCreator) && (children == true)) {
      // user may not delete his own message because it has at least one child
      showWarning("may.not.delete.msg.as.author");
    } else {
      // user isn't author of the current message
      showInfo("may.not.delete.msg");
    }
  }

  private void showReplyView(UserRequest ureq, boolean quote, Message parent) {
    // user has clicked on button 'reply'
    if (focallback.mayReplyMessage()) {

      Message quotedMessage = fm.createMessage();
      String reString = "";
      if(parent!=null && parent.getThreadtop()==null) {
        //add reString only for the first answer
        reString = translate("msg.title.re");
      }     
      quotedMessage.setTitle(reString + currentMsg.getTitle());
      if (quote) {
        // load message to form as quotation       
        StringBuilder quoteSB = new StringBuilder();
        quoteSB.append(TINYMCE_EMPTYLINE_CODE);
        quoteSB.append("<div class=\"b_quote_wrapper\"><div class=\"b_quote_author mceNonEditable\">");
        String date = f.formatDateAndTime(currentMsg.getCreationDate());
        User creator = currentMsg.getCreator().getUser();
        String creatorName = creator.getProperty(UserConstants.FIRSTNAME, ureq.getLocale()) + " " + creator.getProperty(UserConstants.LASTNAME, ureq.getLocale());
        quoteSB.append(getTranslator().translate("msg.quote.intro", new String[]{date, creatorName}));
        quoteSB.append("</div><blockquote class=\"b_quote\">");
        quoteSB.append(currentMsg.getBody());
        quoteSB.append("</blockquote></div>");
        quoteSB.append(TINYMCE_EMPTYLINE_CODE);
        quotedMessage.setBody(quoteSB.toString());
      }

      msgEditCtr = new MessageEditController(ureq, getWindowControl(), focallback, currentMsg, quotedMessage);
      listenTo(msgEditCtr);     

      forumPanel.setContent(msgEditCtr.getInitialComponent());
    } else {
      showInfo("may.not.reply.msg");
    }
  }

  private void showSplitThreadView(UserRequest ureq) {   
    if (focallback.mayEditMessageAsModerator()) {
      // user is forum-moderator -> may delete every message on every level
      int numOfChildren = countNumOfChildren(currentMsg, threadMsgs);
     
      // provide yesNoSplit as argument, this ensures that dc is disposed before newly created
      yesNoSplit = activateYesNoDialog(ureq, null, getTranslator().translate("reallysplitthread", new String[] { currentMsg.getTitle(), Integer.toString(numOfChildren) }), yesNoSplit);

      //activateYesNoDialog means that this controller listens to it, and dialog is shown on screen.
      //nothing further to do here!
      return;
    }
  }
 
  private void showMoveMessageView(UserRequest ureq) {
    if (focallback.mayEditMessageAsModerator()) {
      //prepare the table data
      msgs = fm.getMessagesByForum(forum);
      threadList = prepareListTitles(msgs);
      DefaultTableDataModel tdm = new DefaultTableDataModel(threadList) {
       
          @Override
          public Object getValueAt(int row, int col) {
            Message m = threadList.get(row);
            boolean isSource = m.equalsByPersistableKey(currentMsg.getThreadtop());
            switch (col) {
              case 0:
                String title = StringEscapeUtils.escapeHtml(m.getTitle()).toString();
                return title;
              case 1:
                if (m.getCreator().getStatus().equals(Identity.STATUS_DELETED)) {
                  return m.getCreator().getName();
                } else {
                  String last = m.getCreator().getUser().getProperty(UserConstants.LASTNAME, getLocale());
                  String first = m.getCreator().getUser().getProperty(UserConstants.FIRSTNAME, getLocale());
                  return last + " " + first;
                }
              case 2 :
                Date mod = m.getLastModified();
                return mod;
              case 3:
                return !isSource;

              default: return "error";
            }
          }
       
          @Override
          public int getColumnCount() {
            return 4;
          }
      };
     
      //prepare the table config
      TableGuiConfiguration tableConfig = new TableGuiConfiguration();
      tableConfig.setCustomCssClass("o_forum");
      tableConfig.setSelectedRowUnselectable(true);
      tableConfig.setDownloadOffered(false);
      tableConfig.setTableEmptyMessage(translate("forum.emtpy"));
     
      //prepare the table controller
      moveMessageTableCtr = new TableController(tableConfig, ureq, getWindowControl(), getTranslator(), this);
      moveMessageTableCtr.addColumnDescriptor(true, new DefaultColumnDescriptor("table.thread", 0, null, ureq.getLocale()));
      moveMessageTableCtr.addColumnDescriptor(true, new DefaultColumnDescriptor("table.userfriendlyname", 1, null, ureq.getLocale()));
      moveMessageTableCtr.addColumnDescriptor(true, new DefaultColumnDescriptor("table.lastModified", 2, null, ureq.getLocale()));       
      moveMessageTableCtr.addColumnDescriptor(true, new BooleanColumnDescriptor("table.choose", 3, "move", translate("table.choose"), translate("table.source")));
      moveMessageTableCtr.setTableDataModel(tdm);
     
      //push the modal dialog with the table as content
      cmcMoveMsg = new CloseableModalController(getWindowControl(), "close", moveMessageTableCtr.getInitialComponent());
      cmcMoveMsg.activate();
    }
  }
 
  private void showThreadView(UserRequest ureq, Message m) {
   
    adjustBusinessControlPath(ureq, m);
   
    // remove old messages from velocity and dispose controllers
    disposeCurrentMessages();
    // now fetch current thread
    Message threadTopM = m.getThreadtop();
    currentMsg = m; // in some cases already set, but set current message anyway
    threadMsgs = fm.getThread(threadTopM == null ? m.getKey() : threadTopM.getKey());
    precalcMessageDeepness(threadMsgs);
    // for simplicity no reuse of container, always create new one
    vcThreadView = createVelocityContainer("threadview");
    // to access the function renderFileIconCssClass(..) which is accessed in threadview.html using $myself.renderFileIconCssClass
    vcThreadView.contextPut("myself", this);
   
    backLinkListTitles = LinkFactory.createCustomLink("backLinkLT", "back", "listalltitles", Link.LINK_BACK, vcThreadView, this);
    archiveThreadButton = LinkFactory.createButtonSmall("archive.thread", vcThreadView, this);
       
    boolean isClosed = Status.getStatus(m.getStatusCode()).isClosed();
    vcThreadView.contextPut("isClosed",isClosed);
    if(!isClosed) {
      closeThreadButton = LinkFactory.createButtonSmall("close.thread", vcThreadView, this);
    } else {
      openThreadButton = LinkFactory.createButtonSmall("open.thread", vcThreadView, this);
   
    boolean isHidden = Status.getStatus(m.getStatusCode()).isHidden();
    vcThreadView.contextPut("isHidden",isHidden);   
    if(!isHidden) {
      hideThreadButton = LinkFactory.createButtonSmall("hide.thread", vcThreadView, this);
    } else {
      showThreadButton = LinkFactory.createButtonSmall("show.thread", vcThreadView, this);
   

    //allow to set thread-viewmode prefs and get actual ones
    viewSwitchCtr = new ForumThreadViewModeController(ureq,getWindowControl());
    listenTo(viewSwitchCtr);
    vcThreadView.put("threadViewSwitch", viewSwitchCtr.getInitialComponent());
   
    vcThreadView.contextPut("showThreadTable", Boolean.FALSE);
    vcThreadView.contextPut("threadMode", Boolean.FALSE);
    vcThreadView.contextPut("msgDeepMap", msgDeepMap);
   
    // add all messages that are needed
    currentMessagesMap = new ArrayList<Map<String, Object>>(threadMsgs.size());
    if (viewSwitchCtr.isViewModeFlat(ureq)) {             
      // all messages in flat view
      List<Message> orderedMessages = new ArrayList<Message>();
           
      orderedMessages.addAll(threadMsgs);
      orderedMessages = threadMsgs;
      Collections.sort(orderedMessages);
     
      int msgNum = 0;
      Iterator<Message> iter = orderedMessages.iterator();     
      while (iter.hasNext()) {
        Message msg = iter.next();
        // add message and mark as read
        addMessageToCurrentMessagesAndVC(ureq, msg, vcThreadView, currentMessagesMap, msgNum);
        msgNum++;
      }
      // do logging
      ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_THREAD_READ, getClass(), LoggingResourceable.wrap(currentMsg));
    } else if (viewSwitchCtr.isViewModeMessage(ureq)){
      // single message in thread view, add message and mark as read
      addMessageToCurrentMessagesAndVC(ureq, m, vcThreadView, currentMessagesMap, 0);
      // init single thread list and append
      sttdmodel = new ForumMessagesTableDataModel(threadMsgs, rms);
      sttdmodel.setLocale(ureq.getLocale());
      singleThreadTableCtr.setTableDataModel(sttdmodel);
      int position = PersistenceHelper.indexOf(threadMsgs, currentMsg);
      singleThreadTableCtr.setSelectedRowId(position);     
      vcThreadView.contextPut("showThreadTable", Boolean.TRUE);
      vcThreadView.put("singleThreadTable", singleThreadTableCtr.getInitialComponent());
      // do logging
      ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_READ, getClass(), LoggingResourceable.wrap(currentMsg));
     
    } else {
      //real threaded view with indent
      vcThreadView.contextPut("threadMode", Boolean.TRUE);
      List<Message> orderedMessages = new ArrayList<Message>();
      orderMessagesThreaded(threadMsgs, orderedMessages, (threadTopM == null ? m : threadTopM));
      // all messages in thread view
      //Iterator iter = threadMsgs.iterator();
      Iterator<Message> iter = orderedMessages.iterator();     
     
      int msgNum = 0;
      while (iter.hasNext()) {
        Message msg = iter.next();
        // add message and mark as read
        addMessageToCurrentMessagesAndVC(ureq, msg, vcThreadView, currentMessagesMap, msgNum);
        msgNum++;
      }
      // do logging
      ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_THREAD_READ, getClass(), LoggingResourceable.wrap(m));
    }
    vcThreadView.contextPut("messages", currentMessagesMap);
    // add security callback
    vcThreadView.contextPut("security", focallback);
    forumPanel.setContent(vcThreadView);
  }

  private void scrollToCurrentMessage() {
    // Scroll to message, but only the first time the view is rendered
    if (currentMsg.getThreadtop() == null || currentMessagesMap.size() == 1) {
      vcThreadView.contextPut("goToMessage", Boolean.FALSE);
    } else {
      vcThreadView.contextPut("goToMessage", new ConsumableBoolean(true));
      vcThreadView.contextPut("goToMessageId", currentMsg.getKey());           
    }
  }
 
 
  ////////////////////////////////////////
   // Helper Methods / Classes
  ////////////////////////////////////////

 
  private void precalcMessageDeepness(List<Message> msgList){
    msgDeepMap = new HashMap<Long, Integer>();
    for (Message message : msgList) {
      int deepness = messageDeepness(message, 0);
      msgDeepMap.put(message.getKey(), deepness);
    }   
  }
 
  private int messageDeepness(Message msg, int deep){
    if (deep > 20) return 20;
    if (msg.getParent()==null) return deep;
    else {
      int newDeep = deep + 1;
      return messageDeepness(msg.getParent(), newDeep);
    }
  }
 
 
  private void addMessageToCurrentMessagesAndVC(UserRequest ureq, Message m, VelocityContainer vcContainer, List<Map<String, Object>> allList, int msgCount) {
    // all values belonging to a message are stored in this map
    // these values can be accessed in velocity. make sure you clean up
    // everything
    // you create here in disposeCurrentMessages()!
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("id", m.getKey());
   
   
    if (rms.contains(m.getKey())) {
      // already read
      map.put("newMessage", Boolean.FALSE);
    } else {
      // mark now as read
      markRead(m, ureq.getIdentity());
      map.put("newMessage", Boolean.TRUE);
    }
    // add some data now
    Date creationDate = m.getCreationDate();
    Identity modifier = m.getModifier();
    if (modifier != null) {
      map.put("isModified", Boolean.TRUE);
      map.put("modfname", modifier.getUser().getProperty(UserConstants.FIRSTNAME, ureq.getLocale()));
      map.put("modlname", modifier.getUser().getProperty(UserConstants.LASTNAME, ureq.getLocale()));
    } else {
      map.put("isModified", Boolean.FALSE);
    }
    map.put("title", m.getTitle());
    map.put("body", m.getBody());
    map.put("date", f.formatDateAndTime(creationDate));
    Identity creator = m.getCreator();
    map.put("firstname", Formatter.truncate(creator.getUser().getProperty(UserConstants.FIRSTNAME, ureq.getLocale()),18)); //keeps the first 15 chars
    map.put("lastname", Formatter.truncate(creator.getUser().getProperty(UserConstants.LASTNAME, ureq.getLocale()),18));

//    map.put("username", Formatter.truncate(creator.getName(),18));
   
    map.put("modified", f.formatDateAndTime(m.getLastModified()));
    // message attachments
    OlatRootFolderImpl msgContainer = fm.getMessageContainer(forum.getKey(), m.getKey());
    map.put("messageContainer", msgContainer);
    List<VFSItem> attachments = new ArrayList<VFSItem>(msgContainer.getItems(new VFSItemExcludePrefixFilter(MessageEditController.ATTACHMENT_EXCLUDE_PREFIXES)));
//    List attachments = msgContainer.getItems();
    map.put("attachments", attachments);
    if (attachments == null || attachments.size() == 0) map.put("hasAttachments", Boolean.FALSE);
    else map.put("hasAttachments", Boolean.TRUE);
    // number of children and modify/delete permissions
    int numOfChildren;
    numOfChildren = countNumOfChildren(m, threadMsgs);
    Integer nOfCh = new Integer(numOfChildren);
    map.put("nOfCh", nOfCh);
    boolean userIsMsgCreator = ureq.getIdentity().getKey().equals(creator.getKey());
    Boolean uIsMsgC = new Boolean(userIsMsgCreator);
    map.put("uIsMsgC", uIsMsgC);
    boolean isThreadtop = m.getThreadtop()==null;
    map.put("isThreadtop", Boolean.valueOf(isThreadtop));
    boolean isThreadClosed = Status.getStatus(m.getStatusCode()).isClosed();
    if(!isThreadtop) {
      isThreadClosed = Status.getStatus(m.getThreadtop().getStatusCode()).isClosed();
    }
    map.put("isThreadClosed", isThreadClosed);
    // add portrait to map for later disposal and key for rendering in velocity
    DisplayPortraitController portrait = new DisplayPortraitController(ureq, getWindowControl(), m.getCreator(), true, true);
    // add also to velocity
    map.put("portrait", portrait);
    String portraitComponentVCName = m.getKey().toString();
    map.put("portraitComponentVCName", portraitComponentVCName);
    vcContainer.put(portraitComponentVCName, portrait.getInitialComponent());
    allList.add(map);
    /*
     * those Link objects are used! see event method and the instanceof Link part!
     * but reference won't be used!
     */
    LinkFactory.createCustomLink("dl_"+msgCount, "dl_"+msgCount, "msg.delete", Link.BUTTON_SMALL, vcThreadView, this);
    LinkFactory.createCustomLink("ed_"+msgCount, "ed_"+msgCount, "msg.update", Link.BUTTON_SMALL, vcThreadView, this);
    LinkFactory.createCustomLink("rm_"+msgCount, "rm_"+msgCount, "msg.remove", Link.BUTTON_SMALL, vcThreadView, this);
    LinkFactory.createCustomLink("up_"+msgCount, "up_"+msgCount, "msg.upload", Link.BUTTON_SMALL, vcThreadView, this);
    LinkFactory.createCustomLink("qt_"+msgCount, "qt_"+msgCount, "msg.quote", Link.BUTTON_SMALL, vcThreadView, this);
    LinkFactory.createCustomLink("rp_"+msgCount, "rp_"+msgCount, "msg.reply", Link.BUTTON_SMALL, vcThreadView, this);
    LinkFactory.createCustomLink("split_"+msgCount, "split_"+msgCount, "msg.split", Link.BUTTON_SMALL, vcThreadView, this);
    LinkFactory.createCustomLink("move_"+msgCount, "move_"+msgCount, "msg.move", Link.BUTTON_SMALL, vcThreadView, this);
  }

  private void disposeCurrentMessages() {
    if (currentMessagesMap != null) {
      Iterator<Map<String, Object>> iter = currentMessagesMap.iterator();
      while (iter.hasNext()) {
        Map<String, Object> messageMap = iter.next();
        Controller ctr = (Controller) messageMap.get("portrait");
        ctr.dispose();
        vcThreadView.remove(ctr.getInitialComponent());
      }
    }
  }

  private List<Message> prepareListTitles(List<Message> messages) {
    List<Message> tmpThreadList = new ArrayList<Message>();
    // extract threads from all messages
    List<Object[]> threads = new ArrayList<Object[]>();
    int numTableCols = 7;
    boolean isModerator = focallback.mayEditMessageAsModerator()
    for (Iterator<Message> iter = messages.iterator(); iter.hasNext();) {
      Message thread = iter.next();
      if (thread.getParent() == null) {
        // put all data in a generic object array       
        Object[] mesgWrapper = new Object[numTableCols];
        String title = StringEscapeUtils.escapeHtml(thread.getTitle()).toString();
        title = Formatter.truncate(title, 50)
        Status messageStatus = Status.getStatus(thread.getStatusCode());
        boolean isSticky = messageStatus.isSticky();
        boolean isClosed = messageStatus.isClosed();
        boolean isHidden = messageStatus.isHidden();
        if(isHidden && !isModerator) {
          continue;
        }
       
        mesgWrapper[0] = "status_thread";
        if(isSticky && isClosed) {
          mesgWrapper[0] = "status_sticky_closed";
        } else if (isSticky) {
          mesgWrapper[0] = "status_sticky";
        } else if (isClosed) {
          mesgWrapper[0] = "status_closed";
        }               
        if(isHidden) {
          title = translate("msg.hidden"+ " " + title;
        }
        mesgWrapper[1] = new ForumHelper.MessageWrapper(title,isSticky,collator);
        User creator = thread.getCreator().getUser();
        mesgWrapper[2] = new ForumHelper.MessageWrapper(creator.getProperty(UserConstants.FIRSTNAME, null) + " " + creator.getProperty(UserConstants.LASTNAME, null),isSticky, collator);
        // find latest date, and number of read messages for all children
        // init with thread values
        Date lastModified = thread.getLastModified();
        int readCounter = (rms.contains(thread.getKey()) ? 1 : 0);
        int childCounter = 1;
        for (Iterator<Message> iter2 = messages.iterator(); iter2.hasNext();) {
          Message msg = iter2.next();
          if (msg.getThreadtop() != null && msg.getThreadtop().getKey().equals(thread.getKey())) {
            // a child is found, update values
            childCounter++;
            if (rms.contains(msg.getKey())) readCounter++;
            if (msg.getLastModified().after(lastModified)) lastModified = msg.getLastModified();
          }
        }       
        mesgWrapper[3] = new ForumHelper.MessageWrapper(lastModified,isSticky,collator)
        mesgWrapper[4] = new ForumHelper.MessageWrapper(new Integer((childCounter - readCounter)),isSticky,collator);
        // unread         
        mesgWrapper[5] = new ForumHelper.MessageWrapper(new Integer(childCounter),isSticky,collator);
        // add message itself for later usage
        mesgWrapper[6] = thread;
        tmpThreadList.add(thread);
        threads.add(mesgWrapper);
      }
    }
    // build table model
    attdmodel = new GenericObjectArrayTableDataModel(threads, numTableCols);
    allThreadTableCtr.setTableDataModel(attdmodel);
    allThreadTableCtr.setSortColumn(3, false);
   
    vcListTitles.put("allThreadTable", allThreadTableCtr.getInitialComponent());
    vcListTitles.contextPut("hasThreads", (attdmodel.getRowCount() == 0) ? Boolean.FALSE : Boolean.TRUE);
   
    return tmpThreadList;
  }

  /**
   * @param m
   * @param messages
   * @return number of all children, grandchildren, grand-grandchildren etc. of
   *         a certain message
   */
  private int countNumOfChildren(Message m, List<Message> messages) {
    int counter = 0;
    counter = countChildrenRecursion(m, messages, counter);
    return counter;
  }

  private int countChildrenRecursion(Message m, List<Message> messages, int counter) {
    for (Iterator<Message> iter = messages.iterator(); iter.hasNext();) {
      Message element = iter.next();
      if (element.getParent() != null) {
        if (m.getKey().equals(element.getParent().getKey())) {
          counter = countChildrenRecursion(element, messages, counter);
          counter++;
        }
      }
    }
    return counter;
  }

  private VFSContainer getArchiveContainer(UserRequest ureq) {
    VFSContainer container = new OlatRootFolderImpl(FolderConfig.getUserHomes() + File.separator + ureq.getIdentity().getName() + "/private/archive", null);
    // append export timestamp to avoid overwriting previous export
    Date tmp = new Date(System.currentTimeMillis());
    java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH_mm_ss");
    String folder = "forum_" + forum.getKey().toString()+"_"+formatter.format(tmp);
    VFSItem vfsItem = container.resolve(folder);
    if (vfsItem == null || !(vfsItem instanceof VFSContainer)) {
      vfsItem = container.createChildContainer(folder);
    }
    container = (VFSContainer) vfsItem;
    return container;
  }

  private void adjustBusinessControlPath(UserRequest ureq, Message m) {
    ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrap(m));
    OLATResourceable ores = OresHelper.createOLATResourceableInstance(Message.class,m.getKey());
    ContextEntry ce = BusinessControlFactory.getInstance().createContextEntry(ores);
   
    WindowControl bwControl = BusinessControlFactory.getInstance().createBusinessWindowControl(ce, getWindowControl());   
   
    //Simple way to "register" the new ContextEntry although only a VelocityPage was flipped.
    Controller dummy = new BasicController(ureq, bwControl) {
   
      @Override
      protected void event(UserRequest ureq, Component source, Event event) {
      // TODO Auto-generated method stub
   
      }
   
      @Override
      protected void doDispose() {
      // TODO Auto-generated method stub
   
      }
   
    };
    dummy.dispose();
    dummy = null;
  }

  private Set<Long> getReadSet(Identity s) {
    // FIXME:fj:c put the whole readset of 1 user / 1 forum in one property
    // only: 234,45646,2343,23432 etc.
    // Problem now is that a lot of rows are generated: number of users x
    // visited messages of all forums = e.g. 5000 x 300 = 1.5 million etc.
   
    return ForumManager.getInstance().getReadSet(s, forum);
  }

  private void markRead(Message m, Identity s) {   
    if (!rms.contains(m.getKey())) {
      rms.add(m.getKey());
      ForumManager.getInstance().markAsRead(s, m);
    }
  }

 
  /**
   * [used by velocity in vcThreadView.contextPut("myself", this);]
   * @param filename
   * @return css class that has a background icon for the given filename
   */
  public String renderFileIconCssClass(String filename) {
    String filetype = filename.substring(filename.lastIndexOf(".")+1);
    if (filetype == null) return "b_filetype_file"; // default
    return "b_filetype_" + filetype;
    }


  protected void doDispose() {
    if (singleThreadTableCtr != null) {
      singleThreadTableCtr.dispose();
      singleThreadTableCtr = null;
    }
    if (allThreadTableCtr != null) {
      allThreadTableCtr.dispose();
      allThreadTableCtr = null;
    }
    if (csc != null) {
      csc.dispose();
      csc = null;
    }
    if (fuCtr != null) {
      fuCtr.dispose();
      fuCtr = null;
    }
    if (dpc != null) {
      dpc.dispose();
      dpc = null;
    }
    disposeCurrentMessages();
    CoordinatorManager.getCoordinator().getEventBus().deregisterFor(this, forum);
  }

  /**
   * Get the message value map from a velocity command. The command must have
   * the signature commandname_messagemapid
   *
   * @param identity
   * @param command
   * @return Map the value map for the current message
   */
  private Map<String, Object> getMessageMapFromCommand(Identity identity, String command) {
    String cmdId = command.substring(command.lastIndexOf("_") + 1);
    try {
      Integer id = Integer.valueOf(cmdId);
      return currentMessagesMap.get(id.intValue());
    } catch (NumberFormatException e) {
      throw new AssertException("Tried to parse forum message id from command::" + command
          + " but message id was not a long. Could be a user who tries to hack the system. User::" + identity.getName(), e);
    }
  }
 
  /**
   * Orders the messages in the logical instead of chronological order.
   * @param messages
   * @param orderedList
   * @param startMessage
   */ 
  private void orderMessagesThreaded(List<Message> messages, List<Message> orderedList, Message startMessage) {
    if(messages==null || orderedList==null || startMessage==null) return;
     Iterator<Message> iterMsg = messages.iterator();
    while(iterMsg.hasNext())
    {
     Message msg = iterMsg.next();
     if (msg.getParent() == null)
     { 
      orderedList.add(msg);    
      ArrayList<Message> copiedMessages = new ArrayList<Message>();
      copiedMessages.addAll(messages);
      copiedMessages.remove(msg);
      messages = copiedMessages;
     
      continue;
     }  
     if ((msg.getParent() != null) && (msg.getParent().getKey().equals(startMessage.getKey())))
     {   
      orderedList.add(msg);   
      orderMessagesThreaded(messages, orderedList, msg);
     }
    }
   }
 
 
  /**
   * Calls splitThread on ForumManager and shows the new thread view.
   * @param ureq
   */
  private void splitThread(UserRequest ureq) {
    if (focallback.mayEditMessageAsModerator()) {
      Message newTopMessage = fm.splitThread(currentMsg);           
      showThreadView(ureq,newTopMessage);
      //do logging
      ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_THREAD_SPLIT, getClass(), LoggingResourceable.wrap(currentMsg));
     
    } else {
      showWarning("may.not.split.thread");
    }
  }
 
  /**
   * Calls moveMessage on ForumManager
   * @param ureq
   * @param topMessage
   */
  private void moveMessage(UserRequest ureq, Message topMsg) {
    if (focallback.mayEditMessageAsModerator()) {
      currentMsg = fm.moveMessage(currentMsg, topMsg);
      cmcMoveMsg.deactivate();
      showThreadView(ureq, topMsg);
      ThreadLocalUserActivityLogger.log(ForumLoggingAction.FORUM_MESSAGE_MOVE, getClass(), LoggingResourceable.wrap(currentMsg));
    } else {
      showWarning("may.not.move.message");
    }
  }
 
  /**
   * Sets the closed status to the threadtop message.
   * @param ureq
   * @param msg
   * @param closed
   */
  private void closeThread(UserRequest ureq, Message msg, boolean closed) { 
    //if the input message is not the Threadtop get the Threadtop message
    if(msg != null && msg.getThreadtop()!=null) {
      msg = msg.getThreadtop();
    }
    if (msg != null && msg.getThreadtop()==null) {
      currentMsg = fm.loadMessage(msg.getKey());
      Status status = Status.getStatus(currentMsg.getStatusCode());
      status.setClosed(closed);
      if(currentMsg.getParent()==null) {
        currentMsg.setStatusCode(Status.getStatusCode(status));
        fm.updateMessage(currentMsg, new ForumChangedEvent("close"));
      }
      // do logging
      ILoggingAction loggingAction;
      if (closed)  {
        loggingAction = ForumLoggingAction.FORUM_THREAD_CLOSE;
      } else {
        loggingAction = ForumLoggingAction.FORUM_THREAD_REOPEN;
      }
     
      ThreadLocalUserActivityLogger.log(loggingAction, getClass(), LoggingResourceable.wrap(currentMsg));
      showThreadOverviewView();
    }
  }
 
  /**
   * Sets the hidden status to the threadtop message.
   * @param ureq
   * @param msg
   * @param hidden
   */
  private void hideThread(UserRequest ureq, Message msg, boolean hidden) {   
    //if the input message is not the Threadtop get the Threadtop message
    if(msg != null && msg.getThreadtop()!=null) {
      msg = msg.getThreadtop();
    }
    if (msg != null && msg.getThreadtop()==null) {
      currentMsg = fm.loadMessage(msg.getKey());
      Status status = Status.getStatus(currentMsg.getStatusCode());
      status.setHidden(hidden);
      if(currentMsg.getParent()==null) {
        currentMsg.setStatusCode(Status.getStatusCode(status));
        fm.updateMessage(currentMsg, new ForumChangedEvent("hide"));       
      }
      // do logging
      ILoggingAction loggingAction;
      if (hidden)  {
        loggingAction = ForumLoggingAction.FORUM_THREAD_HIDE;
      } else {
        loggingAction = ForumLoggingAction.FORUM_THREAD_SHOW;
      }
     
      ThreadLocalUserActivityLogger.log(loggingAction, getClass(), LoggingResourceable.wrap(currentMsg));
      showThreadOverviewView();
    }
  }
       
 
  ////////////////////////////////////////
   // Sticky things
  //////////////////////////////////////// 
 
  /**
   *
   * Description:<br>
   * Tree cell renderer for the sticky thread titles.
   *
   * <P>
   * Initial Date:  09.07.2007 <br>
   * @author Lavinia Dumitrescu
   */
  class StickyThreadCellRenderer extends CustomCssCellRenderer {

    @Override
    protected String getCssClass(Object val) {     
      ForumHelper.MessageWrapper messageWrapper = (ForumHelper.MessageWrapper) val;
      if (messageWrapper.isSticky()) {     
        return "o_forum_thread_sticky";
      }
      return "";
    }

    @Override
    protected String getCellValue(Object val) {     
      ForumHelper.MessageWrapper messageWrapper = (ForumHelper.MessageWrapper) val;
      return messageWrapper.toString();
    }   

    @Override
    protected String getHoverText(Object val) {
      return null;
 
  } 
 
 
  /**
   *
   * Description:<br>
   * <code>ColumnDescriptor</code> with special <code>compareTo</code> method implementation.
   * Allows a special column sorting for MessageWrappers considering the sticky attribute.
   *
   * <P>
   * Initial Date:  11.07.2007 <br>
   * @author Lavinia Dumitrescu
   */
  private class StickyColumnDescriptor extends DefaultColumnDescriptor{   
    public StickyColumnDescriptor(String headerKey, int dataColumn, String action, Locale locale) {
       super(headerKey, dataColumn, action, locale, ColumnDescriptor.ALIGNMENT_LEFT);
    }
    /**
     * Sole constructor.
     * @param headerKey
     * @param dataColumn
     * @param action
     * @param locale used ONLY for method getRenderValue in case the Object is of type Date to provide locale-sensitive Date formatting
     * @param alignment left, middle or right; constants in ColumnDescriptor
     */
    public StickyColumnDescriptor(String headerKey, int dataColumn, String action, Locale locale, int alignment) {
      super(headerKey, dataColumn, action, locale, alignment);
    }
   
    /**
     * Delegates comparison to the <code>ForumHelper.compare</code>. In case the <code>ForumHelper.compare</code>
     * returns <code>ForumHelper.NOT_MY_JOB</code>, the comparison is executed by the superclass.
     * @see org.olat.core.gui.components.table.ColumnDescriptor#compareTo(int, int)
     */
    @Override
    public int compareTo(int rowa, int rowb) {
      ForumHelper.MessageWrapper a = (ForumHelper.MessageWrapper)getTable().getTableDataModel().getValueAt(rowa,getDataColumn());
      ForumHelper.MessageWrapper b = (ForumHelper.MessageWrapper)getTable().getTableDataModel().getValueAt(rowb,getDataColumn());
      boolean sortAscending = getTable().isSortAscending();

      int comparisonOutcome = ForumHelper.compare(a,b,sortAscending);
      if(comparisonOutcome == ForumHelper.NOT_MY_JOB) {
        comparisonOutcome = super.compareTo(rowa, rowb);
      }
      return comparisonOutcome;
    }     
  }

  /**
   *
   * Description:<br>
   * <code>ColumnDescriptor</code> with special <code>compareTo</code> method implementation for a <code>CustomCellRenderer</code>.
   * Allows a special column sorting for MessageWrappers considering the sticky attribute.
   *
   * <P>
   * Initial Date:  11.07.2007 <br>
   * @author Lavinia Dumitrescu
   */
  private class StickyRenderColumnDescriptor extends CustomRenderColumnDescriptor {
   
    public StickyRenderColumnDescriptor(String headerKey, int dataColumn, String action, Locale locale, int alignment,
        CustomCellRenderer customCellRenderer) {
      super(headerKey, dataColumn, action, locale, alignment,customCellRenderer);     
    }
   
    /**
     * Delegates comparison to the <code>ForumHelper.compare</code>. In case the <code>ForumHelper.compare</code>
     * returns <code>ForumHelper.NOT_MY_JOB</code>, the comparison is executed by the superclass.
     * @see org.olat.core.gui.components.table.ColumnDescriptor#compareTo(int, int)
     */   
    public int compareTo(int rowa, int rowb) {
      ForumHelper.MessageWrapper a = (ForumHelper.MessageWrapper)getTable().getTableDataModel().getValueAt(rowa,getDataColumn());
      ForumHelper.MessageWrapper b = (ForumHelper.MessageWrapper)getTable().getTableDataModel().getValueAt(rowb,getDataColumn());
      boolean sortAscending = getTable().isSortAscending();

      int comparisonOutcome = ForumHelper.compare(a,b,sortAscending);
      if(comparisonOutcome == ForumHelper.NOT_MY_JOB) {
        comparisonOutcome = super.compareTo(rowa, rowb);
      }
      return comparisonOutcome;
    }       
  }
 
  class MessageIconRenderer extends CustomCssCellRenderer {
   
    protected String getHoverText(Object val) {
      return ControllerFactory.translateResourceableTypeName((String)val, getLocale());
    }
   
    protected String getCellValue(Object val) {
      return "";
    }

    protected String getCssClass(Object val) {
      //val.toString()
      // use small icon and create icon class for resource: o_FileResource-SHAREDFOLDER_icon
      return "b_small_icon " + "o_forum_" + ((String) val) + "_icon";
    }
  }
 
}
TOP

Related Classes of org.olat.modules.fo.ForumController$StickyColumnDescriptor

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.