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:glefter@nuxeo.com">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    protected List<ThreadEntry> commentThread;
083
084    // the id of the comment to delete
085    @RequestParameter
086    protected String deleteCommentId;
087
088    // the id of the comment to reply to
089    @RequestParameter
090    protected String replyCommentId;
091
092    protected String savedReplyCommentId;
093
094    protected Map<String, UIComment> commentMap;
095
096    protected boolean commentStarted;
097
098    protected List<UIComment> flatComments;
099
100    @In(create = true)
101    protected UserSession userSession;
102
103    @In(create = true)
104    protected NavigationContext navigationContext;
105
106    @Override
107    @Create
108    public void initialize() {
109        log.debug("Initializing...");
110        commentMap = new HashMap<String, UIComment>();
111        showCreateForm = false;
112
113        principal = userSession.getCurrentNuxeoPrincipal();
114        principalIsAdmin = principal.isAdministrator();
115    }
116
117    @Override
118    @Destroy
119    public void destroy() {
120        commentMap = null;
121        log.debug("Removing Seam action listener...");
122    }
123
124    @Override
125    public String getPrincipalName() {
126        return principal.getName();
127    }
128
129    @Override
130    public boolean getPrincipalIsAdmin() {
131        return principalIsAdmin;
132    }
133
134    protected DocumentModel initializeComment(DocumentModel comment) {
135        if (comment != null) {
136            if (comment.getProperty("dublincore", "contributors") == null) {
137                String[] contributors = new String[1];
138                contributors[0] = getPrincipalName();
139                comment.setProperty("dublincore", "contributors", contributors);
140            }
141            if (comment.getProperty("dublincore", "created") == null) {
142                comment.setProperty("dublincore", "created", Calendar.getInstance());
143            }
144        }
145        return comment;
146    }
147
148    public DocumentModel addComment(DocumentModel comment, DocumentModel docToComment) {
149        comment = initializeComment(comment);
150        UIComment parentComment = null;
151        if (savedReplyCommentId != null) {
152            parentComment = commentMap.get(savedReplyCommentId);
153        }
154        if (docToComment != null) {
155            commentableDoc = getCommentableDoc(docToComment);
156        }
157        if (commentableDoc == null) {
158            commentableDoc = getCommentableDoc();
159        }
160        // what if commentableDoc is still null? shouldn't, but...
161        if (commentableDoc == null) {
162            throw new NuxeoException("Can't comment on null document");
163        }
164        DocumentModel newComment;
165        if (parentComment != null) {
166            newComment = commentableDoc.addComment(parentComment.getComment(), comment);
167        } else {
168            newComment = commentableDoc.addComment(comment);
169        }
170
171        // automatically validate the comments
172        if (CommentsConstants.COMMENT_LIFECYCLE.equals(newComment.getLifeCyclePolicy())) {
173            new FollowTransitionUnrestricted(documentManager, newComment.getRef(),
174                    CommentsConstants.TRANSITION_TO_PUBLISHED_STATE).runUnrestricted();
175        }
176
177        // Events.instance().raiseEvent(CommentEvents.COMMENT_ADDED, null,
178        // newComment);
179        cleanContextVariable();
180
181        return newComment;
182    }
183
184    @Override
185    public DocumentModel addComment(DocumentModel comment) {
186        return addComment(comment, null);
187    }
188
189    @Override
190    public String addComment() {
191        DocumentModel myComment = documentManager.createDocumentModel(CommentsConstants.COMMENT_DOC_TYPE);
192
193        myComment.setPropertyValue(CommentsConstants.COMMENT_AUTHOR, principal.getName());
194        myComment.setPropertyValue(CommentsConstants.COMMENT_TEXT, newContent);
195        myComment.setPropertyValue(CommentsConstants.COMMENT_CREATION_DATE, Calendar.getInstance());
196        myComment = addComment(myComment);
197
198        // do not navigate to newly-created comment, they are hidden documents
199        return null;
200    }
201
202    @Override
203    public String createComment(DocumentModel docToComment) {
204        DocumentModel myComment = documentManager.createDocumentModel(CommentsConstants.COMMENT_DOC_TYPE);
205
206        myComment.setProperty("comment", "author", principal.getName());
207        myComment.setProperty("comment", "text", newContent);
208        myComment.setProperty("comment", "creationDate", Calendar.getInstance());
209        myComment = addComment(myComment, docToComment);
210
211        // do not navigate to newly-created comment, they are hidden documents
212        return null;
213    }
214
215    @Override
216    @Observer(value = { EventNames.DOCUMENT_SELECTION_CHANGED, EventNames.CONTENT_ROOT_SELECTION_CHANGED,
217            EventNames.DOCUMENT_CHANGED }, create = false)
218    @BypassInterceptors
219    public void documentChanged() {
220        cleanContextVariable();
221    }
222
223    protected CommentableDocument getCommentableDoc() {
224        if (commentableDoc == null) {
225            DocumentModel currentDocument = navigationContext.getCurrentDocument();
226            commentableDoc = currentDocument.getAdapter(CommentableDocument.class);
227        }
228        return commentableDoc;
229    }
230
231    protected CommentableDocument getCommentableDoc(DocumentModel doc) {
232        if (doc == null) {
233            doc = navigationContext.getCurrentDocument();
234        }
235        commentableDoc = doc.getAdapter(CommentableDocument.class);
236        return commentableDoc;
237    }
238
239    /**
240     * Initializes uiComments with Comments of current document.
241     */
242    @Override
243    public void initComments() {
244        DocumentModel currentDoc = navigationContext.getCurrentDocument();
245        if (currentDoc == null) {
246            throw new NuxeoException("Unable to find current Document");
247        }
248        initComments(currentDoc);
249    }
250
251    /**
252     * Initializes uiComments with Comments of current document.
253     */
254    @Override
255    public void initComments(DocumentModel commentedDoc) {
256        commentableDoc = getCommentableDoc(commentedDoc);
257        if (uiComments == null) {
258            uiComments = new ArrayList<UIComment>();
259            if (commentableDoc != null) {
260                List<DocumentModel> comments = commentableDoc.getComments();
261                for (DocumentModel comment : comments) {
262                    UIComment uiComment = createUIComment(null, comment);
263                    uiComments.add(uiComment);
264                }
265            }
266        }
267    }
268
269    public List<UIComment> getComments(DocumentModel doc) {
270        List<UIComment> allComments = new ArrayList<UIComment>();
271        commentableDoc = doc.getAdapter(CommentableDocument.class);
272        if (commentableDoc != null) {
273            List<DocumentModel> comments = commentableDoc.getComments();
274            for (DocumentModel comment : comments) {
275                UIComment uiComment = createUIComment(null, comment);
276                allComments.add(uiComment);
277            }
278        }
279        return allComments;
280    }
281
282    /**
283     * Recursively retrieves all comments of a doc.
284     */
285    @Override
286    public List<ThreadEntry> getCommentsAsThreadOnDoc(DocumentModel doc) {
287        List<ThreadEntry> allComments = new ArrayList<ThreadEntry>();
288        List<UIComment> allUIComments = getComments(doc);
289
290        for (UIComment uiComment : allUIComments) {
291            allComments.add(new ThreadEntry(uiComment.getComment(), 0));
292            if (uiComment.getChildren() != null) {
293                flattenTree(allComments, uiComment, 0);
294            }
295        }
296        return allComments;
297    }
298
299    @Override
300    public List<ThreadEntry> getCommentsAsThread(DocumentModel commentedDoc) {
301        if (commentThread != null) {
302            return commentThread;
303        }
304        commentThread = new ArrayList<ThreadEntry>();
305        if (uiComments == null) {
306            initComments(commentedDoc); // Fetches all the comments associated
307            // with the
308            // document into uiComments (a list of comment
309            // roots).
310        }
311        for (UIComment uiComment : uiComments) {
312            commentThread.add(new ThreadEntry(uiComment.getComment(), 0));
313            if (uiComment.getChildren() != null) {
314                flattenTree(commentThread, uiComment, 0);
315            }
316        }
317        return commentThread;
318    }
319
320    /**
321     * Visits a list of comment trees and puts them into a list of "ThreadEntry"s.
322     */
323    public void flattenTree(List<ThreadEntry> commentThread, UIComment uiComment, int depth) {
324        List<UIComment> uiChildren = uiComment.getChildren();
325        for (UIComment uiChild : uiChildren) {
326            commentThread.add(new ThreadEntry(uiChild.getComment(), depth + 1));
327            if (uiChild.getChildren() != null) {
328                flattenTree(commentThread, uiChild, depth + 1);
329            }
330        }
331    }
332
333    /**
334     * Creates a UIComment wrapping "comment", having "parent" as parent.
335     */
336    protected UIComment createUIComment(UIComment parent, DocumentModel comment) {
337        UIComment wrapper = new UIComment(parent, comment);
338        commentMap.put(wrapper.getId(), wrapper);
339        List<DocumentModel> children = commentableDoc.getComments(comment);
340        for (DocumentModel child : children) {
341            UIComment uiChild = createUIComment(wrapper, child);
342            wrapper.addChild(uiChild);
343        }
344        return wrapper;
345    }
346
347    @Override
348    public String deleteComment(String commentId) {
349        if ("".equals(commentId)) {
350            log.error("No comment id to delete");
351            return null;
352        }
353        if (commentableDoc == null) {
354            log.error("Can't delete comments of null document");
355            return null;
356        }
357        UIComment selectedComment = commentMap.get(commentId);
358        commentableDoc.removeComment(selectedComment.getComment());
359        cleanContextVariable();
360        // Events.instance().raiseEvent(CommentEvents.COMMENT_REMOVED, null,
361        // selectedComment.getComment());
362        return null;
363    }
364
365    @Override
366    public String deleteComment() {
367        return deleteComment(deleteCommentId);
368    }
369
370    @Override
371    public String getNewContent() {
372        return newContent;
373    }
374
375    @Override
376    public void setNewContent(String newContent) {
377        this.newContent = newContent;
378    }
379
380    @Override
381    public String beginComment() {
382        commentStarted = true;
383        savedReplyCommentId = replyCommentId;
384        showCreateForm = false;
385        return null;
386    }
387
388    @Override
389    public String cancelComment() {
390        cleanContextVariable();
391        return null;
392    }
393
394    @Override
395    public boolean getCommentStarted() {
396        return commentStarted;
397    }
398
399    /**
400     * Retrieves children for a given comment.
401     */
402    public void getChildren(UIComment comment) {
403        assert comment != null;
404
405        List<UIComment> children = comment.getChildren();
406
407        if (!children.isEmpty()) {
408            for (UIComment childComment : children) {
409                getChildren(childComment);
410            }
411        }
412        flatComments.add(comment);
413    }
414
415    @Override
416    @SuppressWarnings("unchecked")
417    public List<UIComment> getLastCommentsByDate(String commentNumber, DocumentModel commentedDoc)
418            {
419        int number = Integer.parseInt(commentNumber);
420        List<UIComment> comments = new ArrayList<UIComment>();
421        flatComments = new ArrayList<UIComment>();
422
423        // Initialize uiComments
424        initComments(commentedDoc);
425
426        if (number < 0 || uiComments.isEmpty()) {
427            return null;
428        }
429        for (UIComment comment : uiComments) {
430            getChildren(comment);
431        }
432        if (!flatComments.isEmpty()) {
433            Collections.sort(flatComments);
434        }
435        if (number > flatComments.size()) {
436            number = flatComments.size();
437        }
438        for (int i = 0; i < number; i++) {
439            comments.add(flatComments.get(flatComments.size() - 1 - i));
440        }
441        return comments;
442    }
443
444    @Override
445    public List<UIComment> getLastCommentsByDate(String commentNumber) {
446        return getLastCommentsByDate(commentNumber, null);
447    }
448
449    @Override
450    public String getSavedReplyCommentId() {
451        return savedReplyCommentId;
452    }
453
454    @Override
455    public void setSavedReplyCommentId(String savedReplyCommentId) {
456        this.savedReplyCommentId = savedReplyCommentId;
457    }
458
459    @Override
460    public List<Action> getActionsForComment() {
461        return webActions.getActionsList(COMMENTS_ACTIONS);
462    }
463
464    @Override
465    public List<Action> getActionsForComment(String category) {
466        return webActions.getActionsList(category);
467    }
468
469    @Override
470    public boolean getShowCreateForm() {
471        return showCreateForm;
472    }
473
474    @Override
475    public void setShowCreateForm(boolean flag) {
476        showCreateForm = flag;
477    }
478
479    @Override
480    public void toggleCreateForm(ActionEvent event) {
481        showCreateForm = !showCreateForm;
482    }
483
484    public void cleanContextVariable() {
485        commentableDoc = null;
486        uiComments = null;
487        commentThread = null;
488        showCreateForm = false;
489        commentStarted = false;
490        savedReplyCommentId = null;
491        newContent = null;
492        // NXP-11462: reset factory to force comment fetching after the new
493        // comment is added
494        Contexts.getEventContext().remove("documentThreadedComments");
495    }
496
497}