001/* 002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nuxeo - initial API and implementation 018 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.platform.comment.web; 023 024import java.util.ArrayList; 025import java.util.Calendar; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030 031import javax.faces.event.ActionEvent; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.jboss.seam.annotations.Create; 036import org.jboss.seam.annotations.Destroy; 037import org.jboss.seam.annotations.In; 038import org.jboss.seam.annotations.Observer; 039import org.jboss.seam.annotations.intercept.BypassInterceptors; 040import org.jboss.seam.annotations.web.RequestParameter; 041import org.jboss.seam.contexts.Contexts; 042import org.nuxeo.ecm.core.api.CoreSession; 043import org.nuxeo.ecm.core.api.DocumentModel; 044import org.nuxeo.ecm.core.api.NuxeoException; 045import org.nuxeo.ecm.core.api.NuxeoPrincipal; 046import org.nuxeo.ecm.platform.actions.Action; 047import org.nuxeo.ecm.platform.comment.api.CommentableDocument; 048import org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants; 049import org.nuxeo.ecm.platform.comment.workflow.utils.FollowTransitionUnrestricted; 050import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 051import org.nuxeo.ecm.platform.ui.web.api.WebActions; 052import org.nuxeo.ecm.webapp.helpers.EventNames; 053import org.nuxeo.ecm.webapp.security.UserSession; 054 055/** 056 * @author <a href="mailto:[email protected]">George Lefter</a> 057 */ 058public abstract class AbstractCommentManagerActionsBean implements CommentManagerActions { 059 060 protected static final String COMMENTS_ACTIONS = "COMMENT_ACTIONS"; 061 062 private static final Log log = LogFactory.getLog(AbstractCommentManagerActionsBean.class); 063 064 protected NuxeoPrincipal principal; 065 066 protected boolean principalIsAdmin; 067 068 protected boolean showCreateForm; 069 070 @In(create = true, required = false) 071 protected transient CoreSession documentManager; 072 073 @In(create = true) 074 protected transient WebActions webActions; 075 076 protected String newContent; 077 078 protected CommentableDocument commentableDoc; 079 080 protected List<UIComment> uiComments; 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<>(); 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<>(); 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<>(); 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<>(); 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 List<ThreadEntry> commentThread = new ArrayList<>(); 300 if (uiComments == null) { 301 initComments(commentedDoc); // Fetches all the comments associated 302 // with the 303 // document into uiComments (a list of comment 304 // roots). 305 } 306 for (UIComment uiComment : uiComments) { 307 commentThread.add(new ThreadEntry(uiComment.getComment(), 0)); 308 if (uiComment.getChildren() != null) { 309 flattenTree(commentThread, uiComment, 0); 310 } 311 } 312 return commentThread; 313 } 314 315 /** 316 * Visits a list of comment trees and puts them into a list of "ThreadEntry"s. 317 */ 318 public void flattenTree(List<ThreadEntry> commentThread, UIComment uiComment, int depth) { 319 List<UIComment> uiChildren = uiComment.getChildren(); 320 for (UIComment uiChild : uiChildren) { 321 commentThread.add(new ThreadEntry(uiChild.getComment(), depth + 1)); 322 if (uiChild.getChildren() != null) { 323 flattenTree(commentThread, uiChild, depth + 1); 324 } 325 } 326 } 327 328 /** 329 * Creates a UIComment wrapping "comment", having "parent" as parent. 330 */ 331 protected UIComment createUIComment(UIComment parent, DocumentModel comment) { 332 UIComment wrapper = new UIComment(parent, comment); 333 commentMap.put(wrapper.getId(), wrapper); 334 List<DocumentModel> children = commentableDoc.getComments(comment); 335 for (DocumentModel child : children) { 336 UIComment uiChild = createUIComment(wrapper, child); 337 wrapper.addChild(uiChild); 338 } 339 return wrapper; 340 } 341 342 @Override 343 public String deleteComment(String commentId) { 344 if ("".equals(commentId)) { 345 log.error("No comment id to delete"); 346 return null; 347 } 348 if (commentableDoc == null) { 349 log.error("Can't delete comments of null document"); 350 return null; 351 } 352 UIComment selectedComment = commentMap.get(commentId); 353 commentableDoc.removeComment(selectedComment.getComment()); 354 cleanContextVariable(); 355 // Events.instance().raiseEvent(CommentEvents.COMMENT_REMOVED, null, 356 // selectedComment.getComment()); 357 return null; 358 } 359 360 @Override 361 public String deleteComment() { 362 return deleteComment(deleteCommentId); 363 } 364 365 @Override 366 public String getNewContent() { 367 return newContent; 368 } 369 370 @Override 371 public void setNewContent(String newContent) { 372 this.newContent = newContent; 373 } 374 375 @Override 376 public String beginComment() { 377 commentStarted = true; 378 savedReplyCommentId = replyCommentId; 379 showCreateForm = false; 380 return null; 381 } 382 383 @Override 384 public String cancelComment() { 385 cleanContextVariable(); 386 return null; 387 } 388 389 @Override 390 public boolean getCommentStarted() { 391 return commentStarted; 392 } 393 394 /** 395 * Retrieves children for a given comment. 396 */ 397 public void getChildren(UIComment comment) { 398 assert comment != null; 399 400 List<UIComment> children = comment.getChildren(); 401 402 if (!children.isEmpty()) { 403 for (UIComment childComment : children) { 404 getChildren(childComment); 405 } 406 } 407 flatComments.add(comment); 408 } 409 410 @Override 411 @SuppressWarnings("unchecked") 412 public List<UIComment> getLastCommentsByDate(String commentNumber, DocumentModel commentedDoc) 413 { 414 int number = Integer.parseInt(commentNumber); 415 List<UIComment> comments = new ArrayList<>(); 416 flatComments = new ArrayList<>(); 417 418 // Initialize uiComments 419 initComments(commentedDoc); 420 421 if (number < 0 || uiComments.isEmpty()) { 422 return null; 423 } 424 for (UIComment comment : uiComments) { 425 getChildren(comment); 426 } 427 if (!flatComments.isEmpty()) { 428 Collections.sort(flatComments); 429 } 430 if (number > flatComments.size()) { 431 number = flatComments.size(); 432 } 433 for (int i = 0; i < number; i++) { 434 comments.add(flatComments.get(flatComments.size() - 1 - i)); 435 } 436 return comments; 437 } 438 439 @Override 440 public List<UIComment> getLastCommentsByDate(String commentNumber) { 441 return getLastCommentsByDate(commentNumber, null); 442 } 443 444 @Override 445 public String getSavedReplyCommentId() { 446 return savedReplyCommentId; 447 } 448 449 @Override 450 public void setSavedReplyCommentId(String savedReplyCommentId) { 451 this.savedReplyCommentId = savedReplyCommentId; 452 } 453 454 @Override 455 public List<Action> getActionsForComment() { 456 return webActions.getActionsList(COMMENTS_ACTIONS); 457 } 458 459 @Override 460 public List<Action> getActionsForComment(String category) { 461 return webActions.getActionsList(category); 462 } 463 464 @Override 465 public boolean getShowCreateForm() { 466 return showCreateForm; 467 } 468 469 @Override 470 public void setShowCreateForm(boolean flag) { 471 showCreateForm = flag; 472 } 473 474 @Override 475 public void toggleCreateForm(ActionEvent event) { 476 showCreateForm = !showCreateForm; 477 } 478 479 public void cleanContextVariable() { 480 commentableDoc = null; 481 uiComments = null; 482 showCreateForm = false; 483 commentStarted = false; 484 savedReplyCommentId = null; 485 newContent = null; 486 // NXP-11462: reset factory to force comment fetching after the new 487 // comment is added 488 Contexts.getEventContext().remove("documentThreadedComments"); 489 } 490 491}