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    // 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}