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}