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