1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
68
69
70
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
95
96 public static final String ROLE_FORUM_BASE = "forum-base";
97
98
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
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
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
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
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
267
268 protected void isModerator(HierarchyManager hm, Content node) throws AccessDeniedException {
269
270
271
272
273 isModerator();
274 }
275
276 @Override
277 public void isModerator() throws AccessDeniedException{
278 User currentUser = MgnlContext.getUser();
279
280
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
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
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
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
378 checkAndUpdateLastMessage(thread, messageID);
379
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
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
471
472
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
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
500
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
513
514 protected String cleanup(String s) {
515 if (StringUtils.isEmpty(s)) {
516 return "_";
517 }
518
519 return Path.getValidatedLabel(s).toLowerCase();
520 }
521
522
523
524
525
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
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
568 return;
569 }
570 lastMessage = null;
571
572 for (Content aThreadOrMessage : forumOrThread.getChildren(threadOrMessage.getItemType())) {
573 if (threadOrMessageID.equals(aThreadOrMessage.getUUID())) {
574
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
587
588
589 Content forum = forumOrThread.getParent();
590 if (forum.hasNodeData(LAST_MESSAGE_PROPERTY)) {
591 lastMessage = null;
592
593 if (forumOrThreadLastMessageID.equals(forum.getNodeData(LAST_MESSAGE_PROPERTY).getString())) {
594
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
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
615
616
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
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 }