/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.modules.fo;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.hibernate.Hibernate;
import org.hibernate.type.Type;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.persistence.DBQuery;
import org.olat.core.commons.services.text.TextService;
import org.olat.core.id.Identity;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.LogDelegator;
import org.olat.core.service.ServiceFactory;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.vfs.VFSContainer;
/**
*
* @author Felix Jost
*/
public class ForumManager extends LogDelegator {
private static ForumManager instance = new ForumManager();
private ForumManager() {
// private since singleton
}
/**
* @return the singleton
*/
public static ForumManager getInstance() {
return instance;
}
/**
* @param msgid msg id of the topthread
* @return List messages
*/
public List<Message> getThread(Long msgid) {
// we make a scalar query so that not only the messages, but also the users
// are loaded into the cache as well.
// TODO : Otherwise, hibernate will fetch the user for each message (100
// messages = 101 SQL Queries!)
// FIXME: use join fetch instead
long rstart = 0;
if (isLogDebugEnabled()){
rstart = System.currentTimeMillis();
}
List scalar = DBFactory.getInstance().find(
"select msg, cr, usercr " +
"from org.olat.modules.fo.MessageImpl as msg" +
", org.olat.core.id.Identity as cr " +
", org.olat.user.UserImpl as usercr" + " where msg.creator = cr and cr.user = usercr " +
" and (msg.key = ? or msg.threadtop.key = ?) order by msg.creationDate", new Object[] { msgid, msgid },
new Type[] { Hibernate.LONG, Hibernate.LONG });
int size = scalar.size();
List<Message> messages = new ArrayList<Message>(size);
for (int i = 0; i < size; i++) {
Object[] o = (Object[]) scalar.get(i);
Message m = (Message) o[0];
messages.add(m);
}
if (isLogDebugEnabled()){
long rstop = System.currentTimeMillis();
logDebug("time to fetch thread with topmsg_id " + msgid + " :" + (rstop - rstart), null);
}
return messages;
}
public List<Long> getAllForumKeys(){
List<Long> tmpRes = DBFactory.getInstance().find("select key from org.olat.modules.fo.ForumImpl");
return tmpRes;
}
public List<Message> getMessagesByForum(Forum forum){
return getMessagesByForumID(forum.getKey());
}
/**
* @param forum
* @return List messages
*/
public List<Message> getMessagesByForumID(Long forum_id) {
long rstart = 0;
if(isLogDebugEnabled()){
rstart = System.currentTimeMillis();
}
List scalar = DBFactory.getInstance().find(
"select msg, cr, usercr " + "from org.olat.modules.fo.MessageImpl as msg" +
", org.olat.core.id.Identity as cr " +
", org.olat.user.UserImpl as usercr" +
" where msg.creator = cr and cr.user = usercr and msg.forum.key = ?", forum_id,
Hibernate.LONG);
int size = scalar.size();
List<Message> messages = new ArrayList<Message>(size);
for (int i = 0; i < size; i++) {
Object[] o = (Object[]) scalar.get(i);
Message m = (Message) o[0];
messages.add(m);
}
if(isLogDebugEnabled()){
long rstop = System.currentTimeMillis();
logDebug("time to fetch forum with forum_id " + forum_id + " :" + (rstop - rstart), null);
}
return messages;
}
/**
*
* @param forumkey
* @return the count of all messages by this forum
*/
public Integer countMessagesByForumID(Long forumkey) {
List msgCount = DBFactory.getInstance().find(
"select count(msg.title) from org.olat.modules.fo.MessageImpl as msg where msg.forum.key = ?", forumkey, Hibernate.LONG);
return new Integer( ((Long)msgCount.get(0)).intValue() );
}
/**
* Implementation with one entry per message.
* @param identity
* @param forumkey
* @return number of read messages
*/
public int countReadMessagesByUserAndForum(Identity identity, Long forumkey) {
List<ReadMessage> itemList = DBFactory.getInstance().find("select msg from msg in class org.olat.modules.fo.ReadMessageImpl where msg.identity = ? and msg.forum = ?",
new Object[] {identity.getKey(), forumkey}, new Type[] { Hibernate.LONG, Hibernate.LONG });
return itemList.size();
}
/**
* @param forumKey
* @param latestRead
* @return a List of Object[] with a key(Long), title(String), a creator(Identity), and
* the lastmodified(Date) of the messages of the forum with the given
* key and with last modification after the "latestRead" Date
*/
public List<Message> getNewMessageInfo(Long forumKey, Date latestRead) {
// FIXME:fj: lastModified has no index -> test performance with forum with
// 200 messages
String query = "select msg from org.olat.modules.fo.MessageImpl as msg" +
" where msg.forum.key = :forumKey and msg.lastModified > :latestRead order by msg.lastModified desc";
DBQuery dbquery = DBFactory.getInstance().createQuery(query);
dbquery.setLong("forumKey", forumKey.longValue());
dbquery.setTimestamp("latestRead", latestRead);
dbquery.setCacheable(true);
return dbquery.list();
}
/**
* @return the newly created and persisted forum
*/
public Forum addAForum() {
Forum fo = createForum();
saveForum(fo);
return fo;
}
/**
* @param forumKey
* @return the forum with the given key
*/
public Forum loadForum(Long forumKey) {
ForumImpl fo = (ForumImpl) DBFactory.getInstance().loadObject(ForumImpl.class, forumKey);
return fo;
}
private Forum saveForum(Forum forum) {
DB db = DBFactory.getInstance();
db.saveObject(forum);
return forum;
}
/**
* @param forumKey
*/
public void deleteForum(Long forumKey) {
Forum foToDel = loadForum(forumKey);
if (foToDel == null) throw new AssertException("forum to delete was not found: key=" + forumKey);
// delete properties, messages and the forum itself
doDeleteForum(foToDel);
// delete directory for messages with attachments
deleteForumContainer(forumKey);
}
/**
* deletes all messages belonging to this forum and the forum entry itself
*
* @param forum
*/
private void doDeleteForum(Forum forum) {
Long forumKey = forum.getKey();
DB db = DBFactory.getInstance();
//delete read messsages
db.delete("from readMsg in class org.olat.modules.fo.ReadMessageImpl where readMsg.forum = ? ", forumKey, Hibernate.LONG);
// delete messages
db.delete("from message in class org.olat.modules.fo.MessageImpl where message.forum = ?", forumKey, Hibernate.LONG);
// delete forum
db.delete("from forum in class org.olat.modules.fo.ForumImpl where forum.key = ?", forumKey, Hibernate.LONG);
// delete properties
}
/**
* create (in RAM only) a new Forum
*/
private ForumImpl createForum() {
return new ForumImpl();
}
/**
* sets the parent and threadtop of the message automatically
*
* @param newMessage the new message which has title and body set
* @param creator
* @param replyToMessage
*/
public void replyToMessage(Message newMessage, Identity creator, Message replyToMessage) {
newMessage.setForum(replyToMessage.getForum());
Message top = replyToMessage.getThreadtop();
newMessage.setThreadtop((top != null ? top : replyToMessage));
newMessage.setParent(replyToMessage);
newMessage.setCreator(creator);
saveMessage(newMessage);
}
/**
* @param creator
* @param forum
* @param topMessage
*/
public void addTopMessage(Identity creator, Forum forum, Message topMessage) {
topMessage.setForum(forum);
topMessage.setParent(null);
topMessage.setThreadtop(null);
topMessage.setCreator(creator);
saveMessage(topMessage);
}
/**
* @param messageKey
* @return the message with the given messageKey
*/
public Message loadMessage(Long messageKey) {
Message msg = doloadMessage(messageKey);
return msg;
}
private Message doloadMessage(Long messageKey) {
Message msg = (Message) DBFactory.getInstance().loadObject(MessageImpl.class, messageKey);
return msg;
}
private void saveMessage(Message m) {
// TODO: think about where maxlenrestriction comes: manager or controller
updateCounters(m);
m.setLastModified(new Date());
DBFactory.getInstance().saveObject(m);
}
/**
* creates (in RAM only) a new Message<br>
* fill the values and use saveMessage to make it persistent
*
* @return the message
* @see ForumManager#saveMessage(Message)
*/
public Message createMessage() {
return new MessageImpl();
}
/**
* Update message and fire MultiUserEvent, if any provided.
* If a not null ForumChangedEvent object is provided, then fire event to listeners.
* @param m
* @param event
*/
public void updateMessage(Message m, ForumChangedEvent event) {
updateCounters(m);
m.setLastModified(new Date());
DBFactory.getInstance().updateObject(m);
if (event!=null) {
CoordinatorManager.getCoordinator().getEventBus().fireEventToListenersOf(new ForumChangedEvent("hide"), m.getForum());
}
}
/**
* @param forumKey
* @param m
*/
public void deleteMessageTree(Long forumKey, Message m) {
deleteMessageRecursion(forumKey, m);
}
private void deleteMessageRecursion(Long forumKey, Message m) {
deleteMessageContainer(forumKey, m.getKey());
DB db = DBFactory.getInstance();
Long message_id = m.getKey();
List messages = db
.find("select msg from msg in class org.olat.modules.fo.MessageImpl where msg.parent = ?", message_id, Hibernate.LONG);
for (Iterator iter = messages.iterator(); iter.hasNext();) {
Message element = (Message) iter.next();
deleteMessageRecursion(forumKey, element);
}
/*
* if (! db.contains(m)){ log.debug("Message " + m.getKey() + " not in
* hibernate session, reloading before delete"); m =
* loadMessage(m.getKey()); }
*/
// make sure the message is reloaded if it is not in the hibernate session
// cache
m = (Message) db.loadObject(m);
// delete all properties of one single message
deleteMessageProperties(forumKey, m);
db.deleteObject(m);
if(isLogDebugEnabled()){
logDebug("Deleting message ", m.getKey().toString());
}
}
/**
* @param m
* @return true if the message has children
*/
public boolean hasChildren(Message m) {
boolean children = false;
DB db = DBFactory.getInstance();
Long message_id = m.getKey();
String q = " select count(msg) from org.olat.modules.fo.MessageImpl msg where msg.parent = :input ";
DBQuery query = db.createQuery(q);
query.setLong("input", message_id.longValue());
List result = query.list();
int count = ((Long) result.get(0)).intValue();
if (count > 0) {
children = true;
}
return children;
}
/**
* deletes entry of one message
*/
private void deleteMessageProperties(Long forumKey, Message m) {
DB db = DBFactory.getInstance();
Long messageKey = m.getKey();
StringBuilder query = new StringBuilder();
query.append("from readMsg in class org.olat.modules.fo.ReadMessageImpl ");
query.append("where readMsg.forum = ? ");
query.append("and readMsg.message = ? ");
db.delete(query.toString(), new Object[] { forumKey, messageKey }, new Type[] { Hibernate.LONG, Hibernate.LONG });
}
/**
* @param forumKey
* @param messageKey
* @return the valid container for the attachments to place into
*/
public OlatRootFolderImpl getMessageContainer(Long forumKey, Long messageKey) {
String fKey = forumKey.toString();
String mKey = messageKey.toString();
StringBuilder sb = new StringBuilder();
sb.append("/forum/");
sb.append(fKey);
sb.append("/");
sb.append(mKey);
String pathToMsgDir = sb.toString();
OlatRootFolderImpl messageContainer = new OlatRootFolderImpl(pathToMsgDir, null);
File baseFile = messageContainer.getBasefile();
baseFile.mkdirs();
return messageContainer;
}
private void deleteMessageContainer(Long forumKey, Long messageKey) {
VFSContainer mContainer = getMessageContainer(forumKey, messageKey);
mContainer.delete();
}
private void deleteForumContainer(Long forumKey) {
VFSContainer fContainer = getForumContainer(forumKey);
fContainer.delete();
}
private OlatRootFolderImpl getForumContainer(Long forumKey) {
String fKey = forumKey.toString();
StringBuilder sb = new StringBuilder();
sb.append("/forum/");
sb.append(fKey);
String pathToForumDir = sb.toString();
OlatRootFolderImpl fContainer = new OlatRootFolderImpl(pathToForumDir, null);
File baseFile = fContainer.getBasefile();
baseFile.mkdirs();
return fContainer;
}
public Message findMessage(Long messageId) {
return (Message) DBFactory.getInstance().findObject(MessageImpl.class, messageId);
}
/**
* Splits the current thread starting from the current message.
* It updates the messages of the selected subthread by setting the Parent and the Threadtop.
*
* @param msgid
* @return the top message of the newly created thread.
*/
public Message splitThread(Message msg){
Message newTopMessage = null;
if(msg.getThreadtop()==null) {
newTopMessage = msg;
} else {
//it only make sense to split a thread if the current message is not a threadtop message.
List<Message> threadList = this.getThread(msg.getThreadtop().getKey());
List<Message> subthreadList = new ArrayList<Message>();
subthreadList.add(msg);
getSubthread(msg, threadList, subthreadList);
Iterator<Message> messageIterator = subthreadList.iterator();
Message firstMessage = null;
DB db = DBFactory.getInstance();
if (messageIterator.hasNext()) {
firstMessage = messageIterator.next();
firstMessage = (Message) db.loadObject(firstMessage);
firstMessage.setParent(null);
firstMessage.setThreadtop(null);
this.updateMessage(firstMessage, new ForumChangedEvent("split"));
newTopMessage = firstMessage;
}
while (firstMessage != null && messageIterator.hasNext()) {
Message message = messageIterator.next();
message = (Message) db.loadObject(message);
message.setThreadtop(firstMessage);
this.updateMessage(message, null);
}
}
return newTopMessage;
}
/**
* Moves the current message from the current thread in another thread.
*
* @param msg
* @param topMsg
* @return the moved message
*/
@SuppressWarnings("unchecked")
public Message moveMessage(Message msg, Message topMsg) {
DB db = DBFactory.getInstance();
List<Message> oldThreadList = getThread(msg.getThreadtop().getKey());
List<Message> subThreadList = new ArrayList<Message>();
this.getSubthread(msg, oldThreadList, subThreadList);
// one has to set a new parent for all childs of the moved message
// first message of sublist has to get the parent from the moved message
for(Message childMessage : subThreadList) {
childMessage = (Message)db.loadObject(childMessage);
childMessage.setParent(msg.getParent());
updateMessage(childMessage, null);
}
// now move the message to the choosen thread
Message oldMessage = (Message)db.loadObject(msg);
Message message = createMessage();
message.setCreator(oldMessage.getCreator());
message.setForum(oldMessage.getForum());
message.setModifier(oldMessage.getModifier());
message.setTitle(oldMessage.getTitle());
message.setBody(oldMessage.getBody());
message.setThreadtop(topMsg);
message.setParent(topMsg);
Status status = Status.getStatus(oldMessage.getStatusCode());
status.setMoved(true);
message.setStatusCode(Status.getStatusCode(status));
deleteMessageRecursion(oldMessage.getForum().getKey(), oldMessage);
saveMessage(message);
return message;
}
/**
* This is a recursive method. The subthreadList in an ordered list with all descendents of the input msg.
* @param msg
* @param threadList
* @param subthreadList
*/
private void getSubthread(Message msg, List<Message> threadList, List<Message> subthreadList) {
Iterator<Message> listIterator = threadList.iterator();
while(listIterator.hasNext()) {
Message currMessage = listIterator.next();
if(currMessage.getParent()!=null && currMessage.getParent().getKey().equals(msg.getKey())) {
subthreadList.add(currMessage);
getSubthread(currMessage, threadList, subthreadList);
}
}
}
/**
*
* @param identity
* @param forum
* @return a set with the read messages keys for the input identity and forum.
*/
public Set<Long> getReadSet(Identity identity, Forum forum) {
List<ReadMessage> itemList = DBFactory.getInstance().find("select msg from msg in class org.olat.modules.fo.ReadMessageImpl where msg.identity = ? and msg.forum = ?", new Object[] {identity.getKey(), forum.getKey()}, new Type[] { Hibernate.LONG, Hibernate.LONG });
Set<Long> readSet = new HashSet<Long>();
Iterator<ReadMessage> listIterator = itemList.iterator();
while(listIterator.hasNext()) {
Long msgKey = listIterator.next().getMessage().getKey();
readSet.add(msgKey);
}
return readSet;
}
/**
* Implementation with one entry per forum message.
* Adds a new entry into the ReadMessage for the input message and identity.
* @param msg
* @param identity
*/
public void markAsRead(Identity identity,Message msg) {
//Check if the message was not already deleted
Message retrievedMessage = findMessage(msg.getKey());
if(retrievedMessage!=null) {
ReadMessageImpl readMessage = new ReadMessageImpl();
readMessage.setIdentity(identity);
readMessage.setMessage(msg);
readMessage.setForum(msg.getForum());
DBFactory.getInstance().saveObject(readMessage);
}
}
/**
* Update the counters for words and characters
* @param m the message
*/
public void updateCounters(Message m) {
String body = m.getBody();
String unQuotedBody = new QuoteAndTagFilter().filter(body);
TextService txtService = (TextService)ServiceFactory.getService(TextService.class);
Locale suggestedLocale = txtService.detectLocale(unQuotedBody);
m.setNumOfWords(txtService.wordCount(unQuotedBody, suggestedLocale));
m.setNumOfCharacters(txtService.characterCount(unQuotedBody, suggestedLocale));
}
}