View Javadoc
1   /**
2    * This file Copyright (c) 2003-2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.forum;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.core.HierarchyManager;
38  import info.magnolia.cms.core.NodeData;
39  import info.magnolia.cms.core.Path;
40  import info.magnolia.cms.security.AccessDeniedException;
41  import info.magnolia.cms.security.AccessManager;
42  import info.magnolia.cms.security.Permission;
43  import info.magnolia.cms.security.Role;
44  import info.magnolia.cms.security.RoleManager;
45  import info.magnolia.cms.security.SecuritySupport;
46  import info.magnolia.cms.security.User;
47  import info.magnolia.cms.security.UserManager;
48  import info.magnolia.cms.util.ExclusiveWrite;
49  import info.magnolia.cms.util.HierarchyManagerUtil;
50  import info.magnolia.cms.util.NodeDataUtil;
51  import info.magnolia.cms.util.SiblingsHelper;
52  import info.magnolia.context.MgnlContext;
53  import info.magnolia.module.ModuleRegistry;
54  
55  import java.text.MessageFormat;
56  import java.util.Calendar;
57  import java.util.Collection;
58  
59  import javax.jcr.RepositoryException;
60  import javax.jcr.query.Query;
61  import javax.jcr.query.QueryManager;
62  import javax.jcr.query.QueryResult;
63  
64  import org.apache.commons.lang3.StringUtils;
65  import org.apache.commons.lang3.time.DateFormatUtils;
66  
67  /**
68   * Default implementation of the {@link info.magnolia.module.forum.ForumManager} .
69   * 
70   * @author gjoseph
71   * @version $Revision: $ ($Author: $)
72   */
73  public class DefaultForumManager implements ForumManager {
74      private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultForumManager.class);
75  
76      public static final String FORUM_WORKSPACE = "forum";
77      public static final String FORUM_NODETYPE = "mgnl:forum";
78      public static final String THREAD_NODETYPE = "mgnl:thread";
79      public static final String MESSAGE_NODETYPE = "mgnl:message";
80      public static final String ALLOWS_NESTING_MESSAGES = "allowsNestingMessages";
81      public static final String VALIDATED_PROPERTY = "validated";
82      public static final String LOCKED_PROPERTY = "locked";
83      public static final String FIRST_MESSAGE_PROPERTY = "firstMessage";
84      public static final String LAST_MESSAGE_PROPERTY = "lastMessage";
85      public static final String CREATION_DATE_PROPERTY = "creationDate";
86      public static final String TITLE_PROPERTY = "title";
87      public static final String AUTHORID_PROPERTY = "authorId";
88      public static final String CONTENT_PROPERTY = "content";
89  
90      public static final String ROLE_FORUM_ALL_ADMIN = "forum_ALL-admin";
91      public static final String ROLE_FORUM_ALL_MODERATOR = "forum_ALL-moderator";
92      public static final String ROLE_FORUM_ALL_USER = "forum_ALL-user";
93  
94      /**
95       * @deprecated since 3.3 This role is no more supported.
96       */
97      public static final String ROLE_FORUM_BASE = "forum-base";
98      /**
99       * @deprecated since 3.3 This role is no more supported.
100      */
101     public static final String ROLE_FORUM_MODERATOR_BASE = "forum-moderator-base";
102 
103     private static final String MESSAGES_QUERY_PATTERN = "select * from mgnl:message where jcr:path like ''{0}/%'' and (" + VALIDATED_PROPERTY + "=''true''{1}){2}";
104     private static final String SHOW_UNVALIDATED_MESSAGES_CLAUSE = " or " + VALIDATED_PROPERTY + " is null";
105 
106     private final ForumConfiguration config;
107 
108     public DefaultForumManager() {
109         this(ModuleRegistry.Factory.getInstance().getModuleInstance(ForumConfiguration.class));
110     }
111 
112     /** for tests - or when we finally have IOC. */
113     public DefaultForumManager(ForumConfiguration config) {
114         this.config = config;
115     }
116 
117     @Override
118     public Collection<Content> getForumList() throws RepositoryException {
119         final HierarchyManager hm = getHierarchyManager();
120         return hm.getRoot().getChildren(FORUM_NODETYPE);
121     }
122 
123     @Override
124     public String getForumId(String shortName) throws RepositoryException {
125         if (!StringUtils.startsWith(shortName, "/")) {
126             shortName = "/" + shortName;
127         }
128         return getHierarchyManager().getContent(shortName).getUUID();
129     }
130 
131     @Override
132     public Content getForum(String forumID) throws RepositoryException {
133         final HierarchyManager hm = getHierarchyManager();
134         return getForum(hm, forumID);
135     }
136 
137     @Override
138     public Collection<Content> getThreads(Content forum) throws RepositoryException {
139         final boolean showingUnvalidatedMessages = config.isShowingUnvalidatedMessages();
140         return forum.getChildren(new ThreadsFilter(showingUnvalidatedMessages));
141     }
142 
143     @Override
144     public Content createForum(String name, String title) throws RepositoryException {
145         return createForum(name, title, false);
146     }
147 
148     @Override
149     public Content createForum(String name, String title, boolean withMessageNesting) throws RepositoryException {
150         final HierarchyManager hm = getHierarchyManager();
151         final String cleanName = Path.getUniqueLabel(hm.getWorkspace().getSession(), "/", cleanup(name));
152         final Content repoRoot = hm.getRoot();
153         final Content newForum = repoRoot.createContent(cleanName, FORUM_NODETYPE);
154         newForum.createNodeData("title", title);
155         newForum.setNodeData(ALLOWS_NESTING_MESSAGES, withMessageNesting);
156         repoRoot.save();
157 
158         if (config.isCreateRolesForNewForums()) {
159             createForumRoles(newForum);
160         }
161         return newForum;
162     }
163 
164     protected void createForumRoles(Content forumNode) {
165         final String forumName = forumNode.getName();
166         final String path = forumNode.getHandle();
167         // create roles with system context
168         MgnlContext.doInSystemContext(new MgnlContext.VoidOp() {
169             @Override
170             public void doExec() {
171                 try {
172                     final RoleManager roleManager = SecuritySupport.Factory.getInstance().getRoleManager();
173                     String roleName = "forum-" + forumName + "-user";
174                     if (roleManager.getRole(roleName) == null) {
175                         final Role user = roleManager.createRole(roleName);
176                         addPermission(user, path, Permission.ALL, roleManager);
177                     }
178                 } catch (Exception e) {
179                     throw new RuntimeException(e);
180                 }
181             }
182         });
183     }
184 
185     protected void addPermission(Role user, String path, long permission, RoleManager roleManager) {
186         roleManager.addPermission(user, config.getRepository(), path, permission);
187         roleManager.addPermission(user, config.getRepository(), path + "/*", permission);
188     }
189 
190     @Override
191     public Content createThread(String forumID, String threadTitle, String messageText, String author, boolean isAnonymous) throws RepositoryException {
192         return createThread(forumID, threadTitle, threadTitle, messageText, author, isAnonymous);
193     }
194 
195     @Override
196     public Content createThread(String forumID, String threadTitle, String messageTitle, String messageText, String author, boolean isAnonymous) throws RepositoryException {
197         // validation (TODO : more? or we leave that up to client code, i.e some web framework - here we just validate arguments to avoid jcr failures)
198         if (StringUtils.isEmpty(threadTitle)) {
199             throw new IllegalArgumentException("Thread title must be specified");
200         }
201         final HierarchyManager hm = getHierarchyManager();
202         final Content forum = getForum(hm, forumID);
203         checkedLockedOrModerator(hm, forum);
204 
205         final String shortTitle = cleanup(threadTitle);
206         final Content thread = forum.createContent(shortTitle, THREAD_NODETYPE);
207         thread.createNodeData(TITLE_PROPERTY, threadTitle);
208         final String messageName = generateMessageName();
209         final Content message = createMessageInThread(forum, thread, messageName, messageTitle, messageText, author, isAnonymous);
210         thread.createNodeData(DefaultForumManager.FIRST_MESSAGE_PROPERTY, message);
211         forum.save();
212         return thread;
213     }
214 
215     @Override
216     public Content replyToThread(String threadID, String inReplyToID, String messageTitle, String messageText, String author, boolean isAnonymous) throws RepositoryException {
217         synchronized (ExclusiveWrite.getInstance()) {
218             final HierarchyManager hm = getHierarchyManager();
219             final Content thread = getThread(hm, threadID);
220             final Content forum = getForumFromThread(thread);
221             checkedLockedOrModerator(hm, thread);
222 
223             final Content inReplyTo;
224             if (inReplyToID != null) {
225                 inReplyTo = hm.getContentByUUID(inReplyToID);
226             } else {
227                 inReplyTo = null;
228             }
229 
230             final String messageName = generateMessageName();
231             final Content newMessage = createMessageInThread(forum, thread, inReplyTo, messageName, messageTitle, messageText, author, isAnonymous);
232             forum.save();
233             return newMessage;
234         }
235     }
236 
237     @Override
238     public boolean isAllowedToPostOnForum(Content forum) {
239         return isAllowedToPostOn(forum);
240     }
241 
242     @Override
243     public boolean isAllowedToPostOnThread(Content thread) {
244         return isAllowedToPostOn(thread);
245     }
246 
247     protected boolean isAllowedToPostOn(Content node) {
248         try {
249             final AccessManager am = MgnlContext.getAccessManager(node.getWorkspace().getName());
250             return am.isGranted(node.getHandle(), Permission.WRITE);
251         } catch (RepositoryException e) {
252             throw new RuntimeException(e);
253         }
254     }
255 
256     /**
257      * Throws an exception if thread is locked and user is not moderator.
258      */
259     protected void checkedLockedOrModerator(HierarchyManager hm, Content node) throws AccessDeniedException {
260         final boolean locked = node.getNodeData(LOCKED_PROPERTY).getBoolean();
261         if (locked) {
262             isModerator(hm, node);
263         }
264     }
265 
266     /**
267      * Throws an exception if user is not moderator for the given node.
268      */
269     protected void isModerator(HierarchyManager hm, Content node) throws AccessDeniedException {
270         /*
271          * since we are using a more simplified security on forum shipped with M5.x-versions, this check here will be role- instead of ACL-based
272          * see MGNLFORUM-250, MGNLFORUM-254
273          */
274         isModerator();
275     }
276 
277     @Override
278     public void isModerator() throws AccessDeniedException{
279         User currentUser = MgnlContext.getUser();
280         // Needs to use getAllRoles() instead of .hasRole() because .hasRole() will only check for the roles directly attached to the user, but not the ones inherited from the group.
281         // As roles can not directly be attached to a AD user, it is crucial to be able to define it over its group.
282         Collection<String> allRoles = currentUser.getAllRoles();
283         if (!allRoles.contains(ROLE_FORUM_ALL_MODERATOR) && !allRoles.contains(ROLE_FORUM_ALL_ADMIN)) {
284             throw new AccessDeniedException("User not allowed to perform that action.");
285         }
286     }
287 
288     @Override
289     public Content getThread(String threadID) throws RepositoryException {
290         final HierarchyManager hm = getHierarchyManager();
291         return getThread(hm, threadID);
292     }
293 
294     @Override
295     public PagedResult getMessages(Content thread, long page) throws RepositoryException {
296         if (page <= 0) {
297             throw new IllegalArgumentException("Page number must be >= 1");
298         }
299 
300         final boolean showingUnvalidatedMessages = config.isShowingUnvalidatedMessages();
301 
302         final String threadPath = thread.getHandle();
303         final HierarchyManager hm = getHierarchyManager();
304         final QueryManager qm = hm.getWorkspace().getQueryManager();
305         final Query query = qm.createQuery(getMessagesQueryString(threadPath, showingUnvalidatedMessages, ""), Query.SQL);
306         final QueryResult queryResult = query.execute();
307 
308         final Pager pager = new Pager(hm, queryResult);
309         final long skip = config.getMessagesPerPage() * (page - 1);
310         return pager.page(skip, config.getMessagesPerPage());
311     }
312 
313     @Override
314     public Collection<Content> getForumMessages(String forumName) throws RepositoryException {
315         final String forumPath = "/" + forumName;
316         final boolean showUnvalidatedMessages = config.isShowingUnvalidatedMessages();
317         final String sql = getMessagesQueryString(forumPath, showUnvalidatedMessages, " order by creationDate desc");
318         final HierarchyManager hm = getHierarchyManager();
319         final info.magnolia.cms.core.search.QueryManager qm = hm.getQueryManager();
320         info.magnolia.cms.core.search.Query query = qm.createQuery(sql, Query.SQL);
321         info.magnolia.cms.core.search.QueryResult result = query.execute();
322         return result.getContent(MESSAGE_NODETYPE);
323     }
324 
325     protected String getMessagesQueryString(String path, boolean showUnvalidatedMessages, String orderClause) {
326         return MessageFormat.format(MESSAGES_QUERY_PATTERN, new String[] { path, showUnvalidatedMessages ? SHOW_UNVALIDATED_MESSAGES_CLAUSE : "", orderClause });
327     }
328 
329     @Override
330     public Content getForumFromThread(Content thread) throws RepositoryException {
331         // we go up the ancestors until we find a mgnl:forum, in case stuff is nested ?
332         Content forum = thread.getParent();
333         while (!forum.isNodeType(FORUM_NODETYPE)) {
334             forum = forum.getParent();
335         }
336         return forum;
337     }
338 
339     @Override
340     public Content getThreadFromMessage(Content message) throws RepositoryException {
341         // we go up the ancestors until we find a mgnl:forum, in case messages are nested
342         Content thread = message.getParent();
343         while (!thread.isNodeType(THREAD_NODETYPE)) {
344             thread = thread.getParent();
345         }
346         return thread;
347     }
348 
349     protected Content getForum(HierarchyManager hm, String forumID) throws RepositoryException {
350         return hm.getContentByUUID(forumID);
351     }
352 
353     protected Content getThread(HierarchyManager hm, String threadID) throws RepositoryException {
354         return hm.getContentByUUID(threadID);
355     }
356 
357     @Override
358     public void deleteForum(String forumID) throws RepositoryException {
359         deleteByUuid(forumID);
360     }
361 
362     @Override
363     public void deleteThread(String threadID) throws RepositoryException {
364         // ensure forum last message ref is not in this thread
365         final Content thread = getThread(threadID);
366         final Content forum = getForumFromThread(thread);
367         isModerator(getHierarchyManager(), thread);
368         checkAndUpdateLastMessage(forum, threadID);
369         deleteByUuid(threadID);
370     }
371 
372     @Override
373     public void deleteMessage(String messageID) throws RepositoryException {
374         final Content message = getHierarchyManager().getContentByUUID(messageID);
375         final Content thread = getThreadFromMessage(message);
376         isModerator(getHierarchyManager(), message);
377 
378         // last message first
379         checkAndUpdateLastMessage(thread, messageID);
380         // and also check the first message
381         if (messageID.equals(thread.getNodeData(FIRST_MESSAGE_PROPERTY).getString())) {
382             SiblingsHelper siblings = SiblingsHelper.of(message);
383             if (siblings.isLast()) {
384                 throw new UnsupportedOperationException("Thread needs at least one message. Create another message first or delete whole thread.");
385             } else {
386                 Content nextMessage = siblings.next();
387                 thread.deleteNodeData(FIRST_MESSAGE_PROPERTY);
388                 thread.createNodeData(FIRST_MESSAGE_PROPERTY, nextMessage);
389             }
390         }
391         deleteByUuid(messageID);
392     }
393 
394     @Override
395     public void lockForum(String forumID) throws RepositoryException {
396         changeLock(forumID, true);
397     }
398 
399     @Override
400     public void unlockForum(String forumID) throws RepositoryException {
401         changeLock(forumID, false);
402     }
403 
404     @Override
405     public void lockThread(String threadID) throws RepositoryException {
406         changeLock(threadID, true);
407     }
408 
409     @Override
410     public void unlockThread(String threadID) throws RepositoryException {
411         changeLock(threadID, false);
412     }
413 
414     @Override
415     public void validate(String messageID) throws RepositoryException {
416         changeValidationState(messageID, true);
417     }
418 
419     @Override
420     public void invalidate(String messageID) throws RepositoryException {
421         changeValidationState(messageID, false);
422     }
423 
424     protected void deleteByUuid(String uuid) throws RepositoryException {
425         final Content node = getHierarchyManager().getContentByUUID(uuid);
426         isModerator(getHierarchyManager(),node);
427         final Content parent = node.getParent();
428         node.delete();
429         parent.save();
430     }
431 
432     protected void changeLock(String uuid, boolean value) throws RepositoryException {
433         final HierarchyManager hm = getHierarchyManager();
434         final Content node = hm.getContentByUUID(uuid);
435         isModerator(hm, node);
436         NodeDataUtil.getOrCreateAndSet(node, LOCKED_PROPERTY, value);
437         node.save();
438     }
439 
440     protected void changeValidationState(String messageID, boolean value) throws RepositoryException {
441         final HierarchyManager hm = getHierarchyManager();
442         final Content node = hm.getContentByUUID(messageID);
443         isModerator(hm, node);
444         NodeDataUtil.getOrCreateAndSet(node, VALIDATED_PROPERTY, value);
445         node.save();
446     }
447 
448     /**
449      * Creates a message node in the given thread or as a subnode of <tt>inReplyTo</tt> depending on {@link #allowsNestingMessages(info.magnolia.cms.core.Content)} .
450      */
451     protected Content createMessageInThread(Content forum, Content thread, Content inReplyTo, String messageName, String messageTitle, String messageText, String author, boolean isAnonymous) throws RepositoryException {
452         final boolean allowsNestingMessages = allowsNestingMessages(forum);
453         final Content parent;
454         if (inReplyTo != null && allowsNestingMessages) {
455             parent = inReplyTo;
456         } else {
457             parent = thread;
458         }
459 
460         final Content newMessage = createMessage(parent, messageName, messageTitle, messageText, author, isAnonymous);
461         thread.createNodeData(LAST_MESSAGE_PROPERTY, newMessage);
462         forum.createNodeData(LAST_MESSAGE_PROPERTY, newMessage);
463 
464         if (inReplyTo != null && !allowsNestingMessages) {
465             newMessage.createNodeData("inReplyTo", inReplyTo);
466         }
467         return newMessage;
468     }
469 
470     /**
471      * Creates a message node in the given thread, assuming the parent is a thread node.
472      * 
473      * @deprecated use {@link #createMessageInThread(info.magnolia.cms.core.Content, info.magnolia.cms.core.Content, info.magnolia.cms.core.Content, String, String, String, String, boolean)} instead.
474      */
475     @Deprecated
476     protected Content createMessageInThread(Content forum, Content thread, String name, String title, String messageText, String author, boolean isAnonymous) throws RepositoryException {
477         return createMessageInThread(forum, thread, null, name, title, messageText, author, isAnonymous);
478     }
479 
480     /**
481      * Creates a message node in the given parent, not taking care about the parent node's type.
482      */
483     protected Content createMessage(Content parent, String name, String title, String messageText, String author, boolean isAnonymous) throws RepositoryException {
484         final Content message = parent.createContent(makeNameUnique(parent, name), MESSAGE_NODETYPE);
485         if (StringUtils.isNotEmpty(title)) {
486             message.createNodeData(TITLE_PROPERTY, title);
487         }
488         message.createNodeData(CONTENT_PROPERTY, messageText);
489         if (!isAnonymous) {
490             message.createNodeData(AUTHORID_PROPERTY, author);
491         } else {
492             message.createNodeData(AUTHORID_PROPERTY, UserManager.ANONYMOUS_USER);
493             message.createNodeData("anonymousUsername", author);
494         }
495         message.createNodeData(CREATION_DATE_PROPERTY, Calendar.getInstance());
496         return message;
497     }
498 
499     /**
500      * Generates a message name based on the date pattern specified in the
501      * configuration via the 'messageNamePattern' property.
502      */
503     protected String generateMessageName() {
504         final String messageNamePattern = config.getMessageNamePattern();
505         return DateFormatUtils.format(Calendar.getInstance().getTime(), messageNamePattern);
506     }
507 
508     protected String makeNameUnique(Content parent, String name) {
509         return Path.getUniqueLabel(parent.getJCRNode(), cleanup(name));
510     }
511 
512     /**
513      * Cleans up a String, making it appropriate for usage as a node name.
514      */
515     protected String cleanup(String s) {
516         if (StringUtils.isEmpty(s)) {
517             return "_";
518         }
519         // TODO : getValidatedLabel() should be copied/moved to a more appropriate(ly named) class?
520         return Path.getValidatedLabel(s).toLowerCase();
521     }
522 
523     /**
524      * A ContentFilter which returns children of type THREAD_NODETYPE, where
525      * the firstMessage's referenced node has a validated property is set to true
526      * if showUnvalidatedMessages is true.
527      */
528     private static class ThreadsFilter implements Content.ContentFilter {
529         private final boolean showUnvalidatedMessages;
530 
531         public ThreadsFilter(boolean showUnvalidatedMessages) {
532             this.showUnvalidatedMessages = showUnvalidatedMessages;
533         }
534 
535         @Override
536         public boolean accept(Content content) {
537             if (!content.isNodeType(THREAD_NODETYPE)) {
538                 return false;
539             }
540 
541             final NodeData firstMsgProp = content.getNodeData(DefaultForumManager.FIRST_MESSAGE_PROPERTY);
542             if (!firstMsgProp.isExist()) {
543                 return false;
544             }
545             try {
546                 final Content firstMessage = firstMsgProp.getReferencedContent();
547                 final NodeData validatedProp = firstMessage.getNodeData(VALIDATED_PROPERTY);
548                 return (!validatedProp.isExist() && showUnvalidatedMessages) || (validatedProp.isExist() && validatedProp.getBoolean());
549             } catch (RepositoryException e) {
550                 log.error("Couldn't check if thread[" + content + "] could to be shown: " + e.getMessage(), e);
551                 return false;
552             }
553         }
554     }
555 
556     private void checkAndUpdateLastMessage(final Content forumOrThread, final String threadOrMessageID) throws RepositoryException {
557         Content lastMessage;
558         // get message from ID
559         final Content threadOrMessage = forumOrThread.getHierarchyManager().getContentByUUID(threadOrMessageID);
560         if (threadOrMessage.hasNodeData(LAST_MESSAGE_PROPERTY)) {
561             lastMessage = threadOrMessage.getNodeData(LAST_MESSAGE_PROPERTY).getReferencedContent();
562         } else {
563             lastMessage = threadOrMessage;
564         }
565         Content threadOrForumLM = forumOrThread.getNodeData(LAST_MESSAGE_PROPERTY).getReferencedContent();
566         String forumOrThreadLastMessageID = threadOrForumLM.getUUID();
567         if (!forumOrThreadLastMessageID.equals(lastMessage.getUUID())) {
568             // this message is not the last one => bail out
569             return;
570         }
571         lastMessage = null;
572         // find other last message, while skipping itself
573         for (Content aThreadOrMessage : forumOrThread.getChildren(threadOrMessage.getItemType())) {
574             if (threadOrMessageID.equals(aThreadOrMessage.getUUID())) {
575                 // skip itself
576                 continue;
577             }
578             Content aMessage;
579             if (aThreadOrMessage.hasNodeData(LAST_MESSAGE_PROPERTY)) {
580                 aMessage = aThreadOrMessage.getNodeData(LAST_MESSAGE_PROPERTY).getReferencedContent();
581             } else {
582                 aMessage = aThreadOrMessage;
583             }
584             lastMessage = latestMessage(lastMessage, aMessage);
585         }
586         updateLastMessage(forumOrThread, lastMessage);
587         // changes to the thread/forum will be saved as part of deleting the message/thread ... if that succeeds that is
588 
589         // now if this was a message delete from thread, check if parent forum also need an update
590         Content forum = forumOrThread.getParent();
591         if (forum.hasNodeData(LAST_MESSAGE_PROPERTY)) {
592             lastMessage = null;
593             // do we need to update forum as well?
594             if (forumOrThreadLastMessageID.equals(forum.getNodeData(LAST_MESSAGE_PROPERTY).getString())) {
595                 // the thread pointing to the latest message have already been updated, so iterate over all threads (incl the current) and choose the latest message from all the latest there are
596                 for (Content thread : forum.getChildren(forumOrThread.getItemType())) {
597                     Content aMessage = thread.getNodeData(LAST_MESSAGE_PROPERTY).getReferencedContent();
598                     lastMessage = latestMessage(lastMessage, aMessage);
599                 }
600                 updateLastMessage(forum, lastMessage);
601                 // explicit save for forum and deleteMessage will save just a thread
602                 forum.save();
603             }
604         }
605     }
606 
607     private void updateLastMessage(Content forumOrThread, Content lastMessage) throws RepositoryException {
608         forumOrThread.deleteNodeData(LAST_MESSAGE_PROPERTY);
609         if (lastMessage != null) {
610             forumOrThread.createNodeData(LAST_MESSAGE_PROPERTY, lastMessage);
611         }
612     }
613 
614     /**
615      * Determines which of the 2 messages is "latest". Default implementation considers only a date, however more complex impls can check also whether messages are validated or any other properties.
616      * 
617      * @return the one of the messages that is considered newer then the other.
618      */
619     protected Content latestMessage(Content firstMessage, Content secondMessage) {
620         if (firstMessage == null || secondMessage.getNodeData(CREATION_DATE_PROPERTY).getDate().after(firstMessage.getNodeData(CREATION_DATE_PROPERTY).getDate())) {
621             firstMessage = secondMessage;
622         }
623         return firstMessage;
624     }
625 
626     /**
627      * Returns true if the given forum allows nesting messages.
628      */
629     protected boolean allowsNestingMessages(Content forum) {
630         return forum.getNodeData(ALLOWS_NESTING_MESSAGES).getBoolean();
631     }
632 
633     protected HierarchyManager getHierarchyManager() {
634         return HierarchyManagerUtil.getHierarchyManager(MgnlContext.getInstance(), config.getRepository());
635     }
636 
637 }