/**
* 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> </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";
}
}
}