001/* 002 * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nuxeo - initial API and implementation 016 * 017 * $Id$ 018 */ 019 020package org.nuxeo.ecm.platform.comment.web; 021 022import java.util.ArrayList; 023import java.util.Calendar; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import javax.faces.event.ActionEvent; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.jboss.seam.annotations.Create; 034import org.jboss.seam.annotations.Destroy; 035import org.jboss.seam.annotations.In; 036import org.jboss.seam.annotations.Observer; 037import org.jboss.seam.annotations.intercept.BypassInterceptors; 038import org.jboss.seam.annotations.web.RequestParameter; 039import org.jboss.seam.contexts.Contexts; 040import org.nuxeo.ecm.core.api.CoreSession; 041import org.nuxeo.ecm.core.api.DocumentModel; 042import org.nuxeo.ecm.core.api.NuxeoException; 043import org.nuxeo.ecm.core.api.NuxeoPrincipal; 044import org.nuxeo.ecm.platform.actions.Action; 045import org.nuxeo.ecm.platform.comment.api.CommentableDocument; 046import org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants; 047import org.nuxeo.ecm.platform.comment.workflow.utils.FollowTransitionUnrestricted; 048import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 049import org.nuxeo.ecm.platform.ui.web.api.WebActions; 050import org.nuxeo.ecm.webapp.helpers.EventNames; 051import org.nuxeo.ecm.webapp.security.UserSession; 052 053/** 054 * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a> 055 */ 056public abstract class AbstractCommentManagerActionsBean implements CommentManagerActions { 057 058 protected static final String COMMENTS_ACTIONS = "COMMENT_ACTIONS"; 059 060 private static final Log log = LogFactory.getLog(AbstractCommentManagerActionsBean.class); 061 062 protected NuxeoPrincipal principal; 063 064 protected boolean principalIsAdmin; 065 066 protected boolean showCreateForm; 067 068 @In(create = true, required = false) 069 protected transient CoreSession documentManager; 070 071 @In(create = true) 072 protected transient WebActions webActions; 073 074 protected String newContent; 075 076 protected CommentableDocument commentableDoc; 077 078 protected List<UIComment> uiComments; 079 080 protected List<ThreadEntry> commentThread; 081 082 // the id of the comment to delete 083 @RequestParameter 084 protected String deleteCommentId; 085 086 // the id of the comment to reply to 087 @RequestParameter 088 protected String replyCommentId; 089 090 protected String savedReplyCommentId; 091 092 protected Map<String, UIComment> commentMap; 093 094 protected boolean commentStarted; 095 096 protected List<UIComment> flatComments; 097 098 @In(create = true) 099 protected UserSession userSession; 100 101 @In(create = true) 102 protected NavigationContext navigationContext; 103 104 @Override 105 @Create 106 public void initialize() { 107 log.debug("Initializing..."); 108 commentMap = new HashMap<String, UIComment>(); 109 showCreateForm = false; 110 111 principal = userSession.getCurrentNuxeoPrincipal(); 112 principalIsAdmin = principal.isAdministrator(); 113 } 114 115 @Override 116 @Destroy 117 public void destroy() { 118 commentMap = null; 119 log.debug("Removing Seam action listener..."); 120 } 121 122 @Override 123 public String getPrincipalName() { 124 return principal.getName(); 125 } 126 127 @Override 128 public boolean getPrincipalIsAdmin() { 129 return principalIsAdmin; 130 } 131 132 protected DocumentModel initializeComment(DocumentModel comment) { 133 if (comment != null) { 134 if (comment.getProperty("dublincore", "contributors") == null) { 135 String[] contributors = new String[1]; 136 contributors[0] = getPrincipalName(); 137 comment.setProperty("dublincore", "contributors", contributors); 138 } 139 if (comment.getProperty("dublincore", "created") == null) { 140 comment.setProperty("dublincore", "created", Calendar.getInstance()); 141 } 142 } 143 return comment; 144 } 145 146 public DocumentModel addComment(DocumentModel comment, DocumentModel docToComment) { 147 comment = initializeComment(comment); 148 UIComment parentComment = null; 149 if (savedReplyCommentId != null) { 150 parentComment = commentMap.get(savedReplyCommentId); 151 } 152 if (docToComment != null) { 153 commentableDoc = getCommentableDoc(docToComment); 154 } 155 if (commentableDoc == null) { 156 commentableDoc = getCommentableDoc(); 157 } 158 // what if commentableDoc is still null? shouldn't, but... 159 if (commentableDoc == null) { 160 throw new NuxeoException("Can't comment on null document"); 161 } 162 DocumentModel newComment; 163 if (parentComment != null) { 164 newComment = commentableDoc.addComment(parentComment.getComment(), comment); 165 } else { 166 newComment = commentableDoc.addComment(comment); 167 } 168 169 // automatically validate the comments 170 if (CommentsConstants.COMMENT_LIFECYCLE.equals(newComment.getLifeCyclePolicy())) { 171 new FollowTransitionUnrestricted(documentManager, newComment.getRef(), 172 CommentsConstants.TRANSITION_TO_PUBLISHED_STATE).runUnrestricted(); 173 } 174 175 // Events.instance().raiseEvent(CommentEvents.COMMENT_ADDED, null, 176 // newComment); 177 cleanContextVariable(); 178 179 return newComment; 180 } 181 182 @Override 183 public DocumentModel addComment(DocumentModel comment) { 184 return addComment(comment, null); 185 } 186 187 @Override 188 public String addComment() { 189 DocumentModel myComment = documentManager.createDocumentModel(CommentsConstants.COMMENT_DOC_TYPE); 190 191 myComment.setPropertyValue(CommentsConstants.COMMENT_AUTHOR, principal.getName()); 192 myComment.setPropertyValue(CommentsConstants.COMMENT_TEXT, newContent); 193 myComment.setPropertyValue(CommentsConstants.COMMENT_CREATION_DATE, Calendar.getInstance()); 194 myComment = addComment(myComment); 195 196 // do not navigate to newly-created comment, they are hidden documents 197 return null; 198 } 199 200 @Override 201 public String createComment(DocumentModel docToComment) { 202 DocumentModel myComment = documentManager.createDocumentModel(CommentsConstants.COMMENT_DOC_TYPE); 203 204 myComment.setProperty("comment", "author", principal.getName()); 205 myComment.setProperty("comment", "text", newContent); 206 myComment.setProperty("comment", "creationDate", Calendar.getInstance()); 207 myComment = addComment(myComment, docToComment); 208 209 // do not navigate to newly-created comment, they are hidden documents 210 return null; 211 } 212 213 @Override 214 @Observer(value = { EventNames.DOCUMENT_SELECTION_CHANGED, EventNames.CONTENT_ROOT_SELECTION_CHANGED, 215 EventNames.DOCUMENT_CHANGED }, create = false) 216 @BypassInterceptors 217 public void documentChanged() { 218 cleanContextVariable(); 219 } 220 221 protected CommentableDocument getCommentableDoc() { 222 if (commentableDoc == null) { 223 DocumentModel currentDocument = navigationContext.getCurrentDocument(); 224 commentableDoc = currentDocument.getAdapter(CommentableDocument.class); 225 } 226 return commentableDoc; 227 } 228 229 protected CommentableDocument getCommentableDoc(DocumentModel doc) { 230 if (doc == null) { 231 doc = navigationContext.getCurrentDocument(); 232 } 233 commentableDoc = doc.getAdapter(CommentableDocument.class); 234 return commentableDoc; 235 } 236 237 /** 238 * Initializes uiComments with Comments of current document. 239 */ 240 @Override 241 public void initComments() { 242 DocumentModel currentDoc = navigationContext.getCurrentDocument(); 243 if (currentDoc == null) { 244 throw new NuxeoException("Unable to find current Document"); 245 } 246 initComments(currentDoc); 247 } 248 249 /** 250 * Initializes uiComments with Comments of current document. 251 */ 252 @Override 253 public void initComments(DocumentModel commentedDoc) { 254 commentableDoc = getCommentableDoc(commentedDoc); 255 if (uiComments == null) { 256 uiComments = new ArrayList<UIComment>(); 257 if (commentableDoc != null) { 258 List<DocumentModel> comments = commentableDoc.getComments(); 259 for (DocumentModel comment : comments) { 260 UIComment uiComment = createUIComment(null, comment); 261 uiComments.add(uiComment); 262 } 263 } 264 } 265 } 266 267 public List<UIComment> getComments(DocumentModel doc) { 268 List<UIComment> allComments = new ArrayList<UIComment>(); 269 commentableDoc = doc.getAdapter(CommentableDocument.class); 270 if (commentableDoc != null) { 271 List<DocumentModel> comments = commentableDoc.getComments(); 272 for (DocumentModel comment : comments) { 273 UIComment uiComment = createUIComment(null, comment); 274 allComments.add(uiComment); 275 } 276 } 277 return allComments; 278 } 279 280 /** 281 * Recursively retrieves all comments of a doc. 282 */ 283 @Override 284 public List<ThreadEntry> getCommentsAsThreadOnDoc(DocumentModel doc) { 285 List<ThreadEntry> allComments = new ArrayList<ThreadEntry>(); 286 List<UIComment> allUIComments = getComments(doc); 287 288 for (UIComment uiComment : allUIComments) { 289 allComments.add(new ThreadEntry(uiComment.getComment(), 0)); 290 if (uiComment.getChildren() != null) { 291 flattenTree(allComments, uiComment, 0); 292 } 293 } 294 return allComments; 295 } 296 297 @Override 298 public List<ThreadEntry> getCommentsAsThread(DocumentModel commentedDoc) { 299 if (commentThread != null) { 300 return commentThread; 301 } 302 commentThread = new ArrayList<ThreadEntry>(); 303 if (uiComments == null) { 304 initComments(commentedDoc); // Fetches all the comments associated 305 // with the 306 // document into uiComments (a list of comment 307 // roots). 308 } 309 for (UIComment uiComment : uiComments) { 310 commentThread.add(new ThreadEntry(uiComment.getComment(), 0)); 311 if (uiComment.getChildren() != null) { 312 flattenTree(commentThread, uiComment, 0); 313 } 314 } 315 return commentThread; 316 } 317 318 /** 319 * Visits a list of comment trees and puts them into a list of "ThreadEntry"s. 320 */ 321 public void flattenTree(List<ThreadEntry> commentThread, UIComment uiComment, int depth) { 322 List<UIComment> uiChildren = uiComment.getChildren(); 323 for (UIComment uiChild : uiChildren) { 324 commentThread.add(new ThreadEntry(uiChild.getComment(), depth + 1)); 325 if (uiChild.getChildren() != null) { 326 flattenTree(commentThread, uiChild, depth + 1); 327 } 328 } 329 } 330 331 /** 332 * Creates a UIComment wrapping "comment", having "parent" as parent. 333 */ 334 protected UIComment createUIComment(UIComment parent, DocumentModel comment) { 335 UIComment wrapper = new UIComment(parent, comment); 336 commentMap.put(wrapper.getId(), wrapper); 337 List<DocumentModel> children = commentableDoc.getComments(comment); 338 for (DocumentModel child : children) { 339 UIComment uiChild = createUIComment(wrapper, child); 340 wrapper.addChild(uiChild); 341 } 342 return wrapper; 343 } 344 345 @Override 346 public String deleteComment(String commentId) { 347 if ("".equals(commentId)) { 348 log.error("No comment id to delete"); 349 return null; 350 } 351 if (commentableDoc == null) { 352 log.error("Can't delete comments of null document"); 353 return null; 354 } 355 UIComment selectedComment = commentMap.get(commentId); 356 commentableDoc.removeComment(selectedComment.getComment()); 357 cleanContextVariable(); 358 // Events.instance().raiseEvent(CommentEvents.COMMENT_REMOVED, null, 359 // selectedComment.getComment()); 360 return null; 361 } 362 363 @Override 364 public String deleteComment() { 365 return deleteComment(deleteCommentId); 366 } 367 368 @Override 369 public String getNewContent() { 370 return newContent; 371 } 372 373 @Override 374 public void setNewContent(String newContent) { 375 this.newContent = newContent; 376 } 377 378 @Override 379 public String beginComment() { 380 commentStarted = true; 381 savedReplyCommentId = replyCommentId; 382 showCreateForm = false; 383 return null; 384 } 385 386 @Override 387 public String cancelComment() { 388 cleanContextVariable(); 389 return null; 390 } 391 392 @Override 393 public boolean getCommentStarted() { 394 return commentStarted; 395 } 396 397 /** 398 * Retrieves children for a given comment. 399 */ 400 public void getChildren(UIComment comment) { 401 assert comment != null; 402 403 List<UIComment> children = comment.getChildren(); 404 405 if (!children.isEmpty()) { 406 for (UIComment childComment : children) { 407 getChildren(childComment); 408 } 409 } 410 flatComments.add(comment); 411 } 412 413 @Override 414 @SuppressWarnings("unchecked") 415 public List<UIComment> getLastCommentsByDate(String commentNumber, DocumentModel commentedDoc) 416 { 417 int number = Integer.parseInt(commentNumber); 418 List<UIComment> comments = new ArrayList<UIComment>(); 419 flatComments = new ArrayList<UIComment>(); 420 421 // Initialize uiComments 422 initComments(commentedDoc); 423 424 if (number < 0 || uiComments.isEmpty()) { 425 return null; 426 } 427 for (UIComment comment : uiComments) { 428 getChildren(comment); 429 } 430 if (!flatComments.isEmpty()) { 431 Collections.sort(flatComments); 432 } 433 if (number > flatComments.size()) { 434 number = flatComments.size(); 435 } 436 for (int i = 0; i < number; i++) { 437 comments.add(flatComments.get(flatComments.size() - 1 - i)); 438 } 439 return comments; 440 } 441 442 @Override 443 public List<UIComment> getLastCommentsByDate(String commentNumber) { 444 return getLastCommentsByDate(commentNumber, null); 445 } 446 447 @Override 448 public String getSavedReplyCommentId() { 449 return savedReplyCommentId; 450 } 451 452 @Override 453 public void setSavedReplyCommentId(String savedReplyCommentId) { 454 this.savedReplyCommentId = savedReplyCommentId; 455 } 456 457 @Override 458 public List<Action> getActionsForComment() { 459 return webActions.getActionsList(COMMENTS_ACTIONS); 460 } 461 462 @Override 463 public List<Action> getActionsForComment(String category) { 464 return webActions.getActionsList(category); 465 } 466 467 @Override 468 public boolean getShowCreateForm() { 469 return showCreateForm; 470 } 471 472 @Override 473 public void setShowCreateForm(boolean flag) { 474 showCreateForm = flag; 475 } 476 477 @Override 478 public void toggleCreateForm(ActionEvent event) { 479 showCreateForm = !showCreateForm; 480 } 481 482 public void cleanContextVariable() { 483 commentableDoc = null; 484 uiComments = null; 485 commentThread = null; 486 showCreateForm = false; 487 commentStarted = false; 488 savedReplyCommentId = null; 489 newContent = null; 490 // NXP-11462: reset factory to force comment fetching after the new 491 // comment is added 492 Contexts.getEventContext().remove("documentThreadedComments"); 493 } 494 495}