/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
*/
package org.olat.modules.fo.manager;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;
import org.olat.basesecurity.IdentityRef;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.persistence.PersistenceHelper;
import org.olat.core.commons.services.mark.MarkingService;
import org.olat.core.commons.services.mark.impl.MarkImpl;
import org.olat.core.commons.services.text.TextService;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.Encoder;
import org.olat.core.util.Encoder.Algorithm;
import org.olat.core.util.StringHelper;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.resource.OresHelper;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.login.LoginModule;
import org.olat.modules.fo.Forum;
import org.olat.modules.fo.ForumChangedEvent;
import org.olat.modules.fo.Message;
import org.olat.modules.fo.MessageLight;
import org.olat.modules.fo.MessageRef;
import org.olat.modules.fo.Pseudonym;
import org.olat.modules.fo.QuoteAndTagFilter;
import org.olat.modules.fo.Status;
import org.olat.modules.fo.model.ForumImpl;
import org.olat.modules.fo.model.ForumThread;
import org.olat.modules.fo.model.ForumUserStatistics;
import org.olat.modules.fo.model.MessageImpl;
import org.olat.modules.fo.model.MessageLightImpl;
import org.olat.modules.fo.model.MessageStatistics;
import org.olat.modules.fo.model.PseudonymImpl;
import org.olat.modules.fo.model.PseudonymStatistics;
import org.olat.modules.fo.model.ReadMessageImpl;
import org.olat.modules.fo.ui.MessagePeekview;
import org.olat.user.UserManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* @author Felix Jost
*/
@Service
public class ForumManager {
private static final OLog log = Tracing.createLoggerFor(ForumManager.class);
private static ForumManager INSTANCE;
@Autowired
private DB dbInstance;
@Autowired
private LoginModule loginModule;
@Autowired
private TextService txtService;
@Autowired
private UserManager userManager;
@Autowired
private MarkingService markingService;
/**
* [spring]
*/
private ForumManager() {
INSTANCE = this;
}
/**
* @return the singleton
*/
public static ForumManager getInstance() {
return INSTANCE;
}
public int countThread(Long messageKey) {
String query = "select count(msg) from fomessage as msg where msg.key=:messageKey or msg.threadtop.key=:messageKey";
List<Number> count = dbInstance.getCurrentEntityManager()
.createQuery(query, Number.class)
.setParameter("messageKey", messageKey)
.getResultList();
return count == null || count.isEmpty() || count.get(0) == null ? 0 : count.get(0).intValue();
}
/**
* @param msgid msg id of the topthread
* @return List messages
*/
public List<Message> getThread(Long msgid) {
return getThread(msgid, 0, -1, Message.OrderBy.creationDate, true);
}
public List<Message> getThread(Long msgid, int firstResult, int maxResults, Message.OrderBy orderBy, boolean asc) {
StringBuilder query = new StringBuilder();
query.append("select msg from fomessage as msg")
.append(" left join fetch msg.creator as creator")
.append(" left join fetch msg.modifier as modifier")
.append(" where msg.key=:messageKey or msg.threadtop.key=:messageKey");
if(orderBy != null) {
query.append(" order by msg.").append(orderBy.name()).append(asc ? " ASC " : " DESC ");
}
TypedQuery<Message> dbQuery = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Message.class)
.setParameter("messageKey", msgid)
.setFirstResult(firstResult);
if(maxResults > 0) {
dbQuery.setMaxResults(maxResults);
}
return dbQuery.getResultList();
}
/**
*
* @param forum_id
* @return
*/
public int countThreadsByForumID(Long forum_id) {
return countMessagesByForumID(forum_id, true);
}
/**
*
* @param forum_id
* @param start
* @param limit
* @param orderBy
* @param asc
* @return
*/
public List<Message> getThreadsByForumID(Long forum_id, int firstResult, int maxResults, Message.OrderBy orderBy, boolean asc) {
return getMessagesByForumID(forum_id, firstResult, maxResults, true, orderBy, asc);
}
/**
*
* @param forum
* @return
*/
public List<Message> getMessagesByForum(Forum forum){
if (forum == null) return new ArrayList<Message>(0); // fxdiff: while indexing it can somehow occur, that forum is null!
return getMessagesByForumID(forum.getKey(), 0, -1, false, null, true);
}
/**
*
* @param forumKey
* @param start
* @param limit
* @param onlyThreads
* @param orderBy
* @param asc
* @return
*/
private List<Message> getMessagesByForumID(Long forumKey, int firstResult, int maxResults, boolean onlyThreads, Message.OrderBy orderBy, boolean asc) {
StringBuilder query = new StringBuilder();
query.append("select msg from fomessage as msg")
.append(" left join fetch msg.creator as creator")
.append(" where msg.forum.key=:forumKey ");
if(onlyThreads) {
query.append(" and msg.parent is null");
}
if(orderBy != null) {
query.append(" order by msg.").append(orderBy.name()).append(asc ? " ASC" : " DESC");
}
TypedQuery<Message> dbQuery = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Message.class)
.setParameter("forumKey", forumKey)
.setFirstResult(firstResult);
if(maxResults > 0) {
dbQuery.setMaxResults(maxResults);
}
return dbQuery.getResultList();
}
public List<Message> getMessageChildren(Message parentMessage) {
StringBuilder query = new StringBuilder();
query.append("select msg from fomessage as msg")
.append(" inner join msg.parent as parentMsg")
.append(" where parentMsg.key=:parentKey");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Message.class)
.setParameter("parentKey", parentMessage.getKey())
.getResultList();
}
public List<Message> getTopMessageChildren(Message topMessage) {
StringBuilder query = new StringBuilder();
query.append("select msg from fomessage as msg")
.append(" left join fetch msg.creator as creator")
.append(" left join fetch msg.modifier as modifier")
.append(" where msg.threadtop.key=:messageKey")
.append(" order by msg.creationDate asc");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Message.class)
.setParameter("messageKey", topMessage.getKey())
.getResultList();
}
private int countMessagesByForumID(Long forumKey, boolean onlyThreads) {
StringBuilder query = new StringBuilder();
query.append("select count(msg) from fomessage as msg")
.append(" where msg.forum.key=:forumKey");
if(onlyThreads) {
query.append(" and msg.parent is null");
}
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Number.class)
.setParameter("forumKey", forumKey)
.getSingleResult()
.intValue();
}
/**
* Return the title of a message of the forum.
*/
public Integer countMessagesByForumID(Long forum_id) {
return countMessagesByForumID(forum_id, false);
}
public List<MessagePeekview> getPeekviewMessages(Forum forum, int maxResults) {
StringBuilder query = new StringBuilder();
query.append("select msg from fopeekviewmessage as msg where msg.forumKey=:forumKey order by msg.creationDate desc");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), MessagePeekview.class)
.setParameter("forumKey", forum.getKey())
.setFirstResult(0)
.setMaxResults(maxResults)
.getResultList();
}
public String getForumNameForLogging(Forum forum) {
StringBuilder query = new StringBuilder();
query.append("select msg.title from fomessage as msg where msg.forum.key=:forumKey");
List<String> titles = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), String.class)
.setParameter("forumKey", forum.getKey())
.setFirstResult(0)
.setMaxResults(1)
.getResultList();
return titles == null || titles.isEmpty() || titles.get(0) == null ? null : titles.get(0);
}
public List<ForumThread> getForumThreads(Forum forum, Identity identity) {
StringBuilder sb = new StringBuilder();
sb.append("select msg ")
.append(" , (select count(replies.key) from fomessage as replies")
.append(" where replies.threadtop.key=msg.key and replies.forum.key=:forumKey")
.append(" ) as numOfMessages")
.append(" , (select max(replies.lastModified) from fomessage as replies")
.append(" where replies.threadtop.key=msg.key and replies.forum.key=:forumKey")
.append(" ) as lastModified");
if(identity != null) {
sb.append(" , (select count(read.key) from foreadmessage as read, fomessage as posts")
.append(" where (posts.threadtop.key=msg.key or posts.key=msg.key) and read.message.key=posts.key and read.identity.key=:identityKey")
.append(" ) as numOfReadMessages")
.append(" ,(select count(mark.key) from ").append(MarkImpl.class.getName()).append(" as mark, fomessage as mposts ")
.append(" where mark.creator.key=:identityKey and mark.resId=:forumKey and (mposts.threadtop.key=msg.key or mposts.key=msg.key)")
.append(" and mposts.key=cast(mark.resSubPath as long) and mark.resName='Forum'")
.append(" ) as marks");
}
sb.append(" from fomessage as msg ")
.append(" left join fetch msg.creator as creator")
.append(" where msg.forum.key=:forumKey and msg.threadtop is null");
TypedQuery<Object[]> objectsQuery = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Object[].class)
.setParameter("forumKey", forum.getKey());
if(identity != null) {
objectsQuery.setParameter("identityKey", identity.getKey());
}
List<Object[]> objects = objectsQuery.getResultList();
List<ForumThread> threadList = new ArrayList<>(objects.size());
for(Object[] object:objects) {
Message msg = (Message)object[0];
Number numOfMessagesLong = (Number)object[1];
Date lastModified = (Date)object[2];
int numOfMessages = numOfMessagesLong == null ? 1 : numOfMessagesLong.intValue() + 1;
String creator = userManager.getUserDisplayName(msg.getCreator());
ForumThread thread = new ForumThread(msg, creator, lastModified, numOfMessages);
if(identity != null) {
Number readMessages = (Number)object[3];
int numOfReadMessages = readMessages == null ? 0 : readMessages.intValue();
thread.setNewMessages(numOfMessages - numOfReadMessages);
Number numOfMarkedMessagesLong = (Number)object[4];
int numOfMarkedMessages = numOfMarkedMessagesLong == null ? 0 : numOfMarkedMessagesLong.intValue();
thread.setMarkedMessages(numOfMarkedMessages);
}
threadList.add(thread);
}
return threadList;
}
public boolean existsMessageById(Long messageKey) {
StringBuilder query = new StringBuilder();
query.append("select msg.key from fomessage as msg")
.append(" where msg.key=:messageKey");
List<Long> messages = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Long.class)
.setParameter("messageKey", messageKey)
.getResultList();
return messages == null || messages.isEmpty() || messages.get(0) == null
? false : messages.get(0).longValue() > -1;
}
public Message getMessageById(Long messageKey) {
StringBuilder query = new StringBuilder();
query.append("select msg from fomessage as msg")
.append(" left join fetch msg.creator as creator")
.append(" left join fetch msg.modifier as creator")
.append(" left join fetch msg.threadtop as threadtop")
.append(" where msg.key=:messageKey");
List<Message> messages = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Message.class)
.setParameter("messageKey", messageKey)
.getResultList();
return messages == null || messages.isEmpty() ? null : messages.get(0);
}
public boolean isPseudonymProtected(String pseudonym) {
StringBuilder query = new StringBuilder();
query.append("select pseudonym.key from fopseudonym as pseudo")
.append(" where lower(pseudo.pseudonym)=:pseudonym");
List<Long> pseudonyms = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Long.class)
.setParameter("pseudonym", pseudonym.toLowerCase())
.setFirstResult(0)
.setMaxResults(1)
.getResultList();
return pseudonyms != null && pseudonyms.size() > 0 && pseudonyms.get(0) != null;
}
public boolean isPseudonymInUseInForums(String pseudonym) {
StringBuilder query = new StringBuilder();
query.append("select msg.key from fomessage as msg")
.append(" where lower(msg.pseudonym)=:pseudonym");
List<Long> pseudonyms = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Long.class)
.setParameter("pseudonym", pseudonym.toLowerCase())
.setFirstResult(0)
.setMaxResults(1)
.getResultList();
return pseudonyms != null && pseudonyms.size() > 0 && pseudonyms.get(0) != null;
}
public List<Pseudonym> getPseudonyms(String pseudonym) {
StringBuilder query = new StringBuilder();
query.append("select pseudo from fopseudonym as pseudo")
.append(" where lower(pseudo.pseudonym)=:pseudonym");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Pseudonym.class)
.setParameter("pseudonym", pseudonym.toLowerCase())
.getResultList();
}
public boolean authenticatePseudonym(Pseudonym pseudonym, String password) {
if(pseudonym.getAlgorithm() != null) {
//check if update is needed
Algorithm algorithm = Algorithm.valueOf(pseudonym.getAlgorithm());
String credentials = Encoder.encrypt(password, pseudonym.getSalt(), algorithm);
return credentials.equals(pseudonym.getCredential());
}
return false;
}
public Pseudonym createProtectedPseudonym(String pseudonym, String password) {
PseudonymImpl pseudo = new PseudonymImpl();
pseudo.setCreationDate(new Date());
pseudo.setPseudonym(pseudonym);
Algorithm algorithm = loginModule.getDefaultHashAlgorithm();
String salt = algorithm.isSalted() ? Encoder.getSalt() : null;
String newCredentials = Encoder.encrypt(password, salt, algorithm);
pseudo.setSalt(salt);
pseudo.setCredential(newCredentials);
pseudo.setAlgorithm(algorithm.name());
dbInstance.getCurrentEntityManager().persist(pseudo);
return pseudo;
}
public Pseudonym getPseudonymByKey(Long key) {
StringBuilder query = new StringBuilder();
query.append("select pseudo from fopseudonym as pseudo")
.append(" where pseudo.key=:pseudonymKey");
List<Pseudonym> pseudonyms = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Pseudonym.class)
.setParameter("pseudonymKey", key)
.getResultList();
return pseudonyms.size() > 0 ? pseudonyms.get(0) : null;
}
public void deletePseudonym(Pseudonym pseudonym) {
Pseudonym reloadedPseudonym = dbInstance.getCurrentEntityManager()
.getReference(PseudonymImpl.class, pseudonym.getKey());
dbInstance.getCurrentEntityManager().remove(reloadedPseudonym);
}
public String getPseudonym(Forum forum, IdentityRef identity) {
StringBuilder query = new StringBuilder();
query.append("select msg.pseudonym from fomessage as msg")
.append(" where msg.creator.key=:identityKey and msg.forum.key=:forumKey and msg.pseudonym is not null");
List<String> pseudonyms = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), String.class)
.setParameter("identityKey", identity.getKey())
.setParameter("forumKey", forum.getKey())
.getResultList();
return pseudonyms == null || pseudonyms.isEmpty() ? null : pseudonyms.get(0);
}
public List<PseudonymStatistics> getPseudonymStatistics(String searchString) {
StringBuilder sb = new StringBuilder();
sb.append("select pseudo.key, pseudo.creationDate, pseudo.pseudonym, count(msg.key)")
.append(" from fopseudonym as pseudo")
.append(" left join fomessage as msg on (msg.pseudonym=pseudo.pseudonym)");
if(StringHelper.containsNonWhitespace(searchString)) {
sb.append(" where ");
PersistenceHelper.appendFuzzyLike(sb, "pseudo.pseudonym", "pseudonym", dbInstance.getDbVendor());
}
sb.append(" group by pseudo.key, pseudo.creationDate, pseudo.pseudonym");
TypedQuery<Object[]> query = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Object[].class);
if(StringHelper.containsNonWhitespace(searchString)) {
query.setParameter("pseudonym", PersistenceHelper.makeFuzzyQueryString(searchString));
}
List<Object[]> objects = query.getResultList();
List<PseudonymStatistics> stats = new ArrayList<>(objects.size());
for(Object[] object:objects) {
Long key = (Long)object[0];
Date creationDate = (Date)object[1];
String pseudonym = (String)object[2];
Long numOfMessages = PersistenceHelper.extractLong(object, 3);
stats.add(new PseudonymStatistics(key, creationDate, pseudonym, numOfMessages));
}
return stats;
}
public List<MessageLight> getLightMessagesByForum(Forum forum) {
StringBuilder query = new StringBuilder();
query.append("select msg from folightmessage as msg")
.append(" left join fetch msg.creator as creator")
.append(" left join fetch msg.modifier as modifier")
.append(" left join fetch msg.threadtop as threadtop")
.append(" where msg.forumKey=:forumKey");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), MessageLight.class)
.setParameter("forumKey", forum.getKey())
.getResultList();
}
public List<MessageLight> getLightMessagesByThread(Forum forum, MessageRef thread) {
StringBuilder query = new StringBuilder();
query.append("select msg from folightmessage as msg")
.append(" left join fetch msg.creator as creator")
.append(" left join fetch msg.modifier as modifier")
.append(" inner join fetch msg.threadtop as threadtop")
.append(" where msg.forumKey=:forumKey and threadtop.key=:threadKey");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), MessageLight.class)
.setParameter("forumKey", forum.getKey())
.setParameter("threadKey", thread.getKey())
.getResultList();
}
public List<MessageLight> getLightMessagesOfGuests(Forum forum) {
StringBuilder query = new StringBuilder();
query.append("select msg from folightmessage as msg")
.append(" left join fetch msg.modifier as modifier")
.append(" left join fetch msg.threadtop as threadtop")
.append(" where msg.forumKey=:forumKey and msg.guest=true");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), MessageLight.class)
.setParameter("forumKey", forum.getKey())
.getResultList();
}
/**
* Return the messages of a user written under it's own name
* (not under a pseudonym).
*
* @param forum
* @param user
* @return
*/
public List<MessageLight> getLightMessagesByUser(Forum forum, IdentityRef user) {
StringBuilder query = new StringBuilder();
query.append("select msg from folightmessage as msg")
.append(" left join fetch msg.creator as creator")
.append(" left join fetch msg.modifier as modifier")
.append(" left join fetch msg.threadtop as threadtop")
.append(" where msg.forumKey=:forumKey and msg.creator.key=:userKey and msg.pseudonym is null");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), MessageLight.class)
.setParameter("forumKey", forum.getKey())
.setParameter("userKey", user.getKey())
.getResultList();
}
/**
* Return the messages of a specified user under a specific
* pseudonym.
*
* @param forum
* @param user
* @param pseudonym
* @return
*/
public List<MessageLight> getLightMessagesByUserUnderPseudo(Forum forum, IdentityRef user, String pseudonym) {
StringBuilder query = new StringBuilder();
query.append("select msg from folightmessage as msg")
.append(" left join fetch msg.creator as creator")
.append(" left join fetch msg.modifier as modifier")
.append(" left join fetch msg.threadtop as threadtop")
.append(" where msg.forumKey=:forumKey and msg.creator.key=:userKey and msg.pseudonym=:pseudonym");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), MessageLight.class)
.setParameter("forumKey", forum.getKey())
.setParameter("userKey", user.getKey())
.setParameter("pseudonym", pseudonym)
.getResultList();
}
public List<ForumUserStatistics> getForumUserStatistics(Forum forum) {
StringBuilder query = new StringBuilder();
query.append("select msg from fomessageforstatistics as msg")
.append(" left join msg.creator as creator")
.append(" where msg.forumKey=:forumKey");
List<MessageStatistics> statistics = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), MessageStatistics.class)
.setParameter("forumKey", forum.getKey())
.getResultList();
Map<StatisticsKey, ForumUserStatistics> identityKeyToStats = new HashMap<>();
for(MessageStatistics statistic:statistics) {
StatisticsKey key = new StatisticsKey(statistic);
ForumUserStatistics userStats;
if(identityKeyToStats.containsKey(key)) {
userStats = identityKeyToStats.get(key);
} else {
userStats = new ForumUserStatistics(statistic.getCreator(), statistic.getPseudonym(), statistic.isGuest());
identityKeyToStats.put(key, userStats);
}
userStats.addNumOfCharacters(statistic.getNumOfCharacters());
userStats.addNumOfWords(statistic.getNumOfWords());
if(statistic.getThreadtopKey() == null) {
userStats.addNumOfThreads(1);
} else {
userStats.addNumOfReplies(1);
}
if(userStats.getLastModified() == null ||
(statistic.getLastModified() != null
&& statistic.getLastModified().after(userStats.getLastModified()))) {
userStats.setLastModified(statistic.getLastModified());
}
}
return new ArrayList<>(identityKeyToStats.values());
}
private static class StatisticsKey {
private boolean guest;
private String pseudonym;
private Long identityKey;
public StatisticsKey(MessageStatistics statistic) {
guest = statistic.isGuest();
pseudonym = statistic.getPseudonym();
if(statistic.getCreator() != null) {
identityKey = statistic.getCreator().getKey();
}
}
@Override
public int hashCode() {
return guest ? 27534 :
((identityKey == null ? 3467 : identityKey.hashCode()) + (pseudonym == null ? 567 : pseudonym.hashCode()));
}
@Override
public boolean equals(Object obj) {
if(obj == this) {
return true;
}
if(obj instanceof StatisticsKey) {
StatisticsKey key = (StatisticsKey)obj;
if(guest && key.guest) {
return (pseudonym == null && key.pseudonym == null) || (pseudonym != null && pseudonym.equals(key.pseudonym));
}
return identityKey != null && identityKey.equals(key.identityKey)
&& ((pseudonym == null && key.pseudonym == null) || (pseudonym != null && pseudonym.equals(key.pseudonym)));
}
return false;
}
}
/**
* Implementation with one entry per message.
* @param identity
* @param forumkey
* @return number of read messages
*/
public int countReadMessagesByUserAndForum(IdentityRef identity, Long forumkey) {
String query = "select count(msg) from foreadmessage as msg where msg.identity.key=:identityKey and msg.forum.key=:forumKey";
List<Number> count = dbInstance.getCurrentEntityManager()
.createQuery(query, Number.class)
.setParameter("identityKey", identity.getKey())
.setParameter("forumKey", forumkey)
.getResultList();
return count == null || count.isEmpty() || count.get(0) == null ? 0 : count.get(0).intValue();
}
/**
* @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) {
StringBuilder query = new StringBuilder();
query.append("select msg from fomessage as msg ")
.append(" inner join fetch msg.creator as creator")
.append(" where msg.forum.key=:forumKey and msg.lastModified>:latestRead order by msg.lastModified desc");
return dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Message.class)
.setParameter("forumKey", forumKey.longValue())
.setParameter("latestRead", latestRead, TemporalType.TIMESTAMP)
.getResultList();
}
/**
* @return the newly created and persisted forum
*/
public Forum addAForum() {
ForumImpl fo = new ForumImpl();
fo.setCreationDate(new Date());
dbInstance.getCurrentEntityManager().persist(fo);
return fo;
}
/**
* @param forumKey
* @return the forum with the given key
*/
public Forum loadForum(Long forumKey) {
String q = "select fo from forum as fo where fo.key=:forumKey";
List<Forum> forumList = dbInstance.getCurrentEntityManager()
.createQuery(q, Forum.class)
.setParameter("forumKey", forumKey)
.getResultList();
return forumList == null || forumList.isEmpty() ? null : forumList.get(0);
}
/**
* @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(final Forum forum) {
final Long forumKey = forum.getKey();
//delete read messsages
String deleteReadMessages = "delete from foreadmessage as rmsg where rmsg.forum.key=:forumKey";
dbInstance.getCurrentEntityManager().createQuery(deleteReadMessages)
.setParameter("forumKey", forumKey)
.executeUpdate();
// delete messages
String messagesToDelete = "select msg from fomessage as msg where msg.forum.key=:forumKey and msg.threadtop.key is null";
List<Message> threadsToDelete = dbInstance.getCurrentEntityManager()
.createQuery(messagesToDelete, Message.class)
.setParameter("forumKey", forumKey)
.getResultList();
for(Message threadToDelete:threadsToDelete) {
deleteMessageTree(forumKey, threadToDelete);
dbInstance.getCurrentEntityManager().remove(threadToDelete);
}
dbInstance.commit();
// delete forum
String deleteForum = "delete from forum as fo where fo.key=:forumKey";
dbInstance.getCurrentEntityManager().createQuery(deleteForum)
.setParameter("forumKey", forumKey)
.executeUpdate();
//delete all flags
OLATResourceable ores = OresHelper.createOLATResourceableInstance(Forum.class, forum.getKey());
markingService.getMarkManager().deleteMarks(ores);
}
/**
* 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, Message replyToMessage) {
Message top = replyToMessage.getThreadtop();
newMessage.setThreadtop((top != null ? top : replyToMessage));
newMessage.setParent(replyToMessage);
saveMessage(newMessage);
}
/**
* @param creator
* @param forum
* @param topMessage
*/
public void addTopMessage(Message topMessage) {
topMessage.setParent(null);
topMessage.setThreadtop(null);
saveMessage(topMessage);
}
/**
* @param messageKey
* @return the message with the given messageKey
*/
public Message loadMessage(Long messageKey) {
StringBuilder sb = new StringBuilder();
sb.append("select msg from fomessage msg where msg.key=:messageKey");
List<Message> messages = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Message.class)
.setParameter("messageKey", messageKey)
.getResultList();
return messages == null || messages.isEmpty() ? null : messages.get(0);
}
private Message saveMessage(Message m) {
updateCounters(m);
m.setLastModified(new Date());
if(m.getKey() == null) {
dbInstance.getCurrentEntityManager().persist(m);
} else {
m = dbInstance.getCurrentEntityManager().merge(m);
}
return 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(Forum forum, Identity creator, boolean guest) {
MessageImpl message = new MessageImpl();
message.setForum(forum);
if(guest) {
message.setGuest(guest);
} else {
message.setCreator(creator);
}
return message;
}
/**
* Update message and fire MultiUserEvent, if any provided. If a not null
* ForumChangedEvent object is provided, then fire event to listeners.
*
* @param m
* @param updateLastModifiedDate
* true: the last modified date is updated to trigger a
* notification; false: last modified date is not modified and no
* notification is sent
* @param event
*/
public Message updateMessage(Message message, boolean updateLastModifiedDate) {
updateCounters(message);
// OLAT-6295 Only update last modified for the operations edit(update), show, and open.
// Don't update the last modified date for the operations close, hide, move and split.
if (updateLastModifiedDate) {
message.setLastModified(new Date());
}
return dbInstance.getCurrentEntityManager().merge(message);
}
/**
* @param forumKey
* @param m
*/
public void deleteMessageTree(Long forumKey, Message m) {
deleteMessageRecursion(forumKey, m);
}
private void deleteMessageRecursion(final Long forumKey, Message m) {
deleteMessageContainer(forumKey, m.getKey());
String query = "select msg from fomessage as msg where msg.parent.key=:parentKey";
List<Message> messages = dbInstance.getCurrentEntityManager().createQuery(query, Message.class)
.setParameter("parentKey", m.getKey()).getResultList();
for (Message element:messages) {
deleteMessageRecursion(forumKey, element);
}
// make sure the message is reloaded if it is not in the hibernate session cache
Message reloadedMessage = dbInstance.getCurrentEntityManager().find(MessageImpl.class, m.getKey());
if(reloadedMessage != null) {
// delete all properties of one single message
deleteMessageProperties(forumKey, reloadedMessage);
dbInstance.getCurrentEntityManager().remove(reloadedMessage);
//delete all flags
OLATResourceable ores = OresHelper.createOLATResourceableInstance(Forum.class, forumKey);
markingService.getMarkManager().deleteMarks(ores, m.getKey().toString());
}
if(log.isDebug()){
log.debug("Deleting message ", m.getKey().toString());
}
}
/**
* @param m
* @return true if the message has children
*/
public boolean hasChildren(Message m) {
String q = "select count(msg) from fomessage msg where msg.parent.key=:parentKey";
List<Number> count = dbInstance.getCurrentEntityManager()
.createQuery(q, Number.class)
.setParameter("parentKey", m.getKey())
.getResultList();
return count == null || count.isEmpty() || count.get(0) == null ? false : count.get(0).longValue() > 0;
}
public int countMessageChildren(Long messageKey ) {
String q = "select count(msg) from fomessage msg where msg.parent.key=:parentKey";
List<Number> count = dbInstance.getCurrentEntityManager()
.createQuery(q, Number.class)
.setParameter("parentKey", messageKey)
.getResultList();
return count == null || count.isEmpty() || count.get(0) == null ? 0 : count.get(0).intValue();
}
public void countMessageChildrenRecursively(Message message, Set<Long> messageKeys) {
List<Message> children = getMessageChildren(message);
for (Message child : children) {
messageKeys.add(child.getKey());
if (hasChildren(child)){
countMessageChildrenRecursively(child, messageKeys);
}
}
}
/**
* deletes entry of one message
*/
private void deleteMessageProperties(Long forumKey, Message m) {
String query = "delete from foreadmessage as rmsg where rmsg.forum.key=:forumKey and rmsg.message.key=:messageKey";
dbInstance.getCurrentEntityManager().createQuery(query)
.setParameter("forumKey", forumKey)
.setParameter("messageKey", m.getKey())
.executeUpdate();
}
/**
* @param forumKey
* @param messageKey
* @return the valid container for the attachments to place into
*/
public VFSContainer getMessageContainer(Long forumKey, Long messageKey) {
VFSContainer forumContainer = getForumContainer(forumKey);
VFSItem messageContainer = forumContainer.resolve(messageKey.toString());
if(messageContainer == null) {
return forumContainer.createChildContainer(messageKey.toString());
} else if(messageContainer instanceof VFSContainer) {
return (VFSContainer)messageContainer;
}
log.error("The following message container is not a directory: " + messageContainer);
return null;
}
public File getMessageDirectory(Long forumKey, Long messageKey, boolean create) {
File forumDir = getForumDirectory(forumKey);
File messageDir = new File(forumDir, messageKey.toString());
if(create && !messageDir.exists()) {
messageDir.mkdirs();
}
return messageDir;
}
private void moveMessageContainer(Long fromForumKey, Long fromMessageKey, Long toForumKey, Long toMessageKey) {
// copy message container
VFSContainer toMessageContainer = getMessageContainer(toForumKey, toMessageKey);
VFSContainer fromMessageContainer = getMessageContainer(fromForumKey, fromMessageKey);
for (VFSItem vfsItem : fromMessageContainer.getItems()) {
toMessageContainer.copyFrom(vfsItem);
}
}
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();
}
public VFSContainer getForumContainer(Long forumKey) {
OlatRootFolderImpl fContainer = new OlatRootFolderImpl("/forum", null);
VFSItem forumContainer = fContainer.resolve(forumKey.toString());
if(forumContainer == null) {
return fContainer.createChildContainer(forumKey.toString());
} else if(forumContainer instanceof VFSContainer) {
return (VFSContainer)forumContainer;
}
log.error("The following forum container is not a directory: " + forumContainer);
return null;
}
public File getForumDirectory(Long forumKey) {
File forumsDir = new File(FolderConfig.getCanonicalRoot(), "forum");
File forumDir = new File(forumsDir,forumKey.toString());
if(!forumDir.exists()) {
forumDir.mkdirs();
}
return forumDir;
}
/**
* Splits the current thread starting from the current message.
* It updates the messages of the selected subthread by setting the Parent and the Threadtop.
* The method send a SPLIT event, and make a commit before sending it.
*
* @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 = getThread(msg.getThreadtop().getKey());
List<Message> subthreadList = new ArrayList<>();
getSubthread(msg, threadList, subthreadList);
newTopMessage = getMessageById(msg.getKey());
newTopMessage.setParent(null);
newTopMessage.setThreadtop(null);
newTopMessage = dbInstance.getCurrentEntityManager().merge(newTopMessage);
for(Message message : subthreadList) {
message.setThreadtop(newTopMessage);
message = dbInstance.getCurrentEntityManager().merge(message);
}
dbInstance.commit();// before sending async event
ForumChangedEvent event = new ForumChangedEvent(ForumChangedEvent.SPLIT, newTopMessage.getKey(), null, null);
CoordinatorManager.getInstance().getCoordinator().getEventBus()
.fireEventToListenersOf(event, newTopMessage.getForum());
}
return newTopMessage;
}
/**
* Moves the current message from the current thread in another thread.
*
* @param msg
* @param topMsg
* @return the moved message
*/
public Message moveMessage(Message msg, Message topMsg) {
// one has to set a new parent for all children of the moved message
// first message of sublist has to get the parent from the moved message
List<Message> children = getMessageChildren(msg);
for (Message child : children) {
child.setParent(msg.getParent());
dbInstance.getCurrentEntityManager().merge(child);
}
// now move the message to the chosen thread
Message targetThread = topMsg.getThreadtop();
if(targetThread == null) {
targetThread = topMsg;
}
final Message oldMessage = getMessageById(msg.getKey());
Message message = persistMessageInAnotherLocation(msg, oldMessage.getForum(), targetThread, topMsg);
//move marks
OLATResourceable ores = OresHelper.createOLATResourceableInstance(Forum.class, msg.getForum().getKey());
markingService.getMarkManager().moveMarks(ores, msg.getKey().toString(), message.getKey().toString());
moveMessageContainer(oldMessage.getForum().getKey(), oldMessage.getKey(), message.getForum().getKey(), message.getKey());
deleteMessageRecursion(oldMessage.getForum().getKey(), oldMessage);
return message;
}
/**
* Collect message children recursively.
*
* @param oldParent
* @param setOfIdentity
*/
public void collectThreadMembersRecursively(Message oldParent, Set<Identity> setOfIdentity, Map<Identity, String> pseudonymes) {
List<Message> children = getMessageChildren(oldParent);
children.sort(new Comparator<Message>() {
@Override
public int compare(Message o1, Message o2) {
if (o1 == null) return 1;
if (o2 == null) return -1;
// move posts with pseudonyms toward low indices in list
if (o1.getPseudonym() == null && o2.getPseudonym() != null) {
return 1;
} else if (o1.getPseudonym() != null && o2.getPseudonym() == null) {
return -1;
} else {
return o1.getCreationDate().compareTo(o2.getCreationDate());
}
}
});
for (Message child : children) {
if (!child.isGuest()) {
Identity creator = child.getCreator();
if (creator != null) {
setOfIdentity.add(creator);
String pseudonym = child.getPseudonym();
if(pseudonym != null) {
pseudonymes.put(creator, pseudonym);
} else if (pseudonymes.containsKey(creator)) {
// remove entry if thread also contains same identity without pseudonym
pseudonymes.remove(creator);
}
}
Identity modifier = child.getModifier();
if (creator != null && modifier != null) {
setOfIdentity.add(modifier);
}
}
if (hasChildren(child)) {
collectThreadMembersRecursively(child, setOfIdentity, pseudonymes);
}
}
}
/**
* Move thread to another forum recursively.
*
* @param oldParent the OLD parent message
* @param newParent the NEW parent message
* @param topMsg the top message
* @return the message
*/
private Message moveThreadToAnotherForumRecursively(Message oldParent, Message newParent, Message topMsg) {
// 1) get direct children of the old top message
List<Message> children = getMessageChildren(oldParent);
Message message = null;
// 2) iterate all first level children
for (Message child : children) {
Message oldMessage = getMessageById(child.getKey());
topMsg = getMessageById(topMsg.getKey());
message = persistMessageInAnotherLocation(oldMessage, topMsg.getForum(), topMsg, newParent);
// 3) move the message container to a new destination
moveMessageContainer(oldMessage.getForum().getKey(), oldMessage.getKey(),
message.getForum().getKey(), message.getKey());
// 4) do recursion if children are available
if (hasChildren(child)) {
moveThreadToAnotherForumRecursively(child, message, topMsg);
}
}
return message;
}
/**
* Creates new thread in another forum or appends selected thread to another thread in another forum.
*
* @param msg the OLD parent message
* @param the destination forum
* @param topMsg the top message
* @return the message
*/
public Message createOrAppendThreadInAnotherForum(Message msg, Forum forum, Message topMsg) {
Message oldMessage = getMessageById(msg.getKey());
Message message = persistMessageInAnotherLocation(oldMessage, forum, topMsg, topMsg);
// reload message from database
message = getMessageById(message.getKey());
moveMessageContainer(oldMessage.getForum().getKey(), oldMessage.getKey(),
message.getForum().getKey(), message.getKey());
if (hasChildren(oldMessage)) {
if (topMsg != null) {
// if added to an existing thread choose its top message
message = moveThreadToAnotherForumRecursively(oldMessage, message, message.getThreadtop());
} else {
// if a new thread is created in a forum the parent message is also the top message
message = moveThreadToAnotherForumRecursively(oldMessage, message, message);
}
}
// deletes all children of the old top message recursively
deleteMessageRecursion(oldMessage.getForum().getKey(), oldMessage);
return message;
}
/**
* Move single message to another forum.
*
* @param msg the OLD parent message
* @param topMsg the NEW top message
* @return the message
*/
public Message moveMessageToAnotherForum(Message msg, Forum forum, Message topMsg) {
Message targetThread = null;
if (topMsg != null) {
targetThread = topMsg.getThreadtop();
if (targetThread == null) {
targetThread = topMsg;
}
targetThread = getMessageById(targetThread.getKey());
}
final Message oldParent = getMessageById(msg.getKey());
// one has to set a new parent for all children of the moved message
Message newParent = persistMessageInAnotherLocation(oldParent, forum, targetThread, topMsg);
moveMessageContainer(oldParent.getForum().getKey(), oldParent.getKey(), newParent.getForum().getKey(), newParent.getKey());
targetThread = targetThread == null ? newParent : targetThread;
if (hasChildren(oldParent)) {
moveThreadToAnotherForumRecursively(oldParent, newParent, targetThread);
}
deleteMessageRecursion(oldParent.getForum().getKey(), oldParent);
return newParent;
}
/**
* Persist message in another location.
*/
private Message persistMessageInAnotherLocation(Message oldMessage, Forum forum, Message top, Message parent) {
// 1) take the new top messages forum to create a new child
Message message = createMessage(forum, oldMessage.getCreator(), oldMessage.isGuest());
((MessageImpl)message).setCreationDate(oldMessage.getCreationDate());
message.setLastModified(oldMessage.getLastModified());
message.setModifier(oldMessage.getModifier());
message.setTitle(oldMessage.getTitle());
message.setBody(oldMessage.getBody());
message.setPseudonym(oldMessage.getPseudonym());
// 2) set the thread top to the new top message
message.setThreadtop(top);
// 3) maintain the hierarchy, parent and top message can be equal
message.setParent(parent);
Status status = Status.getStatus(oldMessage.getStatusCode());
if (status != null){
status.setMoved(true);
message.setStatusCode(Status.getStatusCode(status));
}
// 4) save the new massage in the new destination
message = 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(IdentityRef identity, Forum forum) {
StringBuilder query = new StringBuilder();
query.append("select rmsg.message.key from foreadmessage as rmsg")
.append(" inner join rmsg.message as msg")
.append(" where rmsg.forum.key=:forumKey and rmsg.identity.key=:identityKey");
List<Long> messageKeys = dbInstance.getCurrentEntityManager()
.createQuery(query.toString(), Long.class)
.setParameter("forumKey", forum.getKey())
.setParameter("identityKey", identity.getKey())
.getResultList();
return new HashSet<Long>(messageKeys);
}
/**
* Optimized method to mark newly created messages as read.
*
* @param identity
* @param forum
* @param msg
*/
public void markNewMessageAsRead(Identity identity, Forum forum, Message msg) {
//Check if the message was not already deleted
ReadMessageImpl readMessage = new ReadMessageImpl();
readMessage.setIdentity(identity);
readMessage.setMessage(msg);
readMessage.setForum(forum);
dbInstance.getCurrentEntityManager().persist(readMessage);
}
/**
* 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, Forum forum, MessageLight msg) {
//Check if the message was not already deleted
Message retrievedMessage = loadMessage(msg.getKey());
if(retrievedMessage != null) {
ReadMessageImpl readMessage = new ReadMessageImpl();
readMessage.setIdentity(identity);
if(msg instanceof MessageLightImpl) {
readMessage.setMessage(msg);
} else {
msg = dbInstance.getCurrentEntityManager().getReference(MessageLightImpl.class, msg.getKey());
readMessage.setMessage(msg);
}
readMessage.setForum(forum);
dbInstance.getCurrentEntityManager().persist(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);
Locale suggestedLocale = txtService.detectLocale(unQuotedBody);
m.setNumOfWords(txtService.wordCount(unQuotedBody, suggestedLocale));
m.setNumOfCharacters(txtService.characterCount(unQuotedBody, suggestedLocale));
}
public int mergeForums(Long masterForumKey, List<Long> forumsToMerge) {
int rows = 0;
if(forumsToMerge.size() > 0) {
for(Long forumToMerge:forumsToMerge) {
String updateMsg = "update fomessage set forum.key=:masterKey where forum.key=:mergerKey";
rows += dbInstance.getCurrentEntityManager()
.createQuery(updateMsg)
.setParameter("masterKey", masterForumKey)
.setParameter("mergerKey", forumToMerge)
.executeUpdate();
String updateReadMsg = "update foreadmessage set forum.key=:masterKey where forum.key=:mergerKey";
rows += dbInstance.getCurrentEntityManager()
.createQuery(updateReadMsg)
.setParameter("masterKey", masterForumKey)
.setParameter("mergerKey", forumToMerge)
.executeUpdate();
}
}
return rows;
}
}